Implement solfa partition creation screen & options

This commit is contained in:
Hasinjato 2026-07-23 11:07:33 +03:00
parent 1bbf91e29c
commit bd936fe3b8
11 changed files with 611 additions and 97 deletions

View file

@ -9,7 +9,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconToggleButton import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -173,10 +172,10 @@ object ScreenSolfa : Screen {
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 val isEditMode = sharedScreenModel.sourceModeState.value
IconToggleButton( IconToggleButton(
checked = isEditMode, checked = isEditMode,
onCheckedChange = { sharedScreenModel.toggleEditMode() }, onCheckedChange = { sharedScreenModel.toggleSourceMode() },
modifier = Modifier.size(40.dp) modifier = Modifier.size(40.dp)
) { ) {
Icon( Icon(

View file

@ -28,6 +28,7 @@ fun GridActionMenu(
) )
) { ) {
Icon( Icon(
modifier = Modifier.width(50.dp),
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
contentDescription = null contentDescription = null
) )
@ -44,6 +45,7 @@ fun GridActionMenu(
border = BorderStroke(1.dp, MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f)) border = BorderStroke(1.dp, MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f))
) { ) {
Icon( Icon(
modifier = Modifier.width(50.dp),
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = null contentDescription = null
) )

View file

@ -8,6 +8,7 @@ import mg.dot.feufaro.getGlobalTemplate
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 mg.dot.feufaro.viewmodel.PartitionMetadata
import mg.dot.feufaro.transformLyricsInput import mg.dot.feufaro.transformLyricsInput
import java.io.File import java.io.File
import kotlin.math.min import kotlin.math.min
@ -713,7 +714,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
/** Si y a '-' dans template inutile d' ajouter au notes */ /** Si y a '-' dans template inutile d' ajouter au notes */
val hasDashInTemplate = editState.templateFragment.contains("-") val hasDashInTemplate = editState.templateFragment.contains("-")
val newUserTokens = if (hasDashInTemplate) { val newUserTokens = if (hasDashInTemplate) {
rawUserTokens.filter { it.matches(Regex("[drmfsltDRFSTzw].*")) } rawUserTokens.filter { it.matches(Regex("[drmfsltDRFSTzw\\(\\)].*")) }
} else { } else {
rawUserTokens rawUserTokens
} }
@ -759,6 +760,9 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
} else { } else {
// println("Suppression de la note $ut (gérée par le saut du fragment d'origine)") // println("Suppression de la note $ut (gérée par le saut du fragment d'origine)")
} }
} else {
if (ut == "(" || ut == ")") {
resultTokens.add(ut) // Insertion directe de la parenthèse sans incrémenter currentLogicalIdx
} else { } else {
val processedToken = if (ut.matches(Regex("[drmfsltDRFSTzw].*"))) { val processedToken = if (ut.matches(Regex("[drmfsltDRFSTzw].*"))) {
val (fileBase, fileLvl) = parseNoteAndOctave(allTokens[i]) val (fileBase, fileLvl) = parseNoteAndOctave(allTokens[i])
@ -767,22 +771,22 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
} else { } else {
ut ut
} }
// println("j'aoute $processedToken") // println("j'aoute $processedToken")
resultTokens.add(processedToken) resultTokens.add(processedToken)
if (processedToken.matches(Regex("[drmfsltDRFSTzw].*|[-―]"))) { if (processedToken.matches(Regex("[drmfsltDRFSTzw].*|[-―]"))) {
// println( /*println(
// String.format( String.format(
// "NEW | %-10s | INSERT | %-12d | REPLACE", "NEW | %-10s | INSERT | %-12d | REPLACE",
// processedToken, processedToken,
// currentLogicalIdx currentLogicalIdx
// ) )
// ) )*/
currentLogicalIdx++ currentLogicalIdx++
} }
} }
} }
}
var skipped = 0 var skipped = 0
while (skipped < dynamicDeleteCount && i < allTokens.size) { while (skipped < dynamicDeleteCount && i < allTokens.size) {
@ -1281,8 +1285,8 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
.replace("si", "S") .replace("si", "S")
.replace("ta", "T") .replace("ta", "T")
.replace("(","") /*.replace("(","")
.replace(")","") .replace(")","")*/
} }
private fun parseNoteLineToArray(noteLine: String): List<String> { private fun parseNoteLineToArray(noteLine: String): List<String> {
@ -2607,4 +2611,77 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
} }
return currentPointer return currentPointer
} }
private data class MeasurePattern(val groupSize: Int, val groupsPerSegment: Int, val segments: Int)
private val measurePatterns = mapOf(
"2" to MeasurePattern(groupSize = 4, groupsPerSegment = 2, segments = 2),//16
"3" to MeasurePattern(groupSize = 3, groupsPerSegment = 2, segments = 2),//12
"4" to MeasurePattern(groupSize = 4, groupsPerSegment = 4, segments = 2),// 32
"6" to MeasurePattern(groupSize = 6, groupsPerSegment = 1, segments = 2),// 12
"12" to MeasurePattern(groupSize = 12, groupsPerSegment = 1, segments = 2), //24
)
private fun buildTemplatePattern(measure: String): String {
val measur = measure.substringBefore('/').trim()
val pattern = measurePatterns[measur.trim()] ?: measurePatterns.getValue("4")
val group = "4".repeat(pattern.groupSize)
val segment = List(pattern.groupsPerSegment) { group }.joinToString(" ")
return List(pattern.segments) { segment }.joinToString(" / ")
}
suspend fun createNewSolfa(partitionMetadata: PartitionMetadata) {
val lyricsPrefix = if(partitionMetadata.language == "Anglais") {
'E'
} else if(partitionMetadata.language == "Malagasy") {
'Y'
} else {
'Y'
}
val partUrl = if (partitionMetadata.url.isNotBlank() && partitionMetadata.url != "https://") {
"|u:${partitionMetadata.url}"
} else {
""
}
val templatePattern = buildTemplatePattern(partitionMetadata.measure)
val notesPattern = templatePattern.replace(Regex("[0-9]"), "d")
val soflaContent = listOf(
"M0:|t:${partitionMetadata.title}|" +
"c:${partitionMetadata.key}|" +
"r:${partitionMetadata.rhythm}|" +
"a:${partitionMetadata.author}|" +
"h:${partitionMetadata.composer}|" +
"m:${partitionMetadata.measure}" +
partUrl,
"U0:z0:$templatePattern",
"N1: $notesPattern",
"N2: $notesPattern",
"N3: $notesPattern",
"N4: $notesPattern",
"${lyricsPrefix}1:",
).joinToString("\n")
val safeFileName = partitionMetadata.title
.ifBlank { "solfa" }
.replace(Regex("[\\\\/:*?\"<>|]"), "_")
val tempFile = File(System.getProperty("java.io.tmpdir"), "$safeFileName.txt")
tempFile.deleteOnExit()
try {
tempFile.writeText(soflaContent)
println("Fichier temporaire créé : ${tempFile.absolutePath}")
withContext(Dispatchers.Main) {
justBuild(tempFile.absolutePath, soflaContent)
}
} catch (e: Exception) {
println("Erreur lors de la création du fichier temporaire : ${e.message}")
}
}
} }

View file

@ -1042,57 +1042,18 @@ public fun getGlobalTemplate(editState: TUOEditState): String {
val filteredNotes = notes val filteredNotes = notes
.replace(Solfa.REGEX_NOTE_TOADD, "$3$4") .replace(Solfa.REGEX_NOTE_TOADD, "$3$4")
.replace("<", "") .replace("<", "")
val tpl = notesToTemplate(filteredNotes) val tpl = notesToTemplate(filteredNotes).replace(Regex("[()]"), "")
// println("Voix $voiceIdx | Notes: '$filteredNotes' -> Template: '$tpl'") // println("Voix $voiceIdx | Notes: '$filteredNotes' -> Template: '$tpl'")
tpl tpl
} }
val rythmicTemplates = voiceTemplates.map { tpl -> tpl.replace(Regex("[()]"), "") } val rythmicTemplates = voiceTemplates
val maxLen = rythmicTemplates.maxOfOrNull { it.length } ?: 1 val maxLen = rythmicTemplates.maxOfOrNull { it.length } ?: 1
// println("Longueur rythmique max détectée: $maxLen") // println("Longueur rythmique max détectée: $maxLen")
val openBefore = BooleanArray(maxLen)
val closeBefore = BooleanArray(maxLen)
val openAfter = BooleanArray(maxLen)
val closeAfter = BooleanArray(maxLen)
var openAtTheEnd = false
var closeAtTheEnd = false
voiceTemplates.forEach { tpl ->
var rythmicIdx = 0
for (i in tpl.indices) {
val char = tpl[i]
if (char == '(' || char == ')') {
val textAfter = tpl.substring(i + 1)
val hasRythmicAfter = textAfter.any { it != '(' && it != ')' }
if (hasRythmicAfter) {
if (rythmicIdx < maxLen) {
if (char == '(') openBefore[rythmicIdx] = true
if (char == ')') closeBefore[rythmicIdx] = true
}
} else {
if (rythmicIdx > 0) {
if (char == '(') openAfter[rythmicIdx - 1] = true
if (char == ')') closeAfter[rythmicIdx - 1] = true
} else {
if (char == '(') openAtTheEnd = true
if (char == ')') closeAtTheEnd = true
}
}
} else {
rythmicIdx++
}
}
}
val result = StringBuilder() val result = StringBuilder()
for (i in 0 until maxLen) { for (i in 0 until maxLen) {
if (openBefore[i]) result.append('(')
if (closeBefore[i]) result.append(')')
val chars = rythmicTemplates.mapNotNull { it.getOrNull(i) }.toSet() val chars = rythmicTemplates.mapNotNull { it.getOrNull(i) }.toSet()
val previousChars = if(i>0) rythmicTemplates.mapNotNull { it.getOrNull(i-1) }.toSet() else "".toSet() val previousChars = if(i>0) rythmicTemplates.mapNotNull { it.getOrNull(i-1) }.toSet() else "".toSet()
val activeChars = rythmicTemplates.mapNotNull { it.getOrNull(i) } val activeChars = rythmicTemplates.mapNotNull { it.getOrNull(i) }
@ -1116,14 +1077,8 @@ public fun getGlobalTemplate(editState: TUOEditState): String {
result.append(combinedChar) result.append(combinedChar)
} }
// println("all = ${result.toString()}") // println("all = ${result.toString()}")
if (openAfter[i]) result.append('(')
if (closeAfter[i]) result.append(')')
} }
if (openAtTheEnd) result.append('(')
if (closeAtTheEnd) result.append(')')
// println("Template Global Final: '${result.toString()}'") // println("Template Global Final: '${result.toString()}'")
// println("----------------------------------\n") // println("----------------------------------\n")
return result.toString() return result.toString()
@ -1149,8 +1104,9 @@ private fun notesToTemplate(notes: String): String {
for (match in matches) { for (match in matches) {
val value = match.value val value = match.value
when { when {
value.matches(Regex("[.,()-]")) -> template.append(value) value.matches(Regex("[.,-]")) -> template.append(value)
value.trim().isEmpty() -> template.append(" ") value.trim().isEmpty() -> template.append(" ")
value.matches(Regex("[()]")) -> template.append("")
else -> template.append("D") else -> template.append("D")
} }
} }

View file

@ -13,10 +13,8 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
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.automirrored.filled.Undo
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.* import androidx.compose.material3.*
@ -58,6 +56,7 @@ import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.TUODetailDialog import mg.dot.feufaro.TUODetailDialog
import mg.dot.feufaro.data.GridTUOData import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.getPathValidator import mg.dot.feufaro.getPathValidator
import mg.dot.feufaro.getPlatform
import mg.dot.feufaro.ui.rememberFileSaveLauncher 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 mg.dot.feufaro.viewmodel.SolfaScreenModel
@ -567,6 +566,8 @@ fun AutoResizingText(
) )
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LazyVerticalGridTUO( fun LazyVerticalGridTUO(
viewModel: GridTUOData, viewModel: GridTUOData,
@ -605,6 +606,9 @@ fun LazyVerticalGridTUO(
var toggleWithDegreeAndChord by remember { mutableStateOf (false) } var toggleWithDegreeAndChord by remember { mutableStateOf (false) }
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val platform = getPlatform()
val isAndroid = platform.name.startsWith("Android")
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -956,9 +960,10 @@ fun LazyVerticalGridTUO(
val isAbsoluteLastGrid = (globalIndex == tuoList.size - 2) val isAbsoluteLastGrid = (globalIndex == tuoList.size - 2)
if (editMode && isAbsoluteLastGrid) { if (editMode && isAbsoluteLastGrid) {
val lastGridwidth = if(isAndroid) (gridWidthDp / gridColumnCount)*2 else ((gridWidthDp / gridColumnCount)*11)/10
Box( Box(
modifier = Modifier modifier = Modifier
.width(gridWidthDp / gridColumnCount) .width(lastGridwidth)
.fillMaxHeight(), .fillMaxHeight(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@ -987,7 +992,18 @@ fun LazyVerticalGridTUO(
val hairPinState by sharedScreenModel.isPendingHairPinOnEdit.collectAsState() val hairPinState by sharedScreenModel.isPendingHairPinOnEdit.collectAsState()
val tooltipState = rememberTooltipState(isPersistent = true)
val showTooltipWarn = hairPinState.isPending && hairPinState.gridIndex == globalIndex
LaunchedEffect(hairPinState.isPending) {
if (showTooltipWarn) {
tooltipState.show()
} else {
tooltipState.dismiss()
}
}
key(oneTUO.numBlock) { key(oneTUO.numBlock) {
val content : @Composable () -> Unit = {
Box(modifier = Modifier Box(modifier = Modifier
.width(gridWidthDp / gridColumnCount) .width(gridWidthDp / gridColumnCount)
.then( .then(
@ -1171,6 +1187,22 @@ fun LazyVerticalGridTUO(
} }
} }
} }
if (showTooltipWarn) {
TooltipBox(
state = tooltipState,
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text("Autre grille pour compléter")
}
}
) {
content()
}
} else {
content()
}
}
} }
} }
} }
@ -1361,7 +1393,7 @@ fun EditSourceCompose(
sharedScreenModel: SharedScreenModel, sharedScreenModel: SharedScreenModel,
solfaScreenModel: SolfaScreenModel solfaScreenModel: SolfaScreenModel
) { ) {
var isEditorVisible = sharedScreenModel.editModeState.value var isEditorVisible = sharedScreenModel.sourceModeState.value
var sourcePath = sharedScreenModel.activeFilePath.value var sourcePath = sharedScreenModel.activeFilePath.value
var sourceContent = sharedScreenModel.fileContent.value ?: "" var sourceContent = sharedScreenModel.fileContent.value ?: ""
var sourceTitle = sharedScreenModel.songTitle.value var sourceTitle = sharedScreenModel.songTitle.value
@ -1510,7 +1542,7 @@ fun EditSourceCompose(
} }
} }
}, },
onClose = { sharedScreenModel.toggleEditMode() } onClose = { sharedScreenModel.toggleSourceMode() }
) )
} }

