diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt index 73f09c2..dd67d3b 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt @@ -15,6 +15,7 @@ import de.jeanlucmakiola.agendula.domain.Task import de.jeanlucmakiola.agendula.domain.TaskFilter import de.jeanlucmakiola.agendula.domain.TaskFormField import de.jeanlucmakiola.agendula.domain.TaskReminder +import de.jeanlucmakiola.agendula.domain.TaskSection import de.jeanlucmakiola.agendula.domain.asTaskReminder import de.jeanlucmakiola.agendula.domain.asReminderLead import de.jeanlucmakiola.agendula.domain.TaskSortOrder @@ -54,6 +55,7 @@ fun TimeFormatPref.is24Hour(systemIs24Hour: Boolean): Boolean = when (this) { } const val DEFAULT_SNOOZE_MINUTES = 10 +/** Done starts folded away, to keep the open tasks in focus. */ val DEFAULT_COLLAPSED_SECTIONS: Set = setOf(TaskSection.COMPLETED) /** The snooze lengths Settings offers. */ val SNOOZE_PRESETS = listOf(5, 10, 15, 30, 60) @@ -114,6 +116,8 @@ data class Settings( val hiddenFromSmartLists: Set = emptySet(), /** Pre-fill a new task's start with today's date. */ val defaultStartToday: Boolean = false, + /** Task-list sections folded away; Done starts folded. */ + val collapsedSections: Set = DEFAULT_COLLAPSED_SECTIONS, ) { /** [tasks] as [filter] shows them: a smart list drops the lists kept out of it. */ fun visibleIn(filter: TaskFilter, tasks: List): List = @@ -179,9 +183,18 @@ class SettingsPrefs @Inject constructor( .toSet() }, defaultStartToday = p[DEFAULT_START_TODAY] ?: false, + collapsedSections = p[COLLAPSED_SECTIONS] + ?.mapNotNull { name -> runCatching { TaskSection.valueOf(name) }.getOrNull() } + ?.toSet() + ?: DEFAULT_COLLAPSED_SECTIONS, ) } + suspend fun setSectionCollapsed(section: TaskSection, collapsed: Boolean) = dataStore.edit { p -> + val current = p[COLLAPSED_SECTIONS] ?: DEFAULT_COLLAPSED_SECTIONS.mapTo(HashSet()) { it.name } + p[COLLAPSED_SECTIONS] = if (collapsed) current + section.name else current - section.name + } + suspend fun setSortOrder(order: TaskSortOrder) = dataStore.edit { it[SORT_ORDER] = order.name } suspend fun setSnoozeMinutes(minutes: Int) = dataStore.edit { it[SNOOZE_MINUTES] = minutes.coerceAtLeast(1) } @@ -339,6 +352,7 @@ class SettingsPrefs @Inject constructor( val PUSH_ENABLED = booleanPreferencesKey("push_enabled") val SMART_LIST_HIDDEN = stringSetPreferencesKey("smart_list_hidden") val DEFAULT_START_TODAY = booleanPreferencesKey("default_start_today") + val COLLAPSED_SECTIONS = stringSetPreferencesKey("collapsed_sections") val SAVED_FILTERS = stringPreferencesKey("saved_filters") val SAVED_FILTER_NEXT_ID = longPreferencesKey("saved_filter_next_id") } diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListScreen.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListScreen.kt index d7bde7c..ee3a26a 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListScreen.kt @@ -372,8 +372,6 @@ private fun TaskListBody( // rows; inside a single real list both are redundant. val showListName = filter is TaskFilter.Smart || filter is TaskFilter.Tagged || (filter is TaskFilter.Saved && state.savedFilter?.listIds?.size != 1) - // Completed tasks collapse away by default to keep the active list in focus. - var completedExpanded by rememberSaveable { mutableStateOf(false) } // Parents the user expanded to reveal their subtasks inline. Children are // grouped from the same flat task set the list already holds. var expandedParents by rememberSaveable { mutableStateOf(emptySet()) } @@ -421,23 +419,20 @@ private fun TaskListBody( } sections.forEach { section -> - val collapsible = showHeaders && section.section == TaskSection.COMPLETED + // Every header folds its section away (#91); Done starts folded. + val expanded = !showHeaders || section.section !in state.collapsedSections if (showHeaders) { stickyHeader(key = "hdr-${section.section}") { - if (collapsible) { - CollapsibleSectionHeader( - label = sectionLabel(section.section), - count = section.tasks.size, - expanded = completedExpanded, - onToggle = { completedExpanded = !completedExpanded }, - onClear = onClearCompleted, - ) - } else { - SectionHeader(sectionLabel(section.section)) - } + CollapsibleSectionHeader( + label = sectionLabel(section.section), + count = section.tasks.size, + expanded = expanded, + onToggle = { viewModel.setSectionCollapsed(section.section, expanded) }, + onClear = onClearCompleted.takeIf { section.section == TaskSection.COMPLETED }, + ) } } - if (!collapsible || completedExpanded) { + if (expanded) { // Flatten the section into one grouped run: each parent, followed // by its subtasks when expanded. Each row's corners then come from // its place in the whole run (positionOf below) — so the children @@ -1128,19 +1123,6 @@ private fun cardShape(position: Position, full: androidx.compose.ui.unit.Dp, sma Position.Bottom -> RoundedCornerShape(topStart = small, topEnd = small, bottomStart = full, bottomEnd = full) } -@Composable -private fun SectionHeader(text: String) { - // Opaque surface so rows scroll cleanly under the pinned header. - Surface(color = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth()) { - Text( - text = text, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 28.dp, end = 28.dp, top = 16.dp, bottom = 8.dp), - ) - } -} - /** A tappable section header with a count and a chevron that rotates on expand. */ @Composable private fun CollapsibleSectionHeader( diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListViewModel.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListViewModel.kt index fdf75d7..68b88f6 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListViewModel.kt @@ -3,6 +3,7 @@ package de.jeanlucmakiola.agendula.ui.tasklist import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import de.jeanlucmakiola.agendula.data.prefs.DEFAULT_COLLAPSED_SECTIONS import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.agendula.data.sync.RemoteLists import de.jeanlucmakiola.agendula.data.tasks.TasksRepository @@ -14,6 +15,7 @@ import de.jeanlucmakiola.agendula.domain.TaskFiltering import de.jeanlucmakiola.agendula.domain.tagsInUse import de.jeanlucmakiola.agendula.domain.TaskForm import de.jeanlucmakiola.agendula.domain.TaskList +import de.jeanlucmakiola.agendula.domain.TaskSection import de.jeanlucmakiola.agendula.domain.TaskSortOrder import de.jeanlucmakiola.agendula.domain.comparator import de.jeanlucmakiola.agendula.ui.lists.ListWriteFailure @@ -56,6 +58,8 @@ sealed interface TaskListUiState { /** Whether a real list uses the bottom quick-add bar instead of the FAB. */ val bottomAddBar: Boolean = false, val sortOrder: TaskSortOrder = TaskSortOrder.DUE, + /** Sections whose rows are folded under their header. */ + val collapsedSections: Set = DEFAULT_COLLAPSED_SECTIONS, /** This real list is kept out of the smart lists. */ val hiddenFromSmartLists: Boolean = false, /** Tags the unnarrowed view carries, offered as filter chips. */ @@ -144,6 +148,7 @@ class TaskListViewModel @Inject constructor( showAddSubtaskRow = settings.showAddSubtaskRow, bottomAddBar = settings.bottomAddBar, sortOrder = settings.sortOrder, + collapsedSections = settings.collapsedSections, hiddenFromSmartLists = st.list?.id?.let { it in settings.hiddenFromSmartLists } ?: false, ) } else { @@ -277,6 +282,9 @@ class TaskListViewModel @Inject constructor( fun setHiddenFromSmartLists(listId: Long, hidden: Boolean) = viewModelScope.launch { settingsPrefs.setHiddenFromSmartLists(listId, hidden) } + fun setSectionCollapsed(section: TaskSection, collapsed: Boolean) = + viewModelScope.launch { settingsPrefs.setSectionCollapsed(section, collapsed) } + fun setSortOrder(order: TaskSortOrder) = viewModelScope.launch { settingsPrefs.setSortOrder(order) } /**