diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt index 0bfedb6..5d3739b 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt index c5a3385..16cceb5 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt @@ -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 ) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt index b3a3417..b39fd0a 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt @@ -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 } @@ -760,26 +761,29 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository // println("Suppression de la note $ut (gérée par le saut du fragment d'origine)") } } else { - val processedToken = if (ut.matches(Regex("[drmfsltDRFSTzw].*"))) { - val (fileBase, fileLvl) = parseNoteAndOctave(allTokens[i]) - val (uBase, _) = parseNoteAndOctave(ut) - uBase + formatOctave(fileLvl + octaveDiff) + if (ut == "(" || ut == ")") { + resultTokens.add(ut) // Insertion directe de la parenthèse sans incrémenter currentLogicalIdx } else { - ut - } -// println("j'aoute $processedToken") + val processedToken = if (ut.matches(Regex("[drmfsltDRFSTzw].*"))) { + val (fileBase, fileLvl) = parseNoteAndOctave(allTokens[i]) + val (uBase, _) = parseNoteAndOctave(ut) + uBase + formatOctave(fileLvl + octaveDiff) + } else { + ut + } + // println("j'aoute $processedToken") - resultTokens.add(processedToken) - - if (processedToken.matches(Regex("[drmfsltDRFSTzw].*|[-―]"))) { -// println( -// String.format( -// "NEW | %-10s | INSERT | %-12d | REPLACE", -// processedToken, -// currentLogicalIdx -// ) -// ) - currentLogicalIdx++ + resultTokens.add(processedToken) + if (processedToken.matches(Regex("[drmfsltDRFSTzw].*|[-―]"))) { + /*println( + String.format( + "NEW | %-10s | INSERT | %-12d | REPLACE", + processedToken, + currentLogicalIdx + ) + )*/ + currentLogicalIdx++ + } } } } @@ -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 { @@ -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}") + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt index 6f65539..90d73fe 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt @@ -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") } } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index 4230825..065f9ec 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -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( @@ -1170,6 +1186,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() } ) } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt index bd6f501..93dd620 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/NewPartition.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/NewPartition.kt new file mode 100644 index 0000000..2bb0835 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/NewPartition.kt @@ -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") + } + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt index dc81696..86dab1a 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt @@ -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 + ) } } } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/PartitionMetadata.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/PartitionMetadata.kt new file mode 100644 index 0000000..c8881ee --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/PartitionMetadata.kt @@ -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") +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index 74d7a9a..729eb92 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -231,8 +231,8 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode private val _isQRCodeVisible = mutableStateOf(false) val isQRCodeVisible: State = _isQRCodeVisible - private val _editModeState = mutableStateOf(false) - val editModeState: State = _editModeState + private val _sourceModeState = mutableStateOf(false) + val sourceModeState: State = _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 = _createMode + + fun toggleCreateMode(state: Boolean) { + _createMode.value = state + } + private val _harmonyView = MutableStateFlow(false) val harmonyView: StateFlow = _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 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) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt index 90d86a2..9c5df97 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt @@ -50,4 +50,9 @@ class SolfaScreenModel( solfa.saveTUOEdit(editState) } } + fun newSolfa(metadata: PartitionMetadata) { + screenModelScope.launch(Dispatchers.IO) { + solfa.createNewSolfa(metadata) + } + } } \ No newline at end of file