diff --git a/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksComponents.kt b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksComponents.kt index 20174c8..4ad47e3 100644 --- a/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksComponents.kt +++ b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksComponents.kt @@ -237,11 +237,23 @@ fun TasksListNavView( selectedHref: String?, onListClick: (TaskListItem) -> Unit, modifier: Modifier = Modifier, + smartKeys: List = emptyList(), + smartCounts: Map = 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, diff --git a/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksScreen.kt b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksScreen.kt index 544adda..aa0ac6e 100644 --- a/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksScreen.kt +++ b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksScreen.kt @@ -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 -> { diff --git a/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksViewModel.kt b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksViewModel.kt index 1fb7f69..3678ba9 100644 --- a/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksViewModel.kt +++ b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksViewModel.kt @@ -31,10 +31,30 @@ data class TasksUiState( val error: String? = null, val unauthorized: Boolean = false, val detailTask: TaskItem? = null, + // Умные коллекции (P1): агрегат задач всех списков + режим видимости + выбранная коллекция. + val allTasks: List = emptyList(), + val smartVisibility: SmartListVisibility = SmartListVisibility.DEFAULT, + val selectedSmartKey: SmartListKey? = null, ) { val selectedList: TaskListItem? get() = lists.firstOrNull { it.href == selectedListHref } + /** Счётчики умных коллекций по агрегату всех списков (для бейджей в панели). */ + val smartCounts: Map + get() = TasksSmartLists.counts(allTasks, LocalDate.now()) + + /** Умные коллекции, видимые в панели (режим + счётчик). */ + val visibleSmartKeys: List + 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() + 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) {