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

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]
}
}