fretfret

learn the notes on a guitar from the comfort of your android phone
Log | Files | Refs

commit c5de5e146a35881a1a810c5f0783d2c98680f542
parent 2ba2d8212ef4493772a8f8c029c762f5a21a66f7
Author: massi <mdsiboldi@gmail.com>
Date:   Tue,  2 Jul 2024 00:07:32 -0700

state management

Diffstat:
Mapp/build.gradle.kts | 2+-
Mapp/src/main/java/com/example/fretboardtrainer/MainActivity.kt | 297+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mgradle/libs.versions.toml | 5+++--
3 files changed, 167 insertions(+), 137 deletions(-)

diff --git a/app/build.gradle.kts b/app/build.gradle.kts @@ -50,7 +50,6 @@ android { } dependencies { - implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) @@ -59,6 +58,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.androidx.lifecycle.viewmodel.compose) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/com/example/fretboardtrainer/MainActivity.kt b/app/src/main/java/com/example/fretboardtrainer/MainActivity.kt @@ -5,7 +5,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -22,7 +21,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button import androidx.compose.material3.Checkbox @@ -34,15 +32,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar -import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import com.example.fretboardtrainer.ui.theme.FretboardTrainerTheme +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update fun weightForFret(fretIdx: Int): Float { @@ -62,158 +65,184 @@ val FRETS = arrayOf( "6", "7", "8", "9", "10", "11", "12" ); -class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3Api::class) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - val stringSettings = MutableList(6) { true }; - val fretSettings = Array(13) { true }; +fun noteName(note: List<Int>): String { + val startIdx = when (note[1]) { + 0 -> 7 // E + 1 -> 2 // B + 2 -> 10 // G + 3 -> 5 // D + 4 -> 0 // A + 5 -> 7 // E + else -> -1 + } + return NOTES[(startIdx + note[0]) % NOTES.size] +} - // a note is [stringIdx, fretIdx] - fun randomNote(): Array<Int> { - // find all true values in settings - // randomly pick one of those - val notes = mutableSetOf<Array<Int>>(); - for (stringIdx in stringSettings.indices) { - if (stringSettings[stringIdx]) { - for (fretIdx in fretSettings.indices) { - if (fretSettings[fretIdx]) { - notes.add(arrayOf(fretIdx, stringIdx)); - } - } - } - } - return notes.random(); - } +data class FretfretState( + val showSettings: Boolean = false, + val stringSettings: List<Boolean> = List(6) { true }, + val fretSettings: List<Boolean> = List(13) { true }, + val note: List<Int>? = null, + val lastNote: List<Int>? = null, +) - fun noteName(note: Array<Int>): String { - val startIdx = when (note[1]) { - 0 -> 7 // E - 1 -> 2 // B - 2 -> 10 // G - 3 -> 5 // D - 4 -> 0 // A - 5 -> 7 // E - else -> -1 - } - return NOTES[(startIdx + note[0]) % NOTES.size] +class FretfretViewModel : ViewModel() { + private val _uiState = MutableStateFlow(FretfretState()) + val uiState = _uiState.asStateFlow() + + fun toggleString(idx: Int) { + _uiState.update { currentState -> + val res = currentState.stringSettings.toMutableList() + res[idx] = !res[idx] + currentState.copy( + stringSettings = res.toList() + ) } + } - fun toggleString(idx: Int) { - stringSettings[idx] = !stringSettings[idx]; + fun toggleFret(idx: Int) { + _uiState.update { currentState -> + val res = currentState.fretSettings.toMutableList() + res[idx] = !res[idx] + currentState.copy( + fretSettings = res.toList() + ) } + } - fun toggleFret(idx: Int) { - fretSettings[idx] = !fretSettings[idx]; + private fun pickNote(): List<Int> { + // find all true values in settings + // randomly pick one of those + val notes = mutableSetOf<List<Int>>(); + for (stringIdx in uiState.value.stringSettings.indices) { + if (uiState.value.stringSettings[stringIdx]) { + for (fretIdx in uiState.value.fretSettings.indices) { + if (uiState.value.fretSettings[fretIdx]) { + notes.add(listOf(fretIdx, stringIdx)); + } + } + } + } + var newNote = notes.random(); + while (newNote == uiState.value.note) { + newNote = notes.random(); } + return newNote + } - var showName = false; - var note: Array<Int>? = randomNote(); - var lastNote: Array<Int>? = null; - fun updateNote() { - showName = false; - lastNote = note; - do { - note = randomNote(); - } while (lastNote.contentEquals(note)) + fun updateNote() { + _uiState.update { currentState -> + currentState.copy( + lastNote = uiState.value.note, + note = pickNote() + ) } + } - var showSettings = false; + fun toggleSettings() { + val show = !uiState.value.showSettings + _uiState.update { currentState -> + // TODO: keep note if updated settings allow it? + currentState.copy( + lastNote = null, + note = if (show) null else pickNote(), + showSettings = show + ) + } + } +} - fun toggleSettings() { - showSettings = !showSettings; - if (showSettings) { - note = null; - lastNote = null; - } else { - updateNote(); - } +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + setContent { + Fretfret() } + } +} - fun draw() { - setContent { - FretboardTrainerTheme { - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar( - modifier = Modifier.displayCutoutPadding(), - title = { Text("Fretfret") }, - actions = { - IconButton(onClick = { toggleSettings(); draw() }) { - Icon( - imageVector = Icons.Filled.Settings, - contentDescription = "Choose Notes" - ) - } - }) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Fretfret(viewModel: FretfretViewModel = viewModel() ) { + val uiState by viewModel.uiState.collectAsState() + FretboardTrainerTheme { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + modifier = Modifier.displayCutoutPadding(), + title = { Text("Fretfret") }, + actions = { + IconButton(onClick = { viewModel.toggleSettings() }) { + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = "Choose Notes" + ) } - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(horizontal = 20.dp) - .displayCutoutPadding(), - verticalArrangement = Arrangement.SpaceAround - ) { - Row { - if (showSettings) { - StringSettings( - settings = stringSettings.toList(), - toggle = { n -> toggleString(n) }); - } - Column { - Fretboard( - modifier = Modifier.fillMaxWidth(), - note = note - ) - if (showSettings) { - Row(horizontalArrangement = Arrangement.SpaceBetween) { - repeat(13) { n -> - Column( - modifier = Modifier - .weight(weightForFret(n)), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Checkbox(modifier = Modifier - .padding(end = 15.dp, start = 12.dp), - checked = fretSettings[n], - onCheckedChange = { toggleFret(n); draw() }) - Text(FRETS[n]) - } - } - } - } - } - } - if (!showSettings) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row { - Text("last answer: ") - Text(lastNote?.let { noteName(it) } - ?: "you're just getting started! B)") - } - Button( - onClick = { updateNote(); draw() }, - shape = RoundedCornerShape(5.dp) + }) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 20.dp) + .displayCutoutPadding(), + verticalArrangement = Arrangement.SpaceAround + ) { + Row { + if (uiState.showSettings) { + StringSettings( + settings = uiState.stringSettings, + toggle = { n -> viewModel.toggleString(n) }); + } + Column { + Fretboard( + modifier = Modifier.fillMaxWidth(), + note = uiState.note + ) + if (uiState.showSettings) { + Row(horizontalArrangement = Arrangement.SpaceBetween) { + repeat(13) { n -> + Column( + modifier = Modifier + .weight(weightForFret(n)), + horizontalAlignment = Alignment.CenterHorizontally ) { - Text("next") + Checkbox(modifier = Modifier + .padding(end = 15.dp, start = 12.dp), + checked = uiState.fretSettings[n], + onCheckedChange = { viewModel.toggleFret(n) }) + Text(FRETS[n]) } } } } } } + if (!uiState.showSettings) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row { + Text("last answer: ") + Text(uiState.lastNote?.let { noteName(it) } + ?: "you're just getting started! B)") + } + Button( + onClick = { viewModel.updateNote() }, + shape = RoundedCornerShape(5.dp) + ) { + Text("next") + } + } + } } } - draw(); } } @@ -235,7 +264,7 @@ fun StringSettings(settings: List<Boolean>, toggle: (n: Int) -> Unit) { } @Composable -fun Fretboard(modifier: Modifier = Modifier, note: Array<Int>?) { +fun Fretboard(modifier: Modifier = Modifier, note: List<Int>?) { Surface( color = MaterialTheme.colorScheme.surfaceVariant, tonalElevation = 0.dp, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ coreKtx = "1.13.1" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" -lifecycleRuntimeKtx = "2.8.0" +lifecycle = "2.8.3" activityCompose = "1.9.0" composeBom = "2023.08.00" @@ -14,7 +14,8 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle"} androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-ui = { group = "androidx.compose.ui", name = "ui" }