feufaro/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt

849 lines
No EOL
30 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.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.data.DrawerItem
import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.data.getCombinedList
import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.midi.FMediaPlayer
import mg.dot.feufaro.solfa.TUOEditState
import mg.dot.feufaro.solfa.getAllMarker
import mg.dot.feufaro.viewmodel.MidiMarkers
import java.io.File
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()
val internalItems: StateFlow<List<DrawerItem>> = _drawerItems
.map { list -> list.filter { it.path.startsWith("assets://") } }
.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
val externalItems: StateFlow<List<DrawerItem>> = _drawerItems
.map { list ->
list.filter { !it.path.startsWith("assets://") }
.sortedByDescending { item: DrawerItem ->
try {
File(item.path).lastModified()
} catch (e: Exception) {
0L
}
}
}
.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
private val _measureInt = MutableStateFlow<Int>(4)
val measureInt: StateFlow<Int> = _measureInt.asStateFlow()
fun setMeasureInt(measure: Int) {
_measureInt.value = measure
}
private val _silentDurationBefore = MutableStateFlow<Int>(0)
val silentDurationBefore: StateFlow<Int> = _silentDurationBefore.asStateFlow()
fun setSilentDurBefr(silent: Int) {
_silentDurationBefore.value = silent
}
private val playlistFilename = "playlist.json"
private val _playlistPaths = MutableStateFlow<List<String>>(emptyList())
val playlistItems: StateFlow<List<DrawerItem>> = combine(_playlistPaths, _drawerItems) { paths, allItems ->
paths.mapNotNull { path -> allItems.find { it.path == path } }
}.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
fun savePlaylistToDisk(playlist: List<String>) {
screenModelScope.launch {
try {
val itemsJson = playlist.joinToString(
separator = "\",\"",
prefix = "[\"",
postfix = "\"]"
).replace("[\"\"]", "[]")
val finalJson = "{\"playlist\": $itemsJson}"
val data = finalJson.encodeToByteArray()
fileRepository.saveLocalFile(playlistFilename, data)
println("Playlist sauvegardée avec succès !")
} catch (e: Exception) {
println("Erreur lors de la sauvegarde : ${e.message}")
}
}
}
fun toggleFavorite(path: String) {
val current = _playlistPaths.value.toMutableList()
if (current.contains(path)) {
current.remove(path)
} else {
current.add(path)
}
_playlistPaths.value = current
savePlaylistToDisk(current)
}
fun moveUp(index: Int) {
if (index > 0) {
val current = _playlistPaths.value.toMutableList()
val item = current.removeAt(index)
current.add(index - 1, item)
_playlistPaths.value = current
savePlaylistToDisk(current)
}
}
fun moveToTop(index: Int) {
val current = _playlistPaths.value.toMutableList()
val item = current.removeAt(index)
current.add(0, item)
_playlistPaths.value = current
savePlaylistToDisk(current)
}
fun moveDown(index: Int) {
val current = _playlistPaths.value.toMutableList()
if (index < current.size - 1) {
val item = current.removeAt(index)
current.add(index + 1, item)
_playlistPaths.value = current
savePlaylistToDisk(current)
}
}
fun isInPlaylist(path: String): Boolean {
return _playlistPaths.value.contains(path)
}
init {
loadItems()
}
fun loadItems() {
screenModelScope.launch {
_drawerItems.value = getCombinedList(fileRepository)
}
loadPlaylistFromDisk()
}
private fun loadPlaylistFromDisk() {
screenModelScope.launch {
try {
val publicFolder = fileRepository.getAppPublicFolder()
val playlistFilePath = "$publicFolder/$playlistFilename"
val jsonString = fileRepository.readFileContent(playlistFilePath)
if (jsonString.isNotBlank()) {
val paths = jsonString
.replace("{\"playlist\":", "")
.replace("}", "")
.replace("[", "")
.replace("]", "")
.replace("\"", "")
.split(",")
.map { it.trim() }
.filter { it.isNotBlank() }
_playlistPaths.value = paths
println("Playlist chargée : ${paths.size} chants")
}
} catch (e: Exception) {
println("Erreur lors du chargement de la playlist : ${e.message}")
}
}
}
@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 _sourceModeState = mutableStateOf(false)
val sourceModeState: State<Boolean> = _sourceModeState
data class HairPinEditState(
val isPending: Boolean = false,
val gridIndex: Int = 0
)
private val _isPendingHairPinOnEdit = MutableStateFlow(HairPinEditState())
val isPendingHairPinOnEdit: StateFlow<HairPinEditState> = _isPendingHairPinOnEdit.asStateFlow()
fun setPendingHairPin(isPending: Boolean, gridIdx: Int = 0) {
_isPendingHairPinOnEdit.value = HairPinEditState(isPending, gridIdx)
}
private val _modeEditor = MutableStateFlow(false)
val modeEditor: StateFlow<Boolean> = _modeEditor
fun toggleEditorMode(enabled: Boolean) {
_modeEditor.value = enabled
}
private val _createMode = MutableStateFlow(false)
val createMode: StateFlow<Boolean> = _createMode
fun toggleCreateMode(state: Boolean) {
_createMode.value = state
}
private val _harmonyView = MutableStateFlow(false)
val harmonyView: StateFlow<Boolean> = _harmonyView
fun toggleHarmonyMode(enabled: Boolean) {
_harmonyView.value = enabled
}
private val _isFullScreen = MutableStateFlow(false)
val isFullScreen = _isFullScreen.asStateFlow()
fun toggleFullScreen(enabled: Boolean) {
_isFullScreen.value = enabled
}
val transpositionInterval = MutableStateFlow(0)
fun setTranspositionInterval(interval: Int) {
transpositionInterval.value = interval
}
private val _synchronizedSYllables = MutableStateFlow<List<String>>(emptyList())
val synchronizedSyllables: StateFlow<List<String>> = _synchronizedSYllables.asStateFlow()
fun updateSyllablesFromList(measures: List<List<TimeUnitObject>>, stanzaNumber: Int) {
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
// println("\n--- DEBUG updateSyllablesFromList (Strophe $stanzaNumber) ---")
var absoluteIndex = 0
_synchronizedSYllables.value = measures.flatten().map { tuo ->
val rawSyllablesList = tuo.getSingleSyllable(stanzaNumber)
val processedSyllable = rawSyllablesList.joinToString(" ") { syllable ->
val cleaned = syllable.replace(REGEX_CLEAN_PREFIX, "").trim()
cleaned
}
// println("TUO #$absoluteIndex | Template: ${tuo.pTemplate.template} | Raw: $rawSyllablesList | Final: [$processedSyllable]")
absoluteIndex++
processedSyllable
}
// println("--- FIN DEBUG (Total TUOs: $absoluteIndex) ---\n")
}
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
}
fun toggleSourceMode() {
_sourceModeState.value = !_sourceModeState.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 parseMarkers(rawText: String): MutableList<String> {
val tokens = mutableListOf<String>()
if (rawText.isBlank()) return tokens
var remaining = rawText.replace(",", " ")
.replace(Regex("""\b(DC)\b""", RegexOption.IGNORE_CASE), "D.C.")
.replace(Regex("""\b(DS)\b""", RegexOption.IGNORE_CASE), "D.S.")
.replace(Regex("""(D\.?C\.?)\s*(Fin)""", RegexOption.IGNORE_CASE), "D.C.Fin")
.replace(Regex("""(D\.?S\.?)\s*(Fin)""", RegexOption.IGNORE_CASE), "D.S.Fin")
.replace(Regex("""(D\.?C\.?)\s*(al\s*Coda)""", RegexOption.IGNORE_CASE), "D.C.alCoda")
.replace(Regex("""(D\.?S\.?)\s*(al\s*Coda)""", RegexOption.IGNORE_CASE), "D.S.alCoda")
val allStaticMarkers = getAllMarker()
val tempoRegex = Regex("""^(♩\s*=\s*\d+)""", RegexOption.IGNORE_CASE)
val modulaRegex = Regex("""^(D(?:ô|o)?\s*dia\s*(?:C|Db|D|Eb|E|F|Gb|G|Ab|A|Bb|B))""", RegexOption.IGNORE_CASE)
// Lexer
while (remaining.isNotEmpty()) {
remaining = remaining.trimStart()
if (remaining.isEmpty()) break
val tempoMatch = tempoRegex.find(remaining)
if (tempoMatch != null) {
tokens.add(tempoMatch.value)
remaining = remaining.substring(tempoMatch.value.length)
continue
}
val modulaMatch = modulaRegex.find(remaining)
if (modulaMatch != null) {
tokens.add(modulaMatch.value)
remaining = remaining.substring(modulaMatch.value.length)
continue
}
val matchedMarker = allStaticMarkers.find { remaining.startsWith(it, ignoreCase = true) }
if (matchedMarker != null) {
tokens.add(matchedMarker)
remaining = remaining.substring(matchedMarker.length)
} else {
val unknownChunk = remaining.takeWhile { !it.isWhitespace() }
tokens.add(unknownChunk)
remaining = remaining.substring(unknownChunk.length)
}
}
// println("Sortie: ${tokens.joinToString(" | ")}")
return tokens
}
fun updateAndFinalizeMidiData(rawList: List<MidiMarkers>) {
// val timestamps = _tuoTimestamps.value
val tuos = _tuoList.value.drop(1)
val hasDCDS = rawList.any { midiMarker ->
midiMarker.marker.any { text ->
text.contains(Regex("""D\.?C\.?|D\.?S\.?""", RegexOption.IGNORE_CASE))
}
}
val finalizedList = rawList.map { marker ->
val originalMarker = marker.marker
var currentMarker = marker
var newGridIndex = marker.gridIndex ?: 0
val newMarkerList = mutableListOf<String>()
originalMarker.forEach { markerText ->
val index = marker.gridIndex ?: 0 // On utilise l'index passé par l'UI
val isNearEnd = index >= (tuos.size - 2)
// println("MARKERS:> $markerText\n")
val isDC = markerText.trim().matches(Regex("""D\.?C\.?"""))
val isDS = markerText.trim().matches(Regex("""D\.?S\.?"""))
val isFarany = markerText.trim().matches(Regex("""^(fine|fin|farany|end)$""", RegexOption.IGNORE_CASE))
val isDSFin = markerText.trim().contains(Regex("""D\.?S\.?""", RegexOption.IGNORE_CASE)) && markerText.trim().contains(Regex("""(fine|fin|farany|end)""", RegexOption.IGNORE_CASE))
val isRit = Regex("""rit\.?|ritard\.?|ritenuto\.?|ritardando""", RegexOption.IGNORE_CASE)
val isRall = Regex("""rall\.?|rallent\.?|rallentando""", RegexOption.IGNORE_CASE)
var searchIndex = index + 1
var lastValidIndex = index
while (searchIndex < tuos.size && tuos.getOrNull(searchIndex)?.tuNotes?.getOrNull(1)?.toString() == "") {
lastValidIndex = searchIndex
searchIndex++
}
val targetIndex = if (lastValidIndex > index) lastValidIndex else index
when {
isDSFin -> {
newGridIndex = targetIndex
newMarkerList.add("${markerText.trim()}")
}
isFarany -> {
newGridIndex = if(hasDCDS) {
targetIndex
} else {
index
}
newMarkerList.add("Farany")
}
isDC -> {
newGridIndex = targetIndex
newMarkerList.add("${markerText.trim()}")
}
isDS -> {
newGridIndex = targetIndex
newMarkerList.add("${markerText.trim()}")
}
isRit.containsMatchIn(markerText) -> {
currentMarker = currentMarker.copy(lastCallerMarker = targetIndex)
newMarkerList.add("Ritenuto")
}
isRall.containsMatchIn(markerText) -> {
currentMarker = currentMarker.copy(lastCallerMarker = targetIndex)
newMarkerList.add("Rallentando")
}
else -> {
newMarkerList.add(markerText)
}
}
}
currentMarker.copy(
gridIndex = newGridIndex,
marker = newMarkerList
)
}
_midiMarkersList.value = finalizedList
println("Markers finalisés et mis à jour : ${finalizedList.size} ${finalizedList.joinToString(", ")}")
}
private val _tuoEditState = MutableStateFlow<TUOEditState?>(null)
val tuoEditState: StateFlow<TUOEditState?> = _tuoEditState.asStateFlow()
fun openTUOEditor(editState: TUOEditState) {
_tuoEditState.value = editState
println("nMrk = [${editState.marker}] template [${editState.templateFragment}]")
}
fun closeTUOEditor() {
_tuoEditState.value = null
}
fun updateTUOEdit(new: TUOEditState) {
_tuoEditState.value = new
}
fun updateFullTUOList(newList: List<TimeUnitObject>) {
_tuoList.value = newList
}
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
_sourceModeState.value = false
setTranspositionInterval(0)
try {
val midiFileName = fileRepository.getFileName(newMidiFile)
println("Opening xx129 $midiFileName")
_mediaPlayer = FMediaPlayer(filename = midiFileName, sharedScreenModel = this, 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
_sourceModeState.value = false
_harmonyView.value = false
setTranspositionInterval(0)
setSilentDurBefr(0)
_midiMarkersList.value = emptyList()
_tuoTimestamps.value = emptyList()
_searchTitle.value = ""
_isPendingHairPinOnEdit.value = HairPinEditState(false, 0)
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
_mediaPlayer?.syncNavigationMonitor(this)
} 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
fun isStartToken(s: String): Boolean {
val m = s.trim()
return m == "<" ||
m == ">" ||
m.contains("cres", ignoreCase = true) ||
m.contains("dim", ignoreCase = true)
}
val starts = allMarkers
.filter { markerObj ->
markerObj.marker.any { isStartToken(it) }
}
.sortedBy { it.gridIndex }
val ends = allMarkers
.filter { markerObjt ->
markerObjt.marker.any { it.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 startString = start.marker.firstOrNull { isStartToken(it) } ?: return@mapNotNull null
val symbol = when {
startString.contains("<") || startString.contains("cres", ignoreCase = true) -> '<'
startString.contains(">") || startString.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]
}
}