849 lines
No EOL
30 KiB
Kotlin
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]
|
|
}
|
|
|
|
} |