From e1e042706618ead6924bc98a907d9a873fd92a0a Mon Sep 17 00:00:00 2001 From: b-dev-mobile Date: Fri, 10 Jul 2026 07:52:01 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B8=D0=BA=D0=BE=D0=BD=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=88=D1=82=D0=BE=D1=80=D0=BA=D0=B8=20=D0=B2=20=D0=B5=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B9=20=D0=BA=D1=80=D1=83=D0=B3=D0=BB=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B0=D0=BC=D0=BA=D0=B5;=20=D0=BA=D1=8D=D1=88?= =?UTF-8?q?=20=D0=97=D0=B0=D0=B4=D0=B0=D1=87=20=D0=B4=D0=BB=D1=8F=20=D0=B1?= =?UTF-8?q?=D1=8B=D1=81=D1=82=D1=80=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Шторка: КАЖДАЯ иконка теперь в белом круге с рамкой и мягкой тенью, все одного размера (глиф внутри 56% рамки) — как на мобильном сайте (было: плоские глифы без рамки, разного размера). - Задачи грузятся быстрее: TasksCache (в памяти на время процесса) — списки и задачи по спискам показываются мгновенно из кэша при повторном открытии раздела/списка, сеть обновляет в фоне. Гонки учтены: результат применяется, только если пользователь ещё на этом списке. Co-Authored-By: Claude Fable 5 --- .../core/designsystem/F7AppMenuSheet.kt | 41 ++++++++++--------- .../f7cloud/feature/tasks/TasksCache.kt | 28 +++++++++++++ .../f7cloud/feature/tasks/TasksViewModel.kt | 25 ++++++++++- 3 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 feature/tasks/src/main/java/ru/forbion/f7cloud/feature/tasks/TasksCache.kt 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 {