Add custom keyboards for search & edit notes
This commit is contained in:
parent
9da97209c9
commit
340f72855f
8 changed files with 1613 additions and 835 deletions
|
|
@ -15,12 +15,14 @@ import androidx.compose.material.icons.filled.RemoveCircleOutline
|
|||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
|
||||
|
|
@ -141,6 +143,7 @@ private val markerGroups = listOf(
|
|||
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun MarkerPopup(
|
||||
menuPosition: IntOffset,
|
||||
onDismiss: () -> Unit,
|
||||
|
|
@ -155,7 +158,8 @@ fun MarkerPopup(
|
|||
properties = PopupProperties(
|
||||
focusable = true,
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true
|
||||
dismissOnClickOutside = true,
|
||||
usePlatformDefaultWidth = false
|
||||
)
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
package mg.dot.feufaro
|
||||
|
||||
import SharedScreenModel
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
|
|
@ -23,6 +20,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInParent
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
|
|
@ -37,7 +35,7 @@ import mg.dot.feufaro.solfa.MarkerPopup
|
|||
import mg.dot.feufaro.solfa.Solfa
|
||||
import mg.dot.feufaro.solfa.TUOEditState
|
||||
import mg.dot.feufaro.solfa.TimeUnitObject
|
||||
import mg.dot.feufaro.solfa.extractAnchorFromNoteLine
|
||||
import mg.dot.feufaro.ui.LocalSolfaKeyboardState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -61,6 +59,18 @@ fun TUODetailDialog(
|
|||
putAll(editState.notesByVoice)
|
||||
}
|
||||
}
|
||||
val isAndroid = remember { getPlatform().name.startsWith("Android") }
|
||||
val keyboardState = LocalSolfaKeyboardState.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
var selectedVoiceIndex by remember { mutableStateOf(0) }
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
keyboardState.isDialogActive = true
|
||||
onDispose {
|
||||
keyboardState.hide()
|
||||
keyboardState.isDialogActive = false
|
||||
}
|
||||
}
|
||||
val originalNotes = mutableStateMapOf<Int, String>().apply {
|
||||
putAll(editState.notesByVoice)
|
||||
}
|
||||
|
|
@ -114,20 +124,103 @@ fun TUODetailDialog(
|
|||
var markerButtonPos by remember {
|
||||
mutableStateOf(IntOffset.Zero)
|
||||
}
|
||||
LaunchedEffect(selectedVoiceIndex, notes[selectedVoiceIndex]) {
|
||||
if (keyboardState.isVisible) {
|
||||
val currentRawText = notes[selectedVoiceIndex] ?: ""
|
||||
keyboardState.currentText = transformMusicalInput(currentRawText)
|
||||
}
|
||||
}
|
||||
|
||||
fun attachKeyboardToVoice(voiceIndex: Int) {
|
||||
selectedVoiceIndex = voiceIndex
|
||||
if(isAndroid) {
|
||||
keyboardController?.hide()
|
||||
|
||||
val rawInitialText = notes[voiceIndex] ?: ""
|
||||
val formattedInitialText = transformMusicalInput(rawInitialText)
|
||||
keyboardState.currentText = formattedInitialText
|
||||
|
||||
keyboardState.show(
|
||||
initialVoice = voiceIndex,
|
||||
initialValue = formattedInitialText,
|
||||
onNote = { noteToAdd ->
|
||||
val currentText = notes[voiceIndex] ?: ""
|
||||
val newText = currentText + noteToAdd
|
||||
val formattedText = transformMusicalInput(newText)
|
||||
notes[voiceIndex] = formattedText
|
||||
keyboardState.currentText = formattedText
|
||||
},
|
||||
onDelete = {
|
||||
val currentText = notes[voiceIndex] ?: ""
|
||||
if (currentText.isNotEmpty()) {
|
||||
val octaveRegex = Regex("['`,₁₂₃₄₅¹²³⁴⁵]+$")
|
||||
|
||||
val newText = if (octaveRegex.containsMatchIn(currentText)) {
|
||||
currentText.replace(octaveRegex, "")
|
||||
} else {
|
||||
currentText.dropLast(1)
|
||||
}
|
||||
|
||||
notes[voiceIndex] = transformMusicalInput(newText)
|
||||
keyboardState.currentText = transformMusicalInput(newText)
|
||||
}
|
||||
},
|
||||
onSymbol = { symbol ->
|
||||
val currentText = notes[voiceIndex] ?: ""
|
||||
val newText = currentText + symbol
|
||||
notes[voiceIndex] = transformMusicalInput(newText)
|
||||
keyboardState.currentText = transformMusicalInput(newText)
|
||||
},
|
||||
onClose = {
|
||||
onDismiss()
|
||||
},
|
||||
onBuild = {
|
||||
val finalLyricsMap = editState.lyricsByStanza.toMutableMap()
|
||||
finalLyricsMap.putAll(editedLyricsMap)
|
||||
if (!finalLyricsMap.containsKey(currentStanza)) {
|
||||
finalLyricsMap[currentStanza] = lyricsLines.getOrNull(0)?.trim() ?: ""
|
||||
}
|
||||
val state = TUOEditState(
|
||||
tuoIndex = globalIndex,
|
||||
notesByVoice = notes.toMap(),
|
||||
originalNotes = originalNotes.toMap(),
|
||||
originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
|
||||
lyricsByStanza = finalLyricsMap,
|
||||
templateFragment = templateFragment,
|
||||
marker = if(marker.isEmpty()) "_" else marker,
|
||||
originalSep = initialSep,
|
||||
sep = newSep
|
||||
)
|
||||
onSave(state)
|
||||
},
|
||||
onTab = {
|
||||
val nextVoice = (selectedVoiceIndex + 1) % 4
|
||||
attachKeyboardToVoice(nextVoice)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Popup(
|
||||
offset = menuPosition,
|
||||
onDismissRequest = onDismiss,
|
||||
onDismissRequest = {
|
||||
if(!isAndroid && !keyboardState.isVisible) {
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
properties = PopupProperties(
|
||||
focusable = true,
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true
|
||||
focusable = if(isAndroid) false else true,
|
||||
dismissOnBackPress = !isAndroid || !keyboardState.isVisible,
|
||||
dismissOnClickOutside = !isAndroid || !keyboardState.isVisible
|
||||
),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.widthIn(max=125.dp)
|
||||
.heightIn(max = 400.dp),
|
||||
.heightIn(max = 400.dp)
|
||||
.imePadding()
|
||||
.navigationBarsPadding(),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f),
|
||||
) {
|
||||
|
|
@ -500,48 +593,76 @@ fun TUODetailDialog(
|
|||
val validation = validateMusicalInput(currentNote, templateFragment)
|
||||
val voiceAnchor = anchorsByVoice[voice]?.replace("#","")
|
||||
val hasAnchor = !voiceAnchor.isNullOrBlank()
|
||||
val isSelected = selectedVoiceIndex == voice && keyboardState.isVisible
|
||||
|
||||
Box(modifier = Modifier.wrapContentSize()
|
||||
Box(modifier = Modifier
|
||||
//.wrapContentSize()
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
MyTextEditField(
|
||||
value = currentNote ?: "",
|
||||
customFontSize = 14.sp,
|
||||
color = Color.White,
|
||||
customPadding = 8.dp,
|
||||
customBrush = SolidColor(Color.White),
|
||||
isEditable = isEditable,
|
||||
isAddable = canAdd,
|
||||
isWarn = !validation.isValid,
|
||||
funTransform = ::transformMusicalInput,
|
||||
onValueChng = { newValue ->
|
||||
notes[voice] = newValue
|
||||
}
|
||||
)
|
||||
if (hasAnchor) {
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
if(!isAndroid) {
|
||||
MyTextEditField(
|
||||
value = currentNote ?: "",
|
||||
customFontSize = 14.sp,
|
||||
color = Color.White,
|
||||
customPadding = 8.dp,
|
||||
customBrush = SolidColor(Color.White),
|
||||
isEditable = isEditable,
|
||||
isAddable = canAdd,
|
||||
isWarn = !validation.isValid,
|
||||
funTransform = ::transformMusicalInput,
|
||||
onValueChng = { newValue ->
|
||||
notes[voice] = newValue
|
||||
}
|
||||
)
|
||||
if (hasAnchor) {
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) {
|
||||
Text("Ancrée sur ${transformMusicalInput(voiceAnchor)}", fontSize = 11.sp)
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState()
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(3.dp),
|
||||
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.8f),
|
||||
modifier = Modifier.padding(start = 2.dp)
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) {
|
||||
Text("Ancrée sur ${transformMusicalInput(voiceAnchor)}", fontSize = 11.sp)
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState()
|
||||
) {
|
||||
Text(
|
||||
text = "⚓",
|
||||
fontSize = 10.sp,
|
||||
color = Color.Black,
|
||||
modifier = Modifier.padding(horizontal = 2.dp, vertical = 1.dp)
|
||||
)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(3.dp),
|
||||
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.8f),
|
||||
modifier = Modifier.padding(start = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "⚓",
|
||||
fontSize = 10.sp,
|
||||
color = Color.Black,
|
||||
modifier = Modifier.padding(horizontal = 2.dp, vertical = 1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SolfaTextEditField(
|
||||
value = currentNote ?: "",
|
||||
customFontSize = 14.sp,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.tertiary else Color.White,
|
||||
customPadding = 8.dp,
|
||||
customBrush = SolidColor(Color.White),
|
||||
isWarn = !validation.isValid,
|
||||
readOnly = isAndroid,
|
||||
funTransform = ::transformMusicalInput,
|
||||
onValueChng = { newValue ->
|
||||
notes[voice] = newValue
|
||||
}
|
||||
)
|
||||
}
|
||||
if(isAndroid) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.clickable {
|
||||
attachKeyboardToVoice(voice)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -549,124 +670,136 @@ fun TUODetailDialog(
|
|||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// --- SECTION LYRICS ---
|
||||
lyricsLines.forEachIndexed { index, line ->
|
||||
val displayedLine = line
|
||||
LaunchedEffect(displayedLine) {
|
||||
if (displayedLine != line) {
|
||||
lyricsLines[index] = displayedLine
|
||||
val updatedLyricsMap = editState.lyricsByStanza.toMutableMap()
|
||||
updatedLyricsMap[index + 1] = displayedLine
|
||||
if(!isAndroid) {
|
||||
// --- SECTION LYRICS ---
|
||||
lyricsLines.forEachIndexed { index, line ->
|
||||
val displayedLine = line
|
||||
LaunchedEffect(displayedLine) {
|
||||
if (displayedLine != line) {
|
||||
lyricsLines[index] = displayedLine
|
||||
val updatedLyricsMap = editState.lyricsByStanza.toMutableMap()
|
||||
updatedLyricsMap[index + 1] = displayedLine
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(0.8f)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 2.dp)
|
||||
) {
|
||||
|
||||
val isLyricsValid = validateLyricsInput(line, templateFragment)
|
||||
val tooltipState2 = rememberTooltipState(isPersistent = false)
|
||||
Row(
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(0.8f)
|
||||
) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
if (!isLyricsValid) {
|
||||
PlainTooltip(
|
||||
containerColor = Color(0XFF34EB71),
|
||||
contentColor = Color.White
|
||||
) {
|
||||
Text("Veuillez suivre ce format: $templateFragment")
|
||||
}
|
||||
}
|
||||
},
|
||||
state = tooltipState2
|
||||
) {
|
||||
MyTextEditField(
|
||||
value = displayedLine,
|
||||
customFontSize = 13.sp,
|
||||
color = Color.White,
|
||||
customPadding = 8.dp,
|
||||
customBrush = SolidColor(Color.White),
|
||||
isEditable = isEditable,
|
||||
isAddable = canAdd,
|
||||
isWarn = !isLyricsValid,
|
||||
funTransform = { input -> transformLyricsInput(input, templateFragment) },
|
||||
onValueChng = { newValue ->
|
||||
val trimmedValue = newValue.trimStart()
|
||||
val dataToSave = if (trimmedValue.endsWith(" ")) trimmedValue else "$trimmedValue "
|
||||
lyricsLines[index] = trimmedValue
|
||||
val updatedLyricsMap = editState.lyricsByStanza.toMutableMap()
|
||||
editedLyricsMap[index + 1] = dataToSave
|
||||
|
||||
val updatedState = editState.copy(lyricsByStanza = updatedLyricsMap)
|
||||
val isLyricsValid = validateLyricsInput(line, templateFragment)
|
||||
val tooltipState2 = rememberTooltipState(isPersistent = false)
|
||||
Row(
|
||||
) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
if (!isLyricsValid) {
|
||||
PlainTooltip(
|
||||
containerColor = Color(0XFF34EB71),
|
||||
contentColor = Color.White
|
||||
) {
|
||||
Text("Veuillez suivre ce format: $templateFragment")
|
||||
}
|
||||
}
|
||||
},
|
||||
state = tooltipState2
|
||||
) {
|
||||
MyTextEditField(
|
||||
value = displayedLine,
|
||||
customFontSize = 13.sp,
|
||||
color = Color.White,
|
||||
customPadding = 8.dp,
|
||||
customBrush = SolidColor(Color.White),
|
||||
isEditable = isEditable,
|
||||
isAddable = canAdd,
|
||||
isWarn = !isLyricsValid,
|
||||
funTransform = { input ->
|
||||
transformLyricsInput(
|
||||
input,
|
||||
templateFragment
|
||||
)
|
||||
},
|
||||
onValueChng = { newValue ->
|
||||
val trimmedValue = newValue.trimStart()
|
||||
val dataToSave =
|
||||
if (trimmedValue.endsWith(" ")) trimmedValue else "$trimmedValue "
|
||||
lyricsLines[index] = trimmedValue
|
||||
val updatedLyricsMap = editState.lyricsByStanza.toMutableMap()
|
||||
editedLyricsMap[index + 1] = dataToSave
|
||||
|
||||
val updatedState = editState.copy(lyricsByStanza = updatedLyricsMap)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isEditable || canAdd) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (index >= 0) {
|
||||
if (index == 0) lyricsLines.add("_") else lyricsLines.removeAt(index)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = if (index == 0) Icons.Default.Add else Icons.Default.Clear,
|
||||
tint = if (index == 0) MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) else MaterialTheme.colorScheme.tertiary.copy(
|
||||
alpha = 1.5f
|
||||
),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isEditable || canAdd) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (index >= 0) {
|
||||
if (index == 0) lyricsLines.add("_") else lyricsLines.removeAt(index)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = if (index == 0) Icons.Default.Add else Icons.Default.Clear,
|
||||
tint = if (index == 0) MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) else MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
IconButton(onClick = onDismiss, modifier = Modifier.size(20.dp)) {
|
||||
Icon(
|
||||
Icons.Default.Clear,
|
||||
contentDescription = null,
|
||||
tint = Color.LightGray
|
||||
)
|
||||
}
|
||||
if (isEditable || canAdd) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
val finalLyricsMap = editState.lyricsByStanza.toMutableMap()
|
||||
finalLyricsMap.putAll(editedLyricsMap)
|
||||
if (!finalLyricsMap.containsKey(currentStanza)) {
|
||||
finalLyricsMap[currentStanza] = lyricsLines.getOrNull(0)?.trim() ?: ""
|
||||
}
|
||||
val state = TUOEditState(
|
||||
tuoIndex = globalIndex,
|
||||
notesByVoice = notes.toMap(),
|
||||
originalNotes = originalNotes.toMap(),
|
||||
originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
|
||||
lyricsByStanza = finalLyricsMap,
|
||||
templateFragment = templateFragment,
|
||||
marker = if(marker.isEmpty()) "_" else marker,
|
||||
originalSep = initialSep,
|
||||
sep = newSep
|
||||
)
|
||||
onSave(state)
|
||||
},
|
||||
modifier = Modifier.size(22.dp)
|
||||
) {
|
||||
if(!isAndroid) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
IconButton(onClick = onDismiss, modifier = Modifier.size(20.dp)) {
|
||||
Icon(
|
||||
Icons.Default.Build,
|
||||
Icons.Default.Clear,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f)
|
||||
tint = Color.LightGray
|
||||
)
|
||||
}
|
||||
if (isEditable || canAdd) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
val finalLyricsMap = editState.lyricsByStanza.toMutableMap()
|
||||
finalLyricsMap.putAll(editedLyricsMap)
|
||||
if (!finalLyricsMap.containsKey(currentStanza)) {
|
||||
finalLyricsMap[currentStanza] = lyricsLines.getOrNull(0)?.trim() ?: ""
|
||||
}
|
||||
val state = TUOEditState(
|
||||
tuoIndex = globalIndex,
|
||||
notesByVoice = notes.toMap(),
|
||||
originalNotes = originalNotes.toMap(),
|
||||
originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
|
||||
lyricsByStanza = finalLyricsMap,
|
||||
templateFragment = templateFragment,
|
||||
marker = if(marker.isEmpty()) "_" else marker,
|
||||
originalSep = initialSep,
|
||||
sep = newSep
|
||||
)
|
||||
onSave(state)
|
||||
},
|
||||
modifier = Modifier.size(22.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Build,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -907,6 +1040,75 @@ fun MyTextEditField(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SolfaTextEditField(
|
||||
value: String,
|
||||
customFontSize: TextUnit,
|
||||
font: FontFamily? = FontFamily.SansSerif,
|
||||
color: Color,
|
||||
customPadding: Dp,
|
||||
customBrush: Brush,
|
||||
isWarn: Boolean? = false,
|
||||
readOnly: Boolean,
|
||||
funTransform: ((String) -> String)? = null,
|
||||
onValueChng: (String) -> Unit
|
||||
) {
|
||||
var textFieldValueState by remember {
|
||||
mutableStateOf(TextFieldValue(text = value, selection = TextRange(value.length)))
|
||||
}
|
||||
|
||||
LaunchedEffect(value) {
|
||||
if (textFieldValueState.text != value) {
|
||||
val newSelection = if (textFieldValueState.selection.start <= value.length) {
|
||||
textFieldValueState.selection
|
||||
} else {
|
||||
TextRange(value.length)
|
||||
}
|
||||
textFieldValueState = TextFieldValue(text = value, selection = newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = textFieldValueState,
|
||||
onValueChange = { newValue ->
|
||||
println("ça change $newValue => ${textFieldValueState.text}")
|
||||
val transformedText = funTransform?.invoke(newValue.text) ?: newValue.text
|
||||
val finalSelection = if (transformedText.length != newValue.text.length) {
|
||||
if (newValue.selection.start <= transformedText.length) newValue.selection else TextRange(transformedText.length)
|
||||
} else {
|
||||
newValue.selection
|
||||
}
|
||||
|
||||
textFieldValueState = TextFieldValue(
|
||||
text = transformedText,
|
||||
selection = finalSelection
|
||||
)
|
||||
println("et ça trnasforme en = $transformedText")
|
||||
onValueChng(transformedText)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Ascii
|
||||
),
|
||||
textStyle = TextStyle(
|
||||
color = color,
|
||||
fontSize = customFontSize,
|
||||
fontFamily = font
|
||||
),
|
||||
readOnly = readOnly,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(4.dp))
|
||||
.border(
|
||||
if (isWarn!!) 1.dp else 0.dp,
|
||||
if (isWarn!!) MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f) else Color.Transparent,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.padding(customPadding),
|
||||
cursorBrush = customBrush
|
||||
)
|
||||
}
|
||||
|
||||
data class ValidNoteResult(
|
||||
val isValid: Boolean,
|
||||
val message: String
|
||||
|
|
|
|||
|
|
@ -265,6 +265,13 @@ class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, co
|
|||
}
|
||||
}
|
||||
|
||||
private val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
|
||||
private val musicForPtSerifBIRegex = Regex("""^(Largh\.|Grave|Largo|Lento|Adagio|And\.|Andantino|Mod\.|Moderato|Alleg\.|All\.|Viv\.|Vivacissimo|Presto|Prestiss\.|accel\.|rit\.|rall\.|riten\.|string\.|allarg\.|a tempo|Tempo I|rubato|meno mosso|più mosso|cres\.|<|decresc\.|dim\.|>|rfz|fp|pf|sub\.p|sub\.f)""",RegexOption.IGNORE_CASE)
|
||||
private val musicForEmmentRegex = Regex("""^(ppp|pp|mp|mf|fff|ff|p|f|sfz|sf|fz|rfz|fp|pf)\b""",RegexOption.IGNORE_CASE)
|
||||
private val blankPrefixRegex =Regex("""z([0-9A-Z])[:]""")
|
||||
private val noteRegex = Regex("([drmfsltDRFSTw][ia]?|―)[0-9'¹²³⁴₁₂₃₄,]*")
|
||||
private val inclusionRegex = Regex("^I([0-9]):(.*)")
|
||||
|
||||
@Composable
|
||||
fun TimeUnitComposable(
|
||||
tuo: TimeUnitObject,
|
||||
|
|
@ -273,17 +280,34 @@ fun TimeUnitComposable(
|
|||
gridActive: Boolean,
|
||||
isCurrentBlock: Boolean = false,
|
||||
parentFocusRequester: FocusRequester? = null,
|
||||
transpositionInterval: Int
|
||||
transpositionInterval: Int,
|
||||
hairpinRange: HairpinRange? = null
|
||||
) {
|
||||
val col = if (tuo.getNum() % 2 == 0) MaterialTheme.colorScheme.tertiary.copy(alpha = 0.05f) else MaterialTheme.colorScheme.error.copy(alpha = 0.05f)
|
||||
val currentDensity = LocalDensity.current
|
||||
|
||||
val animatedColor by animateColorAsState(
|
||||
targetValue = if (gridActive) MaterialTheme.colorScheme.secondary.copy(alpha = 1f) else col,
|
||||
animationSpec = tween(durationMillis = 100) // Très court pour rester réactif
|
||||
)
|
||||
val animatedColor = if (gridActive) {
|
||||
animateColorAsState(
|
||||
targetValue = MaterialTheme.colorScheme.secondary.copy(alpha = 1f),
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
).value
|
||||
} else {
|
||||
col
|
||||
}
|
||||
|
||||
val focusRequesters = remember { List(4) { FocusRequester() } }
|
||||
|
||||
// Mettre en cache les annotations et les chaînes transposées
|
||||
val multiLineText = remember(tuo, transpositionInterval) {
|
||||
val notes = tuo.noteAsMultiString()
|
||||
Transpose.transposeText(text = notes, interval = transpositionInterval)
|
||||
}
|
||||
|
||||
val annotationsCache = remember(tuo, transpositionInterval) {
|
||||
tuo.annotate()
|
||||
tuo.annotations()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(animatedColor)
|
||||
|
|
@ -389,15 +413,13 @@ fun TimeUnitComposable(
|
|||
.fillMaxHeight()
|
||||
)
|
||||
}
|
||||
tuo.annotate()
|
||||
/*tuo.annotate()*/
|
||||
// Utile pour que Compose recalcule la cellule. La valeur sera toutefois toujours égale à ""
|
||||
val mutableNoteVersionX = if (tuo.mutableNoteVersion == -1) "7" else ""
|
||||
val multiLineNotes = tuo.noteAsMultiString()
|
||||
val multiLineText = Transpose.transposeText(text = multiLineNotes, interval = transpositionInterval)
|
||||
var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) }
|
||||
Box (modifier = Modifier.fillMaxWidth()
|
||||
.drawBehind {
|
||||
tuo.annotations().map { ta ->
|
||||
annotationsCache.map { ta ->
|
||||
ta.underlineSpec.map { us ->
|
||||
var xStart = 0f
|
||||
val separatorLength = if (tuo.sep0 in listOf(":", "!")) 1 else 0
|
||||
|
|
@ -435,7 +457,8 @@ fun TimeUnitComposable(
|
|||
else -> Color.Black
|
||||
}
|
||||
val totalHeight = textLayoutResult?.size?.height ?: 0
|
||||
val nbNotes = (multiLineText + mutableNoteVersionX).split("\n").size
|
||||
// # FIX
|
||||
val nbNotes = multiLineText.count { it == '\n' } +1
|
||||
val yPos = ta.voiceNumber * totalHeight.toFloat() / nbNotes
|
||||
drawLine(
|
||||
colorUnderline,
|
||||
|
|
@ -446,22 +469,24 @@ fun TimeUnitComposable(
|
|||
}
|
||||
}
|
||||
){
|
||||
val annotatedText = buildAnnotatedString {
|
||||
multiLineText.split(">").mapIndexed { index, text ->
|
||||
if (index %2 == 0) {
|
||||
append(text)
|
||||
} else {
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
fontSize = 10.sp,
|
||||
baselineShift = BaselineShift.Superscript,
|
||||
//fontStyle = FontStyle.Italic,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)) {
|
||||
append(text+" ")
|
||||
}
|
||||
}
|
||||
val annotatedText = remember(multiLineText, MaterialTheme.colorScheme.primary) {
|
||||
buildAnnotatedString {
|
||||
multiLineText.split(">").mapIndexed { index, text ->
|
||||
if (index %2 == 0) {
|
||||
append(text)
|
||||
} else {
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
fontSize = 10.sp,
|
||||
baselineShift = BaselineShift.Superscript,
|
||||
//fontStyle = FontStyle.Italic,
|
||||
/*color = MaterialTheme.colorScheme.primary*/
|
||||
)) {
|
||||
append(text+" ")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
|
|
@ -485,6 +510,8 @@ fun TimeUnitComposable(
|
|||
|
||||
data class TUOWidthMeasure(val width: Dp, val isReady: Boolean)
|
||||
|
||||
data class HairpinRange(val startBlock: Int, val endBlock: Int, val symbol: Char)
|
||||
|
||||
@Composable
|
||||
fun bestTUOWidth(items: List<TimeUnitObject>): TUOWidthMeasure {
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
|
|
@ -656,7 +683,9 @@ fun LazyVerticalGridTUO(
|
|||
val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState()
|
||||
val activeRowIndex by sharedScreenModel.activeIndex.collectAsStateWithLifecycle()
|
||||
|
||||
val measures = tuoList.drop(1).chunked(gridColumnCount)
|
||||
val measures = remember(tuoList, gridColumnCount) {
|
||||
tuoList.drop(1).chunked(gridColumnCount)
|
||||
}
|
||||
// Avant column affichage:
|
||||
val metadataList = remember(tuoList) {
|
||||
tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO ->
|
||||
|
|
@ -691,6 +720,32 @@ fun LazyVerticalGridTUO(
|
|||
}.distinctBy { it.gridIndex }
|
||||
}
|
||||
|
||||
val hairpinByEndBlock: Map<Int, HairpinRange> = remember(tuoList) {
|
||||
val map = mutableMapOf<Int, HairpinRange>()
|
||||
var pendingStartBlock: Int? = null
|
||||
var pendingSymbol: Char? = null
|
||||
tuoList.drop(1).forEach { oneTUO ->
|
||||
when (oneTUO.hasHairPin()) {
|
||||
'<', '>' -> {
|
||||
pendingStartBlock = oneTUO.numBlock
|
||||
pendingSymbol = oneTUO.hasHairPin()
|
||||
}
|
||||
'=' -> {
|
||||
val startBlock = pendingStartBlock
|
||||
val symbol = pendingSymbol
|
||||
if (startBlock != null && symbol != null) {
|
||||
map[oneTUO.numBlock] = HairpinRange(startBlock, oneTUO.numBlock, symbol)
|
||||
}
|
||||
pendingStartBlock = null
|
||||
pendingSymbol = null
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
|
||||
// Envoyer les données au ViewModel une seule fois
|
||||
LaunchedEffect(metadataList) {
|
||||
if (metadataList.isNotEmpty()) {
|
||||
|
|
@ -713,7 +768,6 @@ fun LazyVerticalGridTUO(
|
|||
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
val containerWidthDp = gridWidthDp / gridColumnCount
|
||||
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
|
||||
val isAnySyllableTooLong = remember(currentStanza, gridWidthDp, gridColumnCount) {
|
||||
val allMeasuresSyllables = tuoList.drop(1).chunked(gridColumnCount).map { measureTUOss ->
|
||||
measureTUOss.map { it.getSingleSyllable(currentStanza) }.map { column ->
|
||||
|
|
@ -786,7 +840,7 @@ fun LazyVerticalGridTUO(
|
|||
|
||||
if (tuo.isTriolet()) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val arcWidth = with(density) { size.width * 0.65f }
|
||||
val arcWidth = size.width * 0.65f
|
||||
drawArc(
|
||||
color = FEUFAROO_TRIOLET_COLOR,
|
||||
startAngle = 200f,
|
||||
|
|
@ -800,18 +854,14 @@ fun LazyVerticalGridTUO(
|
|||
)
|
||||
}
|
||||
}
|
||||
if ((hairPinSymbol == '=') && (TimeUnitObject.lastHairPinSymbol != null)) {
|
||||
// println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}")
|
||||
val hairPinStart = TimeUnitObject.lastHairPinStart
|
||||
val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol
|
||||
val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount
|
||||
val hairPinEndLine: Int = (tuo.numBlock - 1) / gridColumnCount
|
||||
// if (hairPinStartLine == hairPinEndLine) {
|
||||
|
||||
val hairpinRange = hairpinByEndBlock[tuo.numBlock]
|
||||
if (hairpinRange != null) {
|
||||
Canvas(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
val xStart = if (lastHairPinSymbol == '>') -size.width * (tuo.numBlock - hairPinStart) else size.width / 2
|
||||
val xEnd = if (lastHairPinSymbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairPinStart)
|
||||
val xStart = if (hairpinRange.symbol == '>') -size.width * (tuo.numBlock - hairpinRange.startBlock) else size.width / 2
|
||||
val xEnd = if (hairpinRange.symbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairpinRange.startBlock)
|
||||
val offsetUp = -yHeight * 0.30f
|
||||
val hairpinHeight = yHeight * 0.70f
|
||||
|
||||
|
|
@ -832,20 +882,12 @@ fun LazyVerticalGridTUO(
|
|||
strokeWidth = 0.5f
|
||||
)
|
||||
}
|
||||
TimeUnitObject.endHairPin()
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
if (hairPinSymbol != null && hairPinSymbol != '=') {
|
||||
TimeUnitObject.startHairPin(hairPinSymbol, tuo.numBlock)
|
||||
}
|
||||
// @todo pTemplate.markerToString retourne les marqueurs comme une seule chaîne
|
||||
// problème si template = $QD:,-$QD
|
||||
tuo.pTemplate.resetCalledMarker()
|
||||
|
||||
val musicForPtSerifBIRegex = Regex("""^(Largh\.|Grave|Largo|Lento|Adagio|And\.|Andantino|Mod\.|Moderato|Alleg\.|All\.|Viv\.|Vivacissimo|Presto|Prestiss\.|accel\.|rit\.|rall\.|riten\.|string\.|allarg\.|a tempo|Tempo I|rubato|meno mosso|più mosso|cres\.|<|decresc\.|dim\.|>|rfz|fp|pf|sub\.p|sub\.f)""",RegexOption.IGNORE_CASE)
|
||||
val musicForEmmentRegex = Regex("""^(ppp|pp|mp|mf|fff|ff|p|f|sfz|sf|fz|rfz|fp|pf)\b""",RegexOption.IGNORE_CASE)
|
||||
|
||||
val markerList = tuo.pTemplate.markerToList()
|
||||
Row(
|
||||
modifier = Modifier.wrapContentSize(unbounded = true, align = Alignment.CenterStart),
|
||||
|
|
@ -1063,7 +1105,8 @@ fun LazyVerticalGridTUO(
|
|||
gridActive = isActive,
|
||||
isCurrentBlock = isSelectedByKeyboard && editMode,
|
||||
parentFocusRequester = focusRequester,
|
||||
transpositionInterval = currentInterval
|
||||
transpositionInterval = currentInterval,
|
||||
hairpinRange = hairpinByEndBlock[oneTUO.numBlock]
|
||||
)
|
||||
|
||||
if (showContextualMenu && selectedIndex == globalIndex) {
|
||||
|
|
@ -1119,7 +1162,6 @@ fun LazyVerticalGridTUO(
|
|||
else -> ""
|
||||
}
|
||||
val afterU0T0 = fullLine.substringAfter("0:")
|
||||
val blankPrefixRegex =Regex("""z([0-9A-Z])[:]""")
|
||||
val match = blankPrefixRegex.find(afterU0T0)
|
||||
val blankPrefix = match?.groups?.get(1)?.value ?: ""
|
||||
|
||||
|
|
@ -1217,7 +1259,6 @@ fun LazyVerticalGridTUO(
|
|||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
val columnWidthDp = gridWidthDp / gridColumnCount
|
||||
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
|
||||
val cleanAllTemps = allTemps.map { column ->
|
||||
column.map { syllable ->
|
||||
syllable.replace(REGEX_CLEAN_PREFIX, "").trim()
|
||||
|
|
@ -1295,8 +1336,9 @@ fun makeSpaceBetweenSyllables(
|
|||
val density = LocalDensity.current
|
||||
val noteWidthPx = with(density) { noteWidthDp.toPx() }
|
||||
val style = TextStyle(fontSize = fontSize)
|
||||
val spaceWidthPx = textMeasurer.measure("\u00A0", style).size.width
|
||||
|
||||
val spaceWidthPx = remember(textMeasurer, style) {
|
||||
textMeasurer.measure("\u00A0", style).size.width
|
||||
}
|
||||
|
||||
return { currentSyl, allT, s_i, index ->
|
||||
var textAlign = Alignment.Center
|
||||
|
|
@ -1308,38 +1350,25 @@ fun makeSpaceBetweenSyllables(
|
|||
|
||||
val excessPx = currentWidth - noteWidthPx
|
||||
val isExcess = (excessPx > 0)
|
||||
val neededSpaces = if (isExcess) (excessPx / spaceWidthPx).toInt() + 1 else 0
|
||||
val neededSpaces = if (isExcess && spaceWidthPx > 0) (excessPx / spaceWidthPx).toInt() + 1 else 0
|
||||
|
||||
val nextNextSyl = allT.getOrNull(s_i + 2)?.getOrNull(index) ?: ""
|
||||
// println("801: prev: [$prev_syl]${prev_syl.length}:[${textMeasurer.measure(prev_syl, style).size.width}], current [$currentSyl]:[${textMeasurer.measure(currentSyl, style).size.width}], next [$next_syl]:[${textMeasurer.measure(next_syl, style).size.width}], isExcess=$excessPx & need $neededSpaces \t spwp $spaceWidthPx\n")
|
||||
|
||||
val totalSpacesPossible = (noteWidthPx / spaceWidthPx).toInt()
|
||||
val paddingNeeded = totalSpacesPossible - (currentSyl.length+4)
|
||||
val resultText = when {
|
||||
!isExcess && (paddingNeeded > 0) -> {
|
||||
textAlign = Alignment.TopStart
|
||||
currentSyl + "\u00A0".repeat(paddingNeeded)
|
||||
}
|
||||
isExcess && next_syl.length < prev_syl.length -> {
|
||||
"\u00A0".repeat(neededSpaces) + currentSyl
|
||||
}
|
||||
isExcess && next_syl.length >= prev_syl.length -> {
|
||||
currentSyl + "\u00A0".repeat(neededSpaces)
|
||||
}
|
||||
|
||||
isExcess && next_syl.isNotEmpty() -> {
|
||||
val nextWidth = textMeasurer.measure(next_syl, style).size.width
|
||||
val nextExcess = nextWidth - noteWidthPx
|
||||
|
||||
if (nextExcess > 0 && (allT.getOrNull(s_i + 2)?.getOrNull(index)?.length ?: 0) > next_syl.length) {
|
||||
// Si le mot d'après va se décaler à gauche, moi je me décale aussi à gauche
|
||||
val compensationSpaces = ((nextExcess / spaceWidthPx) / 2).toInt() + 1
|
||||
"\u00A0".repeat(compensationSpaces) + currentSyl
|
||||
!isExcess && (noteWidthPx > 0 && spaceWidthPx > 0) -> {
|
||||
val totalSpacesPossible = (noteWidthPx / spaceWidthPx).toInt()
|
||||
val paddingNeeded = totalSpacesPossible - (currentSyl.length + 4)
|
||||
if (paddingNeeded > 0) {
|
||||
textAlign = Alignment.TopStart
|
||||
currentSyl + "\u00A0".repeat(paddingNeeded)
|
||||
} else {
|
||||
currentSyl
|
||||
}
|
||||
}
|
||||
|
||||
isExcess && spaceWidthPx > 0 && next_syl.length < prev_syl.length -> {
|
||||
"\u00A0".repeat(neededSpaces) + currentSyl
|
||||
}
|
||||
isExcess && spaceWidthPx > 0 -> {
|
||||
currentSyl + "\u00A0".repeat(neededSpaces)
|
||||
}
|
||||
else -> currentSyl
|
||||
}
|
||||
resultText to textAlign
|
||||
|
|
@ -1356,7 +1385,6 @@ private suspend fun expandInclusions(
|
|||
val lastSlash = currentFilePath.lastIndexOf('/')
|
||||
val directory = if (lastSlash != -1) currentFilePath.substring(0, lastSlash + 1) else ""
|
||||
|
||||
val inclusionRegex = Regex("^I([0-9]):(.*)")
|
||||
|
||||
lines.forEach { line ->
|
||||
val match = inclusionRegex.find(line)
|
||||
|
|
@ -1592,7 +1620,6 @@ fun autoFixNote(rawNote: String, template: String): String {
|
|||
// println("==================================================")
|
||||
if (rawNote == "_") return rawNote
|
||||
|
||||
val noteRegex = Regex("([drmfsltDRFSTw][ia]?|―)[0-9'¹²³⁴₁₂₃₄,]*")
|
||||
val actualNotes = if (rawNote.isEmpty()) emptyList() else noteRegex.findAll(rawNote).map { it.value }.toList()
|
||||
// println(" • Notes détectées dans RawNote : $actualNotes (Taille: ${actualNotes.size})")
|
||||
/*if (rawNote.contains("•") || rawNote.contains("―") || rawNote.contains(",")) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
package mg.dot.feufaro.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun CustomKeyboard(
|
||||
onDigitClick: (String) -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
onClearClick: () -> Unit,
|
||||
onPrefixClick: (String) -> Unit,
|
||||
isFullScreen: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
|
||||
tonalElevation = 0.dp,
|
||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp)
|
||||
.then(
|
||||
if (isFullScreen) Modifier.navigationBarsPadding() else Modifier
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val prefixes = listOf("EWS", "FFPM", "FF", "Antema", "Tsanta", "Salamo")
|
||||
LazyRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 1.dp)
|
||||
) {
|
||||
items(prefixes) { prefix ->
|
||||
Button(
|
||||
onClick = { onPrefixClick("$prefix ") },
|
||||
modifier = Modifier.height(38.dp),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.15f),
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
),
|
||||
contentPadding = PaddingValues(horizontal = 14.dp, vertical = 0.dp)
|
||||
) {
|
||||
Text(
|
||||
text = prefix,
|
||||
fontSize = MaterialTheme.typography.titleLarge.fontSize,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val keys = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf("↓", "0", "⌫")
|
||||
)
|
||||
|
||||
for (row in keys) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
for (key in row) {
|
||||
val isActionKey = key == "⌫" || key == "↓"
|
||||
Button(
|
||||
onClick = {
|
||||
when (key) {
|
||||
"⌫" -> onDeleteClick()
|
||||
"↓" -> onClearClick()
|
||||
else -> onDigitClick(key)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(44.dp),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isActionKey)
|
||||
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.6f)
|
||||
else
|
||||
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f),
|
||||
contentColor = if (isActionKey)
|
||||
MaterialTheme.colorScheme.onErrorContainer
|
||||
else
|
||||
Color.White
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = key,
|
||||
fontSize = MaterialTheme.typography.headlineLarge.fontSize,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -59,6 +59,7 @@ fun SimpleDrawerContent(
|
|||
val internalList by sharedScreenModel.internalItems.collectAsState()
|
||||
val externalList by sharedScreenModel.externalItems.collectAsState()
|
||||
val playList by sharedScreenModel.playlistItems.collectAsState()
|
||||
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
var internalExpanded by remember { mutableStateOf(false) }
|
||||
|
|
@ -154,7 +155,6 @@ fun SimpleDrawerContent(
|
|||
if (internalExpanded) {
|
||||
items(internalList) { item ->
|
||||
val isSelected = item.path == activePath
|
||||
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
|
||||
val isFavorite = favoriteLists.contains(item)
|
||||
|
||||
NavigationDrawerItem(
|
||||
|
|
@ -207,7 +207,6 @@ fun SimpleDrawerContent(
|
|||
if (externalExpanded) {
|
||||
items(externalList) { item ->
|
||||
val isSelected = item.path == activePath
|
||||
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
|
||||
val isFavorite = favoriteLists.contains(item)
|
||||
|
||||
if (item.path != "") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,296 @@
|
|||
package mg.dot.feufaro.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardBackspace
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardTab
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
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.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import mg.dot.feufaro.solfa.Transpose
|
||||
|
||||
enum class OctaveMode {
|
||||
LOWER, NORMAL, UPPER
|
||||
}
|
||||
val SOLFA_NOTES = Transpose.noteToNumber
|
||||
|
||||
@Composable
|
||||
fun SolfaKeyboard(
|
||||
currentVoiceIndex: Int,
|
||||
currentValue: String = "",
|
||||
onNoteClick: (String) -> Unit,
|
||||
onTabClick: () -> Unit,
|
||||
onBackspaceClick: () -> Unit,
|
||||
onBuildClick: () -> Unit,
|
||||
onCloseClick: () -> Unit,
|
||||
onCustomSymbolClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var selectedOctave by remember { mutableStateOf("") }
|
||||
val voiceLabels = listOf("S", "A", "T", "B")
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF232323)),
|
||||
color = Color(0xFF232323),
|
||||
tonalElevation = 8.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = onTabClick,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
contentPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp),
|
||||
modifier = Modifier.height(36.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.KeyboardTab,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
Text(
|
||||
text = voiceLabels.getOrElse(currentVoiceIndex) { "S" },
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = currentValue,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
textStyle = TextStyle(
|
||||
color = Color.White,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(1.5f)
|
||||
.height(36.dp)
|
||||
.background(Color.Black.copy(alpha = 0.4f), RoundedCornerShape(4.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
onNoteClick(",")
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF383838)
|
||||
),
|
||||
modifier = Modifier.height(36.dp).weight(0.7f)
|
||||
) {
|
||||
Text(",", fontSize = 14.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
onNoteClick("'")
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF383838)
|
||||
),
|
||||
modifier = Modifier.height(36.dp)
|
||||
) {
|
||||
Text("'", fontSize = 14.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(0.5f))
|
||||
KeyButton("di", Modifier.weight(1f)) { onNoteClick("di") }
|
||||
KeyButton("ri", Modifier.weight(1f)) { onNoteClick("ri") }
|
||||
Spacer(modifier = Modifier.weight(1.3f))
|
||||
KeyButton("fi", Modifier.weight(1f)) { onNoteClick("fi") }
|
||||
KeyButton("si", Modifier.weight(1f)) { onNoteClick("si") }
|
||||
KeyButton("ta", Modifier.weight(1f)) { onNoteClick("ta") }
|
||||
Spacer(modifier = Modifier.weight(0.5f))
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
val diatonicNotes = listOf("d", "r", "m", "f", "s", "l", "t")
|
||||
for (note in diatonicNotes) {
|
||||
KeyButton(note, Modifier.weight(1f)) { onNoteClick(note) }
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
KeyButton("―", Modifier.weight(1f)) { onCustomSymbolClick("-") }
|
||||
KeyButton("• ,", Modifier.weight(1f)) { onCustomSymbolClick(";") }
|
||||
KeyButton("•", Modifier.weight(0.5f)) { onCustomSymbolClick(".") }
|
||||
KeyButton(",", Modifier.weight(0.5f)) { onCustomSymbolClick(" ,") }
|
||||
KeyButton("Espace", Modifier.weight(1.6f)) { onCustomSymbolClick(" ") }
|
||||
KeyButton("(", Modifier.weight(0.6f)) { onCustomSymbolClick("(") }
|
||||
KeyButton(")", Modifier.weight(0.6f)) { onCustomSymbolClick(")") }
|
||||
KeyButton("<", Modifier.weight(0.5f)) { onCustomSymbolClick("<") }
|
||||
KeyButton(">", Modifier.weight(0.5f)) { onCustomSymbolClick(">") }
|
||||
KeyButton("z", Modifier.weight(1f)) { onNoteClick("z") }
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(48.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onCloseClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.errorContainer),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onBackspaceClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4A4A4A)),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.KeyboardBackspace,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onBuildClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1.2f),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Save,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KeyButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color = Color(0xFF383838),
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier.height(49.dp),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = containerColor),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SolfaKeyboardState {
|
||||
var isVisible by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var isDialogActive by mutableStateOf(false)
|
||||
var activeVoiceIndex by mutableStateOf(0)
|
||||
private set
|
||||
|
||||
var currentText by mutableStateOf("")
|
||||
var onNoteInput: ((String) -> Unit)? = null
|
||||
var onBackspace: (() -> Unit)? = null
|
||||
var onCloseDialog: (() -> Unit)? = null
|
||||
var onBuildEdit: (() -> Unit)? = null
|
||||
var onCustomSymbol: ((String) -> Unit)? = null
|
||||
var onTabNextVoice: (() -> Unit)? = null
|
||||
var onMarker: (() -> Unit)? = null
|
||||
|
||||
fun show(
|
||||
initialVoice: Int = 0,
|
||||
initialValue: String = "",
|
||||
onNote: (String) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onSymbol: (String) -> Unit,
|
||||
onBuild: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
onTab: () -> Unit
|
||||
) {
|
||||
if (!isDialogActive) return
|
||||
activeVoiceIndex = initialVoice
|
||||
onNoteInput = onNote
|
||||
onBackspace = onDelete
|
||||
onCustomSymbol = onSymbol
|
||||
onCloseDialog = onClose
|
||||
onBuildEdit = onBuild
|
||||
onTabNextVoice = onTab
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberSolfaKeyboardState() = remember { SolfaKeyboardState() }
|
||||
|
||||
val LocalSolfaKeyboardState = staticCompositionLocalOf <SolfaKeyboardState> {
|
||||
error("Aucun SolfaKeyboardState fourni")
|
||||
}
|
||||
|
|
@ -612,7 +612,6 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
_dcDone.value = false
|
||||
_dsDone.value = false
|
||||
_sourceModeState.value = false
|
||||
setTranspositionInterval(0)
|
||||
try {
|
||||
val midiFileName = fileRepository.getFileName(newMidiFile)
|
||||
println("Opening xx129 $midiFileName")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue