Implement solfa partition creation screen & options
This commit is contained in:
parent
1bbf91e29c
commit
bd936fe3b8
11 changed files with 611 additions and 97 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
@ -767,22 +771,22 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
} else {
|
||||
ut
|
||||
}
|
||||
// println("j'aoute $processedToken")
|
||||
// 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -50,4 +50,9 @@ class SolfaScreenModel(
|
|||
solfa.saveTUOEdit(editState)
|
||||
}
|
||||
}
|
||||
fun newSolfa(metadata: PartitionMetadata) {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
solfa.createNewSolfa(metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue