Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ Rules:
- Released versions are immutable — never re-tag, never amend, never delete an entry
- Merge conflicts must preserve both sides; if both branches used the same version string, renumber the lower-priority one upward

---
## [0.47.1-beta.1] - 2026-06-06

### Changed
- Flow and Symptoms are now tracked with period by default (DB migration v20 sets showInLogPeriod = 1 for both system categories). This means they are archived alongside other period-pinned categories when Pregnancy Tracking is activated, and they respect the showInLogPeriod flag going forward. Existing users get the flag set automatically on upgrade.

---
## [0.47.0-beta.1] - 2026-06-06

### Added
- Period tracking can now be enabled or disabled from Tracking Modes. It is on by default and controls all period-related functionality (Log Period button, period logging screen).
- The period logging screen has a three-dot overflow menu with a "Disable period logging" option, mirroring the Tracking Modes toggle.
- Activating Pregnancy Tracking now prompts you to confirm that period logging will be paused and any categories you log with your period will be archived. You can unarchive them later or remove them from period logging first.

---
## [0.46.0-beta.1] - 2026-06-06

Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ android {
applicationId = "com.mapgie.goflo"
minSdk = 26
targetSdk = 34
versionCode = 99
versionName = "0.46.0-beta.1"
versionCode = 101
versionName = "0.47.1-beta.1"
}

signingConfigs {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/mapgie/goflo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ private fun MainNavHost(app: GoFloApplication, currentTheme: AppTheme, pendingCa
val prefilledDate = startDateStr?.let { runCatching { java.time.LocalDate.parse(it) }.getOrNull() }
val vm: com.mapgie.goflo.ui.screens.log.LogPeriodViewModel = viewModel(
key = "log_${periodId}_${startDateStr}",
factory = com.mapgie.goflo.ui.screens.log.LogPeriodViewModel.Factory(app.repository, periodId, prefilledDate, app.trackingRepository, app)
factory = com.mapgie.goflo.ui.screens.log.LogPeriodViewModel.Factory(app.repository, periodId, prefilledDate, app.trackingRepository, app, app.preferencesStore)
)
com.mapgie.goflo.ui.screens.log.LogPeriodScreen(viewModel = vm, onBack = { navController.popBackStack() })
}
Expand Down
18 changes: 14 additions & 4 deletions app/src/main/java/com/mapgie/goflo/data/database/GoFloDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import com.mapgie.goflo.data.database.entities.TrackingValue
CustomAlarm::class,
CustomAlarmCategory::class,
],
version = 19,
version = 20,
exportSchema = false
)
abstract class GoFloDatabase : RoomDatabase() {
Expand Down Expand Up @@ -560,7 +560,7 @@ abstract class GoFloDatabase : RoomDatabase() {
"numericMin, numericMax, allowDecimals, numericUnit, scaleLabels, " +
"isArchived, allowMultiple, showInLogPeriod, trackAgainstTime) " +
"VALUES ('Flow', 1, 'flow', 0, 'water', 'primary', 'default', " +
"0.0, 10.0, 0, '', '', 0, 0, 0, 0)"
"0.0, 10.0, 0, '', '', 0, 0, 1, 0)"
)
val flowIdCursor = database.query("SELECT last_insert_rowid()")
flowIdCursor.moveToFirst()
Expand All @@ -581,7 +581,7 @@ abstract class GoFloDatabase : RoomDatabase() {
"numericMin, numericMax, allowDecimals, numericUnit, scaleLabels, " +
"isArchived, allowMultiple, showInLogPeriod, trackAgainstTime) " +
"VALUES ('Symptoms', 1, 'symptoms', 1, 'healing', 'tertiary', 'default', " +
"0.0, 10.0, 0, '', '', 0, 0, 0, 0)"
"0.0, 10.0, 0, '', '', 0, 0, 1, 0)"
)
val symptomIdCursor = database.query("SELECT last_insert_rowid()")
symptomIdCursor.moveToFirst()
Expand Down Expand Up @@ -614,14 +614,24 @@ abstract class GoFloDatabase : RoomDatabase() {
}
}

/** Marks Flow and Symptoms as tracked with period by default (v20). */
val MIGRATION_19_20 = object : Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"UPDATE tracking_categories SET showInLogPeriod = 1 " +
"WHERE systemKey IN ('flow', 'symptoms')"
)
}
}

