diff --git a/core/designsystem/src/main/java/ru/forbion/f7cloud/core/designsystem/F7AppMenuSheet.kt b/core/designsystem/src/main/java/ru/forbion/f7cloud/core/designsystem/F7AppMenuSheet.kt index 6dfd071..88f20e1 100644 --- a/core/designsystem/src/main/java/ru/forbion/f7cloud/core/designsystem/F7AppMenuSheet.kt +++ b/core/designsystem/src/main/java/ru/forbion/f7cloud/core/designsystem/F7AppMenuSheet.kt @@ -236,31 +236,34 @@ private fun F7AppMenuGridItem( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { - val localIcon = item.localIcon - if (localIcon != null) { - // Нативный пункт: иконка в круглом бейдже с зелёной обводкой (стиль glass) - Box( - modifier = Modifier - .size(MenuIconSize) - .clip(CircleShape) - .background(Color.White) - .border(1.5.dp, F7Colors.Green30, CircleShape), - contentAlignment = Alignment.Center, - ) { + // Единая круглая рамка (белый круг + рамка + мягкая тень) под КАЖДУЮ иконку — + // как на мобильном сайте: все иконки одного размера в кружке. Глиф внутри меньше рамки. + val frameShape = CircleShape + Box( + modifier = Modifier + .size(MenuIconSize) + .shadow(2.dp, frameShape, spotColor = Color(0xFFE6E6E6)) + .clip(frameShape) + .background(Color.White) + .border(1.dp, Color(0xFFECECEC), frameShape), + contentAlignment = Alignment.Center, + ) { + val localIcon = item.localIcon + if (localIcon != null) { Icon( localIcon, contentDescription = item.label, tint = F7Colors.Primary, - modifier = Modifier.size(MenuIconSize * 0.46f), + modifier = Modifier.size(MenuIconSize * 0.5f), + ) + } else { + AsyncImage( + model = item.iconUrl, + contentDescription = item.label, + modifier = Modifier.size(MenuIconSize * 0.56f), + contentScale = ContentScale.Fit, ) } - } else { - AsyncImage( - model = item.iconUrl, - contentDescription = item.label, - modifier = Modifier.size(MenuIconSize), - contentScale = ContentScale.Fit, - ) } Text( text = item.label, diff --git a/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksCache.kt b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksCache.kt new file mode 100644 index 0000000..b66a4ff --- /dev/null +++ b/feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksCache.kt @@ -0,0 +1,28 @@ +package ru.forbion.f7cloud.feature.tasks + +/** + * Лёгкий кэш в памяти на время жизни процесса: списки задач и задачи по каждому списку. + * VM пересоздаётся при каждом открытии раздела Задач — без кэша это полный PROPFIND+REPORT + * заново. Кэш даёт мгновенный показ, сеть обновляет в фоне. + */ +internal object TasksCache { + @Volatile + private var lists: List? = null + private val tasksByList = java.util.concurrent.ConcurrentHashMap>() + + fun cachedLists(): List? = lists + + fun putLists(value: List) { + lists = value + } + + fun cachedTasks(listHref: String): List? = tasksByList[listHref] + + fun putTasks(listHref: String, value: List) { + tasksByList[listHref] = value + } + + fun invalidateTasks(listHref: String) { + tasksByList.remove(listHref) + } +} 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 b315f73..1fb7f69 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 @@ -74,10 +74,27 @@ class TasksViewModel( val state: StateFlow = _state.asStateFlow() fun load(session: AuthSession) { + // Мгновенно показываем кэш (списки + задачи выбранного), сеть обновляет в фоне. + val cachedLists = TasksCache.cachedLists() + if (cachedLists != null && cachedLists.isNotEmpty()) { + val selected = _state.value.selectedListHref + ?: _state.value.defaultListHref + ?: cachedLists.firstOrNull()?.href + _state.update { + it.copy( + loading = false, + lists = cachedLists, + selectedListHref = selected, + defaultListHref = it.defaultListHref ?: cachedLists.firstOrNull()?.href, + tasks = selected?.let { href -> TasksCache.cachedTasks(href) } ?: it.tasks, + ) + } + } viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy(loading = it.lists.isEmpty(), error = null) } runCatching { repository.listTaskLists(session) } .onSuccess { lists -> + TasksCache.putLists(lists) val selected = _state.value.selectedListHref ?: _state.value.defaultListHref ?: lists.firstOrNull()?.href @@ -107,6 +124,8 @@ class TasksViewModel( _state.update { it.copy( selectedListHref = listHref, + // Мгновенно из кэша, если есть — иначе пусто до загрузки. + tasks = TasksCache.cachedTasks(listHref).orEmpty(), searchQuery = "", detailTask = null, createInputExpanded = false, @@ -381,7 +400,11 @@ class TasksViewModel( _state.update { it.copy(loading = _state.value.tasks.isEmpty(), error = null) } runCatching { repository.loadTasks(session, listHref) } .onSuccess { tasks -> - _state.update { it.copy(loading = false, tasks = tasks) } + TasksCache.putTasks(listHref, tasks) + // Обновляем список задач, только если пользователь всё ещё на этом списке. + if (_state.value.selectedListHref == listHref) { + _state.update { it.copy(loading = false, tasks = tasks) } + } } .onFailure { t -> _state.update {