View file

@ -3,7 +3,6 @@ package mg.dot.feufaro.ui
import SharedScreenModel import SharedScreenModel
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -19,7 +18,6 @@ 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.*
import androidx.compose.runtime.remember
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.alpha import androidx.compose.ui.draw.alpha
@ -27,7 +25,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
@ -127,8 +124,9 @@ fun MainScreenWithDrawer(
} }
} }
val isEditMode by sharedScreenModel.modeEditor.collectAsState() val isEditMode by sharedScreenModel.modeEditor.collectAsState()
LaunchedEffect(isEditMode) { val createMode by sharedScreenModel.createMode.collectAsState()
if(isEditMode) { LaunchedEffect(isEditMode || createMode) {
if(isEditMode || createMode) {
drawerState.close() drawerState.close()
} }
} }
@ -778,11 +776,28 @@ fun MainScreenWithDrawer(
) )
} }
if(createMode) {
NewPartition(
onCreate = { partMtdata ->
solfaScreenModel.newSolfa(partMtdata)
sharedScreenModel.toggleCreateMode(!createMode)
sharedScreenModel.toggleEditorMode(createMode)
if(!isAndroid) {
sharedScreenModel.toggleSourceMode()
}
},
onDismissRequest = {
sharedScreenModel.toggleCreateMode(false)
},
isAndroid
)
}
var isHovered by remember { mutableStateOf(false) } var isHovered by remember { mutableStateOf(false) }
val offsetX by animateDpAsState(targetValue = if (isHovered || isSearchActive) 0.dp else 15.dp) val offsetX by animateDpAsState(targetValue = if (isHovered || isSearchActive) 0.dp else 15.dp)
AnimatedVisibility( AnimatedVisibility(
visible = !isEditMode && !isQrVisible, visible = !isEditMode && !isQrVisible && !createMode,
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(top = 15.dp) .padding(top = 15.dp)

View file

@ -0,0 +1,382 @@
package mg.dot.feufaro.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import mg.dot.feufaro.solfa.Transpose
import mg.dot.feufaro.viewmodel.PartitionMetadata
import mg.dot.feufaro.viewmodel.measureOptions
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewPartition(
onCreate: (PartitionMetadata) -> Unit,
onDismissRequest: () -> Unit,
isAndroid: Boolean
) {
val CardBg = MaterialTheme.colorScheme.background
val FieldBg = MaterialTheme.colorScheme.surface
val BorderColor = MaterialTheme.colorScheme.outline
val TextPrimary = MaterialTheme.colorScheme.onSurface
val TextSecondary = MaterialTheme.colorScheme.onSurfaceVariant
val AccentBlue = MaterialTheme.colorScheme.primary
val IconBg = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
val ButtonBg = MaterialTheme.colorScheme.primary.copy(alpha = 0.18f)
var title by remember { mutableStateOf("") }
var author by remember { mutableStateOf("") }
var composer by remember { mutableStateOf("") }
var rhythm by remember { mutableStateOf("") }
var measure by remember { mutableStateOf("4/4") }
var key by remember { mutableStateOf("C") }
var url by remember { mutableStateOf("https://") }
var keyMenuExpanded by remember { mutableStateOf(false) }
var langMenuExpanded by remember { mutableStateOf(false) }
val availableKeys = remember { Transpose.keyToNumber.toList() }
var lang by remember { if(isAndroid) mutableStateOf("🇲🇬") else mutableStateOf("Malagasy") }
val availableLang = remember { if(isAndroid) listOf("\uD83C\uDDFA\uD83C\uDDF8", "🇲🇬") else listOf("Anglais", "Malagasy") }
val fieldColors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = AccentBlue,
unfocusedBorderColor = BorderColor,
focusedTextColor = TextPrimary,
unfocusedTextColor = TextPrimary,
focusedLabelColor = TextSecondary,
unfocusedLabelColor = TextSecondary,
cursorColor = AccentBlue,
focusedContainerColor = FieldBg,
unfocusedContainerColor = FieldBg,
focusedPlaceholderColor = TextSecondary,
unfocusedPlaceholderColor = TextSecondary,
)
var measureMenuExpanded by remember { mutableStateOf(false) }
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
usePlatformDefaultWidth = false
)
) {
Card(
colors = CardDefaults.cardColors(containerColor = CardBg),
modifier = Modifier
.fillMaxWidth(if(isAndroid) 0.9f else 0.55f)
.pointerInput(Unit) {
detectTapGestures { }
},
shape = RoundedCornerShape(24.dp),
border = BorderStroke(1.dp, BorderColor.copy(alpha = 0.2f)),
elevation = CardDefaults.cardElevation(defaultElevation = 16.dp)
) {
Column(
modifier = Modifier
.padding(20.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(48.dp)
.background(IconBg, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.MusicNote,
contentDescription = null,
tint = AccentBlue
)
}
Spacer(Modifier.width(12.dp))
Column {
Text(
"Nouvelle partition",
color = TextPrimary,
style = MaterialTheme.typography.titleMedium
)
Text(
"Renseigner les informations de la partition",
color = TextSecondary,
style = MaterialTheme.typography.bodySmall
)
}
}
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Titre") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = fieldColors,
textStyle = MaterialTheme.typography.titleMedium
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(
value = author,
onValueChange = { author = it },
label = { Text("Auteur") },
singleLine = true,
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(12.dp),
colors = fieldColors,
textStyle = MaterialTheme.typography.titleMedium
)
OutlinedTextField(
value = composer,
onValueChange = { composer = it },
label = { Text("Compositeur / arrangeur") },
singleLine = true,
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(12.dp),
colors = fieldColors,
textStyle = MaterialTheme.typography.titleMedium
)
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
ExposedDropdownMenuBox(
expanded = keyMenuExpanded,
onExpandedChange = { keyMenuExpanded = it },
modifier = Modifier.weight(1f)
) {
OutlinedTextField(
value = key,
onValueChange = {},
readOnly = true,
label = { Text("Tonalité") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = keyMenuExpanded)
},
modifier = Modifier
.menuAnchor(MenuAnchorType.PrimaryEditable)
.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = fieldColors
)
ExposedDropdownMenu(
expanded = keyMenuExpanded,
onDismissRequest = { keyMenuExpanded = false },
modifier = Modifier.background(CardBg)
) {
availableKeys.forEach { kKey ->
DropdownMenuItem(
text = { Text(kKey, color = TextPrimary) },
onClick = {
key = kKey
keyMenuExpanded = false
}
)
}
}
}
OutlinedTextField(
value = rhythm,
onValueChange = { rhythm = it },
label = { Text("Mesure poétique") },
placeholder = { Text("8.7.8.7.D") },
singleLine = true,
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(12.dp),
colors = fieldColors,
textStyle = MaterialTheme.typography.titleMedium
)
ExposedDropdownMenuBox(
expanded = langMenuExpanded,
onExpandedChange = { langMenuExpanded = it },
modifier = Modifier.weight(1f)
) {
OutlinedTextField(
value = lang,
onValueChange = {},
readOnly = true,
label = { Text("Langue des paroles") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = langMenuExpanded)
},
modifier = Modifier
.menuAnchor(MenuAnchorType.PrimaryEditable)
.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = fieldColors
)
ExposedDropdownMenu(
expanded = langMenuExpanded,
onDismissRequest = { langMenuExpanded = false },
modifier = Modifier.background(CardBg)
) {
availableLang.forEach { klang ->
DropdownMenuItem(
text = { Text(klang, color = TextPrimary) },
onClick = {
lang = klang
langMenuExpanded = false
}
)
}
}
}
}
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
"Mesure",
color = TextSecondary,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 4.dp)
)
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
measureOptions.forEach { (mVal, mStyle) ->
val isSelected = (measure == mVal)
val tooltipState = rememberTooltipState()
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text("$mStyle")
}
},
state = tooltipState
) {
Surface(
selected = isSelected,
onClick = { measure = mVal },
modifier = Modifier
.width(70.dp)
.height(48.dp),
shape = RoundedCornerShape(12.dp),
color = if (isSelected) AccentBlue.copy(alpha = 0.12f) else FieldBg,
border = BorderStroke(
width = if (isSelected) 1.5.dp else 1.dp,
color = if (isSelected) AccentBlue else BorderColor
)
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = mVal,
color = if (isSelected) AccentBlue else TextPrimary,
style = MaterialTheme.typography.bodyLarge,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
)
RadioButton(
selected = isSelected,
onClick = null,
colors = RadioButtonDefaults.colors(
selectedColor = AccentBlue,
unselectedColor = TextSecondary
),
modifier = Modifier.size(20.dp)
)
}
}
}
}
}
}
OutlinedTextField(
value = url,
onValueChange = { url = it },
label = { Text("Source (URL)") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = fieldColors,
textStyle = MaterialTheme.typography.titleMedium
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
OutlinedButton(
onClick = onDismissRequest,
modifier = Modifier.weight(1f),
border = BorderStroke(0.5.dp, BorderColor),
colors = ButtonDefaults.outlinedButtonColors(contentColor = TextSecondary),
shape = RoundedCornerShape(10.dp)
) {
Text("Annuler")
}
Button(
onClick = {
onCreate(
PartitionMetadata(
title = title,
author = author,
composer = composer,
rhythm = rhythm,
measure = measure,
key = key,
url = url,
language = if(lang == "\uD83C\uDDF2\uD83C\uDDEC") "Malagasy" else "Anglais"
)
)
},
enabled = title.isNotBlank(),
modifier = Modifier.weight(2f),
colors = ButtonDefaults.buttonColors(
containerColor = ButtonBg,
contentColor = AccentBlue,
disabledContainerColor = ButtonBg.copy(alpha = 0.4f),
disabledContentColor = AccentBlue.copy(alpha = 0.4f),
),
shape = RoundedCornerShape(10.dp)
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(6.dp))
Text("Créer la partition")
}
}
}
}
}
}

View file

@ -3,6 +3,7 @@ package mg.dot.feufaro.ui
import SharedScreenModel import SharedScreenModel
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -13,14 +14,17 @@ 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.ListAlt import androidx.compose.material.icons.automirrored.filled.ListAlt
import androidx.compose.material.icons.automirrored.filled.NoteAdd
import androidx.compose.material.icons.automirrored.filled.PlaylistPlay import androidx.compose.material.icons.automirrored.filled.PlaylistPlay
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.*
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.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
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
@ -97,16 +101,29 @@ fun SimpleDrawerContent(
label = "Analyse d\'accords" label = "Analyse d\'accords"
) )
Column( Column(
modifier = Modifier.padding(5.dp), horizontalAlignment = Alignment.CenterHorizontally,
horizontalAlignment = Alignment.CenterHorizontally verticalArrangement = Arrangement.Center
) { ) {
IconButton( OutlinedButton(
onClick = { }, onClick = { sharedScreenModel.toggleCreateMode(true) },
modifier = Modifier.background(Color(0xFF81C784), shape = CircleShape).size(40.dp) shape = IconButtonDefaults.filledShape,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground
)
) { ) {
Icon(Icons.Default.Add, contentDescription = null, tint = Color.White) Icon(
imageVector = Icons.AutoMirrored.Default.NoteAdd, contentDescription = null, tint = MaterialTheme.colorScheme.error
)
} }
Text("Ajouter", style = MaterialTheme.typography.labelSmall)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Créer une partition",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} }
} }
} }