fun getInstance(context: Context): GoFloDatabase =
instance ?: synchronized(this) {
instance ?: Room.databaseBuilder(
context.applicationContext,
GoFloDatabase::class.java,
"goflo_database"
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, MIGRATION_17_18, MIGRATION_18_19)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, MIGRATION_17_18, MIGRATION_18_19, MIGRATION_19_20)
.addCallback(object : Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ data class AppPreferences(
val customThemeName: String = "",
/** True when the picked colours are intended for dark mode; false = light mode (default). */
val customThemePickedForDark: Boolean = false,
/** Whether period logging is enabled. Defaults to true; can be toggled from Tracking Modes or the period log screen. */
val periodTrackingEnabled: Boolean = true,
/** Comma-separated list of active tracking mode IDs (e.g. "FERTILITY,PREGNANCY"). */
val activeModes: String = "",
/** ISO-8601 date string for the pregnancy anchor date. Interpretation depends on [pregnancyStartType]. */
Expand Down Expand Up @@ -170,6 +172,7 @@ class AppPreferencesStore(private val context: Context) {
val CUSTOM_TERTIARY_ARGB = intPreferencesKey("custom_tertiary_argb")
val CUSTOM_THEME_NAME = stringPreferencesKey("custom_theme_name")
val CUSTOM_THEME_PICKED_FOR_DARK = booleanPreferencesKey("custom_theme_picked_for_dark")
val PERIOD_TRACKING_ENABLED = booleanPreferencesKey("period_tracking_enabled")
val ACTIVE_MODES = stringPreferencesKey("active_modes")
val PREGNANCY_DATE_STR = stringPreferencesKey("pregnancy_date_str")
val PREGNANCY_START_TYPE = stringPreferencesKey("pregnancy_start_type")
Expand Down Expand Up @@ -211,6 +214,7 @@ class AppPreferencesStore(private val context: Context) {
customTertiaryArgb = prefs[Keys.CUSTOM_TERTIARY_ARGB] ?: 0,
customThemeName = prefs[Keys.CUSTOM_THEME_NAME] ?: "",
customThemePickedForDark = prefs[Keys.CUSTOM_THEME_PICKED_FOR_DARK] ?: false,
periodTrackingEnabled = prefs[Keys.PERIOD_TRACKING_ENABLED] ?: true,
activeModes = prefs[Keys.ACTIVE_MODES] ?: "",
pregnancyDateStr = prefs[Keys.PREGNANCY_DATE_STR] ?: "",
pregnancyStartType = prefs[Keys.PREGNANCY_START_TYPE] ?: "EDD",
Expand Down Expand Up @@ -406,6 +410,10 @@ class AppPreferencesStore(private val context: Context) {
context.dataStore.edit { it[Keys.CUSTOM_THEME_PICKED_FOR_DARK] = dark }
}

suspend fun setPeriodTrackingEnabled(enabled: Boolean) {
context.dataStore.edit { it[Keys.PERIOD_TRACKING_ENABLED] = enabled }
}

suspend fun setActiveModes(modes: String) {
context.dataStore.edit { it[Keys.ACTIVE_MODES] = modes }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class TrackingRepository(
suspend fun getAllCategoriesOnce(): List<TrackingCategory> =
categoryDao.getAllCategoriesOnce()

suspend fun getShowInLogPeriodCategoriesOnce(): List<TrackingCategory> =
categoryDao.getShowInLogPeriodCategoriesOnce()

fun getCategoryById(id: Long): Flow<TrackingCategory?> =
categoryDao.getCategoryById(id)

Expand Down
23 changes: 15 additions & 8 deletions app/src/main/java/com/mapgie/goflo/ui/screens/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ fun HomeScreen(
val id = state.quickLogCategoryId
val cat = state.trackingCategories.firstOrNull { it.id == id }
when {
id == -1L && !state.periodTrackingEnabled -> {
showLogMenu = true
}
id == -1L ->
onNavigate(Screen.LogPeriod.newEntryForDate(date))
cat?.categoryType == "increment" ->
Expand Down Expand Up @@ -201,6 +204,7 @@ fun HomeScreen(
logMenuTargetDate = null
onNavigate(Screen.LogPeriod.newEntryForDate(targetDate))
},
periodTrackingEnabled = state.periodTrackingEnabled,
categories = state.trackingCategories,
onLogCategory = { categoryId ->
showLogMenu = false
Expand Down Expand Up @@ -293,6 +297,7 @@ private fun SpeedDial(
expanded: Boolean,
onToggle: () -> Unit,
onLogPeriod: () -> Unit,
periodTrackingEnabled: Boolean,
categories: List<com.mapgie.goflo.data.database.entities.TrackingCategory>,
onLogCategory: (Long) -> Unit,
) {
Expand Down Expand Up @@ -327,14 +332,16 @@ private fun SpeedDial(
}
}

// Log Period — always at the top of the list
SpeedDialItem(
label = "Log Period",
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
onClick = onLogPeriod
) {
Icon(Icons.Default.DateRange, contentDescription = null)
// Log Period — shown only when period tracking is enabled
if (periodTrackingEnabled) {
SpeedDialItem(
label = "Log Period",
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
onClick = onLogPeriod
) {
Icon(Icons.Default.DateRange, contentDescription = null)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ data class HomeUiState(
val onboardingBannerDismissed: Boolean = false,
/** Non-null when pregnancy mode is active and a valid date has been entered. */
val pregnancyInfo: PregnancyInfo? = null,
/** Whether period logging is enabled. */
val periodTrackingEnabled: Boolean = true,
)

data class PregnancyInfo(
Expand Down Expand Up @@ -146,6 +148,7 @@ class HomeViewModel(
quickLogCategoryId = prefs.quickLogCategoryId,
onboardingBannerDismissed = prefs.onboardingBannerDismissed,
pregnancyInfo = pregnancyInfo,
periodTrackingEnabled = prefs.periodTrackingEnabled,
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), HomeUiState())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults
Expand Down Expand Up @@ -82,6 +85,7 @@ fun LogPeriodScreen(
var showAddSymptomDialog by rememberSaveable { mutableStateOf(false) }
var showOngoingConfirm by rememberSaveable { mutableStateOf(false) }
var showUnsavedChangesDialog by rememberSaveable { mutableStateOf(false) }
var showOverflowMenu by rememberSaveable { mutableStateOf(false) }

val handleBack: () -> Unit = {
if (state.hasChanges) showUnsavedChangesDialog = true else onBack()
Expand Down Expand Up @@ -184,6 +188,24 @@ fun LogPeriodScreen(
IconButton(onClick = handleBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { showOverflowMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "More options")
}
DropdownMenu(
expanded = showOverflowMenu,
onDismissRequest = { showOverflowMenu = false },
) {
DropdownMenuItem(
text = { Text("Disable period logging") },
onClick = {
showOverflowMenu = false
viewModel.disablePeriodTracking()
onBack()
}
)
}
}
)
}
Expand Down Expand Up @@ -221,10 +243,11 @@ fun LogPeriodScreen(
}
}

// Flow section
SectionLabel(state.flowCategoryName)
// Flow section — only shown when the category is tracked with period and not archived
val flowCat = state.flowCategory
if (flowCat?.categoryType == "numeric_slider") {
if (flowCat != null && flowCat.showInLogPeriod && !flowCat.isArchived) {
SectionLabel(state.flowCategoryName)
if (flowCat.categoryType == "numeric_slider") {
val sliderValue = state.flowSliderValue ?: flowCat.numericMin
val scaleMap = flowCat.scaleLabels.decodeScaleLabels()
val scaleLabel = scaleMap[sliderValue.toInt()] ?: sliderValue.toInt().toString()
Expand Down Expand Up @@ -287,8 +310,11 @@ fun LogPeriodScreen(
)
}
}
} // end flow showInLogPeriod guard

// Symptoms section
// Symptoms section — only shown when the category is tracked with period and not archived
val symptomsCat = state.symptomsCategory
if (symptomsCat != null && symptomsCat.showInLogPeriod && !symptomsCat.isArchived) {
SectionLabel(state.symptomsCategoryName)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
Expand Down Expand Up @@ -319,6 +345,7 @@ fun LogPeriodScreen(
)
)
}
} // end symptoms showInLogPeriod guard

// Pinned tracking categories
state.pinnedCategories.forEach { category ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
import com.mapgie.goflo.data.database.entities.PeriodEntry
import com.mapgie.goflo.data.database.entities.TrackingCategory
import com.mapgie.goflo.data.database.entities.TrackingValue
import com.mapgie.goflo.data.preferences.AppPreferencesStore
import com.mapgie.goflo.widget.GoFloWidget
import com.mapgie.goflo.data.repository.PeriodRepository
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -47,8 +48,10 @@ data class LogPeriodUiState(
val flowCategoryName: String = "Flow",
/** User-chosen display name for the Symptoms system category. */
val symptomsCategoryName: String = "Symptoms",
/** Full Flow system category entity — used to know its current categoryType. */
/** Full Flow system category entity — used to know its current categoryType and showInLogPeriod. */
val flowCategory: TrackingCategory? = null,
/** Full Symptoms system category entity — used to check showInLogPeriod. */
val symptomsCategory: TrackingCategory? = null,
/** Current slider position when the Flow category is in slider mode (1-4). */
val flowSliderValue: Float? = null,
/** Ordered list of selectable flow level options (from TrackingValues). */
Expand All @@ -64,7 +67,8 @@ class LogPeriodViewModel(
private val periodId: Long,
private val prefilledDate: LocalDate? = null,
private val trackingRepository: com.mapgie.goflo.data.repository.TrackingRepository? = null,
private val application: Application? = null
private val application: Application? = null,
private val preferencesStore: AppPreferencesStore? = null,
) : ViewModel() {

private val _uiState = MutableStateFlow(LogPeriodUiState(startDate = prefilledDate ?: LocalDate.now()))
Expand Down Expand Up @@ -141,6 +145,7 @@ class LogPeriodViewModel(
flowCategoryName = flowCat?.name ?: state.flowCategoryName,
symptomsCategoryName = symptomsCat?.name ?: state.symptomsCategoryName,
flowCategory = flowCat,
symptomsCategory = symptomsCat,
flowSliderValue = sliderValue,
selectedFlowLabel = selectedFlow,
symptoms = editSymptoms ?: state.symptoms,
Expand All @@ -166,7 +171,8 @@ class LogPeriodViewModel(

private suspend fun loadPinnedCategories() {
val tr = trackingRepository ?: return
val categories = tr.getShowInLogPeriodCategories()
// Exclude system categories (flow, symptoms) — they have dedicated UI sections above.
val categories = tr.getShowInLogPeriodCategories().filter { !it.isSystem }
if (categories.isEmpty()) return

val valuesMap = mutableMapOf<Long, List<String>>()
Expand Down Expand Up @@ -291,6 +297,7 @@ class LogPeriodViewModel(
private suspend fun syncFlowToTrackingLog(state: LogPeriodUiState) {
val tr = trackingRepository ?: return
val flowCategory = tr.getSystemCategoryByKey("flow") ?: return
if (!flowCategory.showInLogPeriod || flowCategory.isArchived) return
val flowLabel = if (flowCategory.categoryType == "numeric_slider") {
val v = state.flowSliderValue ?: flowLabelToSliderValue(state.selectedFlowLabel)
v.toInt().toString()
Expand All @@ -303,6 +310,7 @@ class LogPeriodViewModel(
private suspend fun syncSymptomsToTrackingLog(state: LogPeriodUiState) {
val tr = trackingRepository ?: return
val symptomsCategory = tr.getSystemCategoryByKey("symptoms") ?: return
if (!symptomsCategory.showInLogPeriod || symptomsCategory.isArchived) return
if (state.symptoms.isEmpty()) {
val existing = tr.getExistingLog(state.startDate, symptomsCategory.id) ?: return
tr.deleteLog(existing.log)
Expand Down Expand Up @@ -357,6 +365,10 @@ class LogPeriodViewModel(
}
}

fun disablePeriodTracking() {
viewModelScope.launch { preferencesStore?.setPeriodTrackingEnabled(false) }
}

fun delete() {
val state = _uiState.value
val id = state.existingId ?: return
Expand All @@ -377,11 +389,12 @@ class LogPeriodViewModel(
private val periodId: Long,
private val prefilledDate: LocalDate? = null,
private val trackingRepository: com.mapgie.goflo.data.repository.TrackingRepository? = null,
private val application: Application? = null
private val application: Application? = null,
private val preferencesStore: AppPreferencesStore? = null,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return LogPeriodViewModel(repository, periodId, prefilledDate, trackingRepository, application) as T
return LogPeriodViewModel(repository, periodId, prefilledDate, trackingRepository, application, preferencesStore) as T
}
}

Expand Down
Loading
Loading