feat(tasks): P1 фундамент — 3-state видимость коллекций + разрешение по счётчикам

SmartListMode (HIDDEN/VISIBLE/AUTO) по семантике веб server-show 0/1/2.
TasksSmartLists.counts()/visibleKeys() — сводка и разрешение видимых коллекций.
25 юнит-тестов зелёные. Далее: агрегация в VM + вывод в TasksListNavView.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
b-mob-tasks
2026-07-13 05:54:53 +00:00
parent 286722d953
commit e405dad8fa
2 changed files with 96 additions and 22 deletions
@@ -29,23 +29,36 @@ enum class SmartListKey(val title: String, val icon: String) {
COMPLETED("Завершенные", "check-done-gray.svg"), COMPLETED("Завершенные", "check-done-gray.svg"),
} }
/** Какие умные списки показывать в панели (шит «Параметры задач»). По умолчанию — все. */ /**
* Режим видимости умной коллекции в панели — как в веб-Tasks (server `show` 0/1/2):
* [HIDDEN] всегда скрыта · [VISIBLE] всегда видима · [AUTO] видима только при count > 0.
*/
enum class SmartListMode { HIDDEN, VISIBLE, AUTO }
/**
* Настройка видимости умных коллекций (шит «Параметры задач»). Хранит только НЕ-дефолтные
* режимы — для компактной сериализации в SharedPreferences. Умолчание — [DEFAULT_MODE].
*/
data class SmartListVisibility( data class SmartListVisibility(
private val hidden: Set<SmartListKey> = emptySet(), private val modes: Map<SmartListKey, SmartListMode> = emptyMap(),
) { ) {
fun isVisible(key: SmartListKey): Boolean = key !in hidden fun mode(key: SmartListKey): SmartListMode = modes[key] ?: DEFAULT_MODE
fun withVisible(key: SmartListKey, visible: Boolean): SmartListVisibility = fun withMode(key: SmartListKey, mode: SmartListMode): SmartListVisibility =
SmartListVisibility(if (visible) hidden - key else hidden + key) SmartListVisibility(
if (mode == DEFAULT_MODE) modes - key else modes + (key to mode),
)
/** Ключи скрытых списков — для сериализации в SharedPreferences. */ /** Только заданные вручную (не-дефолтные) режимы — для персиста. */
fun hiddenKeys(): Set<SmartListKey> = hidden fun explicitModes(): Map<SmartListKey, SmartListMode> = modes
companion object { companion object {
val ALL_VISIBLE = SmartListVisibility() /** Умолчание: AUTO (показывать при непустом счётчике) — сверить с сервером live. */
val DEFAULT_MODE = SmartListMode.AUTO
val DEFAULT = SmartListVisibility()
fun of(hidden: Collection<SmartListKey>): SmartListVisibility = fun of(modes: Map<SmartListKey, SmartListMode>): SmartListVisibility =
SmartListVisibility(hidden.toSet()) SmartListVisibility(modes.filterValues { it != DEFAULT_MODE })
} }
} }
@@ -74,6 +87,25 @@ object TasksSmartLists {
fun count(key: SmartListKey, tasks: List<TaskItem>, today: LocalDate): Int = fun count(key: SmartListKey, tasks: List<TaskItem>, today: LocalDate): Int =
tasks.count { matches(key, it, today) } tasks.count { matches(key, it, today) }
/** Счётчики всех коллекций сразу (по агрегату задач всех списков). */
fun counts(tasks: List<TaskItem>, today: LocalDate): Map<SmartListKey, Int> =
SmartListKey.entries.associateWith { key -> count(key, tasks, today) }
/**
* Коллекции для показа в панели (в порядке enum) с учётом режима видимости и счётчиков:
* VISIBLE — всегда, AUTO — при count > 0, HIDDEN — никогда.
*/
fun visibleKeys(
visibility: SmartListVisibility,
counts: Map<SmartListKey, Int>,
): List<SmartListKey> = SmartListKey.entries.filter { key ->
when (visibility.mode(key)) {
SmartListMode.VISIBLE -> true
SmartListMode.HIDDEN -> false
SmartListMode.AUTO -> (counts[key] ?: 0) > 0
}
}
/** Уже стартовала: нет start, либо start сегодня/в прошлом. */ /** Уже стартовала: нет start, либо start сегодня/в прошлом. */
private fun TaskItem.hasStarted(today: LocalDate): Boolean { private fun TaskItem.hasStarted(today: LocalDate): Boolean {
val start = startLocalDate() ?: return true val start = startLocalDate() ?: return true
@@ -182,23 +182,65 @@ class TasksSmartListsTest {
assertEquals(1, TasksSmartLists.count(SmartListKey.COMPLETED, tasks, today)) assertEquals(1, TasksSmartLists.count(SmartListKey.COMPLETED, tasks, today))
} }
// --- Видимость --- // --- Видимость (3-state: Скрыта/Видима/Авто) ---
@Test @Test
fun visibility_defaultAllVisible() { fun visibility_defaultModeIsAuto() {
val v = SmartListVisibility.ALL_VISIBLE val v = SmartListVisibility.DEFAULT
SmartListKey.entries.forEach { assertTrue(v.isVisible(it)) } SmartListKey.entries.forEach { assertEquals(SmartListMode.AUTO, v.mode(it)) }
} }
@Test @Test
fun visibility_hideAndRestore() { fun visibility_setAndClearMode() {
val hidden = SmartListVisibility.ALL_VISIBLE.withVisible(SmartListKey.COMPLETED, false) val v = SmartListVisibility.DEFAULT
assertFalse(hidden.isVisible(SmartListKey.COMPLETED)) .withMode(SmartListKey.COMPLETED, SmartListMode.HIDDEN)
assertTrue(hidden.isVisible(SmartListKey.ALL)) .withMode(SmartListKey.ALL, SmartListMode.VISIBLE)
assertEquals(setOf(SmartListKey.COMPLETED), hidden.hiddenKeys()) assertEquals(SmartListMode.HIDDEN, v.mode(SmartListKey.COMPLETED))
assertEquals(SmartListMode.VISIBLE, v.mode(SmartListKey.ALL))
// Возврат в дефолт (AUTO) — режим не хранится (компактный персист).
val cleared = v.withMode(SmartListKey.ALL, SmartListMode.AUTO)
assertEquals(SmartListMode.AUTO, cleared.mode(SmartListKey.ALL))
assertEquals(setOf(SmartListKey.COMPLETED), cleared.explicitModes().keys)
}
val restored = hidden.withVisible(SmartListKey.COMPLETED, true) @Test
assertTrue(restored.isVisible(SmartListKey.COMPLETED)) fun visibleKeys_autoShowsOnlyNonEmpty() {
assertTrue(restored.hiddenKeys().isEmpty()) val counts = mapOf(
SmartListKey.IMPORTANT to 2,
SmartListKey.TODAY to 0,
SmartListKey.WEEK to 0,
SmartListKey.ALL to 5,
SmartListKey.CURRENT to 5,
SmartListKey.COMPLETED to 0,
)
// Всё по умолчанию AUTO → показываем только непустые, в порядке enum.
assertEquals(
listOf(SmartListKey.IMPORTANT, SmartListKey.ALL, SmartListKey.CURRENT),
TasksSmartLists.visibleKeys(SmartListVisibility.DEFAULT, counts),
)
}
@Test
fun visibleKeys_visibleAlwaysHiddenNever() {
val counts = SmartListKey.entries.associateWith { 0 }
val v = SmartListVisibility.DEFAULT
.withMode(SmartListKey.ALL, SmartListMode.VISIBLE) // видима даже при 0
.withMode(SmartListKey.CURRENT, SmartListMode.HIDDEN)
val visible = TasksSmartLists.visibleKeys(v, counts)
assertEquals(listOf(SmartListKey.ALL), visible) // остальные AUTO+0 скрыты, CURRENT HIDDEN
}
@Test
fun counts_overMixedSet() {
val tasks = listOf(
task(uid = "a", priority = 2, due = today),
task(uid = "b", status = "COMPLETED"),
task(uid = "c"),
)
val counts = TasksSmartLists.counts(tasks, today)
assertEquals(1, counts[SmartListKey.IMPORTANT])
assertEquals(2, counts[SmartListKey.ALL]) // a, c незавершённые
assertEquals(1, counts[SmartListKey.COMPLETED])
assertEquals(2, counts[SmartListKey.CURRENT]) // a, c (без будущего старта)
} }
} }