596 lines
No EOL
21 KiB
Kotlin
596 lines
No EOL
21 KiB
Kotlin
import androidx.compose.runtime.State
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.setValue
|
|
import cafe.adriel.voyager.core.model.ScreenModel
|
|
import cafe.adriel.voyager.core.model.screenModelScope
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.SharingStarted
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
import kotlinx.coroutines.flow.mapLatest
|
|
import kotlinx.coroutines.flow.stateIn
|
|
import kotlinx.coroutines.launch
|
|
import mg.dot.feufaro.DeepLinkHandler
|
|
import mg.dot.feufaro.FileRepository
|
|
import mg.dot.feufaro.data.DrawerItem
|
|
import mg.dot.feufaro.data.GridTUOData
|
|
import mg.dot.feufaro.data.getDynamicDrawerItems
|
|
import mg.dot.feufaro.solfa.TimeUnitObject
|
|
import mg.dot.feufaro.midi.FMediaPlayer
|
|
import mg.dot.feufaro.viewmodel.MidiMarkers
|
|
|
|
class SharedScreenModel(private val fileRepository: FileRepository) : ScreenModel {
|
|
private val _nextLabel = MutableStateFlow<String>("Next ...")
|
|
val nextLabel: StateFlow<String> = _nextLabel.asStateFlow()
|
|
|
|
private val _loadFile = MutableStateFlow<String>("Load ...")
|
|
|
|
val loadFile: StateFlow<String> = _loadFile.asStateFlow()
|
|
private val _measure = MutableStateFlow<String>("")
|
|
val measure: StateFlow<String> = _measure.asStateFlow()
|
|
private val _songTitle = MutableStateFlow<String>("")
|
|
val songTitle: StateFlow<String> = _songTitle.asStateFlow()
|
|
private val _stanza = MutableStateFlow<Int>(0)
|
|
val stanza: StateFlow<Int> = _stanza.asStateFlow()
|
|
private val _songKey = MutableStateFlow<String>("")
|
|
val songKey: StateFlow<String> = _songKey.asStateFlow()
|
|
private val _transposeTo = MutableStateFlow("")
|
|
val transposeTo: StateFlow<String> = _transposeTo.asStateFlow()
|
|
private val _transposeAsIf = MutableStateFlow("")
|
|
val transposeAsIf: StateFlow<String> = _transposeAsIf.asStateFlow()
|
|
private val _songAuthor = MutableStateFlow<String>("")
|
|
val songAuthor: StateFlow<String> = _songAuthor.asStateFlow()
|
|
private val _songComposer = MutableStateFlow<String>("")
|
|
val songComposer: StateFlow<String> = _songComposer.asStateFlow()
|
|
private val _songRhythm = MutableStateFlow<String>("")
|
|
val songRhythm: StateFlow<String> = _songRhythm.asStateFlow()
|
|
private val _nbStanzas = MutableStateFlow<Int>(0)
|
|
val nbStanzas: StateFlow<Int> = _nbStanzas.asStateFlow()
|
|
private var _tuoList =
|
|
MutableStateFlow<List<TimeUnitObject>>(emptyList())
|
|
val tuoList: StateFlow<List<TimeUnitObject>> = _tuoList.asStateFlow()
|
|
private var _playlist =
|
|
MutableStateFlow<List<String>>(emptyList())
|
|
val playlist: StateFlow<List<String>> = _playlist.asStateFlow()
|
|
private var _nextPlayed = MutableStateFlow(0)
|
|
val nextPlayed: StateFlow<Int> = _nextPlayed.asStateFlow()
|
|
private val tempTimeUnitObjectList = mutableListOf<TimeUnitObject>()
|
|
var _hasMarker = MutableStateFlow<Boolean>(false)
|
|
val hasMarker: StateFlow<Boolean> = _hasMarker.asStateFlow()
|
|
|
|
private val _searchTitle = MutableStateFlow<String>("")
|
|
val searchTitle: StateFlow<String> = _searchTitle.asStateFlow()
|
|
|
|
private val _drawerItems = MutableStateFlow<List<DrawerItem>>(emptyList())
|
|
val drawerItems: StateFlow<List<DrawerItem>> = _drawerItems.asStateFlow()
|
|
|
|
init {
|
|
loadItems()
|
|
}
|
|
|
|
private fun loadItems() {
|
|
screenModelScope.launch {
|
|
_drawerItems.value = getDynamicDrawerItems()
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalCoroutinesApi::class)
|
|
val filteredSongs: StateFlow<List<DrawerItem>> = searchTitle
|
|
.mapLatest { currentTitle ->
|
|
val searchTxt = if (currentTitle == "") "" else currentTitle.trim()
|
|
return@mapLatest if(searchTxt.isEmpty()) {
|
|
emptyList()
|
|
} else {
|
|
drawerItems.value.filter { item ->
|
|
item.contentTitle.contains(searchTxt, ignoreCase = true)
|
|
}
|
|
}
|
|
}.stateIn (
|
|
scope = CoroutineScope(Dispatchers.Default),
|
|
started = SharingStarted.WhileSubscribed(5000),
|
|
initialValue = emptyList()
|
|
)
|
|
private val _fileContent = mutableStateOf<String?>(null)
|
|
val fileContent: State<String?> = _fileContent
|
|
|
|
private val _isQRCodeVisible = mutableStateOf(false)
|
|
val isQRCodeVisible: State<Boolean> = _isQRCodeVisible
|
|
|
|
private val _activeFilePath = mutableStateOf("")
|
|
val activeFilePath: State<String> = _activeFilePath
|
|
|
|
fun setFileContent(content: String?, path: String) {
|
|
_fileContent.value = content
|
|
_activeFilePath.value = path
|
|
}
|
|
fun toggleQRCodeVisibility() {
|
|
_isQRCodeVisible.value = !_isQRCodeVisible.value
|
|
}
|
|
val qrCodeContent: State<String?>
|
|
get() = mutableStateOf(
|
|
_activeFilePath.value.takeIf { it.isNotBlank() }?.let { path ->
|
|
val fileName = path.substringAfterLast("/").substringAfterLast("\\")
|
|
"feufaro://song?file=${fileName}"
|
|
}
|
|
)
|
|
|
|
fun updateSearchTxt(searchValue: String) {
|
|
_searchTitle.value = searchValue
|
|
}
|
|
private val _showMidiCtrl = MutableStateFlow<Boolean>(false)
|
|
val showMidiCtrl: StateFlow<Boolean> = _showMidiCtrl.asStateFlow()
|
|
fun setMidiCtrl(value: Boolean) {
|
|
_showMidiCtrl.value = value
|
|
}
|
|
|
|
private val _expandedFAB = MutableStateFlow<Boolean>(false)
|
|
val expandedFAB: StateFlow<Boolean> = _expandedFAB.asStateFlow()
|
|
|
|
fun setExpandedFAB(value: Boolean) {
|
|
_expandedFAB.value = value
|
|
}
|
|
|
|
private val _showSearchMenu = MutableStateFlow<Boolean>(false)
|
|
val showSearchMenu: StateFlow<Boolean> = _showSearchMenu.asStateFlow()
|
|
fun showSearchMenu(value: Boolean) {
|
|
_showSearchMenu.value = value
|
|
}
|
|
|
|
private var _mediaPlayer by mutableStateOf<FMediaPlayer?>(null)
|
|
val mediaPlayer: FMediaPlayer? get() = _mediaPlayer
|
|
|
|
private val _isPlay = MutableStateFlow(false)
|
|
val isPlay = _isPlay.asStateFlow()
|
|
|
|
private val _isPos = MutableStateFlow(true)
|
|
val isPos = _isPos.asStateFlow()
|
|
|
|
private val _isDragging = MutableStateFlow(true)
|
|
val isDragging = _isDragging.asStateFlow()
|
|
|
|
private val _currentPos = MutableStateFlow(0f)
|
|
val currentPos = _currentPos.asStateFlow()
|
|
private val _duration = MutableStateFlow(0f)
|
|
val duration = _duration.asStateFlow()
|
|
|
|
private val _volumeLevel = MutableStateFlow(0.8f)
|
|
val volumeLevel = _volumeLevel.asStateFlow()
|
|
|
|
private val _isPlayMid = MutableStateFlow(false)
|
|
val isPlayMid = _isPlayMid.asStateFlow()
|
|
|
|
private val _canUpdPositionFromPartition = MutableStateFlow(false)
|
|
val canUpdPositionFromPartition = _canUpdPositionFromPartition.asStateFlow()
|
|
|
|
private val _dcDone = MutableStateFlow(false)
|
|
val dcDone = _dcDone.asStateFlow()
|
|
|
|
private val _dsDone = MutableStateFlow(false)
|
|
val dsDone = _dsDone.asStateFlow()
|
|
|
|
private val _tuoTimestamps = MutableStateFlow<List<Long>>(emptyList())
|
|
val tuoTimestamps: StateFlow<List<Long>> = _tuoTimestamps
|
|
|
|
fun updateTimestamps(list: List<Long>) {
|
|
_tuoTimestamps.value = list
|
|
}
|
|
|
|
fun seekToTimestamp(micros: Long) {
|
|
_mediaPlayer?.seekTo(micros / 1000)
|
|
}
|
|
|
|
private val _midiMarkersList = MutableStateFlow<List<MidiMarkers>>(emptyList())
|
|
val midiMarkersList: StateFlow<List<MidiMarkers>> = _midiMarkersList
|
|
private val _activeIndex = MutableStateFlow(-1)
|
|
val activeIndex: StateFlow<Int> = _activeIndex.asStateFlow()
|
|
|
|
val currentGridData: GridTUOData
|
|
get() = GridTUOData(
|
|
tuoList = _tuoList.value,
|
|
measure = _measure.value,
|
|
stanza = _stanza.value
|
|
)
|
|
fun updateActiveIndex(currentPosMs: Long) {
|
|
val currentPosMicros = currentPosMs * 1000
|
|
val index = _tuoTimestamps.value.indexOfLast { it <= currentPosMicros }
|
|
if (_activeIndex.value != index) {
|
|
_activeIndex.value = index.coerceAtLeast(0)
|
|
}
|
|
}
|
|
fun updateActiveIndexByIndex(index: Int) {
|
|
val clamped = index.coerceAtLeast(0)
|
|
if (_activeIndex.value != clamped) {
|
|
_activeIndex.value = clamped
|
|
}
|
|
}
|
|
private val _gridCount = MutableStateFlow(0)
|
|
val gridCount: StateFlow<Int> = _gridCount.asStateFlow()
|
|
|
|
fun addGridCount(nbGrid: Int) {
|
|
_gridCount.value += nbGrid
|
|
}
|
|
fun descGridCount(nbGrid: Int) {
|
|
if(_gridCount.value > 0) {
|
|
_gridCount.value -= nbGrid
|
|
}
|
|
}
|
|
fun resetGridCount() {
|
|
_gridCount.value = 0
|
|
}
|
|
fun seekToGrid(gridIndex: Int) {
|
|
_mediaPlayer?.seekToGrid(gridIndex)
|
|
}
|
|
|
|
fun getFullMarkers(): List<MidiMarkers> {
|
|
return _midiMarkersList.value
|
|
}
|
|
|
|
fun setDcDone(state: Boolean) {
|
|
_dcDone.value = state
|
|
}
|
|
fun setDsDone(state: Boolean) {
|
|
_dsDone.value = state
|
|
}
|
|
fun getDcDone(): Boolean {
|
|
return _dcDone.value
|
|
}
|
|
fun getDsDone(): Boolean {
|
|
return _dsDone.value
|
|
}
|
|
|
|
fun getTotalGridCount(): Int {
|
|
return _tuoList.value.drop(1).size - 1
|
|
}
|
|
fun updateAndFinalizeMidiData(rawList: List<MidiMarkers>) {
|
|
// val timestamps = _tuoTimestamps.value
|
|
val tuos = _tuoList.value.drop(1)
|
|
|
|
val finalizedList = rawList.map { marker ->
|
|
var markerText = marker.marker
|
|
val index = marker.gridIndex ?: 0 // On utilise l'index passé par l'UI
|
|
val isNearEnd = index >= (tuos.size - 2)
|
|
|
|
val isDC = markerText.contains(Regex("""D\.?C\.?"""))
|
|
val isDS = markerText.contains(Regex("""D\.?S\.?"""))
|
|
val isFarany = markerText.trim().equals("Farany", ignoreCase = true)
|
|
|
|
val isRit = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
|
|
|
|
var resultMarker: MidiMarkers = marker
|
|
if(isFarany) {
|
|
var forwardIndex = index
|
|
while (forwardIndex < tuos.size) {
|
|
val sep = tuos.getOrNull(forwardIndex)?.sep0 ?: ""
|
|
if (sep == "/") {
|
|
resultMarker = marker.copy(
|
|
gridIndex = forwardIndex - 1,
|
|
marker = "Farany_GROUP_PART"
|
|
)
|
|
println("Farany finalisé : grille $index → ${forwardIndex - 1}")
|
|
break
|
|
}
|
|
forwardIndex++
|
|
}
|
|
} else if (isDC) {
|
|
var forwardIndex = index
|
|
while (forwardIndex < tuos.size) {
|
|
val currentTuo = tuos.getOrNull(forwardIndex)
|
|
val currentNote = currentTuo?.tuNotes?.getOrNull(1)?.toString() ?: ""
|
|
val currentSep = currentTuo?.sep0 ?: ""
|
|
|
|
// println("Je suis sur $forwardIndex note $currentNote Sep $currentSep \t condition: ${(currentNote == "―")} || ${(currentSep == "/")}")
|
|
// Tant que fin de mesure
|
|
if (currentSep == "/") {
|
|
resultMarker = marker.copy(
|
|
gridIndex = forwardIndex-1,
|
|
marker = "${markerText.trim()}_GROUP_PART"
|
|
)
|
|
break
|
|
}
|
|
forwardIndex++
|
|
}
|
|
|
|
} else if (isDS && isNearEnd) {
|
|
val currentNote = tuos.getOrNull(index)?.tuNotes?.getOrNull(1)?.toString() ?: ""
|
|
val nextNote = tuos.getOrNull(index + 1)?.tuNotes?.getOrNull(1)?.toString() ?: ""
|
|
|
|
// Cas où le marker et la note suivante sont vides
|
|
val isEmptySituation = currentNote.trim().isEmpty() && nextNote.trim().isEmpty()
|
|
|
|
if (isEmptySituation) {
|
|
// vers arrière une note non vide
|
|
var backwardIndex = index - 1
|
|
while (backwardIndex >= 0) {
|
|
val note = tuos.getOrNull(backwardIndex)?.tuNotes?.getOrNull(1)?.toString() ?: ""
|
|
if (note.trim().isNotEmpty()) {
|
|
val cleanText = if (markerText.trim() == "DSFin") "DS" else markerText.trim()
|
|
|
|
resultMarker = marker.copy(
|
|
gridIndex = backwardIndex,
|
|
marker = "${cleanText}_GROUP_PART"
|
|
)
|
|
break
|
|
}
|
|
backwardIndex--
|
|
}
|
|
} else {
|
|
// vers avant le séparateur "/"
|
|
var forwardIndex = index
|
|
while (forwardIndex < tuos.size) {
|
|
val sep = tuos.getOrNull(forwardIndex)?.sep0 ?: ""
|
|
if (sep == "/") {
|
|
val cleanText = if (markerText.trim() == "DSFin") "DS" else markerText.trim()
|
|
|
|
resultMarker = marker.copy(
|
|
gridIndex = forwardIndex,
|
|
marker = "${cleanText}_GROUP_PART"
|
|
)
|
|
break
|
|
}
|
|
forwardIndex++
|
|
}
|
|
}
|
|
} else if(isRit.containsMatchIn(markerText)) {
|
|
var forwardIndex = index + 1
|
|
var foundSeparator = false
|
|
|
|
while (forwardIndex < tuos.size) {
|
|
val sep = tuos.getOrNull(forwardIndex)?.sep0 ?: ""
|
|
if (sep == "/") {
|
|
resultMarker = marker.copy(
|
|
gridIndex = index,
|
|
lastCallerMarker = forwardIndex - 1,
|
|
marker = "Ritenuto"
|
|
)
|
|
println("Rit finalisé : grille $index → ${forwardIndex - 1}")
|
|
foundSeparator = true
|
|
break
|
|
}
|
|
forwardIndex++
|
|
}
|
|
} else {
|
|
marker
|
|
}
|
|
resultMarker
|
|
}
|
|
|
|
_midiMarkersList.value = finalizedList
|
|
println("Markers finalisés et mis à jour : ${finalizedList.size}")
|
|
}
|
|
|
|
fun loadNewSong(newMidiFile: String) {
|
|
_mediaPlayer?.stop()
|
|
_mediaPlayer?.release()
|
|
_mediaPlayer = null
|
|
_isPos.value = true
|
|
_isPlay.value = false
|
|
_currentPos.value = 0f
|
|
_dcDone.value = false
|
|
_dsDone.value = false
|
|
try {
|
|
val midiFileName = fileRepository.getFileName(newMidiFile)
|
|
println("Opening xx129 $midiFileName")
|
|
_mediaPlayer = FMediaPlayer(filename = midiFileName, onFinished = {
|
|
// _isPos.value = true
|
|
// _isPlay.value = false
|
|
_currentPos.value = 0f
|
|
seekToGrid(0)
|
|
_mediaPlayer?.stop()
|
|
setDcDone(false)
|
|
setDsDone(false)
|
|
println("fin de lecture du Midi $newMidiFile")
|
|
_mediaPlayer?.syncNavigationMonitor(this)
|
|
})
|
|
// synchro
|
|
_mediaPlayer?.requestSync(this)
|
|
//finalizeMarkers()
|
|
_mediaPlayer?.syncNavigationMonitor(this)
|
|
} catch(e: Exception) {
|
|
println("Erreur d'ouverture de mediaPlayer : ${e.message} ")
|
|
}
|
|
println("New media Player crée $newMidiFile")
|
|
}
|
|
|
|
fun togglePlayPause() {
|
|
_canUpdPositionFromPartition.value=true
|
|
_mediaPlayer?.let { player ->
|
|
if (_isPlay.value) {
|
|
_isPlay.value = false
|
|
_isPos.value = true
|
|
player.pause()
|
|
} else {
|
|
_isPlay.value = true
|
|
_isPos.value = false
|
|
player.play()
|
|
player.setVolume(_volumeLevel.value)
|
|
if(currentPos.value == 0f) {
|
|
player.seekTo(0)
|
|
}
|
|
}
|
|
// println("128: Status de isPlay ${_isPlay.value} \nisPos ${_isPos.value} \ncurrentPos ${_currentPos.value} \n volume ${_volumeLevel.value}")
|
|
}
|
|
}
|
|
fun stopMidi() {
|
|
_mediaPlayer?.let { player ->
|
|
_isPlay.value = false
|
|
_isPos.value = true
|
|
player.pause()
|
|
}
|
|
}
|
|
fun seekTo(pos: Float) {
|
|
_currentPos.value = pos
|
|
_mediaPlayer?.let { player ->
|
|
player.seekTo(pos.toLong())
|
|
}
|
|
}
|
|
|
|
fun setDragging(dragState: Boolean) {
|
|
_isDragging.value = dragState
|
|
}
|
|
fun setVolume(level: Float) {
|
|
_volumeLevel.value = level
|
|
_mediaPlayer?.let { player ->
|
|
player.setVolume(level) }
|
|
}
|
|
fun updateProgress(){
|
|
_mediaPlayer?.let { player->
|
|
if (_isPlay.value) {
|
|
val p = player.getCurrentPosition().toFloat()
|
|
val d = player.getDuration().toFloat()
|
|
|
|
if (p >= 0) _currentPos.value = p
|
|
if ((d > 0) && _duration.value != d) _duration.value = d
|
|
}
|
|
}
|
|
}
|
|
var currentNoteIndex by mutableStateOf(0f)
|
|
|
|
fun updatePositionFromPartition(index: Int, totalRow: Int) {
|
|
if(_canUpdPositionFromPartition.value) { /*upd pos part only if midi on playing*/
|
|
val duration = _duration.value
|
|
if(totalRow > 0) {
|
|
currentNoteIndex = index.toFloat()
|
|
val newPos = (currentNoteIndex / totalRow.toFloat()) * duration
|
|
seekTo(newPos)
|
|
println("Shared:196 currentNoteIndex $currentNoteIndex, Index $index et curret = ${_currentPos.value}")
|
|
}
|
|
}
|
|
}
|
|
|
|
fun appendData(otherData: String) {
|
|
_nextLabel.value += otherData
|
|
}
|
|
|
|
fun reset() {
|
|
// Close other menus
|
|
_showMidiCtrl.value = false
|
|
_expandedFAB.value = false
|
|
_showSearchMenu.value = false
|
|
_midiMarkersList.value = emptyList()
|
|
_tuoTimestamps.value = emptyList()
|
|
updateSearchTxt("")
|
|
tempTimeUnitObjectList.clear()
|
|
resetGridCount()
|
|
}
|
|
|
|
fun lastTUO(): TimeUnitObject? {
|
|
return tempTimeUnitObjectList.lastOrNull()
|
|
}
|
|
|
|
fun addTUO(newTUO: TimeUnitObject) {
|
|
tempTimeUnitObjectList.add(newTUO)
|
|
}
|
|
|
|
fun doneTUOList() {
|
|
_tuoList.value = tempTimeUnitObjectList.toList()
|
|
}
|
|
|
|
fun setMeasure(theMeasure: String) {
|
|
_measure.value = theMeasure
|
|
}
|
|
|
|
fun setSongTitle(theTitle: String) {
|
|
_songTitle.value = theTitle
|
|
}
|
|
|
|
fun setStanza(theStanza: Int) {
|
|
try {
|
|
_stanza.value = theStanza
|
|
} catch (e: NumberFormatException) {
|
|
_stanza.value = 0
|
|
}
|
|
}
|
|
|
|
fun setSongKey(theSongKey: String) {
|
|
_songKey.value = theSongKey
|
|
}
|
|
|
|
fun setTransposeTo(key: String) {
|
|
_transposeTo.value = key
|
|
}
|
|
|
|
fun setTransposeAsIf(key: String) {
|
|
_transposeAsIf.value = key
|
|
}
|
|
|
|
fun setSongAuthor(theSongAuthor: String) {
|
|
_songAuthor.value = theSongAuthor
|
|
}
|
|
|
|
fun setSongComposer(theSongComposer: String) {
|
|
_songComposer.value = theSongComposer
|
|
}
|
|
|
|
fun setSongRhythm(theSongRhythm: String) {
|
|
_songRhythm.value = theSongRhythm
|
|
}
|
|
|
|
fun setNbStanzas(nbStanzas: Int) {
|
|
_nbStanzas.value = nbStanzas
|
|
}
|
|
|
|
fun setPlaylist(thePlaylist: List<String>) {
|
|
_playlist.value = thePlaylist
|
|
}
|
|
|
|
fun setHasMarker(theHasMarker: Boolean) {
|
|
_hasMarker.value = theHasMarker
|
|
|
|
}
|
|
data class HairPinData(
|
|
val symbol: Char,
|
|
val startGrid: Int,
|
|
val endGrid: Int,
|
|
)
|
|
|
|
fun getHairPins(): List<HairPinData> {
|
|
val allMarkers = _midiMarkersList.value
|
|
val starts = allMarkers
|
|
.filter {
|
|
val m = it.marker.trim()
|
|
m == "<" ||
|
|
m == ">" ||
|
|
m.contains("cres", ignoreCase = true) ||
|
|
m.contains("dim", ignoreCase = true)
|
|
}
|
|
.sortedBy { it.gridIndex }
|
|
val ends = allMarkers
|
|
.filter { it.marker.trim().endsWith("=") }
|
|
.sortedBy { it.gridIndex }
|
|
.toMutableList()
|
|
|
|
// println("HairPins starts: ${starts.size} | ends: ${ends.size}")
|
|
return starts.mapNotNull { start ->
|
|
val startGrid = start.gridIndex ?: return@mapNotNull null
|
|
val symbol = when {
|
|
start.marker.trim() == "<" -> '<'
|
|
start.marker.trim() == ">" -> '>'
|
|
start.marker.contains("cres", ignoreCase = true) -> '<'
|
|
start.marker.contains("dim", ignoreCase = true) -> '>'
|
|
else -> return@mapNotNull null
|
|
}
|
|
|
|
val matchingEnd = ends.firstOrNull { (it.gridIndex ?: 0) > startGrid }
|
|
if (matchingEnd != null) ends.remove(matchingEnd)
|
|
val endGrid = matchingEnd?.gridIndex ?: (startGrid + 4)
|
|
// println("HairPin '$symbol' : $startGrid → $endGrid")
|
|
HairPinData(symbol, startGrid, endGrid)
|
|
}
|
|
}
|
|
|
|
fun playNext() {
|
|
val nextIndex = (_nextPlayed.value + 1) % _playlist.value.size
|
|
_nextPlayed.value = nextIndex
|
|
setStanza(1)
|
|
}
|
|
|
|
fun currentPlayed(): String {
|
|
val playlistIndex = _nextPlayed.value
|
|
return _playlist.value[playlistIndex]
|
|
}
|
|
|
|
} |