feat(tasks): P1 — умные коллекции в панели навигации (агрегат + счётчики + открытие)
TasksListNavView рисует видимые коллекции (visibleKeys по режиму+счётчику), тап → openSmartList (агрегат всех списков, фильтр по коллекции). VM: loadAllTasks агрегирует все списки, reloadCurrent обновляет вид после мутаций, create в коллекции идёт в список по умолчанию. Гейт compile+detekt+test зелёный (25 юнитов). Осталось по P1: 3-state настройки видимости + персист (SharedPreferences). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -237,11 +237,23 @@ fun TasksListNavView(
|
||||
selectedHref: String?,
|
||||
onListClick: (TaskListItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
smartKeys: List<SmartListKey> = emptyList(),
|
||||
smartCounts: Map<SmartListKey, Int> = emptyMap(),
|
||||
onSmartClick: (SmartListKey) -> Unit = {},
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(smartKeys, key = { "smart-${it.name}" }) { key ->
|
||||
SmartCollectionRow(
|
||||
serverUrl = serverUrl,
|
||||
title = key.title,
|
||||
icon = key.icon,
|
||||
count = smartCounts[key] ?: 0,
|
||||
onClick = { onSmartClick(key) },
|
||||
)
|
||||
}
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -304,6 +316,59 @@ fun TasksListNavView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmartCollectionRow(
|
||||
serverUrl: String,
|
||||
title: String,
|
||||
icon: String,
|
||||
count: Int,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(TasksFieldBg)
|
||||
.tasksFieldShadow()
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
AsyncImage(
|
||||
model = tasksAsset(serverUrl, icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Text(
|
||||
title,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = F7Colors.TextPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (count > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(100.dp))
|
||||
.background(F7Colors.PrimaryLight)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
count.toString(),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = F7Colors.PrimaryDark,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TasksGroupHeader(
|
||||
title: String,
|
||||
|
||||
@@ -90,12 +90,15 @@ fun TasksScreen(
|
||||
CircularProgressIndicator(color = F7Colors.Primary)
|
||||
}
|
||||
}
|
||||
state.selectedListHref == null -> {
|
||||
!state.showTaskList -> {
|
||||
TasksListNavView(
|
||||
serverUrl = session.serverUrl,
|
||||
lists = state.lists,
|
||||
selectedHref = null,
|
||||
onListClick = { vm.openList(session, it.href) },
|
||||
smartKeys = state.visibleSmartKeys,
|
||||
smartCounts = state.smartCounts,
|
||||
onSmartClick = { vm.openSmartList(session, it) },
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
|
||||
@@ -31,10 +31,30 @@ data class TasksUiState(
|
||||
val error: String? = null,
|
||||
val unauthorized: Boolean = false,
|
||||
val detailTask: TaskItem? = null,
|
||||
// Умные коллекции (P1): агрегат задач всех списков + режим видимости + выбранная коллекция.
|
||||
val allTasks: List<TaskItem> = emptyList(),
|
||||
val smartVisibility: SmartListVisibility = SmartListVisibility.DEFAULT,
|
||||
val selectedSmartKey: SmartListKey? = null,
|
||||
) {
|
||||
val selectedList: TaskListItem?
|
||||
get() = lists.firstOrNull { it.href == selectedListHref }
|
||||
|
||||
/** Счётчики умных коллекций по агрегату всех списков (для бейджей в панели). */
|
||||
val smartCounts: Map<SmartListKey, Int>
|
||||
get() = TasksSmartLists.counts(allTasks, LocalDate.now())
|
||||
|
||||
/** Умные коллекции, видимые в панели (режим + счётчик). */
|
||||
val visibleSmartKeys: List<SmartListKey>
|
||||
get() = TasksSmartLists.visibleKeys(smartVisibility, smartCounts)
|
||||
|
||||
/** Заголовок текущего экрана списка: реальный список или умная коллекция. */
|
||||
val currentTitle: String
|
||||
get() = selectedSmartKey?.title ?: selectedList?.displayName ?: ""
|
||||
|
||||
/** Показывать экран задач (реальный список ИЛИ умная коллекция выбраны). */
|
||||
val showTaskList: Boolean
|
||||
get() = selectedListHref != null || selectedSmartKey != null
|
||||
|
||||
private fun matchesSearch(task: TaskItem): Boolean {
|
||||
val q = searchQuery.trim()
|
||||
return q.isEmpty() ||
|
||||
@@ -107,6 +127,8 @@ class TasksViewModel(
|
||||
)
|
||||
}
|
||||
selected?.let { loadTasks(session, it) }
|
||||
// Агрегат всех списков — для умных коллекций и их счётчиков.
|
||||
loadAllTasks(session)
|
||||
}
|
||||
.onFailure { t ->
|
||||
_state.update {
|
||||
@@ -124,6 +146,7 @@ class TasksViewModel(
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectedListHref = listHref,
|
||||
selectedSmartKey = null,
|
||||
// Мгновенно из кэша, если есть — иначе пусто до загрузки.
|
||||
tasks = TasksCache.cachedTasks(listHref).orEmpty(),
|
||||
searchQuery = "",
|
||||
@@ -135,10 +158,32 @@ class TasksViewModel(
|
||||
loadTasks(session, listHref)
|
||||
}
|
||||
|
||||
/** Открыть умную коллекцию (агрегат всех списков, отфильтрованный по [key]). */
|
||||
fun openSmartList(session: AuthSession, key: SmartListKey) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectedSmartKey = key,
|
||||
selectedListHref = null,
|
||||
tasks = TasksSmartLists.filter(key, it.allTasks, LocalDate.now()),
|
||||
searchQuery = "",
|
||||
detailTask = null,
|
||||
createInputExpanded = false,
|
||||
createInputText = "",
|
||||
)
|
||||
}
|
||||
loadAllTasks(session)
|
||||
}
|
||||
|
||||
/** Сменить режим видимости умной коллекции (шит настроек). */
|
||||
fun setSmartMode(key: SmartListKey, mode: SmartListMode) {
|
||||
_state.update { it.copy(smartVisibility = it.smartVisibility.withMode(key, mode)) }
|
||||
}
|
||||
|
||||
fun closeList() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectedListHref = null,
|
||||
selectedSmartKey = null,
|
||||
tasks = emptyList(),
|
||||
detailTask = null,
|
||||
createInputExpanded = false,
|
||||
@@ -218,7 +263,11 @@ class TasksViewModel(
|
||||
priority: Int,
|
||||
parentUid: String = "",
|
||||
) {
|
||||
val listHref = _state.value.selectedListHref ?: return
|
||||
// В умной коллекции реального списка нет — создаём в список по умолчанию.
|
||||
val listHref = _state.value.selectedListHref
|
||||
?: _state.value.defaultListHref
|
||||
?: _state.value.lists.firstOrNull()?.href
|
||||
?: return
|
||||
if (summary.isBlank()) {
|
||||
_state.update { it.copy(error = "Введите название задачи") }
|
||||
return
|
||||
@@ -248,7 +297,7 @@ class TasksViewModel(
|
||||
createInputText = "",
|
||||
)
|
||||
}
|
||||
loadTasks(session, listHref)
|
||||
reloadCurrent(session)
|
||||
}.onFailure { t ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
@@ -266,7 +315,6 @@ class TasksViewModel(
|
||||
runCatching {
|
||||
repository.toggleComplete(session, task, !task.isCompleted)
|
||||
}.onSuccess {
|
||||
val listHref = _state.value.selectedListHref
|
||||
val detail = _state.value.detailTask
|
||||
if (detail?.uid == task.uid) {
|
||||
_state.update {
|
||||
@@ -276,7 +324,7 @@ class TasksViewModel(
|
||||
))
|
||||
}
|
||||
}
|
||||
listHref?.let { loadTasks(session, it) }
|
||||
reloadCurrent(session)
|
||||
}.onFailure { t ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
@@ -309,7 +357,7 @@ class TasksViewModel(
|
||||
)
|
||||
}.onSuccess {
|
||||
_state.update { it.copy(saving = false, detailTask = null) }
|
||||
_state.value.selectedListHref?.let { loadTasks(session, it) }
|
||||
reloadCurrent(session)
|
||||
}.onFailure { t ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
@@ -331,7 +379,7 @@ class TasksViewModel(
|
||||
_state.update {
|
||||
it.copy(saving = false, detailTask = it.detailTask?.takeIf { d -> d.uid != task.uid })
|
||||
}
|
||||
_state.value.selectedListHref?.let { loadTasks(session, it) }
|
||||
reloadCurrent(session)
|
||||
}
|
||||
.onFailure { t ->
|
||||
_state.update {
|
||||
@@ -353,7 +401,7 @@ class TasksViewModel(
|
||||
runCatching { repository.deleteTask(session, task) }
|
||||
.onSuccess {
|
||||
_state.update { it.copy(saving = false, detailTask = null) }
|
||||
_state.value.selectedListHref?.let { loadTasks(session, it) }
|
||||
reloadCurrent(session)
|
||||
}
|
||||
.onFailure { t ->
|
||||
_state.update {
|
||||
@@ -376,7 +424,7 @@ class TasksViewModel(
|
||||
runCatching { repository.deleteCompletedTasks(session, completed) }
|
||||
.onSuccess {
|
||||
_state.update { it.copy(deletingCompleted = false, completedExpanded = false) }
|
||||
loadTasks(session, listHref)
|
||||
reloadCurrent(session)
|
||||
}
|
||||
.onFailure { t ->
|
||||
_state.update {
|
||||
@@ -391,8 +439,46 @@ class TasksViewModel(
|
||||
}
|
||||
|
||||
fun refreshTasks(session: AuthSession) {
|
||||
_state.value.selectedListHref?.let { loadTasks(session, it) }
|
||||
?: load(session)
|
||||
val href = _state.value.selectedListHref
|
||||
when {
|
||||
href != null -> loadTasks(session, href)
|
||||
_state.value.selectedSmartKey != null -> loadAllTasks(session)
|
||||
else -> load(session)
|
||||
}
|
||||
}
|
||||
|
||||
/** Перезагрузить текущий вид: реальный список и/или агрегат (если открыта умная коллекция). */
|
||||
private fun reloadCurrent(session: AuthSession) {
|
||||
val href = _state.value.selectedListHref
|
||||
href?.let { loadTasks(session, it) }
|
||||
if (_state.value.selectedSmartKey != null || href == null) {
|
||||
loadAllTasks(session)
|
||||
}
|
||||
}
|
||||
|
||||
/** Загрузить задачи ВСЕХ списков в агрегат (умные коллекции + счётчики). */
|
||||
private fun loadAllTasks(session: AuthSession) {
|
||||
val lists = _state.value.lists
|
||||
if (lists.isEmpty()) return
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val aggregate = mutableListOf<TaskItem>()
|
||||
lists.forEach { list ->
|
||||
runCatching { repository.loadTasks(session, list.href) }
|
||||
.onSuccess { tasks ->
|
||||
TasksCache.putTasks(list.href, tasks)
|
||||
aggregate += tasks
|
||||
}
|
||||
}
|
||||
_state.update { st ->
|
||||
st.copy(
|
||||
allTasks = aggregate,
|
||||
// Если открыта умная коллекция — переотфильтровать её содержимое.
|
||||
tasks = st.selectedSmartKey?.let {
|
||||
TasksSmartLists.filter(it, aggregate, LocalDate.now())
|
||||
} ?: st.tasks,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTasks(session: AuthSession, listHref: String) {
|
||||
|
||||
Reference in New Issue
Block a user