diff --git a/app/src/main/java/com/mensinator/app/App.kt b/app/src/main/java/com/mensinator/app/App.kt index a1e7bd44..d5df04ec 100644 --- a/app/src/main/java/com/mensinator/app/App.kt +++ b/app/src/main/java/com/mensinator/app/App.kt @@ -1,8 +1,10 @@ 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 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 @@ -22,6 +24,7 @@ class App : Application() { singleOf(::ExportImport) { bind() } singleOf(::NotificationScheduler) { bind() } + 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/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/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/ManageSymptomScreen.kt deleted file mode 100644 index fc46943e..00000000 --- a/app/src/main/java/com/mensinator/app/ManageSymptomScreen.kt +++ /dev/null @@ -1,275 +0,0 @@ -package com.mensinator.app - -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.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -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.unit.DpOffset -import androidx.compose.ui.unit.dp -import com.mensinator.app.data.ColorSource -import com.mensinator.app.settings.ResourceMapper -import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding -import com.mensinator.app.ui.theme.isDarkMode -import org.koin.compose.koinInject - - -//Maps Database keys to res/strings.xml for multilanguage support -@Composable -fun ManageSymptomScreen( - showCreateSymptom: MutableState, - modifier: Modifier, -) { - 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) } - - - Column( - modifier = modifier - .fillMaxSize() - .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 - 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 resKey = ResourceMapper.getStringResourceId(symptom.name) - val selectedColor = ColorSource.getColorMap(isDarkMode())[selectedColorName] ?: Color.Gray - - val symptomDisplayName = ResourceMapper.getStringResourceOrCustom(symptom.name) - Card( - onClick = { - symptomToRename = symptom - showRenameDialog = true - }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(25.dp), - ) { - 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 = dbHelper.getAllSymptoms().filter { it.isActive } - if (activeSymptoms.contains(symptom)) { - showDeleteDialog = true - symptomToDelete = symptom - } else { - symptom.let { symptom -> - savedSymptoms = savedSymptoms.filter { it.id != symptom.id } - dbHelper.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( - 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( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - Box( - modifier = Modifier - .size(25.dp) - .clip(RoundedCornerShape(26.dp)) - .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(50.dp) - .clip(RoundedCornerShape(100.dp)), - 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 -> - dbHelper.updateSymptom( - symptom.id, - symptom.active, - symptom.color - ) - } - }, - text = { - Box( - modifier = Modifier - .size(25.dp) - .clip(RoundedCornerShape(26.dp)) - .background(colorValue) // Use the color from the map - ) - } - ) - } - } - } - } - } - } - } - - Spacer(modifier = Modifier.weight(0.05f)) - - Switch( - checked = symptom.active == 1, - 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 -> - dbHelper.updateSymptom( - symptom.id, - symptom.active, - symptom.color - ) - } - }, - ) - Spacer(modifier = Modifier.weight(0.05f)) - } - } - } - } - if (showCreateSymptom.value) { - 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 - }, - onCancel = { - showCreateSymptom.value = false // Close the new symptom dialog - }, - ) - } - - if (showRenameDialog && symptomToRename != null) { - val symptomKey = ResourceMapper.getStringResourceId(symptomToRename!!.name) - val symptomDisplayName = - symptomKey?.let { stringResource(id = it) } ?: symptomToRename!!.name - - RenameSymptomDialog( - symptomDisplayName = symptomDisplayName, - onRename = { newName -> - dbHelper.renameSymptom(symptomToRename!!.id, newName) - initialSymptoms = dbHelper.getAllSymptoms() - savedSymptoms = initialSymptoms - showRenameDialog = false - }, - onCancel = { - showRenameDialog = false - } - ) - } - - // Show the delete confirmation dialog - if (showDeleteDialog) { - DeleteSymptomDialog( - onSave = { - symptomToDelete?.let { symptom -> - savedSymptoms = savedSymptoms.filter { it.id != symptom.id } - dbHelper.deleteSymptom(symptom.id) - } - showDeleteDialog = false - }, - onCancel = { showDeleteDialog = false }, - ) - } -} 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 97% 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..72a23edb 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 /* @@ -99,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 { @@ -141,7 +142,6 @@ class PeriodDatabaseHelper(context: Context) : Log.e(TAG, "Cursor is null while querying for dates") } - db.close() return dates } @@ -154,7 +154,6 @@ class PeriodDatabaseHelper(context: Context) : count = cursor.getInt(0) } cursor.close() - db.close() return count } @@ -168,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 { @@ -192,8 +190,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() - return symptoms } @@ -206,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 { @@ -248,7 +243,6 @@ class PeriodDatabaseHelper(context: Context) : Log.e(TAG, "Error querying for symptom dates", e) } finally { cursor.close() - db.close() } return dates @@ -295,10 +289,9 @@ class PeriodDatabaseHelper(context: Context) : } // Close the database connection - db.close() } - override fun getSymptomsFromDate(date: LocalDate): List { + override fun getActiveSymptomIdsForDate(date: LocalDate): List { val db = readableDatabase val symptoms = mutableListOf() @@ -319,7 +312,6 @@ class PeriodDatabaseHelper(context: Context) : } } - db.close() return symptoms } @@ -344,8 +336,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() - return symptomColors } @@ -373,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 } @@ -400,7 +389,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() return setting } @@ -434,7 +422,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() } override fun getOvulationDatesForMonth(year: Int, month: Int): Set { @@ -472,7 +459,6 @@ class PeriodDatabaseHelper(context: Context) : Log.e("TAG", "Error querying for ovulation dates", e) } finally { cursor.close() - db.close() } return dates @@ -487,7 +473,6 @@ class PeriodDatabaseHelper(context: Context) : count = cursor.getInt(0) } cursor.close() - db.close() return count } @@ -543,7 +528,6 @@ class PeriodDatabaseHelper(context: Context) : Log.e(TAG, "Cursor is null while querying for periodId") } - db.close() return periodId } @@ -574,8 +558,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() - return firstLatestDate } @@ -594,7 +576,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() return oldestPeriodDate } @@ -613,7 +594,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() return newestOvulationDate } @@ -642,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 @@ -672,7 +650,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() return ovulationDates } @@ -691,7 +668,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() return ovulationDate } @@ -724,8 +700,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() - return dateList } @@ -749,8 +723,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() - return firstNextDate } @@ -766,7 +738,6 @@ class PeriodDatabaseHelper(context: Context) : } cursor.close() - db.close() return count } @@ -785,7 +756,6 @@ class PeriodDatabaseHelper(context: Context) : } while (cursor.moveToNext()) } cursor.close() - db.close() return ovulationDates } @@ -800,7 +770,6 @@ class PeriodDatabaseHelper(context: Context) : } else { Log.d(TAG, "No symptoms to delete") } - db.close() } override fun getDBVersion(): String { @@ -828,7 +797,6 @@ class PeriodDatabaseHelper(context: Context) : return LocalDate.parse(dateString) } cursor.close() - db.close() return latestPeriodStart } } 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 97% 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..6955cf7a 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,8 +32,15 @@ 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.navigation.displayCutoutExcludingStatusBarsPadding +import com.mensinator.app.data.isActive +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 @@ -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..363e0cda --- /dev/null +++ b/app/src/main/java/com/mensinator/app/calendar/SymptomDialogs.kt @@ -0,0 +1,127 @@ +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 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) + } + } + } + }, + ) +} + +@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/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/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 1c6389b8..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,12 +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.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 +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 @@ -115,6 +112,7 @@ fun SettingsScreen( modifier = modifier .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()) + .displayCutoutExcludingStatusBarsPadding() ) { Spacer(Modifier.height(16.dp)) SettingSectionHeader(text = stringResource(R.string.colors)) 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/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/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/symptoms/ManageSymptomScreen.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt new file mode 100644 index 00000000..9d766fab --- /dev/null +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomScreen.kt @@ -0,0 +1,278 @@ +package com.mensinator.app.symptoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.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 +import org.koin.androidx.compose.koinViewModel + +private object SymptomScreenConstants { + val colorCircleSize = 24.dp +} + +@Composable +fun ManageSymptomScreen( + modifier: Modifier = Modifier, + viewModel: ManageSymptomsViewModel = koinViewModel(), + setFabOnClick: (() -> Unit) -> Unit, +) { + val state = viewModel.viewState.collectAsState() + val symptoms = state.value.allSymptoms + + LaunchedEffect(Unit) { + setFabOnClick { viewModel.onAction(UiAction.ShowCreationDialog) } + viewModel.refreshData() + } + + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) // Make the column scrollable + .displayCutoutExcludingStatusBarsPadding() + .padding(16.dp) + .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 + ) { + symptoms.forEach { symptom -> + SymptomItem( + onAction = { viewModel.onAction(it) }, + symptom = symptom, + showDeletionIcon = symptoms.size > 1 + ) + } + } + + if (state.value.showCreateSymptomDialog) { + CreateNewSymptomDialog( + onSave = { newSymptomName -> + viewModel.onAction(UiAction.CreateSymptom(newSymptomName)) + viewModel.onAction(UiAction.HideCreationDialog) + }, + onCancel = { + viewModel.onAction(UiAction.HideCreationDialog) + }, + ) + } + + val symptomToRename = state.value.symptomToRename + if (symptomToRename != null) { + val symptomKey = ResourceMapper.getStringResourceId(symptomToRename.name) + val symptomDisplayName = symptomKey?.let { stringResource(id = it) } ?: symptomToRename.name + + RenameSymptomDialog( + symptomDisplayName = symptomDisplayName, + onRename = { newName -> + val updatedSymptom = symptomToRename.copy(name = newName) + viewModel.onAction(UiAction.RenameSymptom(updatedSymptom)) + viewModel.onAction(UiAction.HideRenamingDialog) + }, + onCancel = { + viewModel.onAction(UiAction.HideRenamingDialog) + } + ) + } + + val symptomToDelete = state.value.symptomToDelete + if (symptomToDelete != null) { + DeleteSymptomDialog( + onSave = { + viewModel.onAction(UiAction.DeleteSymptom(symptomToDelete)) + viewModel.onAction(UiAction.HideDeletionDialog) + }, + onCancel = { + viewModel.onAction(UiAction.HideDeletionDialog) + }, + ) + } +} + +@Composable +private fun SymptomItem( + onAction: (uiAction: UiAction) -> Unit, + symptom: Symptom, + showDeletionIcon: Boolean, + modifier: Modifier = Modifier, +) { + val selectedColor = ColorSource.getColorMap(isDarkMode())[symptom.color] ?: Color.Gray + val symptomDisplayName = ResourceMapper.getStringResourceOrCustom(symptom.name) + + Card( + onClick = { + onAction(UiAction.ShowRenamingDialog(symptom)) + }, + modifier = modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.extraLarge, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp) + ) { + if (showDeletionIcon) { + IconButton( + onClick = { onAction(UiAction.ShowDeletionDialog(symptom)) }, + modifier = Modifier.size(20.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(id = R.string.close) + ) + } + } + Text( + text = symptomDisplayName, + textAlign = TextAlign.Left, + modifier = Modifier + .weight(1f) // Let the text expand to fill available space + .padding(4.dp) + ) + + ColorPicker(selectedColor, symptom, onAction) + + Spacer(modifier = Modifier.weight(0.05f)) + + Switch( + checked = symptom.isActive, + onCheckedChange = { checked -> + val updatedSymptom = symptom.copy(active = if (checked) 1 else 0) + onAction(UiAction.UpdateSymptom(updatedSymptom)) + }, + ) + Spacer(modifier = Modifier.weight(0.05f)) + } + } +} + +// Color Picker Dropdown Menu +@Composable +private fun ColorPicker( + selectedColor: Color, + symptom: Symptom, + onAction: (uiAction: UiAction) -> Unit, +) { + 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) + onAction(UiAction.UpdateSymptom(updatedSymptom)) + }, + text = { + Box( + modifier = Modifier + .size(SymptomScreenConstants.colorCircleSize) + .clip(CircleShape) + .background(colorValue) // Use the color from the map + ) + } + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SymptomItemPreview() { + MensinatorTheme { + SymptomItem( + 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/SymptomDialogs.kt b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt similarity index 51% 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..2a789a07 100644 --- a/app/src/main/java/com/mensinator/app/SymptomDialogs.kt +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsDialogs.kt @@ -1,114 +1,24 @@ -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( - newSymptom: String, onSave: (String) -> Unit, onCancel: () -> Unit, modifier: Modifier = Modifier, ) { - var symptomName by remember { mutableStateOf(newSymptom) } - //val symptomKey = ResourceMapper.getStringResourceId(symptomName) + var symptomName by remember { mutableStateOf("") } AlertDialog( onDismissRequest = onCancel, @@ -117,6 +27,7 @@ fun CreateNewSymptomDialog( onClick = { onSave(symptomName) }, + enabled = symptomName.isNotBlank() ) { Text(stringResource(id = R.string.save_button)) } @@ -225,7 +136,6 @@ fun DeleteSymptomDialog( private fun CreateNewSymptomDialogPreview() { MensinatorTheme { CreateNewSymptomDialog( - newSymptom = "preview", onSave = {}, 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..a8124be6 --- /dev/null +++ b/app/src/main/java/com/mensinator/app/symptoms/ManageSymptomsViewModel.kt @@ -0,0 +1,92 @@ +package com.mensinator.app.symptoms + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mensinator.app.business.IPeriodDatabaseHelper +import com.mensinator.app.data.Symptom +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 kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ManageSymptomsViewModel( + private val periodDatabaseHelper: IPeriodDatabaseHelper, +) : ViewModel() { + + private val _viewState = MutableStateFlow( + ViewState() + ) + val viewState: StateFlow = _viewState.asStateFlow() + + data class ViewState( + val allSymptoms: List = listOf(), + val showCreateSymptomDialog: Boolean = false, + val symptomToRename: Symptom? = null, + 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) { + _viewState.update { + it.copy( + allSymptoms = periodDatabaseHelper.getAllSymptoms(), + ) + } + } + } + + private fun createNewSymptom(name: String) { + periodDatabaseHelper.createNewSymptom(name) + viewModelScope.launch { refreshData() } + } + + private fun updateSymptom(symptom: Symptom) { + periodDatabaseHelper.updateSymptom(symptom.id, symptom.active, symptom.color) + viewModelScope.launch { refreshData() } + } + + private fun renameSymptom(symptom: Symptom) { + periodDatabaseHelper.renameSymptom(symptom.id, symptom.name) + viewModelScope.launch { refreshData() } + } + + private fun deleteSymptom(symptom: Symptom) { + periodDatabaseHelper.deleteSymptom(symptom.id) + viewModelScope.launch { refreshData() } + } + + sealed class UiAction { + data object HideRenamingDialog : UiAction() + data class ShowRenamingDialog(val symptom: Symptom): UiAction() + + data object HideDeletionDialog : UiAction() + data class ShowDeletionDialog(val symptom: Symptom): UiAction() + + data object HideCreationDialog : UiAction() + data object ShowCreationDialog: UiAction() + + 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() + } +} 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 80% 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 f3296471..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 @@ -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 @@ -25,10 +25,13 @@ 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 com.mensinator.app.ui.theme.UiConstants import org.koin.compose.koinInject enum class Screen(@StringRes val titleRes: Int) { @@ -64,30 +67,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( @@ -149,31 +134,54 @@ 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)) } } 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( - topBar = { MensinatorTopBar(currentScreen) } + floatingActionButton = { + if (currentScreen == Screen.Symptoms) { + FloatingActionButton( + onClick = { fabOnClick?.invoke() }, + shape = CircleShape, + modifier = Modifier + .displayCutoutPadding() + .size(UiConstants.floatingActionButtonSize) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(R.string.delete_button) + ) + } + } + }, + topBar = { MensinatorTopBar(currentScreen) }, + contentWindowInsets = WindowInsets(0.dp), ) { topBarPadding -> ManageSymptomScreen( - showCreateSymptom, - modifier = Modifier.padding(topBarPadding) + modifier = Modifier.padding(topBarPadding), + setFabOnClick = setFabOnClick ) } } composable(route = Screen.Settings.name) { Scaffold( - topBar = { MensinatorTopBar(currentScreen) } + topBar = { MensinatorTopBar(currentScreen) }, + contentWindowInsets = WindowInsets(0.dp), ) { topBarPadding -> Column { SettingsScreen( @@ -187,6 +195,5 @@ fun MensinatorApp( } } } - } 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 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 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