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

View file

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

View file

@ -8,6 +8,7 @@ import mg.dot.feufaro.getGlobalTemplate
import mg.dot.feufaro.launchFilePicker
import mg.dot.feufaro.midi.MidiPitch
import mg.dot.feufaro.midi.MidiWriterKotlin
import mg.dot.feufaro.viewmodel.PartitionMetadata
import mg.dot.feufaro.transformLyricsInput
import java.io.File
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 */
val hasDashInTemplate = editState.templateFragment.contains("-")
val newUserTokens = if (hasDashInTemplate) {
rawUserTokens.filter { it.matches(Regex("[drmfsltDRFSTzw].*")) }
rawUserTokens.filter { it.matches(Regex("[drmfsltDRFSTzw\\(\\)].*")) }
} else {
rawUserTokens
}
@ -759,6 +760,9 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
} else {
// 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 {
val processedToken = if (ut.matches(Regex("[drmfsltDRFSTzw].*"))) {
val (fileBase, fileLvl) = parseNoteAndOctave(allTokens[i])
@ -770,19 +774,19 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
// println("j'aoute $processedToken")
resultTokens.add(processedToken)
if (processedToken.matches(Regex("[drmfsltDRFSTzw].*|[-―]"))) {
// println(
// String.format(
// "NEW | %-10s | INSERT | %-12d | REPLACE",
// processedToken,
// currentLogicalIdx
// )
// )
/*println(
String.format(
"NEW | %-10s | INSERT | %-12d | REPLACE",
processedToken,
currentLogicalIdx
)
)*/
currentLogicalIdx++
}
}
}
}
var skipped = 0
while (skipped < dynamicDeleteCount && i < allTokens.size) {
@ -1281,8 +1285,8 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
.replace("si", "S")
.replace("ta", "T")
.replace("(","")
.replace(")","")
/*.replace("(","")
.replace(")","")*/
}
private fun parseNoteLineToArray(noteLine: String): List<String> {
@ -2607,4 +2611,77 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
}
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
.replace(Solfa.REGEX_NOTE_TOADD, "$3$4")
.replace("<", "")
val tpl = notesToTemplate(filteredNotes)
val tpl = notesToTemplate(filteredNotes).replace(Regex("[()]"), "")
// println("Voix $voiceIdx | Notes: '$filteredNotes' -> Template: '$tpl'")
tpl
}
val rythmicTemplates = voiceTemplates.map { tpl -> tpl.replace(Regex("[()]"), "") }
val rythmicTemplates = voiceTemplates
val maxLen = rythmicTemplates.maxOfOrNull { it.length } ?: 1
// 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()
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 previousChars = if(i>0) rythmicTemplates.mapNotNull { it.getOrNull(i-1) }.toSet() else "".toSet()
val activeChars = rythmicTemplates.mapNotNull { it.getOrNull(i) }
@ -1116,14 +1077,8 @@ public fun getGlobalTemplate(editState: TUOEditState): String {
result.append(combinedChar)
}
// 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("----------------------------------\n")
return result.toString()
@ -1149,8 +1104,9 @@ private fun notesToTemplate(notes: String): String {
for (match in matches) {
val value = match.value
when {
value.matches(Regex("[.,()-]")) -> template.append(value)
value.matches(Regex("[.,-]")) -> template.append(value)
value.trim().isEmpty() -> template.append(" ")
value.matches(Regex("[()]")) -> template.append("")
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.material.icons.Icons
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.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.*
@ -58,6 +56,7 @@ import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.TUODetailDialog
import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.getPathValidator
import mg.dot.feufaro.getPlatform
import mg.dot.feufaro.ui.rememberFileSaveLauncher
import mg.dot.feufaro.viewmodel.MidiMarkers
import mg.dot.feufaro.viewmodel.SolfaScreenModel
@ -567,6 +566,8 @@ fun AutoResizingText(
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LazyVerticalGridTUO(
viewModel: GridTUOData,
@ -605,6 +606,9 @@ fun LazyVerticalGridTUO(
var toggleWithDegreeAndChord by remember { mutableStateOf (false) }
val focusManager = LocalFocusManager.current
val platform = getPlatform()
val isAndroid = platform.name.startsWith("Android")
Column(
modifier = Modifier
.fillMaxWidth()
@ -956,9 +960,10 @@ fun LazyVerticalGridTUO(
val isAbsoluteLastGrid = (globalIndex == tuoList.size - 2)
if (editMode && isAbsoluteLastGrid) {
val lastGridwidth = if(isAndroid) (gridWidthDp / gridColumnCount)*2 else ((gridWidthDp / gridColumnCount)*11)/10
Box(
modifier = Modifier
.width(gridWidthDp / gridColumnCount)
.width(lastGridwidth)
.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
@ -987,7 +992,18 @@ fun LazyVerticalGridTUO(
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) {
val content : @Composable () -> Unit = {
Box(modifier = Modifier
.width(gridWidthDp / gridColumnCount)
.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,
solfaScreenModel: SolfaScreenModel
) {
var isEditorVisible = sharedScreenModel.editModeState.value
var isEditorVisible = sharedScreenModel.sourceModeState.value
var sourcePath = sharedScreenModel.activeFilePath.value
var sourceContent = sharedScreenModel.fileContent.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 androidx.compose.animation.*
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
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.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
@ -127,8 +124,9 @@ fun MainScreenWithDrawer(
}
}
val isEditMode by sharedScreenModel.modeEditor.collectAsState()
LaunchedEffect(isEditMode) {
if(isEditMode) {
val createMode by sharedScreenModel.createMode.collectAsState()
LaunchedEffect(isEditMode || createMode) {
if(isEditMode || createMode) {
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) }
val offsetX by animateDpAsState(targetValue = if (isHovered || isSearchActive) 0.dp else 15.dp)
AnimatedVisibility(
visible = !isEditMode && !isQrVisible,
visible = !isEditMode && !isQrVisible && !createMode,
modifier = Modifier
.align(Alignment.TopEnd)
.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 androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.material.icons.Icons
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.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
@ -97,16 +101,29 @@ fun SimpleDrawerContent(
label = "Analyse d\'accords"
)
Column(
modifier = Modifier.padding(5.dp),
horizontalAlignment = Alignment.CenterHorizontally
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
IconButton(
onClick = { },
modifier = Modifier.background(Color(0xFF81C784), shape = CircleShape).size(40.dp)
OutlinedButton(
onClick = { sharedScreenModel.toggleCreateMode(true) },
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)
val isQRCodeVisible: State<Boolean> = _isQRCodeVisible
private val _editModeState = mutableStateOf(false)
val editModeState: State<Boolean> = _editModeState
private val _sourceModeState = mutableStateOf(false)
val sourceModeState: State<Boolean> = _sourceModeState
data class HairPinEditState(
val isPending: Boolean = false,
@ -251,6 +251,13 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_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)
val harmonyView: StateFlow<Boolean> = _harmonyView
fun toggleHarmonyMode(enabled: Boolean) {
@ -304,8 +311,8 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
fun toggleQRCodeVisibility() {
_isQRCodeVisible.value = !_isQRCodeVisible.value
}
fun toggleEditMode() {
_editModeState.value = !_editModeState.value
fun toggleSourceMode() {
_sourceModeState.value = !_sourceModeState.value
}
val qrCodeContent: State<String?>
get() = mutableStateOf(
@ -604,7 +611,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_currentPos.value = 0f
_dcDone.value = false
_dsDone.value = false
_editModeState.value = false
_sourceModeState.value = false
setTranspositionInterval(0)
try {
val midiFileName = fileRepository.getFileName(newMidiFile)
@ -705,7 +712,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_showMidiCtrl.value = false
_expandedFAB.value = false
_showSearchMenu.value = false
_editModeState.value = false
_sourceModeState.value = false
_harmonyView.value = false
setTranspositionInterval(0)
setSilentDurBefr(0)

View file

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