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

TasksPrefsStore (SharedPreferences) хранит режим Скрыта/Видима/Авто по коллекции.
TasksViewModel через Factory(context), setSmartMode персистит; шит настроек 3-state
от VM (убраны фиктивные локальные remember). Панель реагирует через visibleKeys.
Гейт compile+detekt+test зелёный.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
b-mob-tasks
2026-07-13 06:12:17 +00:00
parent cfeb9b3b79
commit bd56c4bc7b
4 changed files with 71 additions and 35 deletions
@@ -1016,17 +1016,12 @@ fun TasksSettingsSheet(
lists: List<TaskListItem>, lists: List<TaskListItem>,
defaultListHref: String?, defaultListHref: String?,
onDefaultListChange: (String?) -> Unit, onDefaultListChange: (String?) -> Unit,
visibility: SmartListVisibility,
onSmartModeChange: (SmartListKey, SmartListMode) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSwipeBack: () -> Unit = onDismiss, onSwipeBack: () -> Unit = onDismiss,
) { ) {
if (!visible) return if (!visible) return
val visibilityOptions = listOf("Показывать", "Скрывать")
var importantVisible by remember { mutableStateOf(visibilityOptions.first()) }
var todayVisible by remember { mutableStateOf(visibilityOptions.first()) }
var weekVisible by remember { mutableStateOf(visibilityOptions.first()) }
var allVisible by remember { mutableStateOf(visibilityOptions.first()) }
var currentVisible by remember { mutableStateOf(visibilityOptions.first()) }
var completedVisible by remember { mutableStateOf(visibilityOptions.first()) }
var defaultMenuOpen by remember { mutableStateOf(false) } var defaultMenuOpen by remember { mutableStateOf(false) }
Dialog( Dialog(
@@ -1099,23 +1094,15 @@ fun TasksSettingsSheet(
HorizontalDivider(color = F7Colors.Border) HorizontalDivider(color = F7Colors.Border)
SmartListSettingRow(serverUrl, "important-gray.svg", "Важное", importantVisible) { Text("Видимость умных списков", fontSize = 14.sp, fontWeight = FontWeight.Medium)
importantVisible = it SmartListKey.entries.forEach { key ->
} SmartListSettingRow(
SmartListSettingRow(serverUrl, "calendar-today-gray.svg", "Сегодня", todayVisible) { serverUrl = serverUrl,
todayVisible = it iconPath = key.icon,
} title = key.title,
SmartListSettingRow(serverUrl, "calendar-nextweek-gray.svg", "На неделе", weekVisible) { mode = visibility.mode(key),
weekVisible = it onModeChange = { onSmartModeChange(key, it) },
} )
SmartListSettingRow(serverUrl, "calendar-icon-gray.svg", "Все", allVisible) {
allVisible = it
}
SmartListSettingRow(serverUrl, "current-gray.svg", "Текущие", currentVisible) {
currentVisible = it
}
SmartListSettingRow(serverUrl, "check-done-gray.svg", "Завершенные", completedVisible) {
completedVisible = it
} }
} }
} }
@@ -1127,11 +1114,16 @@ private fun SmartListSettingRow(
serverUrl: String, serverUrl: String,
iconPath: String, iconPath: String,
title: String, title: String,
selected: String, mode: SmartListMode,
onSelect: (String) -> Unit, onModeChange: (SmartListMode) -> Unit,
) { ) {
var menuOpen by remember { mutableStateOf(false) } var menuOpen by remember { mutableStateOf(false) }
val options = listOf("Показывать", "Скрывать") val options = listOf(
SmartListMode.VISIBLE to "Показывать",
SmartListMode.HIDDEN to "Скрывать",
SmartListMode.AUTO to "Автоматически",
)
val currentLabel = options.firstOrNull { it.first == mode }?.second ?: "Автоматически"
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
AsyncImage(model = tasksAsset(serverUrl, iconPath), contentDescription = null, modifier = Modifier.size(18.dp)) AsyncImage(model = tasksAsset(serverUrl, iconPath), contentDescription = null, modifier = Modifier.size(18.dp))
@@ -1149,7 +1141,7 @@ private fun SmartListSettingRow(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text(selected) Text(currentLabel)
AsyncImage( AsyncImage(
model = tasksAsset(serverUrl, "chevron-down-16.svg"), model = tasksAsset(serverUrl, "chevron-down-16.svg"),
contentDescription = null, contentDescription = null,
@@ -1157,11 +1149,11 @@ private fun SmartListSettingRow(
) )
} }
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
options.forEach { option -> options.forEach { (optionMode, label) ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(option) }, text = { Text(label) },
onClick = { onClick = {
onSelect(option) onModeChange(optionMode)
menuOpen = false menuOpen = false
}, },
) )
@@ -0,0 +1,29 @@
package ru.forbion.f7cloud.feature.tasks
import android.content.Context
/**
* Персист настроек Задач в SharedPreferences (по образцу `CalendarPrefsStore`).
* Сейчас — режим видимости умных коллекций (3-state), переживает перезапуск.
*/
class TasksPrefsStore(context: Context) {
private val prefs = context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
fun getVisibility(): SmartListVisibility {
val modes = SmartListKey.entries.mapNotNull { key ->
val raw = prefs.getString(keyMode(key), null) ?: return@mapNotNull null
runCatching { SmartListMode.valueOf(raw) }.getOrNull()?.let { key to it }
}.toMap()
return SmartListVisibility.of(modes)
}
fun setMode(key: SmartListKey, mode: SmartListMode) {
prefs.edit().putString(keyMode(key), mode.name).apply()
}
companion object {
private const val PREFS = "f7_tasks_prefs"
private fun keyMode(key: SmartListKey) = "smart_mode::${key.name}"
}
}
@@ -10,8 +10,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -22,6 +22,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.auth.AuthSession
@@ -38,7 +39,8 @@ fun TasksScreen(
onOpenListConsumed: () -> Unit = {}, onOpenListConsumed: () -> Unit = {},
onUnauthorized: () -> Unit = {}, onUnauthorized: () -> Unit = {},
) { ) {
val vm: TasksViewModel = viewModel() val context = LocalContext.current
val vm: TasksViewModel = viewModel(factory = TasksViewModel.Factory(context))
val state by vm.state.collectAsState() val state by vm.state.collectAsState()
// Родитель для создаваемой подзадачи (открывает диалог ввода названия). // Родитель для создаваемой подзадачи (открывает диалог ввода названия).
var subtaskParent by remember { mutableStateOf<TaskItem?>(null) } var subtaskParent by remember { mutableStateOf<TaskItem?>(null) }
@@ -156,6 +158,8 @@ fun TasksScreen(
lists = state.lists, lists = state.lists,
defaultListHref = state.defaultListHref, defaultListHref = state.defaultListHref,
onDefaultListChange = vm::setDefaultList, onDefaultListChange = vm::setDefaultList,
visibility = state.smartVisibility,
onSmartModeChange = vm::setSmartMode,
onDismiss = vm::closeSettings, onDismiss = vm::closeSettings,
onSwipeBack = vm::closeSettings, onSwipeBack = vm::closeSettings,
) )
@@ -1,6 +1,8 @@
package ru.forbion.f7cloud.feature.tasks package ru.forbion.f7cloud.feature.tasks
import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -88,9 +90,11 @@ data class TasksUiState(
} }
class TasksViewModel( class TasksViewModel(
context: Context,
private val repository: TasksRepository = TasksRepository(), private val repository: TasksRepository = TasksRepository(),
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(TasksUiState()) private val prefsStore = TasksPrefsStore(context.applicationContext)
private val _state = MutableStateFlow(TasksUiState(smartVisibility = prefsStore.getVisibility()))
val state: StateFlow<TasksUiState> = _state.asStateFlow() val state: StateFlow<TasksUiState> = _state.asStateFlow()
fun load(session: AuthSession) { fun load(session: AuthSession) {
@@ -174,8 +178,9 @@ class TasksViewModel(
loadAllTasks(session) loadAllTasks(session)
} }
/** Сменить режим видимости умной коллекции (шит настроек). */ /** Сменить режим видимости умной коллекции (шит настроек) + персист. */
fun setSmartMode(key: SmartListKey, mode: SmartListMode) { fun setSmartMode(key: SmartListKey, mode: SmartListMode) {
prefsStore.setMode(key, mode)
_state.update { it.copy(smartVisibility = it.smartVisibility.withMode(key, mode)) } _state.update { it.copy(smartVisibility = it.smartVisibility.withMode(key, mode)) }
} }
@@ -503,4 +508,10 @@ class TasksViewModel(
} }
} }
} }
class Factory(private val context: Context) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
TasksViewModel(context.applicationContext) as T
}
} }