View file

@ -0,0 +1,22 @@
package mg.dot.feufaro.viewmodel
data class PartitionMetadata(
val title: String,
val author: String,
val composer: String,
val rhythm: String,
val measure: String,
val url: String,
val key: String,
val language: String,
)
val measureOptions = listOf(
Pair("2/4", "Marches, polkas, chants folkloriques"),
Pair("4/4", "Gospel, louange, hymnes, jazz, classique"),
Pair("3/4", "Valse, hymnes, chorals, classique"),
Pair("6/8", "Ballades, gospel, traditionnelle"),
Pair("9/4", "Œuvres spéciales, contemporain"),
Pair("2/2", "Chorals, hymnes, musique sacrée"),
Pair("3/2", "Hymnes classiques, chorals lents"),
Pair("12/8", "Blues, gospel, soul, slow")
)

View file

@ -231,8 +231,8 @@ 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) private val _sourceModeState = mutableStateOf(false)
val editModeState: State<Boolean> = _editModeState val sourceModeState: State<Boolean> = _sourceModeState
data class HairPinEditState( data class HairPinEditState(
val isPending: Boolean = false, val isPending: Boolean = false,
@ -251,6 +251,13 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_modeEditor.value = enabled _modeEditor.value = enabled
} }
private val _createMode = MutableStateFlow(false)
val createMode: StateFlow<Boolean> = _createMode
fun toggleCreateMode(state: Boolean) {
_createMode.value = state
}
private val _harmonyView = MutableStateFlow(false) private val _harmonyView = MutableStateFlow(false)
val harmonyView: StateFlow<Boolean> = _harmonyView val harmonyView: StateFlow<Boolean> = _harmonyView
fun toggleHarmonyMode(enabled: Boolean) { fun toggleHarmonyMode(enabled: Boolean) {
@ -304,8 +311,8 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
fun toggleQRCodeVisibility() { fun toggleQRCodeVisibility() {
_isQRCodeVisible.value = !_isQRCodeVisible.value _isQRCodeVisible.value = !_isQRCodeVisible.value
} }
fun toggleEditMode() { fun toggleSourceMode() {
_editModeState.value = !_editModeState.value _sourceModeState.value = !_sourceModeState.value
} }
val qrCodeContent: State<String?> val qrCodeContent: State<String?>
get() = mutableStateOf( get() = mutableStateOf(
@ -604,7 +611,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 _sourceModeState.value = false
setTranspositionInterval(0) setTranspositionInterval(0)
try { try {
val midiFileName = fileRepository.getFileName(newMidiFile) val midiFileName = fileRepository.getFileName(newMidiFile)
@ -705,7 +712,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 _sourceModeState.value = false
_harmonyView.value = false _harmonyView.value = false
setTranspositionInterval(0) setTranspositionInterval(0)
setSilentDurBefr(0) setSilentDurBefr(0)

View file

@ -50,4 +50,9 @@ class SolfaScreenModel(
solfa.saveTUOEdit(editState) solfa.saveTUOEdit(editState)
} }
} }
fun newSolfa(metadata: PartitionMetadata) {
screenModelScope.launch(Dispatchers.IO) {
solfa.createNewSolfa(metadata)
}
}
} }