fretfret

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

commit e0bea9b61b20333758d066af351abb4853328ccc
parent 3afc5df8cf41ca3ec9dd2728d61c767fedbcf966
Author: massi <mdsiboldi@gmail.com>
Date:   Wed,  3 Jul 2024 02:24:39 -0700

persist settings

Diffstat:
M.idea/deploymentTargetSelector.xml | 2+-
Mapp/build.gradle.kts | 1+
Mapp/src/main/java/com/example/fretboardtrainer/MainActivity.kt | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mapp/src/test/java/com/example/fretboardtrainer/ExampleUnitTest.kt | 10++++++++++
Mgradle/libs.versions.toml | 4++++
5 files changed, 99 insertions(+), 13 deletions(-)

diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml @@ -4,7 +4,7 @@ <selectionStates> <SelectionState runConfigName="app"> <option name="selectionMode" value="DROPDOWN" /> - <DropdownSelection timestamp="2024-07-01T20:44:48.239762038Z"> + <DropdownSelection timestamp="2024-07-03T09:18:11.113954846Z"> <Target type="DEFAULT_BOOT"> <handle> <DeviceId pluginId="PhysicalDevice" identifier="serial=1C161FDF600FF6" /> diff --git a/app/build.gradle.kts b/app/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.datastore.preferences) 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 @@ -1,5 +1,7 @@ package com.example.fretboardtrainer +import android.app.Application +import android.content.Context import android.content.pm.ActivityInfo import android.os.Bundle import androidx.activity.ComponentActivity @@ -40,13 +42,51 @@ 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.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope 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.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +val FRET_COUNT = 13; // open + 12 frets +val STRING_COUNT = 6; + +val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") + +fun serializeBoolList(l: List<Boolean>): Int { + var num: Int = 0; + for (b in l) { + num = num shl 1 + if (b) { + num = num or 1 + } + } + return num +} + +fun deserializeBoolList(num: Int, len: Int): List<Boolean> { + var l = len - 1 + var n = num + val list = MutableList<Boolean>(len) { false } + while (l >= 0) { + if ((n and 1) == 1) { + list[l] = true + } + n = n shr 1 + l-- + } + return list +} fun weightForFret(fretIdx: Int): Float { var weight = 24 - fretIdx * 1.0f @@ -78,18 +118,43 @@ fun noteName(note: List<Int>): String { return NOTES[(startIdx + note[0]) % NOTES.size] } +// TODO: persist string and fret settings across app launches. data class FretfretState( val showSettings: Boolean = false, - val stringSettings: List<Boolean> = List(6) { true }, - val fretSettings: List<Boolean> = List(13) { true }, + val stringSettings: List<Boolean> = List(STRING_COUNT) { true }, + val fretSettings: List<Boolean> = List(FRET_COUNT) { true }, val note: List<Int>? = null, val lastNote: List<Int>? = null, ) -class FretfretViewModel : ViewModel() { +val STRING_KEY = intPreferencesKey("string_key") +val FRET_KEY = intPreferencesKey("fret_key") + +class FretfretViewModel(application: Application) : AndroidViewModel(application) { private val _uiState = MutableStateFlow(FretfretState()) val uiState = _uiState.asStateFlow() + init { + viewModelScope.launch { + _uiState.update { currentState -> + currentState.copy( + stringSettings = application.dataStore.data.map { prefs -> + when (prefs[STRING_KEY]) { + is Int -> deserializeBoolList(prefs[STRING_KEY]!!, STRING_COUNT) + else -> List(STRING_COUNT) { true } + } + }.first(), + fretSettings = application.dataStore.data.map { prefs -> + when (prefs[FRET_KEY]) { + is Int -> deserializeBoolList(prefs[FRET_KEY]!!, FRET_COUNT) + else -> List(FRET_COUNT) { true } + } + }.first(), + ) + } + } + } + fun toggleString(idx: Int) { _uiState.update { currentState -> val res = currentState.stringSettings.toMutableList() @@ -98,6 +163,7 @@ class FretfretViewModel : ViewModel() { stringSettings = res.toList() ) } + viewModelScope.launch { updateSettings() } } fun toggleFret(idx: Int) { @@ -108,6 +174,7 @@ class FretfretViewModel : ViewModel() { fretSettings = res.toList() ) } + viewModelScope.launch { updateSettings() } } private fun pickNote(): List<Int> { @@ -139,6 +206,13 @@ class FretfretViewModel : ViewModel() { } } + private suspend fun updateSettings() { + getApplication<Application>().dataStore.edit { settings -> + settings[STRING_KEY] = serializeBoolList(uiState.value.stringSettings); + settings[FRET_KEY] = serializeBoolList(uiState.value.fretSettings); + } + } + fun toggleSettings() { val show = !uiState.value.showSettings _uiState.update { currentState -> @@ -165,7 +239,7 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Fretfret(viewModel: FretfretViewModel = viewModel() ) { +fun Fretfret(viewModel: FretfretViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsState() FretboardTrainerTheme { Scaffold( @@ -205,7 +279,7 @@ fun Fretfret(viewModel: FretfretViewModel = viewModel() ) { ) if (uiState.showSettings) { Row(horizontalArrangement = Arrangement.SpaceBetween) { - repeat(13) { n -> + repeat(FRET_COUNT) { n -> Column( modifier = Modifier .weight(weightForFret(n)), @@ -246,16 +320,13 @@ fun Fretfret(viewModel: FretfretViewModel = viewModel() ) { } } -val FRET_COUNT = 13; // open + 12 frets -val STRING_COUNT = 6; - @Composable fun StringSettings(settings: List<Boolean>, toggle: (n: Int) -> Unit) { Column { - repeat(6) { n -> + repeat(STRING_COUNT) { n -> Row(verticalAlignment = Alignment.CenterVertically) { Text(STRINGS[n]) - Checkbox(modifier = Modifier.height((200 / 6).dp), + Checkbox(modifier = Modifier.height((200 / STRING_COUNT).dp), checked = settings[n], onCheckedChange = { toggle(n) }) } @@ -318,7 +389,7 @@ fun TopBar() { @Composable fun Strings(fretIdx: Int, noteAt: Int?, color: Color, openStringColor: Color) { Column(modifier = Modifier.fillMaxSize()) { - repeat(6) { n: Int -> + repeat(STRING_COUNT) { n: Int -> Box( modifier = Modifier .fillMaxWidth() diff --git a/app/src/test/java/com/example/fretboardtrainer/ExampleUnitTest.kt b/app/src/test/java/com/example/fretboardtrainer/ExampleUnitTest.kt @@ -14,4 +14,14 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } + + @Test + fun serializes_settings() { + assertEquals(0b1010, serializeBoolList(listOf(true, false, true, false))) + } + + @Test + fun deserializes_settings() { + assertEquals(listOf(true, false, true, false), deserializeBoolList(0b1010, 4)) + } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml @@ -8,6 +8,8 @@ espressoCore = "3.5.1" lifecycle = "2.8.3" activityCompose = "1.9.0" composeBom = "2023.08.00" +datastorePreferencesCore = "1.1.1" +datastorePreferences = "1.1.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -25,6 +27,8 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-datastore-preferences-core = { group = "androidx.datastore", name = "datastore-preferences-core", version.ref = "datastorePreferencesCore" } +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }