Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e39b2e2fe | |||
| e1e0427066 |
+2
-2
@@ -39,8 +39,8 @@ android {
|
||||
applicationId 'ru.forbion.f7cloud.mobile'
|
||||
minSdk 26
|
||||
targetSdk 36
|
||||
versionCode 140
|
||||
versionName '0.5.132'
|
||||
versionCode 141
|
||||
versionName '0.5.133'
|
||||
missingDimensionStrategy 'default', 'f7'
|
||||
multiDexEnabled true
|
||||
|
||||
|
||||
+22
-19
@@ -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,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package ru.forbion.f7cloud.feature.tasks
|
||||
|
||||
/**
|
||||
* Лёгкий кэш в памяти на время жизни процесса: списки задач и задачи по каждому списку.
|
||||
* VM пересоздаётся при каждом открытии раздела Задач — без кэша это полный PROPFIND+REPORT
|
||||
* заново. Кэш даёт мгновенный показ, сеть обновляет в фоне.
|
||||
*/
|
||||
internal object TasksCache {
|
||||
@Volatile
|
||||
private var lists: List<TaskListItem>? = null
|
||||
private val tasksByList = java.util.concurrent.ConcurrentHashMap<String, List<TaskItem>>()
|
||||
|
||||
fun cachedLists(): List<TaskListItem>? = lists
|
||||
|
||||
fun putLists(value: List<TaskListItem>) {
|
||||
lists = value
|
||||
}
|
||||
|
||||
fun cachedTasks(listHref: String): List<TaskItem>? = tasksByList[listHref]
|
||||
|
||||
fun putTasks(listHref: String, value: List<TaskItem>) {
|
||||
tasksByList[listHref] = value
|
||||
}
|
||||
|
||||
fun invalidateTasks(listHref: String) {
|
||||
tasksByList.remove(listHref)
|
||||
}
|
||||
}
|
||||
@@ -74,10 +74,27 @@ class TasksViewModel(
|
||||
val state: StateFlow<TasksUiState> = _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 {
|
||||
|
||||
Reference in New Issue
Block a user