Add custom keyboards for search & edit notes

This commit is contained in:
Hasinjato 2026-08-03 11:25:36 +03:00
parent 9da97209c9
commit 340f72855f
8 changed files with 1613 additions and 835 deletions

View file

@ -15,12 +15,14 @@ import androidx.compose.material.icons.filled.RemoveCircleOutline
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
@ -141,6 +143,7 @@ private val markerGroups = listOf(
@Composable @Composable
@OptIn(ExperimentalComposeUiApi::class)
fun MarkerPopup( fun MarkerPopup(
menuPosition: IntOffset, menuPosition: IntOffset,
onDismiss: () -> Unit, onDismiss: () -> Unit,
@ -155,7 +158,8 @@ fun MarkerPopup(
properties = PopupProperties( properties = PopupProperties(
focusable = true, focusable = true,
dismissOnBackPress = true, dismissOnBackPress = true,
dismissOnClickOutside = true dismissOnClickOutside = true,
usePlatformDefaultWidth = false
) )
) { ) {

View file

@ -1,14 +1,11 @@
package mg.dot.feufaro package mg.dot.feufaro
import SharedScreenModel import SharedScreenModel
import androidx.compose.foundation.background import androidx.compose.foundation.*
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward 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.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily 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.Solfa
import mg.dot.feufaro.solfa.TUOEditState import mg.dot.feufaro.solfa.TUOEditState
import mg.dot.feufaro.solfa.TimeUnitObject import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.solfa.extractAnchorFromNoteLine import mg.dot.feufaro.ui.LocalSolfaKeyboardState
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -61,6 +59,18 @@ fun TUODetailDialog(
putAll(editState.notesByVoice) 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 { val originalNotes = mutableStateMapOf<Int, String>().apply {
putAll(editState.notesByVoice) putAll(editState.notesByVoice)
} }
@ -114,20 +124,103 @@ fun TUODetailDialog(
var markerButtonPos by remember { var markerButtonPos by remember {
mutableStateOf(IntOffset.Zero) 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( Popup(
offset = menuPosition, offset = menuPosition,
onDismissRequest = onDismiss, onDismissRequest = {
if(!isAndroid && !keyboardState.isVisible) {
onDismiss()
}
},
properties = PopupProperties( properties = PopupProperties(
focusable = true, focusable = if(isAndroid) false else true,
dismissOnBackPress = true, dismissOnBackPress = !isAndroid || !keyboardState.isVisible,
dismissOnClickOutside = true dismissOnClickOutside = !isAndroid || !keyboardState.isVisible
), ),
) { ) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.widthIn(max=125.dp) .widthIn(max=125.dp)
.heightIn(max = 400.dp), .heightIn(max = 400.dp)
.imePadding()
.navigationBarsPadding(),
shape = MaterialTheme.shapes.small, shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f), color = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f),
) { ) {
@ -500,9 +593,13 @@ fun TUODetailDialog(
val validation = validateMusicalInput(currentNote, templateFragment) val validation = validateMusicalInput(currentNote, templateFragment)
val voiceAnchor = anchorsByVoice[voice]?.replace("#","") val voiceAnchor = anchorsByVoice[voice]?.replace("#","")
val hasAnchor = !voiceAnchor.isNullOrBlank() val hasAnchor = !voiceAnchor.isNullOrBlank()
val isSelected = selectedVoiceIndex == voice && keyboardState.isVisible
Box(modifier = Modifier.wrapContentSize() Box(modifier = Modifier
//.wrapContentSize()
.fillMaxWidth()
) { ) {
if(!isAndroid) {
MyTextEditField( MyTextEditField(
value = currentNote ?: "", value = currentNote ?: "",
customFontSize = 14.sp, customFontSize = 14.sp,
@ -543,12 +640,37 @@ fun TUODetailDialog(
} }
} }
} }
} 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)
}
)
}
} }
} }
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
if(!isAndroid) {
// --- SECTION LYRICS --- // --- SECTION LYRICS ---
lyricsLines.forEachIndexed { index, line -> lyricsLines.forEachIndexed { index, line ->
val displayedLine = line val displayedLine = line
@ -594,10 +716,16 @@ fun TUODetailDialog(
isEditable = isEditable, isEditable = isEditable,
isAddable = canAdd, isAddable = canAdd,
isWarn = !isLyricsValid, isWarn = !isLyricsValid,
funTransform = { input -> transformLyricsInput(input, templateFragment) }, funTransform = { input ->
transformLyricsInput(
input,
templateFragment
)
},
onValueChng = { newValue -> onValueChng = { newValue ->
val trimmedValue = newValue.trimStart() val trimmedValue = newValue.trimStart()
val dataToSave = if (trimmedValue.endsWith(" ")) trimmedValue else "$trimmedValue " val dataToSave =
if (trimmedValue.endsWith(" ")) trimmedValue else "$trimmedValue "
lyricsLines[index] = trimmedValue lyricsLines[index] = trimmedValue
val updatedLyricsMap = editState.lyricsByStanza.toMutableMap() val updatedLyricsMap = editState.lyricsByStanza.toMutableMap()
editedLyricsMap[index + 1] = dataToSave editedLyricsMap[index + 1] = dataToSave
@ -617,7 +745,9 @@ fun TUODetailDialog(
}) { }) {
Icon( Icon(
imageVector = if (index == 0) Icons.Default.Add else Icons.Default.Clear, 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), tint = if (index == 0) MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) else MaterialTheme.colorScheme.tertiary.copy(
alpha = 1.5f
),
contentDescription = null contentDescription = null
) )
} }
@ -626,7 +756,9 @@ fun TUODetailDialog(
} }
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
} }
}
if(!isAndroid) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
@ -672,6 +804,7 @@ fun TUODetailDialog(
} }
} }
} }
}
if (showMarkerPopup) { if (showMarkerPopup) {
MarkerPopup( MarkerPopup(
menuPosition = IntOffset( menuPosition = IntOffset(
@ -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( data class ValidNoteResult(
val isValid: Boolean, val isValid: Boolean,
val message: String val message: String

View file

@ -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 @Composable
fun TimeUnitComposable( fun TimeUnitComposable(
tuo: TimeUnitObject, tuo: TimeUnitObject,
@ -273,17 +280,34 @@ fun TimeUnitComposable(
gridActive: Boolean, gridActive: Boolean,
isCurrentBlock: Boolean = false, isCurrentBlock: Boolean = false,
parentFocusRequester: FocusRequester? = null, 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 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 currentDensity = LocalDensity.current
val animatedColor by animateColorAsState( val animatedColor = if (gridActive) {
targetValue = if (gridActive) MaterialTheme.colorScheme.secondary.copy(alpha = 1f) else col, animateColorAsState(
animationSpec = tween(durationMillis = 100) // Très court pour rester réactif targetValue = MaterialTheme.colorScheme.secondary.copy(alpha = 1f),
) animationSpec = tween(durationMillis = 100)
).value
} else {
col
}
val focusRequesters = remember { List(4) { FocusRequester() } } 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( Column(
modifier = Modifier modifier = Modifier
.background(animatedColor) .background(animatedColor)
@ -389,15 +413,13 @@ fun TimeUnitComposable(
.fillMaxHeight() .fillMaxHeight()
) )
} }
tuo.annotate() /*tuo.annotate()*/
// Utile pour que Compose recalcule la cellule. La valeur sera toutefois toujours égale à "" // Utile pour que Compose recalcule la cellule. La valeur sera toutefois toujours égale à ""
val mutableNoteVersionX = if (tuo.mutableNoteVersion == -1) "7" else "" 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) } var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) }
Box (modifier = Modifier.fillMaxWidth() Box (modifier = Modifier.fillMaxWidth()
.drawBehind { .drawBehind {
tuo.annotations().map { ta -> annotationsCache.map { ta ->
ta.underlineSpec.map { us -> ta.underlineSpec.map { us ->
var xStart = 0f var xStart = 0f
val separatorLength = if (tuo.sep0 in listOf(":", "!")) 1 else 0 val separatorLength = if (tuo.sep0 in listOf(":", "!")) 1 else 0
@ -435,7 +457,8 @@ fun TimeUnitComposable(
else -> Color.Black else -> Color.Black
} }
val totalHeight = textLayoutResult?.size?.height ?: 0 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 val yPos = ta.voiceNumber * totalHeight.toFloat() / nbNotes
drawLine( drawLine(
colorUnderline, colorUnderline,
@ -446,7 +469,8 @@ fun TimeUnitComposable(
} }
} }
){ ){
val annotatedText = buildAnnotatedString { val annotatedText = remember(multiLineText, MaterialTheme.colorScheme.primary) {
buildAnnotatedString {
multiLineText.split(">").mapIndexed { index, text -> multiLineText.split(">").mapIndexed { index, text ->
if (index %2 == 0) { if (index %2 == 0) {
append(text) append(text)
@ -456,7 +480,7 @@ fun TimeUnitComposable(
fontSize = 10.sp, fontSize = 10.sp,
baselineShift = BaselineShift.Superscript, baselineShift = BaselineShift.Superscript,
//fontStyle = FontStyle.Italic, //fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.primary /*color = MaterialTheme.colorScheme.primary*/
)) { )) {
append(text+" ") append(text+" ")
} }
@ -464,6 +488,7 @@ fun TimeUnitComposable(
} }
} }
}
Text( Text(
text = annotatedText, text = annotatedText,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
@ -485,6 +510,8 @@ fun TimeUnitComposable(
data class TUOWidthMeasure(val width: Dp, val isReady: Boolean) data class TUOWidthMeasure(val width: Dp, val isReady: Boolean)
data class HairpinRange(val startBlock: Int, val endBlock: Int, val symbol: Char)
@Composable @Composable
fun bestTUOWidth(items: List<TimeUnitObject>): TUOWidthMeasure { fun bestTUOWidth(items: List<TimeUnitObject>): TUOWidthMeasure {
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()
@ -656,7 +683,9 @@ fun LazyVerticalGridTUO(
val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState() val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState()
val activeRowIndex by sharedScreenModel.activeIndex.collectAsStateWithLifecycle() 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: // Avant column affichage:
val metadataList = remember(tuoList) { val metadataList = remember(tuoList) {
tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO -> tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO ->
@ -691,6 +720,32 @@ fun LazyVerticalGridTUO(
}.distinctBy { it.gridIndex } }.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 // Envoyer les données au ViewModel une seule fois
LaunchedEffect(metadataList) { LaunchedEffect(metadataList) {
if (metadataList.isNotEmpty()) { if (metadataList.isNotEmpty()) {
@ -713,7 +768,6 @@ fun LazyVerticalGridTUO(
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()
val containerWidthDp = gridWidthDp / gridColumnCount val containerWidthDp = gridWidthDp / gridColumnCount
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
val isAnySyllableTooLong = remember(currentStanza, gridWidthDp, gridColumnCount) { val isAnySyllableTooLong = remember(currentStanza, gridWidthDp, gridColumnCount) {
val allMeasuresSyllables = tuoList.drop(1).chunked(gridColumnCount).map { measureTUOss -> val allMeasuresSyllables = tuoList.drop(1).chunked(gridColumnCount).map { measureTUOss ->
measureTUOss.map { it.getSingleSyllable(currentStanza) }.map { column -> measureTUOss.map { it.getSingleSyllable(currentStanza) }.map { column ->
@ -786,7 +840,7 @@ fun LazyVerticalGridTUO(
if (tuo.isTriolet()) { if (tuo.isTriolet()) {
Canvas(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier.fillMaxSize()) {
val arcWidth = with(density) { size.width * 0.65f } val arcWidth = size.width * 0.65f
drawArc( drawArc(
color = FEUFAROO_TRIOLET_COLOR, color = FEUFAROO_TRIOLET_COLOR,
startAngle = 200f, startAngle = 200f,
@ -800,18 +854,14 @@ fun LazyVerticalGridTUO(
) )
} }
} }
if ((hairPinSymbol == '=') && (TimeUnitObject.lastHairPinSymbol != null)) {
// println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}") val hairpinRange = hairpinByEndBlock[tuo.numBlock]
val hairPinStart = TimeUnitObject.lastHairPinStart if (hairpinRange != null) {
val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol
val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount
val hairPinEndLine: Int = (tuo.numBlock - 1) / gridColumnCount
// if (hairPinStartLine == hairPinEndLine) {
Canvas( Canvas(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
val xStart = if (lastHairPinSymbol == '>') -size.width * (tuo.numBlock - hairPinStart) else size.width / 2 val xStart = if (hairpinRange.symbol == '>') -size.width * (tuo.numBlock - hairpinRange.startBlock) else size.width / 2
val xEnd = if (lastHairPinSymbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairPinStart) val xEnd = if (hairpinRange.symbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairpinRange.startBlock)
val offsetUp = -yHeight * 0.30f val offsetUp = -yHeight * 0.30f
val hairpinHeight = yHeight * 0.70f val hairpinHeight = yHeight * 0.70f
@ -832,20 +882,12 @@ fun LazyVerticalGridTUO(
strokeWidth = 0.5f 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 // @todo pTemplate.markerToString retourne les marqueurs comme une seule chaîne
// problème si template = $QD:,-$QD // problème si template = $QD:,-$QD
tuo.pTemplate.resetCalledMarker() 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() val markerList = tuo.pTemplate.markerToList()
Row( Row(
modifier = Modifier.wrapContentSize(unbounded = true, align = Alignment.CenterStart), modifier = Modifier.wrapContentSize(unbounded = true, align = Alignment.CenterStart),
@ -1063,7 +1105,8 @@ fun LazyVerticalGridTUO(
gridActive = isActive, gridActive = isActive,
isCurrentBlock = isSelectedByKeyboard && editMode, isCurrentBlock = isSelectedByKeyboard && editMode,
parentFocusRequester = focusRequester, parentFocusRequester = focusRequester,
transpositionInterval = currentInterval transpositionInterval = currentInterval,
hairpinRange = hairpinByEndBlock[oneTUO.numBlock]
) )
if (showContextualMenu && selectedIndex == globalIndex) { if (showContextualMenu && selectedIndex == globalIndex) {
@ -1119,7 +1162,6 @@ fun LazyVerticalGridTUO(
else -> "" else -> ""
} }
val afterU0T0 = fullLine.substringAfter("0:") val afterU0T0 = fullLine.substringAfter("0:")
val blankPrefixRegex =Regex("""z([0-9A-Z])[:]""")
val match = blankPrefixRegex.find(afterU0T0) val match = blankPrefixRegex.find(afterU0T0)
val blankPrefix = match?.groups?.get(1)?.value ?: "" val blankPrefix = match?.groups?.get(1)?.value ?: ""
@ -1217,7 +1259,6 @@ fun LazyVerticalGridTUO(
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
val columnWidthDp = gridWidthDp / gridColumnCount val columnWidthDp = gridWidthDp / gridColumnCount
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
val cleanAllTemps = allTemps.map { column -> val cleanAllTemps = allTemps.map { column ->
column.map { syllable -> column.map { syllable ->
syllable.replace(REGEX_CLEAN_PREFIX, "").trim() syllable.replace(REGEX_CLEAN_PREFIX, "").trim()
@ -1295,8 +1336,9 @@ fun makeSpaceBetweenSyllables(
val density = LocalDensity.current val density = LocalDensity.current
val noteWidthPx = with(density) { noteWidthDp.toPx() } val noteWidthPx = with(density) { noteWidthDp.toPx() }
val style = TextStyle(fontSize = fontSize) 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 -> return { currentSyl, allT, s_i, index ->
var textAlign = Alignment.Center var textAlign = Alignment.Center
@ -1308,38 +1350,25 @@ fun makeSpaceBetweenSyllables(
val excessPx = currentWidth - noteWidthPx val excessPx = currentWidth - noteWidthPx
val isExcess = (excessPx > 0) 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 resultText = when {
!isExcess && (noteWidthPx > 0 && spaceWidthPx > 0) -> {
val totalSpacesPossible = (noteWidthPx / spaceWidthPx).toInt() val totalSpacesPossible = (noteWidthPx / spaceWidthPx).toInt()
val paddingNeeded = totalSpacesPossible - (currentSyl.length + 4) val paddingNeeded = totalSpacesPossible - (currentSyl.length + 4)
val resultText = when { if (paddingNeeded > 0) {
!isExcess && (paddingNeeded > 0) -> {
textAlign = Alignment.TopStart textAlign = Alignment.TopStart
currentSyl + "\u00A0".repeat(paddingNeeded) 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
} else { } else {
currentSyl currentSyl
} }
} }
isExcess && spaceWidthPx > 0 && next_syl.length < prev_syl.length -> {
"\u00A0".repeat(neededSpaces) + currentSyl
}
isExcess && spaceWidthPx > 0 -> {
currentSyl + "\u00A0".repeat(neededSpaces)
}
else -> currentSyl else -> currentSyl
} }
resultText to textAlign resultText to textAlign
@ -1356,7 +1385,6 @@ private suspend fun expandInclusions(
val lastSlash = currentFilePath.lastIndexOf('/') val lastSlash = currentFilePath.lastIndexOf('/')
val directory = if (lastSlash != -1) currentFilePath.substring(0, lastSlash + 1) else "" val directory = if (lastSlash != -1) currentFilePath.substring(0, lastSlash + 1) else ""
val inclusionRegex = Regex("^I([0-9]):(.*)")
lines.forEach { line -> lines.forEach { line ->
val match = inclusionRegex.find(line) val match = inclusionRegex.find(line)
@ -1592,7 +1620,6 @@ fun autoFixNote(rawNote: String, template: String): String {
// println("==================================================") // println("==================================================")
if (rawNote == "_") return rawNote 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() 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})") // println(" • Notes détectées dans RawNote : $actualNotes (Taille: ${actualNotes.size})")
/*if (rawNote.contains("") || rawNote.contains("") || rawNote.contains(",")) { /*if (rawNote.contains("") || rawNote.contains("") || rawNote.contains(",")) {

View file

@ -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
)
}
}
}
}
}
}
}

View file

@ -30,11 +30,16 @@ import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -182,11 +187,15 @@ fun MainScreenWithDrawer(
val isQrVisible = sharedScreenModel.isQRCodeVisible.value val isQrVisible = sharedScreenModel.isQRCodeVisible.value
val sharedInteractionSource = remember { MutableInteractionSource() }
LaunchedEffect(drawerState.isOpen) { LaunchedEffect(drawerState.isOpen) {
if (drawerState.isOpen) { if (drawerState.isOpen) {
sharedScreenModel.loadItems() sharedScreenModel.loadItems()
} }
} }
val density = LocalDensity.current
var keyboardHeightDp by remember { mutableStateOf(0.dp) }
ModalNavigationDrawer(drawerState = drawerState, drawerContent = { ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
SimpleDrawerContent( SimpleDrawerContent(
@ -253,6 +262,22 @@ fun MainScreenWithDrawer(
) )
} }
val favoritePaths by sharedScreenModel.playlistItems.collectAsState() val favoritePaths by sharedScreenModel.playlistItems.collectAsState()
val keyboardController = LocalSoftwareKeyboardController.current
var isNumericKeyboard by remember { mutableStateOf(true) }
LaunchedEffect(isNumericKeyboard, isSearchActive) {
if (isSearchActive) {
if (isAndroid && isNumericKeyboard) {
keyboardController?.hide()
} else {
keyboardController?.show()
}
}
}
val solfaKeyboardState = remember { SolfaKeyboardState() }
CompositionLocalProvider(LocalSolfaKeyboardState provides solfaKeyboardState) {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val isLandscape = maxWidth > maxHeight val isLandscape = maxWidth > maxHeight
val topAppBarHeight = if (isAndroid) { val topAppBarHeight = if (isAndroid) {
@ -265,6 +290,7 @@ fun MainScreenWithDrawer(
50.dp 50.dp
} }
CompositionLocalProvider(LocalSolfaKeyboardState provides solfaKeyboardState) {
Scaffold( Scaffold(
contentWindowInsets = if (isFullScreenEnabled) { contentWindowInsets = if (isFullScreenEnabled) {
WindowInsets(0, 0, 0, 0) WindowInsets(0, 0, 0, 0)
@ -280,7 +306,8 @@ fun MainScreenWithDrawer(
modifier = Modifier.height(topAppBarHeight), modifier = Modifier.height(topAppBarHeight),
windowInsets = if (!isLandscape) { windowInsets = if (!isLandscape) {
WindowInsets.safeDrawing.only( WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal + WindowInsetsSides.Top) WindowInsetsSides.Horizontal + WindowInsetsSides.Top
)
} else { } else {
WindowInsets(0, 0, 0, 0) WindowInsets(0, 0, 0, 0)
}, },
@ -300,12 +327,16 @@ fun MainScreenWithDrawer(
textInput = newValue textInput = newValue
sharedScreenModel.updateSearchTxt(newValue) sharedScreenModel.updateSearchTxt(newValue)
}, },
readOnly = isAndroid && isNumericKeyboard,
textStyle = LocalTextStyle.current.copy( textStyle = LocalTextStyle.current.copy(
color = Color.White, color = Color.White,
fontSize = 16.sp fontSize = 16.sp
), ),
singleLine = true, singleLine = true,
cursorBrush = SolidColor(Color.White), cursorBrush = SolidColor(Color.White),
keyboardOptions = KeyboardOptions(
keyboardType = if (isAndroid && isNumericKeyboard) KeyboardType.Text else KeyboardType.Number
),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(38.dp) .height(38.dp)
@ -321,7 +352,7 @@ fun MainScreenWithDrawer(
enabled = true, enabled = true,
singleLine = true, singleLine = true,
visualTransformation = VisualTransformation.None, visualTransformation = VisualTransformation.None,
interactionSource = remember { MutableInteractionSource() }, interactionSource = sharedInteractionSource,
placeholder = { placeholder = {
Text( Text(
text = "FFPM 1 Andriananahary ...", text = "FFPM 1 Andriananahary ...",
@ -338,7 +369,26 @@ fun MainScreenWithDrawer(
) )
}, },
trailingIcon = { trailingIcon = {
if (textInput.isNotEmpty()) { Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
if (isAndroid) {
IconButton(
onClick = {
isNumericKeyboard = !isNumericKeyboard
},
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = if (isNumericKeyboard) Icons.Default.Abc else Icons.Filled._123,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(22.dp)
)
}
}
IconButton( IconButton(
onClick = { onClick = {
textInput = "" textInput = ""
@ -348,14 +398,17 @@ fun MainScreenWithDrawer(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Clear, imageVector = Icons.Default.Clear,
contentDescription = "Effacer", contentDescription = null,
tint = Color.White.copy(alpha = 0.8f), tint = Color.White.copy(alpha = 0.8f),
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
} }
} }
}, },
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), contentPadding = PaddingValues(
horizontal = 10.dp,
vertical = 0.dp
),
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
@ -419,7 +472,7 @@ fun MainScreenWithDrawer(
color = if (isCurrentlyTransposed && !isPendingChange) Color(0xFFFFD700) else Color.White, color = if (isCurrentlyTransposed && !isPendingChange) Color(0xFFFFD700) else Color.White,
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = sharedInteractionSource,
indication = null indication = null
) { ) {
isEyeVisible = !isEyeVisible isEyeVisible = !isEyeVisible
@ -471,9 +524,12 @@ fun MainScreenWithDrawer(
} }
} }
val centralIcon = if (isPendingChange) Icons.Filled.Check else Icons.Filled.SwapHoriz val centralIcon =
val centralTint = if (isPendingChange) MaterialTheme.colorScheme.tertiary else Color.White if (isPendingChange) Icons.Filled.Check else Icons.Filled.SwapHoriz
val tooltipText = if (isPendingChange) "Transposer en $tempUiKey" else if (isCurrentlyTransposed) "Revenir en $songKey" else "Transposer" val centralTint =
if (isPendingChange) MaterialTheme.colorScheme.tertiary else Color.White
val tooltipText =
if (isPendingChange) "Transposer en $tempUiKey" else if (isCurrentlyTransposed) "Revenir en $songKey" else "Transposer"
TooltipBox( TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
@ -553,7 +609,10 @@ fun MainScreenWithDrawer(
}, },
modifier = Modifier.fillMaxWidth().height(36.dp), modifier = Modifier.fillMaxWidth().height(36.dp),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f)), border = BorderStroke(
1.dp,
MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f)
),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.tertiary contentColor = MaterialTheme.colorScheme.tertiary
) )
@ -580,8 +639,10 @@ fun MainScreenWithDrawer(
Row( Row(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
if (!solfaKeyboardState.isVisible) {
Column( Column(
modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End, modifier = Modifier.fillMaxWidth().padding(5.dp),
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(7.dp) verticalArrangement = Arrangement.spacedBy(7.dp)
) { ) {
if (isEditMode) { if (isEditMode) {
@ -704,7 +765,7 @@ fun MainScreenWithDrawer(
icon = if (showMidiCtrl) Icons.Filled.StopCircle else Icons.Filled.PlayCircle icon = if (showMidiCtrl) Icons.Filled.StopCircle else Icons.Filled.PlayCircle
) )
} }
if (!showMidiCtrl) { if (!showMidiCtrl && !isSearchActive) {
MyFAB( MyFAB(
onClick = { onClick = {
sharedScreenModel.setExpandedFAB(!isExpanded) sharedScreenModel.setExpandedFAB(!isExpanded)
@ -758,16 +819,22 @@ fun MainScreenWithDrawer(
} }
} }
} }
}
}) { paddingValues -> }) { paddingValues ->
Box( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) { ) {
content(PaddingValues(0.dp)) content(PaddingValues(0.dp))
AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = isQrVisible, visible = isQrVisible,
enter = slideInVertically( enter = slideInVertically(
initialOffsetY = { fullHeight -> fullHeight } initialOffsetY = { fullHeight -> fullHeight }
@ -802,8 +869,8 @@ fun MainScreenWithDrawer(
var isHovered by remember { mutableStateOf(false) } var isHovered by remember { mutableStateOf(false) }
val offsetX by animateDpAsState(targetValue = if (isHovered || isSearchActive) 0.dp else 15.dp) val offsetX by animateDpAsState(targetValue = if (isHovered || isSearchActive) 0.dp else 15.dp)
AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = !isEditMode && !isQrVisible && !createMode, visible = !isEditMode && !isQrVisible && !createMode && !isExpanded,
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(top = 15.dp) .padding(top = 15.dp)
@ -850,7 +917,7 @@ fun MainScreenWithDrawer(
} }
} }
AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = isSearchActive && textInput.isNotEmpty(), visible = isSearchActive && textInput.isNotEmpty(),
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(), exit = fadeOut() + shrinkVertically(),
@ -858,11 +925,13 @@ fun MainScreenWithDrawer(
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
.fillMaxWidth(0.85f) .fillMaxWidth(0.85f)
.padding(top = 8.dp) .padding(top = 8.dp)
.padding(bottom = if (isAndroid && isNumericKeyboard) keyboardHeightDp else 0.dp)
) { ) {
Card( Card(
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
modifier = Modifier.fillMaxHeight()
) { ) {
val sortedSongs = remember(filteredSongs) { val sortedSongs = remember(filteredSongs) {
filteredSongs.sortedBy { item -> filteredSongs.sortedBy { item ->
@ -918,7 +987,9 @@ fun MainScreenWithDrawer(
Icon( Icon(
imageVector = Icons.Default.Star, imageVector = Icons.Default.Star,
contentDescription = null, contentDescription = null,
tint = if (!isFavorite) Color.LightGray else Color(0xFFFFD700) tint = if (!isFavorite) Color.LightGray else Color(
0xFFFFD700
)
) )
} }
}, },
@ -935,6 +1006,63 @@ fun MainScreenWithDrawer(
} }
} }
} }
// --- CLAVIER NUMÉRIQUE PERSONNALISÉ (Sur couche supérieure) ---
androidx.compose.animation.AnimatedVisibility(
visible = isSearchActive && isAndroid && isNumericKeyboard,
enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }) + fadeOut(),
modifier = Modifier.align(Alignment.BottomCenter)
) {
CustomKeyboard(
modifier = Modifier.onGloballyPositioned { coordinates ->
keyboardHeightDp = with(density) { coordinates.size.height.toDp() }
},
onDigitClick = { digit ->
textInput += digit
sharedScreenModel.updateSearchTxt(textInput)
},
onDeleteClick = {
if (textInput.isNotEmpty()) {
textInput = textInput.dropLast(1)
sharedScreenModel.updateSearchTxt(textInput)
}
},
onClearClick = {
textInput = ""
sharedScreenModel.updateSearchTxt("")
},
onPrefixClick = { prefix ->
sharedScreenModel.updateSearchTxt(prefix)
},
isFullScreen = isFullScreenEnabled
)
}
}
AnimatedVisibility(
visible = solfaKeyboardState.isVisible && solfaKeyboardState.isDialogActive && isAndroid,
enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }) + fadeOut()
) {
SolfaKeyboard(
currentVoiceIndex = solfaKeyboardState.activeVoiceIndex,
currentValue = solfaKeyboardState.currentText,
onNoteClick = { note -> solfaKeyboardState.onNoteInput?.invoke(note) },
onTabClick = { solfaKeyboardState.onTabNextVoice?.invoke() },
onBackspaceClick = { solfaKeyboardState.onBackspace?.invoke() },
onCloseClick = { solfaKeyboardState.onCloseDialog?.invoke() },
onBuildClick = { solfaKeyboardState.onBuildEdit?.invoke() },
onCustomSymbolClick = { symbol -> solfaKeyboardState.onCustomSymbol?.invoke(symbol) },
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.3f)
)
}
}
}
} }
} }
} }

View file

@ -59,6 +59,7 @@ fun SimpleDrawerContent(
val internalList by sharedScreenModel.internalItems.collectAsState() val internalList by sharedScreenModel.internalItems.collectAsState()
val externalList by sharedScreenModel.externalItems.collectAsState() val externalList by sharedScreenModel.externalItems.collectAsState()
val playList by sharedScreenModel.playlistItems.collectAsState() val playList by sharedScreenModel.playlistItems.collectAsState()
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
val listState = rememberLazyListState() val listState = rememberLazyListState()
var internalExpanded by remember { mutableStateOf(false) } var internalExpanded by remember { mutableStateOf(false) }
@ -154,7 +155,6 @@ fun SimpleDrawerContent(
if (internalExpanded) { if (internalExpanded) {
items(internalList) { item -> items(internalList) { item ->
val isSelected = item.path == activePath val isSelected = item.path == activePath
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
val isFavorite = favoriteLists.contains(item) val isFavorite = favoriteLists.contains(item)
NavigationDrawerItem( NavigationDrawerItem(
@ -207,7 +207,6 @@ fun SimpleDrawerContent(
if (externalExpanded) { if (externalExpanded) {
items(externalList) { item -> items(externalList) { item ->
val isSelected = item.path == activePath val isSelected = item.path == activePath
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
val isFavorite = favoriteLists.contains(item) val isFavorite = favoriteLists.contains(item)
if (item.path != "") { if (item.path != "") {

View file

@ -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")
}

View file

@ -612,7 +612,6 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_dcDone.value = false _dcDone.value = false
_dsDone.value = false _dsDone.value = false
_sourceModeState.value = false _sourceModeState.value = false
setTranspositionInterval(0)
try { try {
val midiFileName = fileRepository.getFileName(newMidiFile) val midiFileName = fileRepository.getFileName(newMidiFile)
println("Opening xx129 $midiFileName") println("Opening xx129 $midiFileName")