From a311e8961e7be7bf5e2c726698a622088bd7d250 Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Sun, 1 Dec 2024 12:14:07 +0100 Subject: [PATCH 01/15] Move files into packages --- app/src/main/java/com/mensinator/app/App.kt | 1 + .../app/{ => business}/CalculationsHelper.kt | 2 +- .../app/{ => business}/DatabaseUtils.kt | 2 +- .../app/{ => business}/ExportImport.kt | 2 +- .../app/{ => business}/ICalculationsHelper.kt | 2 +- .../app/{ => business}/IExportImport.kt | 2 +- .../{ => business}/INotificationScheduler.kt | 2 +- .../{ => business}/IOvulationPrediction.kt | 2 +- .../{ => business}/IPeriodDatabaseHelper.kt | 6 +- .../app/{ => business}/IPeriodPrediction.kt | 2 +- .../{ => business}/NotificationScheduler.kt | 3 +- .../app/{ => business}/OvulationPrediction.kt | 2 +- .../{ => business}/PeriodDatabaseHelper.kt | 6 +- .../app/{ => business}/PeriodPrediction.kt | 2 +- .../app/{ => calendar}/CalendarScreen.kt | 16 +- .../mensinator/app/calendar/SymptomDialogs.kt | 137 ++++++++++++++++++ .../com/mensinator/app/{ => data}/Setting.kt | 2 +- .../com/mensinator/app/{ => data}/Symptom.kt | 2 +- .../app/navigation/MensinatorApp.kt | 5 +- .../app/{ => settings}/ExportImportDialog.kt | 3 +- .../app/{ => settings}/FaqDialog.kt | 3 +- .../mensinator/app/settings/SettingsScreen.kt | 3 - .../app/settings/SettingsViewModel.kt | 4 +- .../app/statistics/StatisticsViewModel.kt | 4 + .../app/{ => symptoms}/ManageSymptomScreen.kt | 12 +- .../ManageSymptomsDialogs.kt} | 103 +------------ .../app/symptoms/ManageSymptomsViewModel.kt | 41 ++++++ .../mensinator/app/PeriodPredictionTest.kt | 4 + 28 files changed, 247 insertions(+), 128 deletions(-) rename app/src/main/java/com/mensinator/app/{ => business}/CalculationsHelper.kt (99%) rename app/src/main/java/com/mensinator/app/{ => business}/DatabaseUtils.kt (99%) rename app/src/main/java/com/mensinator/app/{ => business}/ExportImport.kt (99%) rename app/src/main/java/com/mensinator/app/{ => business}/ICalculationsHelper.kt (97%) rename app/src/main/java/com/mensinator/app/{ => business}/IExportImport.kt (84%) rename app/src/main/java/com/mensinator/app/{ => business}/INotificationScheduler.kt (84%) rename app/src/main/java/com/mensinator/app/{ => business}/IOvulationPrediction.kt (75%) rename app/src/main/java/com/mensinator/app/{ => business}/IPeriodDatabaseHelper.kt (95%) rename app/src/main/java/com/mensinator/app/{ => business}/IPeriodPrediction.kt (74%) rename app/src/main/java/com/mensinator/app/{ => business}/NotificationScheduler.kt (95%) rename app/src/main/java/com/mensinator/app/{ => business}/OvulationPrediction.kt (98%) rename app/src/main/java/com/mensinator/app/{ => business}/PeriodDatabaseHelper.kt (99%) rename app/src/main/java/com/mensinator/app/{ => business}/PeriodPrediction.kt (92%) rename app/src/main/java/com/mensinator/app/{ => calendar}/CalendarScreen.kt (98%) create mode 100644 app/src/main/java/com/mensinator/app/calendar/SymptomDialogs.kt rename app/src/main/java/com/mensinator/app/{ => data}/Setting.kt (80%) rename app/src/main/java/com/mensinator/app/{ => data}/Symptom.kt (85%) rename app/src/main/java/com/mensinator/app/{ => settings}/ExportImportDialog.kt (98%) rename app/src/main/java/com/mensinator/app/{ => settings}/FaqDialog.kt (98%) rename app/src/main/java/com/mensinator/app/{ => symptoms}/ManageSymptomScreen.kt (96%) rename app/src/main/java/com/mensinator/app/{SymptomDialogs.kt => symptoms/ManageSymptomsDialogs.kt} (53%) create mode 100644 app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt diff --git a/app/src/main/java/com/mensinator/app/App.kt b/app/src/main/java/com/mensinator/app/App.kt index a1e7bd44..946848c5 100644 --- a/app/src/main/java/com/mensinator/app/App.kt +++ b/app/src/main/java/com/mensinator/app/App.kt @@ -1,6 +1,7 @@ package com.mensinator.app import android.app.Application +import com.mensinator.app.business.* import com.mensinator.app.settings.SettingsViewModel import com.mensinator.app.statistics.StatisticsViewModel import org.koin.android.ext.koin.androidContext diff --git a/app/src/main/java/com/mensinator/app/CalculationsHelper.kt b/app/src/main/java/com/mensinator/app/business/CalculationsHelper.kt similarity index 99% rename from app/src/main/java/com/mensinator/app/CalculationsHelper.kt rename to app/src/main/java/com/mensinator/app/business/CalculationsHelper.kt index deb3f699..aab60082 100644 --- a/app/src/main/java/com/mensinator/app/CalculationsHelper.kt +++ b/app/src/main/java/com/mensinator/app/business/CalculationsHelper.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import android.util.Log import com.mensinator.app.extensions.roundToTwoDecimalPoints diff --git a/app/src/main/java/com/mensinator/app/DatabaseUtils.kt b/app/src/main/java/com/mensinator/app/business/DatabaseUtils.kt similarity index 99% rename from app/src/main/java/com/mensinator/app/DatabaseUtils.kt rename to app/src/main/java/com/mensinator/app/business/DatabaseUtils.kt index d46b05a5..c6f88407 100644 --- a/app/src/main/java/com/mensinator/app/DatabaseUtils.kt +++ b/app/src/main/java/com/mensinator/app/business/DatabaseUtils.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import android.database.sqlite.SQLiteDatabase diff --git a/app/src/main/java/com/mensinator/app/ExportImport.kt b/app/src/main/java/com/mensinator/app/business/ExportImport.kt similarity index 99% rename from app/src/main/java/com/mensinator/app/ExportImport.kt rename to app/src/main/java/com/mensinator/app/business/ExportImport.kt index c69f5ad4..5c3ccbb9 100644 --- a/app/src/main/java/com/mensinator/app/ExportImport.kt +++ b/app/src/main/java/com/mensinator/app/business/ExportImport.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import android.content.ContentValues import android.content.Context diff --git a/app/src/main/java/com/mensinator/app/ICalculationsHelper.kt b/app/src/main/java/com/mensinator/app/business/ICalculationsHelper.kt similarity index 97% rename from app/src/main/java/com/mensinator/app/ICalculationsHelper.kt rename to app/src/main/java/com/mensinator/app/business/ICalculationsHelper.kt index 354bfe75..30305e71 100644 --- a/app/src/main/java/com/mensinator/app/ICalculationsHelper.kt +++ b/app/src/main/java/com/mensinator/app/business/ICalculationsHelper.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import java.time.LocalDate diff --git a/app/src/main/java/com/mensinator/app/IExportImport.kt b/app/src/main/java/com/mensinator/app/business/IExportImport.kt similarity index 84% rename from app/src/main/java/com/mensinator/app/IExportImport.kt rename to app/src/main/java/com/mensinator/app/business/IExportImport.kt index 0fd77b18..523a6a0c 100644 --- a/app/src/main/java/com/mensinator/app/IExportImport.kt +++ b/app/src/main/java/com/mensinator/app/business/IExportImport.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business interface IExportImport { fun getDocumentsExportFilePath(): String diff --git a/app/src/main/java/com/mensinator/app/INotificationScheduler.kt b/app/src/main/java/com/mensinator/app/business/INotificationScheduler.kt similarity index 84% rename from app/src/main/java/com/mensinator/app/INotificationScheduler.kt rename to app/src/main/java/com/mensinator/app/business/INotificationScheduler.kt index a4fe2b74..7f7ad648 100644 --- a/app/src/main/java/com/mensinator/app/INotificationScheduler.kt +++ b/app/src/main/java/com/mensinator/app/business/INotificationScheduler.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import java.time.LocalDate diff --git a/app/src/main/java/com/mensinator/app/IOvulationPrediction.kt b/app/src/main/java/com/mensinator/app/business/IOvulationPrediction.kt similarity index 75% rename from app/src/main/java/com/mensinator/app/IOvulationPrediction.kt rename to app/src/main/java/com/mensinator/app/business/IOvulationPrediction.kt index ab86ae86..09cfdbd5 100644 --- a/app/src/main/java/com/mensinator/app/IOvulationPrediction.kt +++ b/app/src/main/java/com/mensinator/app/business/IOvulationPrediction.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import java.time.LocalDate diff --git a/app/src/main/java/com/mensinator/app/IPeriodDatabaseHelper.kt b/app/src/main/java/com/mensinator/app/business/IPeriodDatabaseHelper.kt similarity index 95% rename from app/src/main/java/com/mensinator/app/IPeriodDatabaseHelper.kt rename to app/src/main/java/com/mensinator/app/business/IPeriodDatabaseHelper.kt index 8b6df3a0..8305c526 100644 --- a/app/src/main/java/com/mensinator/app/IPeriodDatabaseHelper.kt +++ b/app/src/main/java/com/mensinator/app/business/IPeriodDatabaseHelper.kt @@ -1,7 +1,9 @@ -package com.mensinator.app +package com.mensinator.app.business import android.database.sqlite.SQLiteDatabase import androidx.annotation.WorkerThread +import com.mensinator.app.data.Symptom +import com.mensinator.app.data.Setting import java.time.LocalDate interface IPeriodDatabaseHelper { @@ -39,7 +41,7 @@ interface IPeriodDatabaseHelper { fun updateSymptomDate(dates: List, symptomId: List) // This function is used to get symptoms for a given date - fun getSymptomsFromDate(date: LocalDate): List + fun getActiveSymptomIdsForDate(date: LocalDate): List fun getSymptomColorForDate(date: LocalDate): List diff --git a/app/src/main/java/com/mensinator/app/IPeriodPrediction.kt b/app/src/main/java/com/mensinator/app/business/IPeriodPrediction.kt similarity index 74% rename from app/src/main/java/com/mensinator/app/IPeriodPrediction.kt rename to app/src/main/java/com/mensinator/app/business/IPeriodPrediction.kt index eec35cd8..7248048f 100644 --- a/app/src/main/java/com/mensinator/app/IPeriodPrediction.kt +++ b/app/src/main/java/com/mensinator/app/business/IPeriodPrediction.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import java.time.LocalDate diff --git a/app/src/main/java/com/mensinator/app/NotificationScheduler.kt b/app/src/main/java/com/mensinator/app/business/NotificationScheduler.kt similarity index 95% rename from app/src/main/java/com/mensinator/app/NotificationScheduler.kt rename to app/src/main/java/com/mensinator/app/business/NotificationScheduler.kt index 4ad72e73..53e46ba5 100644 --- a/app/src/main/java/com/mensinator/app/NotificationScheduler.kt +++ b/app/src/main/java/com/mensinator/app/business/NotificationScheduler.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import android.app.AlarmManager @@ -7,6 +7,7 @@ import android.content.Context import android.content.Intent import android.util.Log import androidx.core.app.NotificationManagerCompat +import com.mensinator.app.NotificationReceiver import java.time.LocalDate import java.time.ZoneId diff --git a/app/src/main/java/com/mensinator/app/OvulationPrediction.kt b/app/src/main/java/com/mensinator/app/business/OvulationPrediction.kt similarity index 98% rename from app/src/main/java/com/mensinator/app/OvulationPrediction.kt rename to app/src/main/java/com/mensinator/app/business/OvulationPrediction.kt index 43fe31fb..113a792d 100644 --- a/app/src/main/java/com/mensinator/app/OvulationPrediction.kt +++ b/app/src/main/java/com/mensinator/app/business/OvulationPrediction.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import android.util.Log import java.time.LocalDate diff --git a/app/src/main/java/com/mensinator/app/PeriodDatabaseHelper.kt b/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt similarity index 99% rename from app/src/main/java/com/mensinator/app/PeriodDatabaseHelper.kt rename to app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt index efc8d2a0..81fd8fd2 100644 --- a/app/src/main/java/com/mensinator/app/PeriodDatabaseHelper.kt +++ b/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt @@ -1,10 +1,12 @@ -package com.mensinator.app +package com.mensinator.app.business import android.content.ContentValues import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.util.Log +import com.mensinator.app.data.Symptom +import com.mensinator.app.data.Setting import java.time.LocalDate /* @@ -298,7 +300,7 @@ class PeriodDatabaseHelper(context: Context) : db.close() } - override fun getSymptomsFromDate(date: LocalDate): List { + override fun getActiveSymptomIdsForDate(date: LocalDate): List { val db = readableDatabase val symptoms = mutableListOf() diff --git a/app/src/main/java/com/mensinator/app/PeriodPrediction.kt b/app/src/main/java/com/mensinator/app/business/PeriodPrediction.kt similarity index 92% rename from app/src/main/java/com/mensinator/app/PeriodPrediction.kt rename to app/src/main/java/com/mensinator/app/business/PeriodPrediction.kt index e9592476..064b3ffc 100644 --- a/app/src/main/java/com/mensinator/app/PeriodPrediction.kt +++ b/app/src/main/java/com/mensinator/app/business/PeriodPrediction.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.business import java.time.LocalDate diff --git a/app/src/main/java/com/mensinator/app/CalendarScreen.kt b/app/src/main/java/com/mensinator/app/calendar/CalendarScreen.kt similarity index 98% rename from app/src/main/java/com/mensinator/app/CalendarScreen.kt rename to app/src/main/java/com/mensinator/app/calendar/CalendarScreen.kt index 3dcf5f61..77375f3d 100644 --- a/app/src/main/java/com/mensinator/app/CalendarScreen.kt +++ b/app/src/main/java/com/mensinator/app/calendar/CalendarScreen.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.calendar import android.content.Context @@ -32,7 +32,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.mensinator.app.R +import com.mensinator.app.data.Symptom +import com.mensinator.app.business.INotificationScheduler +import com.mensinator.app.business.IOvulationPrediction +import com.mensinator.app.business.IPeriodDatabaseHelper +import com.mensinator.app.business.IPeriodPrediction import com.mensinator.app.data.ColorSource +import com.mensinator.app.data.isActive import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.settings.ResourceMapper import com.mensinator.app.settings.StringSetting @@ -630,11 +637,12 @@ fun CalendarScreen(modifier: Modifier) { // Show the SymptomsDialog if (showSymptomsDialog && selectedDates.value.isNotEmpty()) { val activeSymptoms = dbHelper.getAllSymptoms().filter { it.isActive } + val date = selectedDates.value.last() - SymptomsDialog( - date = selectedDates.value.last(), // Pass the last selected date + EditSymptomsForDaysDialog( + date = date, // Pass the last selected date symptoms = activeSymptoms, - dbHelper = dbHelper, + currentlyActiveSymptomIds = dbHelper.getActiveSymptomIdsForDate(date).toSet(), onSave = { selectedSymptoms -> val selectedSymptomIds = selectedSymptoms.map { it.id } val datesToUpdate = selectedDates.value.toList() diff --git a/app/src/main/java/com/mensinator/app/calendar/SymptomDialogs.kt b/app/src/main/java/com/mensinator/app/calendar/SymptomDialogs.kt new file mode 100644 index 00000000..23f8fcb1 --- /dev/null +++ b/app/src/main/java/com/mensinator/app/calendar/SymptomDialogs.kt @@ -0,0 +1,137 @@ +package com.mensinator.app.calendar + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.mensinator.app.settings.ResourceMapper +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mensinator.app.R +import com.mensinator.app.data.Symptom +import com.mensinator.app.ui.theme.MensinatorTheme +import java.time.LocalDate + +@Composable +fun EditSymptomsForDaysDialog( + date: LocalDate, + symptoms: List, + currentlyActiveSymptomIds: Set, + onSave: (List) -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + var selectedSymptoms by remember { + mutableStateOf( + symptoms.filter { it.id in currentlyActiveSymptomIds }.toSet() + ) + } + + AlertDialog( + onDismissRequest = { onCancel() }, + confirmButton = { + Button( + onClick = { + onSave(selectedSymptoms.toList()) + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.save_symptoms_button)) + } + }, + modifier = modifier, + dismissButton = { + Button( + onClick = { + onCancel() + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.cancel_button)) + } + }, + title = { + Text(text = stringResource(id = R.string.symptoms_dialog_title, date)) + }, + text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + symptoms.forEach { symptom -> + val symptomKey = ResourceMapper.getStringResourceId(symptom.name) + val symptomDisplayName = symptomKey?.let { stringResource(id = it) } ?: symptom.name + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .clickable { + selectedSymptoms = if (selectedSymptoms.contains(symptom)) { + selectedSymptoms - symptom + } else { + selectedSymptoms + symptom + } + }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = selectedSymptoms.contains(symptom), + onCheckedChange = null + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = symptomDisplayName) + } + } +// Spacer(modifier = Modifier.height(16.dp)) +// Button( +// onClick = { +// onCreateNewSymptom() +// }, +// modifier = Modifier.fillMaxWidth() +// ) { +// Text(text = stringResource(id = R.string.create_new_symptom_button)) +// } + } + }, + ) +} + +@Preview +@Composable +private fun EditSymptomsForDaysDialog_OneDayPreview() { + val symptoms = listOf( + Symptom(1, "Light", 0, ""), + Symptom(2, "Medium", 1, ""), + ) + MensinatorTheme { + EditSymptomsForDaysDialog( + date = LocalDate.now(), + symptoms = symptoms, + currentlyActiveSymptomIds = setOf(2), + onSave = {}, + onCancel = { }, + ) + } +} + +// TODO: Fix within https://github.com/EmmaTellblom/Mensinator/issues/203 +@Preview +@Composable +private fun EditSymptomsForDaysDialog_MultipleDaysPreview() { + val symptoms = listOf( + Symptom(1, "Light", 0, ""), + Symptom(2, "Medium", 1, ""), + ) + MensinatorTheme { + EditSymptomsForDaysDialog( + date = LocalDate.now(), + symptoms = symptoms, + currentlyActiveSymptomIds = setOf(2), + onSave = {}, + onCancel = { }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mensinator/app/Setting.kt b/app/src/main/java/com/mensinator/app/data/Setting.kt similarity index 80% rename from app/src/main/java/com/mensinator/app/Setting.kt rename to app/src/main/java/com/mensinator/app/data/Setting.kt index 25e4822b..2269ce38 100644 --- a/app/src/main/java/com/mensinator/app/Setting.kt +++ b/app/src/main/java/com/mensinator/app/data/Setting.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.data data class Setting( val key: String, diff --git a/app/src/main/java/com/mensinator/app/Symptom.kt b/app/src/main/java/com/mensinator/app/data/Symptom.kt similarity index 85% rename from app/src/main/java/com/mensinator/app/Symptom.kt rename to app/src/main/java/com/mensinator/app/data/Symptom.kt index caa9b45d..75ba5027 100644 --- a/app/src/main/java/com/mensinator/app/Symptom.kt +++ b/app/src/main/java/com/mensinator/app/data/Symptom.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.data data class Symptom( val id: Int, diff --git a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt index f3296471..f1ac1724 100644 --- a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt +++ b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt @@ -25,10 +25,12 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import com.mensinator.app.* import com.mensinator.app.R +import com.mensinator.app.business.IPeriodDatabaseHelper +import com.mensinator.app.calendar.CalendarScreen import com.mensinator.app.settings.SettingsScreen import com.mensinator.app.statistics.StatisticsScreen +import com.mensinator.app.symptoms.ManageSymptomScreen import org.koin.compose.koinInject enum class Screen(@StringRes val titleRes: Int) { @@ -187,6 +189,5 @@ fun MensinatorApp( } } } - } diff --git a/app/src/main/java/com/mensinator/app/ExportImportDialog.kt b/app/src/main/java/com/mensinator/app/settings/ExportImportDialog.kt similarity index 98% rename from app/src/main/java/com/mensinator/app/ExportImportDialog.kt rename to app/src/main/java/com/mensinator/app/settings/ExportImportDialog.kt index 0cc7b987..7fd73727 100644 --- a/app/src/main/java/com/mensinator/app/ExportImportDialog.kt +++ b/app/src/main/java/com/mensinator/app/settings/ExportImportDialog.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.settings import android.util.Log import android.widget.Toast @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.mensinator.app.R import com.mensinator.app.ui.theme.MensinatorTheme import java.io.File import java.io.FileOutputStream diff --git a/app/src/main/java/com/mensinator/app/FaqDialog.kt b/app/src/main/java/com/mensinator/app/settings/FaqDialog.kt similarity index 98% rename from app/src/main/java/com/mensinator/app/FaqDialog.kt rename to app/src/main/java/com/mensinator/app/settings/FaqDialog.kt index 4c8bdb3c..e53ba3f2 100644 --- a/app/src/main/java/com/mensinator/app/FaqDialog.kt +++ b/app/src/main/java/com/mensinator/app/settings/FaqDialog.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -17,6 +17,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.mensinator.app.R import com.mensinator.app.ui.theme.MensinatorTheme diff --git a/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt b/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt index 1c6389b8..4180754b 100644 --- a/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt +++ b/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt @@ -28,9 +28,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.mensinator.app.ExportDialog -import com.mensinator.app.FaqDialog -import com.mensinator.app.ImportDialog import com.mensinator.app.NotificationDialog import com.mensinator.app.R import com.mensinator.app.data.ColorSource diff --git a/app/src/main/java/com/mensinator/app/settings/SettingsViewModel.kt b/app/src/main/java/com/mensinator/app/settings/SettingsViewModel.kt index f76192ac..141c929d 100644 --- a/app/src/main/java/com/mensinator/app/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/mensinator/app/settings/SettingsViewModel.kt @@ -7,8 +7,8 @@ import android.util.Log import android.widget.Toast import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel -import com.mensinator.app.IExportImport -import com.mensinator.app.IPeriodDatabaseHelper +import com.mensinator.app.business.IExportImport +import com.mensinator.app.business.IPeriodDatabaseHelper import com.mensinator.app.R import com.mensinator.app.data.ColorSource import com.mensinator.app.settings.ColorSetting.* diff --git a/app/src/main/java/com/mensinator/app/statistics/StatisticsViewModel.kt b/app/src/main/java/com/mensinator/app/statistics/StatisticsViewModel.kt index 6696884f..6ac3fefc 100644 --- a/app/src/main/java/com/mensinator/app/statistics/StatisticsViewModel.kt +++ b/app/src/main/java/com/mensinator/app/statistics/StatisticsViewModel.kt @@ -4,6 +4,10 @@ import android.annotation.SuppressLint import android.content.Context import androidx.lifecycle.ViewModel import com.mensinator.app.* +import com.mensinator.app.business.ICalculationsHelper +import com.mensinator.app.business.IOvulationPrediction +import com.mensinator.app.business.IPeriodDatabaseHelper +import com.mensinator.app.business.IPeriodPrediction import com.mensinator.app.extensions.formatToOneDecimalPoint import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/mensinator/app/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt similarity index 96% rename from app/src/main/java/com/mensinator/app/ManageSymptomScreen.kt rename to app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt index fc46943e..c23eca92 100644 --- a/app/src/main/java/com/mensinator/app/ManageSymptomScreen.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.symptoms import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -19,10 +19,17 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import com.mensinator.app.* +import com.mensinator.app.R +import com.mensinator.app.business.IPeriodDatabaseHelper import com.mensinator.app.data.ColorSource +import com.mensinator.app.data.Symptom +import com.mensinator.app.data.isActive import com.mensinator.app.settings.ResourceMapper import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding +import com.mensinator.app.settings.SettingsViewModel import com.mensinator.app.ui.theme.isDarkMode +import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject @@ -30,7 +37,8 @@ import org.koin.compose.koinInject @Composable fun ManageSymptomScreen( showCreateSymptom: MutableState, - modifier: Modifier, + modifier: Modifier = Modifier, + //viewModel: SettingsViewModel = koinViewModel(), ) { val dbHelper: IPeriodDatabaseHelper = koinInject() var initialSymptoms = remember { dbHelper.getAllSymptoms() } diff --git a/app/src/main/java/com/mensinator/app/SymptomDialogs.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt similarity index 53% rename from app/src/main/java/com/mensinator/app/SymptomDialogs.kt rename to app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt index d6d0f68c..47d5dd0c 100644 --- a/app/src/main/java/com/mensinator/app/SymptomDialogs.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt @@ -1,104 +1,16 @@ -package com.mensinator.app +package com.mensinator.app.symptoms -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.mensinator.app.settings.ResourceMapper -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import com.mensinator.app.R import com.mensinator.app.ui.theme.MensinatorTheme -import java.time.LocalDate - - -@Composable -fun SymptomsDialog( - date: LocalDate, - symptoms: List, - dbHelper: IPeriodDatabaseHelper, - onSave: (List) -> Unit, - onCancel: () -> Unit, - modifier: Modifier = Modifier, -) { - var selectedSymptoms by remember { mutableStateOf(emptySet()) } - - LaunchedEffect(date) { - val symptomIdsForDate = dbHelper.getSymptomsFromDate(date).toSet() - selectedSymptoms = symptoms.filter { it.id in symptomIdsForDate }.toSet() - } - - AlertDialog( - onDismissRequest = { onCancel() }, - confirmButton = { - Button( - onClick = { - onSave(selectedSymptoms.toList()) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.save_symptoms_button)) - } - }, - modifier = modifier, - dismissButton = { - Button( - onClick = { - onCancel() - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.cancel_button)) - } - }, - title = { - Text(text = stringResource(id = R.string.symptoms_dialog_title, date)) - }, - text = { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - symptoms.forEach { symptom -> - val symptomKey = ResourceMapper.getStringResourceId(symptom.name) - val symptomDisplayName = symptomKey?.let { stringResource(id = it) } ?: symptom.name - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - .clickable { - selectedSymptoms = if (selectedSymptoms.contains(symptom)) { - selectedSymptoms - symptom - } else { - selectedSymptoms + symptom - } - }, - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = selectedSymptoms.contains(symptom), - onCheckedChange = null - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = symptomDisplayName, fontSize = 16.sp) - } - } -// Spacer(modifier = Modifier.height(16.dp)) -// Button( -// onClick = { -// onCreateNewSymptom() -// }, -// modifier = Modifier.fillMaxWidth() -// ) { -// Text(text = stringResource(id = R.string.create_new_symptom_button)) -// } - } - }, - ) -} - @Composable fun CreateNewSymptomDialog( @@ -108,7 +20,6 @@ fun CreateNewSymptomDialog( modifier: Modifier = Modifier, ) { var symptomName by remember { mutableStateOf(newSymptom) } - //val symptomKey = ResourceMapper.getStringResourceId(symptomName) AlertDialog( onDismissRequest = onCancel, diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt new file mode 100644 index 00000000..3a1ffc8a --- /dev/null +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt @@ -0,0 +1,41 @@ +package com.mensinator.app.symptoms + +import android.annotation.SuppressLint +import android.content.Context +import androidx.lifecycle.ViewModel +import com.mensinator.app.* +import com.mensinator.app.business.ICalculationsHelper +import com.mensinator.app.business.IOvulationPrediction +import com.mensinator.app.business.IPeriodDatabaseHelper +import com.mensinator.app.business.IPeriodPrediction +import com.mensinator.app.extensions.formatToOneDecimalPoint +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +class ManageSymptomsViewModel( + @SuppressLint("StaticFieldLeak") private val appContext: Context, + private val periodDatabaseHelper: IPeriodDatabaseHelper, +) : ViewModel() { + + + private val _viewState = MutableStateFlow( + ViewState() + ) + val viewState: StateFlow = _viewState.asStateFlow() + + data class ViewState( + val trackedPeriods: String? = null, + ) + + fun refreshData() { + _viewState.update { + it.copy( + trackedPeriods = periodDatabaseHelper.getPeriodCount().toString(), + ) + } + } +} diff --git a/app/src/test/java/com/mensinator/app/PeriodPredictionTest.kt b/app/src/test/java/com/mensinator/app/PeriodPredictionTest.kt index 2df8e3db..927c85d0 100644 --- a/app/src/test/java/com/mensinator/app/PeriodPredictionTest.kt +++ b/app/src/test/java/com/mensinator/app/PeriodPredictionTest.kt @@ -1,5 +1,9 @@ package com.mensinator.app +import com.mensinator.app.business.ICalculationsHelper +import com.mensinator.app.business.IPeriodDatabaseHelper +import com.mensinator.app.business.IPeriodPrediction +import com.mensinator.app.business.PeriodPrediction import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK From c002a76f828574abc88d7eafb76d5286f8dfb95b Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Sun, 26 Jan 2025 21:53:54 +0100 Subject: [PATCH 02/15] cleanup --- .../java/com/mensinator/app/calendar/SymptomDialogs.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/calendar/SymptomDialogs.kt b/app/src/main/java/com/mensinator/app/calendar/SymptomDialogs.kt index 23f8fcb1..363e0cda 100644 --- a/app/src/main/java/com/mensinator/app/calendar/SymptomDialogs.kt +++ b/app/src/main/java/com/mensinator/app/calendar/SymptomDialogs.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.mensinator.app.settings.ResourceMapper import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.mensinator.app.R import com.mensinator.app.data.Symptom import com.mensinator.app.ui.theme.MensinatorTheme @@ -85,15 +84,6 @@ fun EditSymptomsForDaysDialog( Text(text = symptomDisplayName) } } -// Spacer(modifier = Modifier.height(16.dp)) -// Button( -// onClick = { -// onCreateNewSymptom() -// }, -// modifier = Modifier.fillMaxWidth() -// ) { -// Text(text = stringResource(id = R.string.create_new_symptom_button)) -// } } }, ) From 25f5ed8ffaca570ddf4e512c901db457ca2d5617 Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Sun, 26 Jan 2025 22:41:50 +0100 Subject: [PATCH 03/15] Use ViewModel --- app/src/main/java/com/mensinator/app/App.kt | 2 + .../app/navigation/MensinatorApp.kt | 43 ++++---- .../app/symptoms/ManageSymptomScreen.kt | 100 +++++++++--------- .../app/symptoms/ManageSymptomsViewModel.kt | 65 +++++++++--- 4 files changed, 125 insertions(+), 85 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/App.kt b/app/src/main/java/com/mensinator/app/App.kt index 946848c5..447ee683 100644 --- a/app/src/main/java/com/mensinator/app/App.kt +++ b/app/src/main/java/com/mensinator/app/App.kt @@ -4,6 +4,7 @@ import android.app.Application import com.mensinator.app.business.* import com.mensinator.app.settings.SettingsViewModel import com.mensinator.app.statistics.StatisticsViewModel +import com.mensinator.app.symptoms.ManageSymptomsViewModel import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin @@ -23,6 +24,7 @@ class App : Application() { singleOf(::ExportImport) { bind() } singleOf(::NotificationScheduler) { bind() } + viewModel { ManageSymptomsViewModel(get(), get()) } viewModel { SettingsViewModel(get(), get(), get()) } viewModel { StatisticsViewModel(get(), get(), get(), get(), get()) } } diff --git a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt index f1ac1724..d4d7cd98 100644 --- a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt +++ b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt @@ -13,7 +13,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -66,30 +66,12 @@ fun MensinatorApp( // var nextOvulationCalculated by remember { mutableStateOf("Not enough data") } // var follicleGrowthDays by remember { mutableStateOf("0") } - val showCreateSymptom = rememberSaveable { mutableStateOf(false) } - val backStackEntry by navController.currentBackStackEntryAsState() val currentScreen = Screen.valueOf( backStackEntry?.destination?.route ?: Screen.Calendar.name ) Scaffold( - floatingActionButton = { - if (currentScreen == Screen.Symptoms) { - FloatingActionButton( - onClick = { showCreateSymptom.value = true }, - shape = CircleShape, - modifier = Modifier - .displayCutoutPadding() - .padding(5.dp) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = stringResource(R.string.delete_button) - ) - } - } - }, bottomBar = { val barItems = listOf( BarItem( @@ -164,12 +146,31 @@ fun MensinatorApp( } } composable(route = Screen.Symptoms.name) { + // Adapted from https://stackoverflow.com/a/71191082/3991578 + // Needed so that the action button can cause the dialog to be shown + val (fabOnClick, setFabOnClick) = remember { mutableStateOf<(() -> Unit)?>(null) } Scaffold( + floatingActionButton = { + if (currentScreen == Screen.Symptoms) { + FloatingActionButton( + onClick = { fabOnClick?.invoke() }, + shape = CircleShape, + modifier = Modifier + .displayCutoutPadding() + .padding(5.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(R.string.delete_button) + ) + } + } + }, topBar = { MensinatorTopBar(currentScreen) } ) { topBarPadding -> ManageSymptomScreen( - showCreateSymptom, - modifier = Modifier.padding(topBarPadding) + modifier = Modifier.padding(topBarPadding), + setFabOnClick = setFabOnClick ) } } diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt index c23eca92..f6e17d17 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -19,46 +19,36 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import com.mensinator.app.* import com.mensinator.app.R -import com.mensinator.app.business.IPeriodDatabaseHelper import com.mensinator.app.data.ColorSource -import com.mensinator.app.data.Symptom -import com.mensinator.app.data.isActive import com.mensinator.app.settings.ResourceMapper import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding -import com.mensinator.app.settings.SettingsViewModel import com.mensinator.app.ui.theme.isDarkMode import org.koin.androidx.compose.koinViewModel -import org.koin.compose.koinInject //Maps Database keys to res/strings.xml for multilanguage support @Composable fun ManageSymptomScreen( - showCreateSymptom: MutableState, modifier: Modifier = Modifier, - //viewModel: SettingsViewModel = koinViewModel(), + viewModel: ManageSymptomsViewModel = koinViewModel(), + setFabOnClick: (() -> Unit) -> Unit, ) { - val dbHelper: IPeriodDatabaseHelper = koinInject() - var initialSymptoms = remember { dbHelper.getAllSymptoms() } - var savedSymptoms by remember { mutableStateOf(initialSymptoms) } - - // State to manage the rename dialog visibility - var showRenameDialog by remember { mutableStateOf(false) } - var symptomToRename by remember { mutableStateOf(null) } - - // State to manage the dialog visibility - var showDeleteDialog by remember { mutableStateOf(false) } - var symptomToDelete by remember { mutableStateOf(null) } + val state = viewModel.viewState.collectAsState() + var initialSymptoms = state.value.allSymptoms + var savedSymptoms = initialSymptoms + LaunchedEffect(Unit) { + setFabOnClick { viewModel.showCreateSymptomDialog(true) } + viewModel.refreshData() + } Column( modifier = modifier .fillMaxSize() .verticalScroll(rememberScrollState()) // Make the column scrollable .displayCutoutExcludingStatusBarsPadding() - .padding (16.dp) + .padding(16.dp) .padding(bottom = 50.dp), // To be able to overscroll the list, to not have the FloatingActionButton overlapping verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -67,13 +57,13 @@ fun ManageSymptomScreen( var expanded by remember { mutableStateOf(false) } var selectedColorName by remember { mutableStateOf(symptom.color) } //val resKey = ResourceMapper.getStringResourceId(symptom.name) - val selectedColor = ColorSource.getColorMap(isDarkMode())[selectedColorName] ?: Color.Gray + val selectedColor = + ColorSource.getColorMap(isDarkMode())[selectedColorName] ?: Color.Gray val symptomDisplayName = ResourceMapper.getStringResourceOrCustom(symptom.name) Card( onClick = { - symptomToRename = symptom - showRenameDialog = true + viewModel.setSymptomToRename(symptom) }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(25.dp), @@ -88,14 +78,13 @@ fun ManageSymptomScreen( if (savedSymptoms.size > 1) { IconButton( onClick = { - val activeSymptoms = dbHelper.getAllSymptoms().filter { it.isActive } + val activeSymptoms = state.value.activeSymptoms if (activeSymptoms.contains(symptom)) { - showDeleteDialog = true - symptomToDelete = symptom + viewModel.setSymptomToDelete(symptom) } else { symptom.let { symptom -> savedSymptoms = savedSymptoms.filter { it.id != symptom.id } - dbHelper.deleteSymptom(symptom.id) + viewModel.deleteSymptom(symptom.id) } } }, @@ -184,7 +173,7 @@ fun ManageSymptomScreen( } // Save settings to the database savedSymptoms.forEach { symptom -> - dbHelper.updateSymptom( + viewModel.updateSymptom( symptom.id, symptom.active, symptom.color @@ -220,7 +209,7 @@ fun ManageSymptomScreen( } // Save settings to the database savedSymptoms.forEach { symptom -> - dbHelper.updateSymptom( + viewModel.updateSymptom( symptom.id, symptom.active, symptom.color @@ -233,51 +222,60 @@ fun ManageSymptomScreen( } } } - if (showCreateSymptom.value) { + if (state.value.showCreateSymptomDialog) { CreateNewSymptomDialog( newSymptom = "", // Pass an empty string for new symptoms onSave = { newSymptomName -> - dbHelper.createNewSymptom(newSymptomName) - initialSymptoms = dbHelper.getAllSymptoms() //reset the data to make the new symptom appear - savedSymptoms = initialSymptoms - showCreateSymptom.value = false // Close the new symptom dialog + viewModel.createNewSymptom(newSymptomName) + // todo check whether this is needed + //initialSymptoms = state.value.allSymptoms // reset the data to make the new symptom appear + //savedSymptoms = initialSymptoms + viewModel.showCreateSymptomDialog(false) }, onCancel = { - showCreateSymptom.value = false // Close the new symptom dialog + viewModel.showCreateSymptomDialog(false) }, ) } - if (showRenameDialog && symptomToRename != null) { - val symptomKey = ResourceMapper.getStringResourceId(symptomToRename!!.name) + val symptomToRename = state.value.symptomToRename + if (symptomToRename != null) { + val symptomKey = ResourceMapper.getStringResourceId(symptomToRename.name) val symptomDisplayName = - symptomKey?.let { stringResource(id = it) } ?: symptomToRename!!.name + symptomKey?.let { stringResource(id = it) } ?: symptomToRename.name RenameSymptomDialog( symptomDisplayName = symptomDisplayName, onRename = { newName -> - dbHelper.renameSymptom(symptomToRename!!.id, newName) - initialSymptoms = dbHelper.getAllSymptoms() - savedSymptoms = initialSymptoms - showRenameDialog = false + viewModel.renameSymptom(symptomToRename.id, newName) + + // todo check whether this is needed + //initialSymptoms = state.value.allSymptoms + //savedSymptoms = initialSymptoms + + viewModel.setSymptomToRename(null) }, onCancel = { - showRenameDialog = false + viewModel.setSymptomToRename(null) } ) } - // Show the delete confirmation dialog - if (showDeleteDialog) { + val symptomToDelete = state.value.symptomToDelete + if (symptomToDelete != null) { DeleteSymptomDialog( onSave = { - symptomToDelete?.let { symptom -> - savedSymptoms = savedSymptoms.filter { it.id != symptom.id } - dbHelper.deleteSymptom(symptom.id) - } - showDeleteDialog = false + viewModel.deleteSymptom(symptomToDelete.id) + + + // todo check whether this is needed + //savedSymptoms = savedSymptoms.filter { it.id != symptomToDelete.id } + + viewModel.setSymptomToDelete(null) + }, + onCancel = { + viewModel.setSymptomToDelete(null) }, - onCancel = { showDeleteDialog = false }, ) } } diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt index 3a1ffc8a..d5e2df5b 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt @@ -3,18 +3,17 @@ package com.mensinator.app.symptoms import android.annotation.SuppressLint import android.content.Context import androidx.lifecycle.ViewModel -import com.mensinator.app.* -import com.mensinator.app.business.ICalculationsHelper -import com.mensinator.app.business.IOvulationPrediction +import androidx.lifecycle.viewModelScope import com.mensinator.app.business.IPeriodDatabaseHelper -import com.mensinator.app.business.IPeriodPrediction -import com.mensinator.app.extensions.formatToOneDecimalPoint +import com.mensinator.app.data.Symptom +import com.mensinator.app.data.isActive +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class ManageSymptomsViewModel( @SuppressLint("StaticFieldLeak") private val appContext: Context, @@ -28,14 +27,54 @@ class ManageSymptomsViewModel( val viewState: StateFlow = _viewState.asStateFlow() data class ViewState( - val trackedPeriods: String? = null, + val allSymptoms: List = listOf(), + val activeSymptoms: List = listOf(), + val showCreateSymptomDialog: Boolean = false, + val symptomToRename: Symptom? = null, + val symptomToDelete: Symptom? = null, ) - fun refreshData() { - _viewState.update { - it.copy( - trackedPeriods = periodDatabaseHelper.getPeriodCount().toString(), - ) + suspend fun refreshData() { + withContext(Dispatchers.IO) { + val allSymptoms = periodDatabaseHelper.getAllSymptoms() + _viewState.update { + it.copy( + allSymptoms = periodDatabaseHelper.getAllSymptoms(), + activeSymptoms = allSymptoms.filter { it.isActive }, + ) + } } } + + fun createNewSymptom(name: String) { + periodDatabaseHelper.createNewSymptom(name) + viewModelScope.launch { refreshData() } + } + + fun updateSymptom(id: Int, active: Int, color: String) { + periodDatabaseHelper.updateSymptom(id, active, color) + viewModelScope.launch { refreshData() } + } + + fun renameSymptom(id: Int, name: String) { + periodDatabaseHelper.renameSymptom(id, name) + viewModelScope.launch { refreshData() } + } + + fun deleteSymptom(id: Int) { + periodDatabaseHelper.deleteSymptom(id) + viewModelScope.launch { refreshData() } + } + + fun showCreateSymptomDialog(show: Boolean) { + _viewState.update { it.copy(showCreateSymptomDialog = show) } + } + + fun setSymptomToRename(symptom: Symptom?) { + _viewState.update { it.copy(symptomToRename = symptom) } + } + + fun setSymptomToDelete(symptom: Symptom?) { + _viewState.update { it.copy(symptomToDelete = symptom) } + } } From 05761134bd25f23b55a35243401f97e649b600e7 Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Sun, 26 Jan 2025 22:42:21 +0100 Subject: [PATCH 04/15] Don't close database. If parallel requests to the database are ongoing, closing the db results in crashes --- .../app/business/PeriodDatabaseHelper.kt | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt b/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt index 81fd8fd2..deef2b68 100644 --- a/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt +++ b/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt @@ -101,7 +101,7 @@ class PeriodDatabaseHelper(context: Context) : if (rowsUpdated == 0) { db.insert(TABLE_PERIODS, null, values) } - db.close() + //db.close() } override fun getPeriodDatesForMonth(year: Int, month: Int): Map { @@ -143,7 +143,7 @@ class PeriodDatabaseHelper(context: Context) : Log.e(TAG, "Cursor is null while querying for dates") } - db.close() + //db.close() return dates } @@ -156,7 +156,7 @@ class PeriodDatabaseHelper(context: Context) : count = cursor.getInt(0) } cursor.close() - db.close() + //db.close() return count } @@ -170,7 +170,7 @@ class PeriodDatabaseHelper(context: Context) : } else { Log.d(TAG, "No date $date found in $TABLE_PERIODS to remove") } - db.close() + //db.close() } override fun getAllSymptoms(): List { @@ -194,7 +194,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return symptoms } @@ -208,7 +208,7 @@ class PeriodDatabaseHelper(context: Context) : } // Insert the new symptom into the symptoms table db.insert(TABLE_SYMPTOMS, null, values) - db.close() // Close the database connection to free up resources + //db.close() // Close the database connection to free up resources } override fun getSymptomDatesForMonth(year: Int, month: Int): Set { @@ -250,7 +250,7 @@ class PeriodDatabaseHelper(context: Context) : Log.e(TAG, "Error querying for symptom dates", e) } finally { cursor.close() - db.close() + //db.close() } return dates @@ -297,7 +297,7 @@ class PeriodDatabaseHelper(context: Context) : } // Close the database connection - db.close() + //db.close() } override fun getActiveSymptomIdsForDate(date: LocalDate): List { @@ -321,7 +321,7 @@ class PeriodDatabaseHelper(context: Context) : } } - db.close() + //db.close() return symptoms } @@ -346,7 +346,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return symptomColors } @@ -375,7 +375,7 @@ class PeriodDatabaseHelper(context: Context) : } val rowsUpdated = db.update(TABLE_APP_SETTINGS, contentValues, "$COLUMN_SETTING_KEY = ?", arrayOf(key)) - db.close() + //db.close() return rowsUpdated > 0 } @@ -402,7 +402,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return setting } @@ -436,7 +436,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() } override fun getOvulationDatesForMonth(year: Int, month: Int): Set { @@ -474,7 +474,7 @@ class PeriodDatabaseHelper(context: Context) : Log.e("TAG", "Error querying for ovulation dates", e) } finally { cursor.close() - db.close() + //db.close() } return dates @@ -489,7 +489,7 @@ class PeriodDatabaseHelper(context: Context) : count = cursor.getInt(0) } cursor.close() - db.close() + //db.close() return count } @@ -545,7 +545,7 @@ class PeriodDatabaseHelper(context: Context) : Log.e(TAG, "Cursor is null while querying for periodId") } - db.close() + //db.close() return periodId } @@ -576,7 +576,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return firstLatestDate } @@ -596,7 +596,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return oldestPeriodDate } @@ -615,7 +615,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return newestOvulationDate } @@ -645,7 +645,7 @@ class PeriodDatabaseHelper(context: Context) : throw IllegalStateException("No symptom found with ID: $id") } - db.close() + //db.close() } // This function is used to get the latest X ovulation dates where they are followed by a period @@ -674,7 +674,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return ovulationDates } @@ -693,7 +693,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return ovulationDate } @@ -726,7 +726,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return dateList } @@ -751,7 +751,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return firstNextDate } @@ -768,7 +768,7 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() + //db.close() return count } @@ -787,7 +787,7 @@ class PeriodDatabaseHelper(context: Context) : } while (cursor.moveToNext()) } cursor.close() - db.close() + //db.close() return ovulationDates } @@ -802,7 +802,7 @@ class PeriodDatabaseHelper(context: Context) : } else { Log.d(TAG, "No symptoms to delete") } - db.close() + //db.close() } override fun getDBVersion(): String { @@ -830,7 +830,7 @@ class PeriodDatabaseHelper(context: Context) : return LocalDate.parse(dateString) } cursor.close() - db.close() + //db.close() return latestPeriodStart } } From 56b4bb719b3d476235f37b5a160ddcefb123563c Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Sun, 26 Jan 2025 22:47:48 +0100 Subject: [PATCH 05/15] add todos --- .../app/symptoms/ManageSymptomScreen.kt | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt index f6e17d17..bd62c6f7 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -26,6 +27,14 @@ import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.ui.theme.isDarkMode import org.koin.androidx.compose.koinViewModel +// TODO: Improve Composable structure +// TODO: Use tokens for shapes +// TODO: Maybe delete savedSymptoms +// TODO: Define/use constant for 50.dp FAB size +// TODO: +// TODO: +// TODO: +// TODO: //Maps Database keys to res/strings.xml for multilanguage support @Composable @@ -35,8 +44,7 @@ fun ManageSymptomScreen( setFabOnClick: (() -> Unit) -> Unit, ) { val state = viewModel.viewState.collectAsState() - var initialSymptoms = state.value.allSymptoms - var savedSymptoms = initialSymptoms + var savedSymptoms = state.value.allSymptoms LaunchedEffect(Unit) { setFabOnClick { viewModel.showCreateSymptomDialog(true) } @@ -151,8 +159,7 @@ fun ManageSymptomScreen( ) { ColorSource.colorsGroupedByHue.forEach { colorGroup -> Row( - modifier = Modifier - .wrapContentSize(), + modifier = Modifier.wrapContentSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { @@ -162,7 +169,7 @@ fun ManageSymptomScreen( DropdownMenuItem( modifier = Modifier .size(50.dp) - .clip(RoundedCornerShape(100.dp)), + .clip(CircleShape), onClick = { selectedColorName = colorName expanded = false @@ -222,14 +229,13 @@ fun ManageSymptomScreen( } } } + if (state.value.showCreateSymptomDialog) { + // TODO: remove newSymptom parameter? CreateNewSymptomDialog( newSymptom = "", // Pass an empty string for new symptoms onSave = { newSymptomName -> viewModel.createNewSymptom(newSymptomName) - // todo check whether this is needed - //initialSymptoms = state.value.allSymptoms // reset the data to make the new symptom appear - //savedSymptoms = initialSymptoms viewModel.showCreateSymptomDialog(false) }, onCancel = { @@ -248,11 +254,6 @@ fun ManageSymptomScreen( symptomDisplayName = symptomDisplayName, onRename = { newName -> viewModel.renameSymptom(symptomToRename.id, newName) - - // todo check whether this is needed - //initialSymptoms = state.value.allSymptoms - //savedSymptoms = initialSymptoms - viewModel.setSymptomToRename(null) }, onCancel = { @@ -266,11 +267,6 @@ fun ManageSymptomScreen( DeleteSymptomDialog( onSave = { viewModel.deleteSymptom(symptomToDelete.id) - - - // todo check whether this is needed - //savedSymptoms = savedSymptoms.filter { it.id != symptomToDelete.id } - viewModel.setSymptomToDelete(null) }, onCancel = { From 89bfc0be47494f7b9b5c648fdab638cb085dec7b Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Sun, 26 Jan 2025 23:05:44 +0100 Subject: [PATCH 06/15] Improve fab height consistency, cleanup --- .../com/mensinator/app/navigation/MensinatorApp.kt | 3 ++- .../mensinator/app/symptoms/ManageSymptomScreen.kt | 13 ++++--------- .../app/symptoms/ManageSymptomsDialogs.kt | 4 +--- .../java/com/mensinator/app/ui/theme/UiConstants.kt | 7 +++++++ 4 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/mensinator/app/ui/theme/UiConstants.kt diff --git a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt index d4d7cd98..74eb3816 100644 --- a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt +++ b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt @@ -31,6 +31,7 @@ import com.mensinator.app.calendar.CalendarScreen import com.mensinator.app.settings.SettingsScreen import com.mensinator.app.statistics.StatisticsScreen import com.mensinator.app.symptoms.ManageSymptomScreen +import com.mensinator.app.ui.theme.UiConstants import org.koin.compose.koinInject enum class Screen(@StringRes val titleRes: Int) { @@ -157,7 +158,7 @@ fun MensinatorApp( shape = CircleShape, modifier = Modifier .displayCutoutPadding() - .padding(5.dp) + .size(UiConstants.floatingActionButtonSize) ) { Icon( imageVector = Icons.Default.Add, diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt index bd62c6f7..630eace7 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -22,19 +22,16 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.mensinator.app.R import com.mensinator.app.data.ColorSource -import com.mensinator.app.settings.ResourceMapper import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding +import com.mensinator.app.settings.ResourceMapper +import com.mensinator.app.ui.theme.UiConstants import com.mensinator.app.ui.theme.isDarkMode import org.koin.androidx.compose.koinViewModel // TODO: Improve Composable structure // TODO: Use tokens for shapes + // TODO: Maybe delete savedSymptoms -// TODO: Define/use constant for 50.dp FAB size -// TODO: -// TODO: -// TODO: -// TODO: //Maps Database keys to res/strings.xml for multilanguage support @Composable @@ -57,7 +54,7 @@ fun ManageSymptomScreen( .verticalScroll(rememberScrollState()) // Make the column scrollable .displayCutoutExcludingStatusBarsPadding() .padding(16.dp) - .padding(bottom = 50.dp), // To be able to overscroll the list, to not have the FloatingActionButton overlapping + .padding(bottom = UiConstants.floatingActionButtonSize * 1.25f), // To be able to overscroll the list, to not have the FloatingActionButton overlapping verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -231,9 +228,7 @@ fun ManageSymptomScreen( } if (state.value.showCreateSymptomDialog) { - // TODO: remove newSymptom parameter? CreateNewSymptomDialog( - newSymptom = "", // Pass an empty string for new symptoms onSave = { newSymptomName -> viewModel.createNewSymptom(newSymptomName) viewModel.showCreateSymptomDialog(false) diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt index 47d5dd0c..b38e31ad 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt @@ -14,12 +14,11 @@ import com.mensinator.app.ui.theme.MensinatorTheme @Composable fun CreateNewSymptomDialog( - newSymptom: String, onSave: (String) -> Unit, onCancel: () -> Unit, modifier: Modifier = Modifier, ) { - var symptomName by remember { mutableStateOf(newSymptom) } + var symptomName by remember { mutableStateOf("") } AlertDialog( onDismissRequest = onCancel, @@ -136,7 +135,6 @@ fun DeleteSymptomDialog( private fun CreateNewSymptomDialogPreview() { MensinatorTheme { CreateNewSymptomDialog( - newSymptom = "preview", onSave = {}, onCancel = {} ) diff --git a/app/src/main/java/com/mensinator/app/ui/theme/UiConstants.kt b/app/src/main/java/com/mensinator/app/ui/theme/UiConstants.kt new file mode 100644 index 00000000..ce56b3f5 --- /dev/null +++ b/app/src/main/java/com/mensinator/app/ui/theme/UiConstants.kt @@ -0,0 +1,7 @@ +package com.mensinator.app.ui.theme + +import androidx.compose.ui.unit.dp + +object UiConstants { + val floatingActionButtonSize = 56.dp +} \ No newline at end of file From eb83bb8dfd3d53fce39b2db92843846902bf1603 Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Sun, 26 Jan 2025 23:06:05 +0100 Subject: [PATCH 07/15] Fix wrong insets --- .../com/mensinator/app/navigation/MensinatorApp.kt | 12 ++++++++---- .../com/mensinator/app/settings/SettingsScreen.kt | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt index 74eb3816..22c84caa 100644 --- a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt +++ b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt @@ -134,14 +134,16 @@ fun MensinatorApp( ) {//create a new file for every page and pass it inside the composable composable(route = Screen.Calendar.name) { Scaffold( - topBar = { MensinatorTopBar(currentScreen) } + topBar = { MensinatorTopBar(currentScreen) }, + contentWindowInsets = WindowInsets(0.dp), ) { topBarPadding -> CalendarScreen(modifier = Modifier.padding(topBarPadding)) } } composable(route = Screen.Statistic.name) { Scaffold( - topBar = { MensinatorTopBar(currentScreen) } + topBar = { MensinatorTopBar(currentScreen) }, + contentWindowInsets = WindowInsets(0.dp), ) { topBarPadding -> StatisticsScreen(modifier = Modifier.padding(topBarPadding)) } @@ -167,7 +169,8 @@ fun MensinatorApp( } } }, - topBar = { MensinatorTopBar(currentScreen) } + topBar = { MensinatorTopBar(currentScreen) }, + contentWindowInsets = WindowInsets(0.dp), ) { topBarPadding -> ManageSymptomScreen( modifier = Modifier.padding(topBarPadding), @@ -177,7 +180,8 @@ fun MensinatorApp( } composable(route = Screen.Settings.name) { Scaffold( - topBar = { MensinatorTopBar(currentScreen) } + topBar = { MensinatorTopBar(currentScreen) }, + contentWindowInsets = WindowInsets(0.dp), ) { topBarPadding -> Column { SettingsScreen( diff --git a/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt b/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt index 4180754b..a24998e5 100644 --- a/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt +++ b/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mensinator.app.NotificationDialog import com.mensinator.app.R import com.mensinator.app.data.ColorSource +import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.ui.theme.MensinatorTheme import com.mensinator.app.ui.theme.isDarkMode import org.koin.androidx.compose.koinViewModel @@ -112,6 +113,7 @@ fun SettingsScreen( modifier = modifier .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()) + .displayCutoutExcludingStatusBarsPadding() ) { Spacer(Modifier.height(16.dp)) SettingSectionHeader(text = stringResource(R.string.colors)) From c40061169fed60ae8a84d3c59ecd5d6e28261891 Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Thu, 30 Jan 2025 11:27:25 +0100 Subject: [PATCH 08/15] Use shapes, extract constant --- .../app/symptoms/ManageSymptomScreen.kt | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt index 630eace7..e1053278 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -1,11 +1,9 @@ package com.mensinator.app.symptoms import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close @@ -22,18 +20,22 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.mensinator.app.R import com.mensinator.app.data.ColorSource +import com.mensinator.app.data.isActive import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.settings.ResourceMapper +import com.mensinator.app.ui.theme.MensinatorTheme import com.mensinator.app.ui.theme.UiConstants import com.mensinator.app.ui.theme.isDarkMode import org.koin.androidx.compose.koinViewModel // TODO: Improve Composable structure -// TODO: Use tokens for shapes // TODO: Maybe delete savedSymptoms -//Maps Database keys to res/strings.xml for multilanguage support +private object SymptomScreenConstants { + val colorCircleSize = 24.dp +} + @Composable fun ManageSymptomScreen( modifier: Modifier = Modifier, @@ -61,7 +63,6 @@ fun ManageSymptomScreen( savedSymptoms.forEach { symptom -> var expanded by remember { mutableStateOf(false) } var selectedColorName by remember { mutableStateOf(symptom.color) } - //val resKey = ResourceMapper.getStringResourceId(symptom.name) val selectedColor = ColorSource.getColorMap(isDarkMode())[selectedColorName] ?: Color.Gray @@ -71,7 +72,7 @@ fun ManageSymptomScreen( viewModel.setSymptomToRename(symptom) }, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(25.dp), + shape = MaterialTheme.shapes.extraLarge, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -112,13 +113,6 @@ fun ManageSymptomScreen( Box { // Color Dropdown wrapped in a Box for alignment Card( - modifier = Modifier - .padding(start = 10.dp) - .clickable { } - .clip(RoundedCornerShape(26.dp)), // Make the entire row round - colors = CardDefaults.cardColors( - containerColor = Color.Transparent, - ), onClick = { expanded = true } ) { Row( @@ -127,8 +121,8 @@ fun ManageSymptomScreen( ) { Box( modifier = Modifier - .size(25.dp) - .clip(RoundedCornerShape(26.dp)) + .size(SymptomScreenConstants.colorCircleSize) + .clip(CircleShape) .background(selectedColor), ) Icon( @@ -165,7 +159,7 @@ fun ManageSymptomScreen( if (colorValue != null) { DropdownMenuItem( modifier = Modifier - .size(50.dp) + .size(SymptomScreenConstants.colorCircleSize * 2) .clip(CircleShape), onClick = { selectedColorName = colorName @@ -187,8 +181,8 @@ fun ManageSymptomScreen( text = { Box( modifier = Modifier - .size(25.dp) - .clip(RoundedCornerShape(26.dp)) + .size(SymptomScreenConstants.colorCircleSize) + .clip(CircleShape) .background(colorValue) // Use the color from the map ) } @@ -204,10 +198,9 @@ fun ManageSymptomScreen( Spacer(modifier = Modifier.weight(0.05f)) Switch( - checked = symptom.active == 1, + checked = symptom.isActive, onCheckedChange = { checked -> - val updatedSymptom = - symptom.copy(active = if (checked) 1 else 0) + val updatedSymptom = symptom.copy(active = if (checked) 1 else 0) savedSymptoms = savedSymptoms.map { if (it.id == symptom.id) updatedSymptom else it } From b953703821e9bbd3ff72e346d9a21c07efee0680 Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Thu, 30 Jan 2025 12:04:35 +0100 Subject: [PATCH 09/15] Extract item --- .../app/symptoms/ManageSymptomScreen.kt | 330 +++++++++--------- 1 file changed, 168 insertions(+), 162 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt index e1053278..72b60e88 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -16,10 +16,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.mensinator.app.R import com.mensinator.app.data.ColorSource +import com.mensinator.app.data.Symptom import com.mensinator.app.data.isActive import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.settings.ResourceMapper @@ -28,8 +30,6 @@ import com.mensinator.app.ui.theme.UiConstants import com.mensinator.app.ui.theme.isDarkMode import org.koin.androidx.compose.koinViewModel -// TODO: Improve Composable structure - // TODO: Maybe delete savedSymptoms private object SymptomScreenConstants { @@ -43,7 +43,7 @@ fun ManageSymptomScreen( setFabOnClick: (() -> Unit) -> Unit, ) { val state = viewModel.viewState.collectAsState() - var savedSymptoms = state.value.allSymptoms + val symptoms = state.value.allSymptoms LaunchedEffect(Unit) { setFabOnClick { viewModel.showCreateSymptomDialog(true) } @@ -60,163 +60,12 @@ fun ManageSymptomScreen( verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - savedSymptoms.forEach { symptom -> - var expanded by remember { mutableStateOf(false) } - var selectedColorName by remember { mutableStateOf(symptom.color) } - val selectedColor = - ColorSource.getColorMap(isDarkMode())[selectedColorName] ?: Color.Gray - - val symptomDisplayName = ResourceMapper.getStringResourceOrCustom(symptom.name) - Card( - onClick = { - viewModel.setSymptomToRename(symptom) - }, - modifier = Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.extraLarge, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp) - ) { - if (savedSymptoms.size > 1) { - IconButton( - onClick = { - val activeSymptoms = state.value.activeSymptoms - if (activeSymptoms.contains(symptom)) { - viewModel.setSymptomToDelete(symptom) - } else { - symptom.let { symptom -> - savedSymptoms = savedSymptoms.filter { it.id != symptom.id } - viewModel.deleteSymptom(symptom.id) - } - } - }, - modifier = Modifier.size(20.dp) - ) { - Icon( - Icons.Default.Close, - contentDescription = stringResource(id = R.string.close) - ) - } - } - Spacer(modifier = Modifier.padding(start = 5.dp)) - Text( - text = symptomDisplayName, - textAlign = TextAlign.Left, - modifier = Modifier.weight(1f) // Let the text expand to fill available space - ) - - //Color Picker Dropdown Menu - Box { - // Color Dropdown wrapped in a Box for alignment - Card( - onClick = { expanded = true } - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - Box( - modifier = Modifier - .size(SymptomScreenConstants.colorCircleSize) - .clip(CircleShape) - .background(selectedColor), - ) - Icon( - painter = painterResource(id = R.drawable.keyboard_arrow_down_24px), - contentDescription = stringResource(id = R.string.selection_color), - modifier = Modifier.wrapContentSize() - ) - } - } - - DropdownMenu( - offset = DpOffset(x = (-50).dp, y = (10).dp), - expanded = expanded, - onDismissRequest = { expanded = false }, - modifier = Modifier - .wrapContentSize() - ) { - // Retrieve the colorMap from DataSource - val colorMap = ColorSource.getColorMap(isDarkMode()) - - Column( - modifier = Modifier.wrapContentSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - ColorSource.colorsGroupedByHue.forEach { colorGroup -> - Row( - modifier = Modifier.wrapContentSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - colorGroup.forEach { colorName -> - val colorValue = colorMap[colorName] - if (colorValue != null) { - DropdownMenuItem( - modifier = Modifier - .size(SymptomScreenConstants.colorCircleSize * 2) - .clip(CircleShape), - onClick = { - selectedColorName = colorName - expanded = false - val updatedSymptom = - symptom.copy(color = colorName) - savedSymptoms = savedSymptoms.map { - if (it.id == symptom.id) updatedSymptom else it - } - // Save settings to the database - savedSymptoms.forEach { symptom -> - viewModel.updateSymptom( - symptom.id, - symptom.active, - symptom.color - ) - } - }, - text = { - Box( - modifier = Modifier - .size(SymptomScreenConstants.colorCircleSize) - .clip(CircleShape) - .background(colorValue) // Use the color from the map - ) - } - ) - } - } - } - } - } - } - } - - Spacer(modifier = Modifier.weight(0.05f)) - - Switch( - checked = symptom.isActive, - onCheckedChange = { checked -> - val updatedSymptom = symptom.copy(active = if (checked) 1 else 0) - savedSymptoms = savedSymptoms.map { - if (it.id == symptom.id) updatedSymptom else it - } - // Save settings to the database - savedSymptoms.forEach { symptom -> - viewModel.updateSymptom( - symptom.id, - symptom.active, - symptom.color - ) - } - }, - ) - Spacer(modifier = Modifier.weight(0.05f)) - } - } + symptoms.forEach { symptom -> + SymptomItem( + viewModel = viewModel, + symptom = symptom, + showDeletionIcon = symptoms.size > 1 + ) } } @@ -235,8 +84,7 @@ fun ManageSymptomScreen( val symptomToRename = state.value.symptomToRename if (symptomToRename != null) { val symptomKey = ResourceMapper.getStringResourceId(symptomToRename.name) - val symptomDisplayName = - symptomKey?.let { stringResource(id = it) } ?: symptomToRename.name + val symptomDisplayName = symptomKey?.let { stringResource(id = it) } ?: symptomToRename.name RenameSymptomDialog( symptomDisplayName = symptomDisplayName, @@ -263,3 +111,161 @@ fun ManageSymptomScreen( ) } } + +@Composable +private fun SymptomItem( + viewModel: ManageSymptomsViewModel, + symptom: Symptom, + showDeletionIcon: Boolean +) { + val selectedColor = ColorSource.getColorMap(isDarkMode())[symptom.color] ?: Color.Gray + val symptomDisplayName = ResourceMapper.getStringResourceOrCustom(symptom.name) + + Card( + onClick = { + viewModel.setSymptomToRename(symptom) + }, + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.extraLarge, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp) + ) { + if (showDeletionIcon) { + IconButton( + onClick = { viewModel.setSymptomToDelete(symptom) }, + modifier = Modifier.size(20.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(id = R.string.close) + ) + } + } + Spacer(modifier = Modifier.padding(start = 5.dp)) + Text( + text = symptomDisplayName, + textAlign = TextAlign.Left, + modifier = Modifier.weight(1f) // Let the text expand to fill available space + ) + + //Color Picker Dropdown Menu + ColorPicker(selectedColor, symptom, viewModel) + + Spacer(modifier = Modifier.weight(0.05f)) + + Switch( + checked = symptom.isActive, + onCheckedChange = { checked -> + val updatedSymptom = symptom.copy(active = if (checked) 1 else 0) + viewModel.updateSymptom( + updatedSymptom.id, + updatedSymptom.active, + updatedSymptom.color + ) + }, + ) + Spacer(modifier = Modifier.weight(0.05f)) + } + } +} + +@Composable +private fun ColorPicker( + selectedColor: Color, + symptom: Symptom, + viewModel: ManageSymptomsViewModel +) { + var expanded by remember { mutableStateOf(false) } + + Box { + // Color Dropdown wrapped in a Box for alignment + Card( + onClick = { expanded = true } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Box( + modifier = Modifier + .size(SymptomScreenConstants.colorCircleSize) + .clip(CircleShape) + .background(selectedColor), + ) + Icon( + painter = painterResource(id = R.drawable.keyboard_arrow_down_24px), + contentDescription = stringResource(id = R.string.selection_color), + modifier = Modifier.wrapContentSize() + ) + } + } + + DropdownMenu( + offset = DpOffset(x = (-50).dp, y = (10).dp), + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.wrapContentSize() + ) { + // Retrieve the colorMap from DataSource + val colorMap = ColorSource.getColorMap(isDarkMode()) + + Column( + modifier = Modifier.wrapContentSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + ColorSource.colorsGroupedByHue.forEach { colorGroup -> + Row( + modifier = Modifier.wrapContentSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + colorGroup.forEach { colorName -> + val colorValue = colorMap[colorName] ?: return@Row + DropdownMenuItem( + modifier = Modifier + .size(SymptomScreenConstants.colorCircleSize * 2) + .clip(CircleShape), + onClick = { + expanded = false + val updatedSymptom = symptom.copy(color = colorName) + viewModel.updateSymptom( + updatedSymptom.id, + updatedSymptom.active, + updatedSymptom.color + ) + }, + text = { + Box( + modifier = Modifier + .size(SymptomScreenConstants.colorCircleSize) + .clip(CircleShape) + .background(colorValue) // Use the color from the map + ) + } + ) + } + } + } + } + } + } +} + + +// TODO: Broken +@Preview(showBackground = true) +@Composable +private fun SymptomItemPreview() { + MensinatorTheme { + SymptomItem( + viewModel = koinViewModel(), + symptom = Symptom(1, "Medium flow", 1, "red"), + showDeletionIcon = true + ) + } +} \ No newline at end of file From 28bd91b04b5f2f4e0c3a9fba4657ebfb37280d23 Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Thu, 30 Jan 2025 12:30:47 +0100 Subject: [PATCH 10/15] Introduce UiAction class to enable previewing of composables --- .../app/symptoms/ManageSymptomScreen.kt | 84 ++++++++++--------- .../app/symptoms/ManageSymptomsViewModel.kt | 51 +++++++---- 2 files changed, 81 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt index 72b60e88..d6943564 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -25,13 +25,12 @@ import com.mensinator.app.data.Symptom import com.mensinator.app.data.isActive import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.settings.ResourceMapper +import com.mensinator.app.symptoms.ManageSymptomsViewModel.UiAction import com.mensinator.app.ui.theme.MensinatorTheme import com.mensinator.app.ui.theme.UiConstants import com.mensinator.app.ui.theme.isDarkMode import org.koin.androidx.compose.koinViewModel -// TODO: Maybe delete savedSymptoms - private object SymptomScreenConstants { val colorCircleSize = 24.dp } @@ -46,7 +45,7 @@ fun ManageSymptomScreen( val symptoms = state.value.allSymptoms LaunchedEffect(Unit) { - setFabOnClick { viewModel.showCreateSymptomDialog(true) } + setFabOnClick { viewModel.onAction(UiAction.ShowCreationDialog) } viewModel.refreshData() } @@ -62,7 +61,7 @@ fun ManageSymptomScreen( ) { symptoms.forEach { symptom -> SymptomItem( - viewModel = viewModel, + onAction = { viewModel.onAction(it) }, symptom = symptom, showDeletionIcon = symptoms.size > 1 ) @@ -72,11 +71,11 @@ fun ManageSymptomScreen( if (state.value.showCreateSymptomDialog) { CreateNewSymptomDialog( onSave = { newSymptomName -> - viewModel.createNewSymptom(newSymptomName) - viewModel.showCreateSymptomDialog(false) + viewModel.onAction(UiAction.CreateSymptom(newSymptomName)) + viewModel.onAction(UiAction.HideCreationDialog) }, onCancel = { - viewModel.showCreateSymptomDialog(false) + viewModel.onAction(UiAction.HideCreationDialog) }, ) } @@ -89,11 +88,13 @@ fun ManageSymptomScreen( RenameSymptomDialog( symptomDisplayName = symptomDisplayName, onRename = { newName -> - viewModel.renameSymptom(symptomToRename.id, newName) - viewModel.setSymptomToRename(null) + val updatedSymptom = symptomToRename.copy(name = newName) + viewModel.onAction(UiAction.RenameSymptom(updatedSymptom)) + viewModel.onAction(UiAction.HideRenamingDialog) }, onCancel = { - viewModel.setSymptomToRename(null) + viewModel.onAction(UiAction.HideRenamingDialog) + } ) } @@ -102,11 +103,11 @@ fun ManageSymptomScreen( if (symptomToDelete != null) { DeleteSymptomDialog( onSave = { - viewModel.deleteSymptom(symptomToDelete.id) - viewModel.setSymptomToDelete(null) + viewModel.onAction(UiAction.DeleteSymptom(symptomToDelete)) + viewModel.onAction(UiAction.HideDeletionDialog) }, onCancel = { - viewModel.setSymptomToDelete(null) + viewModel.onAction(UiAction.HideDeletionDialog) }, ) } @@ -114,18 +115,19 @@ fun ManageSymptomScreen( @Composable private fun SymptomItem( - viewModel: ManageSymptomsViewModel, + onAction: (uiAction: UiAction) -> Unit, symptom: Symptom, - showDeletionIcon: Boolean + showDeletionIcon: Boolean, + modifier: Modifier = Modifier, ) { val selectedColor = ColorSource.getColorMap(isDarkMode())[symptom.color] ?: Color.Gray val symptomDisplayName = ResourceMapper.getStringResourceOrCustom(symptom.name) Card( onClick = { - viewModel.setSymptomToRename(symptom) + onAction(UiAction.ShowRenamingDialog(symptom)) }, - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), shape = MaterialTheme.shapes.extraLarge, ) { Row( @@ -136,7 +138,7 @@ private fun SymptomItem( ) { if (showDeletionIcon) { IconButton( - onClick = { viewModel.setSymptomToDelete(symptom) }, + onClick = { onAction(UiAction.ShowDeletionDialog(symptom)) }, modifier = Modifier.size(20.dp) ) { Icon( @@ -145,15 +147,15 @@ private fun SymptomItem( ) } } - Spacer(modifier = Modifier.padding(start = 5.dp)) Text( text = symptomDisplayName, textAlign = TextAlign.Left, - modifier = Modifier.weight(1f) // Let the text expand to fill available space + modifier = Modifier + .weight(1f) // Let the text expand to fill available space + .padding(4.dp) ) - //Color Picker Dropdown Menu - ColorPicker(selectedColor, symptom, viewModel) + ColorPicker(selectedColor, symptom, onAction) Spacer(modifier = Modifier.weight(0.05f)) @@ -161,11 +163,7 @@ private fun SymptomItem( checked = symptom.isActive, onCheckedChange = { checked -> val updatedSymptom = symptom.copy(active = if (checked) 1 else 0) - viewModel.updateSymptom( - updatedSymptom.id, - updatedSymptom.active, - updatedSymptom.color - ) + onAction(UiAction.UpdateSymptom(updatedSymptom)) }, ) Spacer(modifier = Modifier.weight(0.05f)) @@ -173,11 +171,12 @@ private fun SymptomItem( } } +// Color Picker Dropdown Menu @Composable private fun ColorPicker( selectedColor: Color, symptom: Symptom, - viewModel: ManageSymptomsViewModel + onAction: (uiAction: UiAction) -> Unit, ) { var expanded by remember { mutableStateOf(false) } @@ -233,11 +232,7 @@ private fun ColorPicker( onClick = { expanded = false val updatedSymptom = symptom.copy(color = colorName) - viewModel.updateSymptom( - updatedSymptom.id, - updatedSymptom.active, - updatedSymptom.color - ) + onAction(UiAction.UpdateSymptom(updatedSymptom)) }, text = { Box( @@ -256,16 +251,29 @@ private fun ColorPicker( } } - -// TODO: Broken @Preview(showBackground = true) @Composable private fun SymptomItemPreview() { MensinatorTheme { SymptomItem( - viewModel = koinViewModel(), - symptom = Symptom(1, "Medium flow", 1, "red"), - showDeletionIcon = true + onAction = {}, + symptom = Symptom(1, "Medium flow", 1, "Red"), + showDeletionIcon = true, + modifier = Modifier.padding(8.dp) + ) + } +} + + +@Preview(showBackground = true) +@Composable +private fun SymptomItemLongTextPreview() { + MensinatorTheme { + SymptomItem( + onAction = {}, + symptom = Symptom(2, "Very long text that could span multiple lines ".repeat(2), 0, "DarkBlue"), + showDeletionIcon = false, + modifier = Modifier.padding(8.dp) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt index d5e2df5b..28b7113b 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt @@ -20,7 +20,6 @@ class ManageSymptomsViewModel( private val periodDatabaseHelper: IPeriodDatabaseHelper, ) : ViewModel() { - private val _viewState = MutableStateFlow( ViewState() ) @@ -34,6 +33,22 @@ class ManageSymptomsViewModel( val symptomToDelete: Symptom? = null, ) + fun onAction(uiAction: UiAction) = when (uiAction) { + UiAction.HideCreationDialog -> _viewState.update { it.copy(showCreateSymptomDialog = false) } + UiAction.ShowCreationDialog -> _viewState.update { it.copy(showCreateSymptomDialog = true) } + + UiAction.HideDeletionDialog -> _viewState.update { it.copy(symptomToDelete = null) } + is UiAction.ShowDeletionDialog -> _viewState.update { it.copy(symptomToDelete = uiAction.symptom ) } + + UiAction.HideRenamingDialog -> _viewState.update { it.copy(symptomToRename = null) } + is UiAction.ShowRenamingDialog -> _viewState.update { it.copy( symptomToRename = uiAction.symptom ) } + + is UiAction.CreateSymptom -> createNewSymptom(uiAction.name) + is UiAction.UpdateSymptom -> updateSymptom(uiAction.symptom) + is UiAction.DeleteSymptom -> deleteSymptom(uiAction.symptom) + is UiAction.RenameSymptom -> renameSymptom(uiAction.symptom) + } + suspend fun refreshData() { withContext(Dispatchers.IO) { val allSymptoms = periodDatabaseHelper.getAllSymptoms() @@ -46,35 +61,39 @@ class ManageSymptomsViewModel( } } - fun createNewSymptom(name: String) { + private fun createNewSymptom(name: String) { periodDatabaseHelper.createNewSymptom(name) viewModelScope.launch { refreshData() } } - fun updateSymptom(id: Int, active: Int, color: String) { - periodDatabaseHelper.updateSymptom(id, active, color) + private fun updateSymptom(symptom: Symptom) { + periodDatabaseHelper.updateSymptom(symptom.id, symptom.active, symptom.color) viewModelScope.launch { refreshData() } } - fun renameSymptom(id: Int, name: String) { - periodDatabaseHelper.renameSymptom(id, name) + private fun renameSymptom(symptom: Symptom) { + periodDatabaseHelper.renameSymptom(symptom.id, symptom.name) viewModelScope.launch { refreshData() } } - fun deleteSymptom(id: Int) { - periodDatabaseHelper.deleteSymptom(id) + private fun deleteSymptom(symptom: Symptom) { + periodDatabaseHelper.deleteSymptom(symptom.id) viewModelScope.launch { refreshData() } } - fun showCreateSymptomDialog(show: Boolean) { - _viewState.update { it.copy(showCreateSymptomDialog = show) } - } + sealed class UiAction { + data object HideRenamingDialog : UiAction() + data class ShowRenamingDialog(val symptom: Symptom): UiAction() - fun setSymptomToRename(symptom: Symptom?) { - _viewState.update { it.copy(symptomToRename = symptom) } - } + data object HideDeletionDialog : UiAction() + data class ShowDeletionDialog(val symptom: Symptom): UiAction() + + data object HideCreationDialog : UiAction() + data object ShowCreationDialog: UiAction() - fun setSymptomToDelete(symptom: Symptom?) { - _viewState.update { it.copy(symptomToDelete = symptom) } + data class CreateSymptom(val name: String): UiAction() + data class UpdateSymptom(val symptom: Symptom): UiAction() + data class DeleteSymptom(val symptom: Symptom): UiAction() + data class RenameSymptom(val symptom: Symptom): UiAction() } } From fdc56cf6a79765687f0f8c7712c6d85196dbbf1a Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Thu, 30 Jan 2025 12:34:34 +0100 Subject: [PATCH 11/15] Move some other files --- app/src/main/java/com/mensinator/app/MainActivity.kt | 2 +- .../main/java/com/mensinator/app/calendar/CalendarScreen.kt | 2 +- .../com/mensinator/app/{ => settings}/NotificationDialog.kt | 3 ++- .../main/java/com/mensinator/app/settings/SettingsScreen.kt | 3 +-- .../java/com/mensinator/app/statistics/StatisticsScreen.kt | 2 +- .../java/com/mensinator/app/symptoms/ManageSymptomScreen.kt | 2 +- .../java/com/mensinator/app/{ => ui}/navigation/BarItem.kt | 2 +- .../com/mensinator/app/{ => ui}/navigation/MensinatorApp.kt | 2 +- .../com/mensinator/app/{ => ui}/navigation/MensinatorTopBar.kt | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) rename app/src/main/java/com/mensinator/app/{ => settings}/NotificationDialog.kt (96%) rename app/src/main/java/com/mensinator/app/{ => ui}/navigation/BarItem.kt (71%) rename app/src/main/java/com/mensinator/app/{ => ui}/navigation/MensinatorApp.kt (99%) rename app/src/main/java/com/mensinator/app/{ => ui}/navigation/MensinatorTopBar.kt (96%) diff --git a/app/src/main/java/com/mensinator/app/MainActivity.kt b/app/src/main/java/com/mensinator/app/MainActivity.kt index 2d779415..12abc4e7 100644 --- a/app/src/main/java/com/mensinator/app/MainActivity.kt +++ b/app/src/main/java/com/mensinator/app/MainActivity.kt @@ -8,7 +8,7 @@ import android.view.WindowManager import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity -import com.mensinator.app.navigation.MensinatorApp +import com.mensinator.app.ui.navigation.MensinatorApp import com.mensinator.app.ui.theme.MensinatorTheme class MainActivity : AppCompatActivity() { diff --git a/app/src/main/java/com/mensinator/app/calendar/CalendarScreen.kt b/app/src/main/java/com/mensinator/app/calendar/CalendarScreen.kt index 77375f3d..6955cf7a 100644 --- a/app/src/main/java/com/mensinator/app/calendar/CalendarScreen.kt +++ b/app/src/main/java/com/mensinator/app/calendar/CalendarScreen.kt @@ -40,7 +40,7 @@ import com.mensinator.app.business.IPeriodDatabaseHelper import com.mensinator.app.business.IPeriodPrediction import com.mensinator.app.data.ColorSource import com.mensinator.app.data.isActive -import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding +import com.mensinator.app.ui.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.settings.ResourceMapper import com.mensinator.app.settings.StringSetting import com.mensinator.app.ui.theme.isDarkMode diff --git a/app/src/main/java/com/mensinator/app/NotificationDialog.kt b/app/src/main/java/com/mensinator/app/settings/NotificationDialog.kt similarity index 96% rename from app/src/main/java/com/mensinator/app/NotificationDialog.kt rename to app/src/main/java/com/mensinator/app/settings/NotificationDialog.kt index 94b5f57a..40c62dd9 100644 --- a/app/src/main/java/com/mensinator/app/NotificationDialog.kt +++ b/app/src/main/java/com/mensinator/app/settings/NotificationDialog.kt @@ -1,4 +1,4 @@ -package com.mensinator.app +package com.mensinator.app.settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.mensinator.app.R import com.mensinator.app.ui.theme.MensinatorTheme @Composable diff --git a/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt b/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt index a24998e5..4d587904 100644 --- a/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt +++ b/app/src/main/java/com/mensinator/app/settings/SettingsScreen.kt @@ -28,10 +28,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.mensinator.app.NotificationDialog import com.mensinator.app.R import com.mensinator.app.data.ColorSource -import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding +import com.mensinator.app.ui.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.ui.theme.MensinatorTheme import com.mensinator.app.ui.theme.isDarkMode import org.koin.androidx.compose.koinViewModel diff --git a/app/src/main/java/com/mensinator/app/statistics/StatisticsScreen.kt b/app/src/main/java/com/mensinator/app/statistics/StatisticsScreen.kt index 7f7e22a9..5b7aa751 100644 --- a/app/src/main/java/com/mensinator/app/statistics/StatisticsScreen.kt +++ b/app/src/main/java/com/mensinator/app/statistics/StatisticsScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mensinator.app.R -import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding +import com.mensinator.app.ui.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.ui.theme.MensinatorTheme import org.koin.androidx.compose.koinViewModel diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt index d6943564..009d3185 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -23,7 +23,7 @@ import com.mensinator.app.R import com.mensinator.app.data.ColorSource import com.mensinator.app.data.Symptom import com.mensinator.app.data.isActive -import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding +import com.mensinator.app.ui.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.settings.ResourceMapper import com.mensinator.app.symptoms.ManageSymptomsViewModel.UiAction import com.mensinator.app.ui.theme.MensinatorTheme diff --git a/app/src/main/java/com/mensinator/app/navigation/BarItem.kt b/app/src/main/java/com/mensinator/app/ui/navigation/BarItem.kt similarity index 71% rename from app/src/main/java/com/mensinator/app/navigation/BarItem.kt rename to app/src/main/java/com/mensinator/app/ui/navigation/BarItem.kt index fa18407e..b7052bbb 100644 --- a/app/src/main/java/com/mensinator/app/navigation/BarItem.kt +++ b/app/src/main/java/com/mensinator/app/ui/navigation/BarItem.kt @@ -1,4 +1,4 @@ -package com.mensinator.app.navigation +package com.mensinator.app.ui.navigation data class BarItem( val screen: Screen, diff --git a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt b/app/src/main/java/com/mensinator/app/ui/navigation/MensinatorApp.kt similarity index 99% rename from app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt rename to app/src/main/java/com/mensinator/app/ui/navigation/MensinatorApp.kt index 22c84caa..7c17e3c8 100644 --- a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt +++ b/app/src/main/java/com/mensinator/app/ui/navigation/MensinatorApp.kt @@ -1,4 +1,4 @@ -package com.mensinator.app.navigation +package com.mensinator.app.ui.navigation import android.util.Log import androidx.annotation.StringRes diff --git a/app/src/main/java/com/mensinator/app/navigation/MensinatorTopBar.kt b/app/src/main/java/com/mensinator/app/ui/navigation/MensinatorTopBar.kt similarity index 96% rename from app/src/main/java/com/mensinator/app/navigation/MensinatorTopBar.kt rename to app/src/main/java/com/mensinator/app/ui/navigation/MensinatorTopBar.kt index 3323461c..3131321d 100644 --- a/app/src/main/java/com/mensinator/app/navigation/MensinatorTopBar.kt +++ b/app/src/main/java/com/mensinator/app/ui/navigation/MensinatorTopBar.kt @@ -1,4 +1,4 @@ -package com.mensinator.app.navigation +package com.mensinator.app.ui.navigation import androidx.compose.foundation.layout.* import androidx.compose.material3.MaterialTheme From 20d40b57d53cf1cd9a03c23e819b44d3223cdc4c Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Thu, 30 Jan 2025 12:36:27 +0100 Subject: [PATCH 12/15] Only allow symptom creation when name is specified --- .../java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt index b38e31ad..2a789a07 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt @@ -27,6 +27,7 @@ fun CreateNewSymptomDialog( onClick = { onSave(symptomName) }, + enabled = symptomName.isNotBlank() ) { Text(stringResource(id = R.string.save_button)) } From 7d821fb2c3c65a86681c92a3dab3277f08de677c Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Thu, 30 Jan 2025 12:39:25 +0100 Subject: [PATCH 13/15] Remove db.close calls --- .../app/business/PeriodDatabaseHelper.kt | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt b/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt index deef2b68..72a23edb 100644 --- a/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt +++ b/app/src/main/java/com/mensinator/app/business/PeriodDatabaseHelper.kt @@ -101,7 +101,6 @@ class PeriodDatabaseHelper(context: Context) : if (rowsUpdated == 0) { db.insert(TABLE_PERIODS, null, values) } - //db.close() } override fun getPeriodDatesForMonth(year: Int, month: Int): Map { @@ -143,7 +142,6 @@ class PeriodDatabaseHelper(context: Context) : Log.e(TAG, "Cursor is null while querying for dates") } - //db.close() return dates } @@ -156,7 +154,6 @@ class PeriodDatabaseHelper(context: Context) : count = cursor.getInt(0) } cursor.close() - //db.close() return count } @@ -170,7 +167,6 @@ class PeriodDatabaseHelper(context: Context) : } else { Log.d(TAG, "No date $date found in $TABLE_PERIODS to remove") } - //db.close() } override fun getAllSymptoms(): List { @@ -194,8 +190,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() - return symptoms } @@ -208,7 +202,6 @@ class PeriodDatabaseHelper(context: Context) : } // Insert the new symptom into the symptoms table db.insert(TABLE_SYMPTOMS, null, values) - //db.close() // Close the database connection to free up resources } override fun getSymptomDatesForMonth(year: Int, month: Int): Set { @@ -250,7 +243,6 @@ class PeriodDatabaseHelper(context: Context) : Log.e(TAG, "Error querying for symptom dates", e) } finally { cursor.close() - //db.close() } return dates @@ -297,7 +289,6 @@ class PeriodDatabaseHelper(context: Context) : } // Close the database connection - //db.close() } override fun getActiveSymptomIdsForDate(date: LocalDate): List { @@ -321,7 +312,6 @@ class PeriodDatabaseHelper(context: Context) : } } - //db.close() return symptoms } @@ -346,8 +336,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() - return symptomColors } @@ -375,7 +363,6 @@ class PeriodDatabaseHelper(context: Context) : } val rowsUpdated = db.update(TABLE_APP_SETTINGS, contentValues, "$COLUMN_SETTING_KEY = ?", arrayOf(key)) - //db.close() return rowsUpdated > 0 } @@ -402,7 +389,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() return setting } @@ -436,7 +422,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() } override fun getOvulationDatesForMonth(year: Int, month: Int): Set { @@ -474,7 +459,6 @@ class PeriodDatabaseHelper(context: Context) : Log.e("TAG", "Error querying for ovulation dates", e) } finally { cursor.close() - //db.close() } return dates @@ -489,7 +473,6 @@ class PeriodDatabaseHelper(context: Context) : count = cursor.getInt(0) } cursor.close() - //db.close() return count } @@ -545,7 +528,6 @@ class PeriodDatabaseHelper(context: Context) : Log.e(TAG, "Cursor is null while querying for periodId") } - //db.close() return periodId } @@ -576,8 +558,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() - return firstLatestDate } @@ -596,7 +576,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() return oldestPeriodDate } @@ -615,7 +594,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() return newestOvulationDate } @@ -644,8 +622,6 @@ class PeriodDatabaseHelper(context: Context) : if (rowsAffected == 0) { throw IllegalStateException("No symptom found with ID: $id") } - - //db.close() } // This function is used to get the latest X ovulation dates where they are followed by a period @@ -674,7 +650,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() return ovulationDates } @@ -693,7 +668,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() return ovulationDate } @@ -726,8 +700,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() - return dateList } @@ -751,8 +723,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() - return firstNextDate } @@ -768,7 +738,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - //db.close() return count } @@ -787,7 +756,6 @@ class PeriodDatabaseHelper(context: Context) : } while (cursor.moveToNext()) } cursor.close() - //db.close() return ovulationDates } @@ -802,7 +770,6 @@ class PeriodDatabaseHelper(context: Context) : } else { Log.d(TAG, "No symptoms to delete") } - //db.close() } override fun getDBVersion(): String { @@ -830,7 +797,6 @@ class PeriodDatabaseHelper(context: Context) : return LocalDate.parse(dateString) } cursor.close() - //db.close() return latestPeriodStart } } From 91dde4e1f4d78e4a135f6f8db766e71e797ae7d5 Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Thu, 30 Jan 2025 12:48:10 +0100 Subject: [PATCH 14/15] Cleanup --- .../java/com/mensinator/app/symptoms/ManageSymptomScreen.kt | 3 +-- .../com/mensinator/app/symptoms/ManageSymptomsViewModel.kt | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt index 009d3185..9d766fab 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -23,9 +23,9 @@ import com.mensinator.app.R import com.mensinator.app.data.ColorSource import com.mensinator.app.data.Symptom import com.mensinator.app.data.isActive -import com.mensinator.app.ui.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.settings.ResourceMapper import com.mensinator.app.symptoms.ManageSymptomsViewModel.UiAction +import com.mensinator.app.ui.navigation.displayCutoutExcludingStatusBarsPadding import com.mensinator.app.ui.theme.MensinatorTheme import com.mensinator.app.ui.theme.UiConstants import com.mensinator.app.ui.theme.isDarkMode @@ -94,7 +94,6 @@ fun ManageSymptomScreen( }, onCancel = { viewModel.onAction(UiAction.HideRenamingDialog) - } ) } diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt index 28b7113b..cf3a6754 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.mensinator.app.business.IPeriodDatabaseHelper import com.mensinator.app.data.Symptom -import com.mensinator.app.data.isActive import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,7 +26,6 @@ class ManageSymptomsViewModel( data class ViewState( val allSymptoms: List = listOf(), - val activeSymptoms: List = listOf(), val showCreateSymptomDialog: Boolean = false, val symptomToRename: Symptom? = null, val symptomToDelete: Symptom? = null, @@ -51,11 +49,9 @@ class ManageSymptomsViewModel( suspend fun refreshData() { withContext(Dispatchers.IO) { - val allSymptoms = periodDatabaseHelper.getAllSymptoms() _viewState.update { it.copy( allSymptoms = periodDatabaseHelper.getAllSymptoms(), - activeSymptoms = allSymptoms.filter { it.isActive }, ) } } From 5d021d6834b93745690a9d95afd8e636b843fa96 Mon Sep 17 00:00:00 2001 From: Carsten Hagemann Date: Thu, 30 Jan 2025 12:49:12 +0100 Subject: [PATCH 15/15] Cleanup --- app/src/main/java/com/mensinator/app/App.kt | 2 +- .../com/mensinator/app/symptoms/ManageSymptomsViewModel.kt | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/com/mensinator/app/App.kt b/app/src/main/java/com/mensinator/app/App.kt index 447ee683..d5df04ec 100644 --- a/app/src/main/java/com/mensinator/app/App.kt +++ b/app/src/main/java/com/mensinator/app/App.kt @@ -24,7 +24,7 @@ class App : Application() { singleOf(::ExportImport) { bind() } singleOf(::NotificationScheduler) { bind() } - viewModel { ManageSymptomsViewModel(get(), get()) } + viewModel { ManageSymptomsViewModel(get()) } viewModel { SettingsViewModel(get(), get(), get()) } viewModel { StatisticsViewModel(get(), get(), get(), get(), get()) } } diff --git a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt index cf3a6754..a8124be6 100644 --- a/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt @@ -1,7 +1,5 @@ package com.mensinator.app.symptoms -import android.annotation.SuppressLint -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.mensinator.app.business.IPeriodDatabaseHelper @@ -15,7 +13,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ManageSymptomsViewModel( - @SuppressLint("StaticFieldLeak") private val appContext: Context, private val periodDatabaseHelper: IPeriodDatabaseHelper, ) : ViewModel() {