Compare commits
2 commits
c06053c5dc
...
4940608a9b
| Author | SHA1 | Date | |
|---|---|---|---|
| 4940608a9b | |||
| ffb6a4c080 |
5 changed files with 441 additions and 9 deletions
|
|
@ -1,16 +1,119 @@
|
||||||
package mg.dot.feufaro.ui
|
package mg.dot.feufaro.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.blur
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun MyVerticalScrollbar (
|
actual fun MyVerticalScrollbar(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
scrollState: ScrollState?,
|
scrollState: ScrollState?,
|
||||||
lazyListState: LazyListState?,
|
lazyListState: LazyListState?,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
){
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
var containerHeight by remember { mutableStateOf(1f) }
|
||||||
|
val thumbHeight = 75.dp
|
||||||
|
|
||||||
|
val scrollPercentage by remember(scrollState, lazyListState) {
|
||||||
|
derivedStateOf {
|
||||||
|
if (scrollState != null && scrollState.maxValue > 0) {
|
||||||
|
scrollState.value.toFloat() / scrollState.maxValue
|
||||||
|
} else if (lazyListState != null) {
|
||||||
|
val layoutInfo = lazyListState.layoutInfo
|
||||||
|
val totalItems = layoutInfo.totalItemsCount
|
||||||
|
if (totalItems == 0) 0f else {
|
||||||
|
val firstVisibleItem = lazyListState.firstVisibleItemIndex
|
||||||
|
firstVisibleItem.toFloat() / totalItems
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.onGloballyPositioned { coordinates ->
|
||||||
|
containerHeight = coordinates.size.height.toFloat()
|
||||||
|
}
|
||||||
|
) {
|
||||||
content()
|
content()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(15.dp)
|
||||||
|
.background(Color.Transparent)
|
||||||
|
) {
|
||||||
|
val thumbHeightPx = with(density) { thumbHeight.toPx() }
|
||||||
|
val maxOffset = containerHeight - thumbHeightPx
|
||||||
|
val currentThumbOffset = (scrollPercentage * maxOffset).coerceIn(0f, maxOffset.coerceAtLeast(0f))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(y = with(density) { currentThumbOffset.toDp() })
|
||||||
|
.width(10.dp)
|
||||||
|
.height(thumbHeight)
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.clip(RoundedCornerShape(5.dp))
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
MaterialTheme.colorScheme.secondary.copy(alpha = 1f),
|
||||||
|
MaterialTheme.colorScheme.secondary.copy(alpha = 0.75f),
|
||||||
|
MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.draggable(
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
state = rememberDraggableState { delta ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (maxOffset > 0) {
|
||||||
|
val scrollRatio = delta / maxOffset
|
||||||
|
if (scrollState != null) {
|
||||||
|
scrollState.scrollBy(scrollRatio * scrollState.maxValue)
|
||||||
|
} else if (lazyListState != null) {
|
||||||
|
val totalItems = lazyListState.layoutInfo.totalItemsCount
|
||||||
|
lazyListState.scrollBy(scrollRatio * totalItems * 120f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
.size(6.dp)
|
||||||
|
.blur(1.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.background, RoundedCornerShape(3.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
package mg.dot.feufaro.solfa
|
||||||
|
|
||||||
|
|
||||||
|
object HarmonicAnalyzer {
|
||||||
|
private val solfaToSemitones = mapOf(
|
||||||
|
"d" to 0, "D" to 1,
|
||||||
|
"r" to 2, "R" to 3,
|
||||||
|
"m" to 4,
|
||||||
|
"f" to 5, "F" to 6,
|
||||||
|
"s" to 7, "S" to 8,
|
||||||
|
"l" to 9, "T" to 10,
|
||||||
|
"t" to 11
|
||||||
|
)
|
||||||
|
|
||||||
|
private val semitoneToDegree = mapOf(
|
||||||
|
0 to "I", 1 to "bII",
|
||||||
|
2 to "II", 3 to "bIII",
|
||||||
|
4 to "III",
|
||||||
|
5 to "IV", 6 to "#IV",
|
||||||
|
7 to "V", 8 to "bVI",
|
||||||
|
9 to "VI", 10 to "bVII",
|
||||||
|
11 to "VII"
|
||||||
|
)
|
||||||
|
|
||||||
|
private val noteNames = listOf(
|
||||||
|
"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"
|
||||||
|
)
|
||||||
|
|
||||||
|
private val keyOffsets = mapOf(
|
||||||
|
"C" to 0, "C#" to 1, "Db" to 1, "D" to 2, "Eb" to 3,
|
||||||
|
"E" to 4, "F" to 5, "F#" to 6, "Gb" to 6, "G" to 7,
|
||||||
|
"Ab" to 8, "A" to 9, "Bb" to 10,"B" to 11
|
||||||
|
)
|
||||||
|
|
||||||
|
private val chordDefinitions = listOf(
|
||||||
|
ChordDef("maj7", setOf(0, 4, 7, 11), setOf(4, 11), 12),
|
||||||
|
ChordDef("7", setOf(0, 4, 7, 10), setOf(4, 10), 12),
|
||||||
|
ChordDef("m7", setOf(0, 3, 7, 10), setOf(3, 10), 12),
|
||||||
|
ChordDef("dim7", setOf(0, 3, 6, 9), setOf(3, 6, 9), 12),
|
||||||
|
ChordDef("m7b5", setOf(0, 3, 6, 10), setOf(3, 6, 10), 12),
|
||||||
|
ChordDef("7sus4", setOf(0, 5, 7, 10), setOf(5, 10), 12),
|
||||||
|
ChordDef("maj7sus2", setOf(0, 2, 7, 11), setOf(2, 11), 12),
|
||||||
|
ChordDef("maj7#11", setOf(0, 4, 6, 7), setOf(4, 6), 12),
|
||||||
|
ChordDef("add9", setOf(0, 2, 4, 7), setOf(2, 4), 11),
|
||||||
|
ChordDef("madd9", setOf(0, 2, 3, 7), setOf(2, 3), 11),
|
||||||
|
ChordDef("add11", setOf(0, 4, 5, 7), setOf(4, 5), 11),
|
||||||
|
|
||||||
|
ChordDef("", setOf(0, 4, 7), setOf(4), 8),
|
||||||
|
ChordDef("m", setOf(0, 3, 7), setOf(3), 8),
|
||||||
|
ChordDef("dim", setOf(0, 3, 6), setOf(3, 6), 8),
|
||||||
|
ChordDef("aug", setOf(0, 4, 8), setOf(4, 8), 8),
|
||||||
|
ChordDef("sus2", setOf(0, 2, 7), setOf(2), 8),
|
||||||
|
ChordDef("sus4", setOf(0, 5, 7), setOf(5), 8),
|
||||||
|
|
||||||
|
ChordDef("5", setOf(0, 7), setOf(7), 5),
|
||||||
|
ChordDef("", setOf(0, 4), setOf(4), 5),
|
||||||
|
ChordDef("m", setOf(0, 3), setOf(3), 5),
|
||||||
|
|
||||||
|
ChordDef("", setOf(0), setOf(), 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val MULTI_NOTE_SEPARATORS = Regex("""[•/|+ ,]+""")
|
||||||
|
|
||||||
|
|
||||||
|
fun splitSlices(notes: Map<Int, String>): List<Map<Int, String>> {
|
||||||
|
val splitRaw: Map<Int, List<String>> = notes.mapValues { (_, raw) ->
|
||||||
|
val parts = raw.split(MULTI_NOTE_SEPARATORS).map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
if (parts.isEmpty()) listOf("") else parts
|
||||||
|
}
|
||||||
|
|
||||||
|
val sliceCount = splitRaw.values.maxOfOrNull { it.size } ?: 1
|
||||||
|
|
||||||
|
return (0 until sliceCount).map { idx ->
|
||||||
|
splitRaw.mapValues { (_, parts) ->
|
||||||
|
parts.getOrElse(idx) { parts.last() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun analyzeDegree(notes: Map<Int, String>): String {
|
||||||
|
val slices = splitSlices(notes)
|
||||||
|
val isMulti = slices.size > 1
|
||||||
|
|
||||||
|
val degrees = slices.map { slice ->
|
||||||
|
val cleaned = cleanNotes(slice)
|
||||||
|
if (!isMulti) printTable(cleaned)
|
||||||
|
val match = findChordMatch(cleaned) ?: return@map ""
|
||||||
|
val rootDegree = semitoneToDegree[match.root] ?: ""
|
||||||
|
buildSlashNotation("$rootDegree${match.quality}", match, ::semitoneToDegreeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMulti) {
|
||||||
|
printTableMulti(notes, slices)
|
||||||
|
}
|
||||||
|
|
||||||
|
val degreeLine = degrees.filter { it.isNotEmpty() }.joinToString(" ")
|
||||||
|
|
||||||
|
/*println("├──────────────────┤")
|
||||||
|
println("│ DEGREE: $degreeLine")
|
||||||
|
println("└──────────────────┘")*/
|
||||||
|
|
||||||
|
return degreeLine
|
||||||
|
}
|
||||||
|
|
||||||
|
fun analyzeChordName(notes: Map<Int, String>, songKey: String = "C"): String {
|
||||||
|
val keyOffset = keyOffsets[songKey] ?: 0
|
||||||
|
val slices = splitSlices(notes)
|
||||||
|
|
||||||
|
val names = slices.map { slice ->
|
||||||
|
val cleaned = cleanNotes(slice)
|
||||||
|
val match = findChordMatch(cleaned) ?: return@map ""
|
||||||
|
val absoluteRoot = (keyOffset + match.root) % 12
|
||||||
|
val chordName = noteNames[absoluteRoot] + match.quality
|
||||||
|
if (match.bass != -1 && match.bass != match.root) {
|
||||||
|
val absoluteBass = (keyOffset + match.bass) % 12
|
||||||
|
"$chordName/${noteNames[absoluteBass]}"
|
||||||
|
} else {
|
||||||
|
chordName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names.filter { it.isNotEmpty() }.joinToString(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanOneNote(raw: String): String {
|
||||||
|
var clean = raw
|
||||||
|
.replace(Regex("[()₁₂₃₄¹²³⁴]"), "")
|
||||||
|
.replace("'", "").replace(",", "")
|
||||||
|
.replace("z", "").replace("w", "").replace("@", "")
|
||||||
|
.replace("―", "")
|
||||||
|
.replace("—", "")
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
clean = when {
|
||||||
|
clean.contains("di") -> "D"
|
||||||
|
clean.contains("ri") -> "R"
|
||||||
|
clean.contains("fi") -> "F"
|
||||||
|
clean.contains("si") -> "S"
|
||||||
|
clean.contains("ta") -> "T"
|
||||||
|
else -> clean
|
||||||
|
}
|
||||||
|
return clean
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanNotes(notes: Map<Int, String>): Map<Int, String> =
|
||||||
|
notes.mapValues { (_, raw) -> cleanOneNote(raw) }
|
||||||
|
|
||||||
|
private fun printTable(cleanedNotes: Map<Int, String>) {
|
||||||
|
val voiceNames = mapOf(1 to "S", 2 to "A", 3 to "T", 4 to "B")
|
||||||
|
// println("┌──────┬───────────┐")
|
||||||
|
// println("│ Voix │ Note │")
|
||||||
|
// println("├──────┼───────────┤")
|
||||||
|
(1..4).forEach { i ->
|
||||||
|
//println("│ ${voiceNames[i]} │ ${(cleanedNotes[i] ?: "").padEnd(9)} │")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printTableMulti(
|
||||||
|
raw: Map<Int, String>,
|
||||||
|
slices: List<Map<Int, String>>
|
||||||
|
) {
|
||||||
|
val voiceNames = mapOf(1 to "S", 2 to "A", 3 to "T", 4 to "B")
|
||||||
|
val headers = (1..slices.size).joinToString(" │ ") { "Note${it} " }
|
||||||
|
// println("┌──────┬───────────┐")
|
||||||
|
// println("│ Voix │ $headers│")
|
||||||
|
// println("├──────┼───────────┤")
|
||||||
|
(1..4).forEach { voiceIdx ->
|
||||||
|
val cols = slices.mapIndexed { _, slice ->
|
||||||
|
(slice[voiceIdx] ?: "").padEnd(5)
|
||||||
|
}.joinToString(" • ")
|
||||||
|
//println("│ ${voiceNames[voiceIdx]} │ $cols │")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findChordMatch(notes: Map<Int, String>): FullMatch? {
|
||||||
|
val activeNotes = notes.values
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
.mapNotNull { solfaToSemitones[it] }
|
||||||
|
if (activeNotes.isEmpty()) return null
|
||||||
|
|
||||||
|
val uniqueSemitones = activeNotes.toSet()
|
||||||
|
val bassRaw = notes[4]?.trim() ?: ""
|
||||||
|
val bassSemitone = solfaToSemitones[bassRaw] ?: -1
|
||||||
|
var bestMatch: FullMatch? = null
|
||||||
|
var bestScore = -1
|
||||||
|
|
||||||
|
if (bassSemitone != -1) {
|
||||||
|
val intervals = uniqueSemitones.map { (it - bassSemitone + 12) % 12 }.toSet()
|
||||||
|
val result = detectQuality(intervals)
|
||||||
|
if (result != null && result.weight >= 8) {
|
||||||
|
bestMatch = FullMatch(bassSemitone, result.quality, bassSemitone)
|
||||||
|
bestScore = result.weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (rootCandidate in uniqueSemitones) {
|
||||||
|
val intervals = uniqueSemitones.map { (it - rootCandidate + 12) % 12 }.toSet()
|
||||||
|
val result = detectQuality(intervals) ?: continue
|
||||||
|
val bassBonus = if (rootCandidate == bassSemitone) 2 else 0
|
||||||
|
val diatonicBonus = if (rootCandidate in setOf(0,2,4,5,7,9,11)) 1 else 0
|
||||||
|
val score = result.weight + bassBonus + diatonicBonus
|
||||||
|
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score
|
||||||
|
bestMatch = FullMatch(rootCandidate, result.quality, bassSemitone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detectQuality(intervals: Set<Int>): QualityResult? {
|
||||||
|
if (!intervals.contains(0)) return null
|
||||||
|
|
||||||
|
var bestDef: ChordDef? = null
|
||||||
|
var bestScore = -1
|
||||||
|
|
||||||
|
for (def in chordDefinitions) {
|
||||||
|
if (!intervals.containsAll(def.mandatory)) continue
|
||||||
|
|
||||||
|
val matchingNotes = intervals.count { it in def.structure }
|
||||||
|
val foreignNotes = intervals.count { it !in def.structure }
|
||||||
|
|
||||||
|
val maxForeign = if (def.structure.size <= 2) 0 else 1
|
||||||
|
if (foreignNotes > maxForeign) continue
|
||||||
|
|
||||||
|
val score = def.baseWeight + matchingNotes - (foreignNotes * 3)
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score
|
||||||
|
bestDef = def
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (bestDef != null) QualityResult(bestDef.name, bestScore) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun semitoneToDegreeStr(semitone: Int): String =
|
||||||
|
semitoneToDegree[semitone] ?: ""
|
||||||
|
|
||||||
|
private fun buildSlashNotation(
|
||||||
|
chordName: String,
|
||||||
|
match: FullMatch,
|
||||||
|
toStr: (Int) -> String
|
||||||
|
): String {
|
||||||
|
return if (match.bass != -1 && match.bass != match.root) {
|
||||||
|
"$chordName/${toStr(match.bass)}"
|
||||||
|
} else {
|
||||||
|
chordName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ChordDef(
|
||||||
|
val name: String,
|
||||||
|
val structure: Set<Int>,
|
||||||
|
val mandatory: Set<Int>,
|
||||||
|
val baseWeight: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class FullMatch(val root: Int, val quality: String, val bass: Int)
|
||||||
|
private data class QualityResult(val quality: String, val weight: Int)
|
||||||
|
}
|
||||||
|
|
@ -587,6 +587,8 @@ fun LazyVerticalGridTUO(
|
||||||
}
|
}
|
||||||
|
|
||||||
val editMode by sharedScreenModel.modeEditor.collectAsState()
|
val editMode by sharedScreenModel.modeEditor.collectAsState()
|
||||||
|
val showFullChord by sharedScreenModel.harmonyView.collectAsState()
|
||||||
|
var toggleWithDegreeAndChord by remember { mutableStateOf (false) }
|
||||||
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -777,6 +779,63 @@ fun LazyVerticalGridTUO(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(showFullChord) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
measureTUOs.forEachIndexed { indexInMeasure, tuo ->
|
||||||
|
val chordMap = (1..4).mapNotNull { voice ->
|
||||||
|
val noteStr = tuo.tuNotes.getOrNull(voice)?.toString()
|
||||||
|
if (noteStr != null) voice to noteStr else null
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
val degreeName = remember(chordMap) {
|
||||||
|
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeDegree(chordMap) else null
|
||||||
|
}
|
||||||
|
val chordName = remember(chordMap) {
|
||||||
|
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(chordMap, sharedScreenModel.songKey.value) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(gridWidthDp / gridColumnCount)
|
||||||
|
) {
|
||||||
|
val maxFontSize = 12.sp
|
||||||
|
val minFontSize = 10.sp
|
||||||
|
var currentFontSize by remember { mutableStateOf(maxFontSize) }
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = { toggleWithDegreeAndChord = !toggleWithDegreeAndChord },
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
modifier = Modifier.height(24.dp).fillMaxWidth()
|
||||||
|
) {
|
||||||
|
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
val textToDisplay = if (toggleWithDegreeAndChord) (chordName ?: "") else (degreeName ?: "")
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = textToDisplay,
|
||||||
|
onTextLayout = { textLayoutResult ->
|
||||||
|
if (textLayoutResult.hasVisualOverflow && currentFontSize > minFontSize) {
|
||||||
|
currentFontSize = (currentFontSize.value * 0.9f).sp
|
||||||
|
}
|
||||||
|
},
|
||||||
|
softWrap = false,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Visible,
|
||||||
|
style = TextStyle(
|
||||||
|
color = if (!toggleWithDegreeAndChord) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary.copy(red = MaterialTheme.colorScheme.secondary.red * 0.4f, green = MaterialTheme.colorScheme.secondary.green * 0.4f, blue = MaterialTheme.colorScheme.secondary.blue * 0.4f ),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = currentFontSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
measureTUOs.forEachIndexed { indexInMeasure, oneTUO ->
|
measureTUOs.forEachIndexed { indexInMeasure, oneTUO ->
|
||||||
val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure
|
val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ fun SimpleDrawerContent(
|
||||||
var playListExpanded by remember { mutableStateOf(false) }
|
var playListExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val editMode by sharedScreenModel.modeEditor.collectAsState()
|
val editMode by sharedScreenModel.modeEditor.collectAsState()
|
||||||
var state1 by remember { mutableStateOf(false) }
|
val chordView by sharedScreenModel.harmonyView.collectAsState()
|
||||||
var state2 by remember { mutableStateOf(false) }
|
var state2 by remember { mutableStateOf(false) }
|
||||||
ModalDrawerSheet(
|
ModalDrawerSheet(
|
||||||
modifier = Modifier.width(300.dp)
|
modifier = Modifier.width(300.dp)
|
||||||
|
|
@ -85,14 +85,16 @@ fun SimpleDrawerContent(
|
||||||
sharedScreenModel.toggleEditorMode(newState)
|
sharedScreenModel.toggleEditorMode(newState)
|
||||||
},
|
},
|
||||||
thumbIcon = Icons.Default.Edit,
|
thumbIcon = Icons.Default.Edit,
|
||||||
label = "Mode Edit"
|
label = "Edition"
|
||||||
)
|
)
|
||||||
|
|
||||||
CustomSwitchItem(
|
CustomSwitchItem(
|
||||||
checked = state1,
|
checked = chordView,
|
||||||
onCheckedChange = { state1 = it },
|
onCheckedChange = { newStateMod ->
|
||||||
|
sharedScreenModel.toggleHarmonyMode(newStateMod)
|
||||||
|
},
|
||||||
thumbIcon = Icons.Default.MusicNote,
|
thumbIcon = Icons.Default.MusicNote,
|
||||||
label = "Analyse chords",
|
label = "Analyse d\'accords"
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(5.dp),
|
modifier = Modifier.padding(5.dp),
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,12 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
fun toggleEditorMode(enabled: Boolean) {
|
fun toggleEditorMode(enabled: Boolean) {
|
||||||
_modeEditor.value = enabled
|
_modeEditor.value = enabled
|
||||||
}
|
}
|
||||||
|
private val _harmonyView = MutableStateFlow(false)
|
||||||
|
val harmonyView: StateFlow<Boolean> = _harmonyView
|
||||||
|
fun toggleHarmonyMode(enabled: Boolean) {
|
||||||
|
_harmonyView.value = enabled
|
||||||
|
}
|
||||||
|
|
||||||
private val _isFullScreen = MutableStateFlow(false)
|
private val _isFullScreen = MutableStateFlow(false)
|
||||||
val isFullScreen = _isFullScreen.asStateFlow()
|
val isFullScreen = _isFullScreen.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -658,6 +664,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
_expandedFAB.value = false
|
_expandedFAB.value = false
|
||||||
_showSearchMenu.value = false
|
_showSearchMenu.value = false
|
||||||
_editModeState.value = false
|
_editModeState.value = false
|
||||||
|
_harmonyView.value = false
|
||||||
setTranspositionInterval(0)
|
setTranspositionInterval(0)
|
||||||
_midiMarkersList.value = emptyList()
|
_midiMarkersList.value = emptyList()
|
||||||
_tuoTimestamps.value = emptyList()
|
_tuoTimestamps.value = emptyList()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue