From 540b508250b28923730c09129447688d41ab48c0 Mon Sep 17 00:00:00 2001 From: Hasinjato Date: Thu, 10 Sep 2026 11:27:48 +0300 Subject: [PATCH] Fix! A-B looping ux --- .../mg/dot/feufaro/solfa/TimeUnitObject.kt | 51 +++++++++++-------- .../mg/dot/feufaro/ui/MidiControlPanel.kt | 28 +++++----- .../feufaro/viewmodel/SharedScreenModel.kt | 28 ++++++++++ .../kotlin/mg/dot/feufaro/midi/MidiPlayer.kt | 16 +++++- 4 files changed, 86 insertions(+), 37 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index deb9180..9699886 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -1,6 +1,7 @@ package mg.dot.feufaro.solfa import SharedScreenModel +import SharedScreenModel.ABModeState import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.* @@ -286,19 +287,21 @@ fun TimeUnitComposable( parentFocusRequester: FocusRequester? = null, transpositionInterval: Int, hairpinRange: HairpinRange? = null, - isInABrange: Boolean = false + isInABrange: Boolean = false, + abModeState: ABModeState, ) { 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 colorUnderline = MaterialTheme.colorScheme.onSecondary - val animatedColor = when { - gridActive -> animateColorAsState( - targetValue = MaterialTheme.colorScheme.secondary.copy(alpha = 1f), - animationSpec = tween(durationMillis = 100) - ).value + val targetColor = when { + gridActive && abModeState != ABModeState.WAITING_FOR_A -> MaterialTheme.colorScheme.secondary isInABrange -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.25f) else -> col } + val animatedColor by animateColorAsState( + targetValue = targetColor, + animationSpec = tween(durationMillis = 100) + ) val focusRequesters = remember { List(4) { FocusRequester() } } @@ -694,6 +697,7 @@ fun LazyVerticalGridTUO( val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState() val activeRowIndex by sharedScreenModel.activeIndex.collectAsStateWithLifecycle() + val abModeState by sharedScreenModel.abModeState.collectAsState() val measures = remember(tuoList, gridColumnCount) { tuoList.drop(1).chunked(gridColumnCount) } @@ -1107,20 +1111,26 @@ fun LazyVerticalGridTUO( selectedIndex = globalIndex showEditDialog = true } else { - val isAtStart = loopState.first == -1L - val isWaitingForB = loopState.first != -1L && !loopState.third - - if (isWaitingForB) { - val enableSmartB = prefs.getBoolean("smartBpoint", true) - val smartIndex = if (enableSmartB) { - sharedScreenModel.calculateSmartPointB(globalIndex, isZeroIndexed = true) - } else { - globalIndex + when (abModeState) { + ABModeState.WAITING_FOR_A -> { + sharedScreenModel.mediaPlayer?.setPointA(globalIndex) + sharedScreenModel.setABModeState(ABModeState.WAITING_FOR_B) + } + ABModeState.WAITING_FOR_B -> { + val enableSmartB = prefs.getBoolean("smartBpoint", true) + val smartIndex = if (enableSmartB) { + sharedScreenModel.calculateSmartPointB(globalIndex, isZeroIndexed = true) + } else { + globalIndex + } + sharedScreenModel.mediaPlayer?.setPointB(smartIndex) + + sharedScreenModel.setABModeState(ABModeState.LOOP_ACTIVE) + } + ABModeState.IDLE, ABModeState.LOOP_ACTIVE -> { + sharedScreenModel.seekToGrid(globalIndex) + sharedScreenModel.updateActiveIndexByIndex(globalIndex) } - sharedScreenModel.mediaPlayer?.setPointB(smartIndex) - } else { - sharedScreenModel.seekToGrid(globalIndex) - sharedScreenModel.updateActiveIndexByIndex(globalIndex) } } }, @@ -1146,7 +1156,8 @@ fun LazyVerticalGridTUO( parentFocusRequester = focusRequester, transpositionInterval = currentInterval, hairpinRange = hairpinByEndBlock[oneTUO.numBlock], - isInABrange = isInABrange + isInABrange = isInABrange, + abModeState = abModeState ) if (showContextualMenu && selectedIndex == globalIndex) { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt index 3c88dfc..5ad6c5a 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt @@ -1,6 +1,7 @@ package mg.dot.feufaro.ui import SharedScreenModel +import SharedScreenModel.ABModeState import androidx.compose.animation.* import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween @@ -361,19 +362,20 @@ fun MidiControlPanel( verticalAlignment = Alignment.CenterVertically, ) { Column { + val abModeState by sharedScreenModel.abModeState.collectAsState() val isAtStart = loopState.first == -1L - val isWaitingForB = loopState.first != -1L && !loopState.third - val isLooping = loopState.third + val isWaitingForB = abModeState == ABModeState.WAITING_FOR_B + val isLooping = abModeState == ABModeState.LOOP_ACTIVE + + val abButtonText = when (abModeState) { + ABModeState.WAITING_FOR_A -> "A ?" + ABModeState.WAITING_FOR_B -> "B ?" + ABModeState.IDLE, ABModeState.LOOP_ACTIVE -> "A B" + } OutlinedButton( onClick = { - when { - isAtStart -> mediaPlayer.setPointA(sharedScreenModel.activeIndex.value) - isWaitingForB -> mediaPlayer.setPointB(sharedScreenModel.activeIndex.value) - isLooping -> { - mediaPlayer.clearLoop() - } - } + sharedScreenModel.toggleABSelectionMode() }, colors = ButtonDefaults.buttonColors( containerColor = when { @@ -391,7 +393,7 @@ fun MidiControlPanel( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - text = "A", + text = abButtonText, color = if (isWaitingForB || isLooping) Color.White else Color.Black, fontSize = MaterialTheme.typography.titleLarge.fontSize ) @@ -402,12 +404,6 @@ fun MidiControlPanel( tint = if (isLooping) Color.White else Color.Black, modifier = Modifier.size(16.dp) ) - - Text( - text = "B", - color = if (isLooping) Color.White else Color.Black, - fontSize = MaterialTheme.typography.titleLarge.fontSize - ) } } } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index 859761c..8ffab14 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -451,9 +451,37 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode val loopingState: StateFlow = _loopingState.asStateFlow() fun updateLoopingState(a: Int, b: Int) { + updateActiveIndexByIndex(a) _loopingState.value = LoopRange(a, b) } + enum class ABModeState { + IDLE, + WAITING_FOR_A, + WAITING_FOR_B, + LOOP_ACTIVE + } + + private val _abModeState = MutableStateFlow(ABModeState.IDLE) + val abModeState: StateFlow = _abModeState.asStateFlow() + + fun toggleABSelectionMode() { + if(_isPlay.value) { + togglePlayPause() + } + _abModeState.value = when (_abModeState.value) { + ABModeState.IDLE -> ABModeState.WAITING_FOR_A + ABModeState.WAITING_FOR_A, ABModeState.WAITING_FOR_B -> ABModeState.IDLE + ABModeState.LOOP_ACTIVE -> { + mediaPlayer?.clearLoop() + ABModeState.IDLE + } + } + } + + fun setABModeState(state: ABModeState) { + _abModeState.value = state + } fun calculateSmartPointB(rawPointB: Int, isZeroIndexed: Boolean = false): Int { val currentMeasure = _measure.value val numerator = currentMeasure.substringBefore("/").toIntOrNull() ?: 4 diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt index 4fb08a6..8216b7c 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/midi/MidiPlayer.kt @@ -93,6 +93,15 @@ actual class FMediaPlayer actual constructor( loadSavedInstrumentsToPlayer(this) sequencer?.addMetaEventListener { meta -> if(meta.type == 47){ + if (isLoopingAB) { + val resolution = sequencer?.sequence?.resolution?.toDouble() ?: 60.0 + val gridA = (pointA / resolution).toInt() + seekToGrid(gridA) + if (sequencer?.isRunning == false) { + sequencer?.start() + } + return@addMetaEventListener + } val totalGrids = (sequencer!!.sequence.tickLength / sequencer!!.sequence.resolution).toInt() val currentGrid = (sequencer!!.tickPosition / sequencer!!.sequence.resolution).toInt() val pendingDc = getPendingDcStep() @@ -979,7 +988,12 @@ actual class FMediaPlayer actual constructor( while(isLoopingAB) { val currentTick = sequencer?.tickPosition?: 0L if (currentTick >= pointB) { - sequencer?.tickPosition = pointA + val resolution = sequencer?.sequence?.resolution?.toDouble() ?: 60.0 + val gridA = (pointA / resolution).toInt() + seekToGrid(gridA) + if (sequencer?.isRunning == false) { + sequencer?.start() + } } delay(50) }