ux: единый экран ошибки F7ErrorState + retry (Этап 2 п.12)

Новый F7ErrorState: центрированная иконка + сообщение + «Повторить»
вместо россыпи красного текста. F7ModuleScreen при ошибке без данных
показывает его (+ новый onErrorRetry — retry без хедер-кнопки ↻).
Подключено: Контакты/Конференции/Карточки/Поддержка (через F7ModuleScreen),
Файлы (onErrorRetry=refreshCurrent), Почта (пустой список + ошибка →
F7ErrorState, убран верхний баннер-текст). Retry везде дергает load/refresh.
This commit is contained in:
b-dev-mobile
2026-07-09 14:08:36 +00:00
parent 8f7eaff611
commit a674721353
5 changed files with 75 additions and 11 deletions
@@ -16,8 +16,11 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@@ -43,11 +46,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
/** /**
@@ -69,6 +75,46 @@ fun F7ScreenBackground(modifier: Modifier = Modifier, content: @Composable () ->
} }
} }
/**
* Единый экран ошибки (по центру): иконка + сообщение + кнопка «Повторить».
* Используется везде, где загрузка упала и данных нет — вместо россыпи красного текста.
*/
@Composable
fun F7ErrorState(
message: String,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null,
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(F7Colors.Error.copy(alpha = 0.12f)),
contentAlignment = Alignment.Center,
) {
Text("!", color = F7Colors.Error, fontSize = 30.sp, fontWeight = FontWeight.Bold)
}
Spacer(Modifier.height(12.dp))
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
color = F7Colors.TextSecondary,
textAlign = TextAlign.Center,
)
if (onRetry != null) {
Spacer(Modifier.height(16.dp))
F7SecondaryButton(text = "Повторить", onClick = onRetry)
}
}
}
@Composable @Composable
fun F7ModuleScreen( fun F7ModuleScreen(
title: String? = null, title: String? = null,
@@ -76,6 +122,9 @@ fun F7ModuleScreen(
loading: Boolean = false, loading: Boolean = false,
error: String? = null, error: String? = null,
onRefresh: (() -> Unit)? = null, onRefresh: (() -> Unit)? = null,
// Действие «Повторить» в экране ошибки; по умолчанию — как onRefresh.
// Позволяет дать retry, не показывая хедер-кнопку ↻ (когда onRefresh не задан).
onErrorRetry: (() -> Unit)? = onRefresh,
headerActions: @Composable RowScope.() -> Unit = {}, headerActions: @Composable RowScope.() -> Unit = {},
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
@@ -126,14 +175,22 @@ fun F7ModuleScreen(
CircularProgressIndicator(color = F7Colors.Primary) CircularProgressIndicator(color = F7Colors.Primary)
} }
if (!error.isNullOrBlank()) { if (!error.isNullOrBlank()) {
Text(text = error, color = F7Colors.Error, style = MaterialTheme.typography.bodyMedium) // Ошибка без данных → центрированный экран с «Повторить»
F7ErrorState(
message = error,
onRetry = onErrorRetry,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
)
} else {
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
content = content,
)
} }
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
content = content,
)
} }
} }
@@ -79,6 +79,7 @@ fun ContactsScreen(
modifier = modifier, modifier = modifier,
loading = state.loading && state.contacts.isEmpty(), loading = state.loading && state.contacts.isEmpty(),
error = state.error, error = state.error,
onErrorRetry = { vm.refresh(session, force = true) },
) { ) {
// Поиск + кнопка создания контакта (создание раньше вызывалось из нижней // Поиск + кнопка создания контакта (создание раньше вызывалось из нижней
// панели, теперь — из шапки, панель стала навигационной) // панели, теперь — из шапки, панель стала навигационной)
@@ -172,6 +172,7 @@ fun FilesScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
loading = state.loading && displayItems.isEmpty(), loading = state.loading && displayItems.isEmpty(),
error = state.error, error = state.error,
onErrorRetry = { vm.refreshCurrent(session) },
headerActions = {}, headerActions = {},
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
@@ -48,6 +48,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.auth.AuthSession
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7Colors
import ru.forbion.f7cloud.core.designsystem.F7ErrorState
import ru.forbion.f7cloud.core.designsystem.F7OverlayDismissHandler import ru.forbion.f7cloud.core.designsystem.F7OverlayDismissHandler
import ru.forbion.f7cloud.core.designsystem.f7SwipeFromRightToDismiss import ru.forbion.f7cloud.core.designsystem.f7SwipeFromRightToDismiss
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
@@ -599,9 +600,6 @@ private fun MailInboxScreen(
color = F7Colors.TextPrimary, color = F7Colors.TextPrimary,
) )
} }
if (!error.isNullOrBlank()) {
Text(text = error, color = F7Colors.Error, style = MaterialTheme.typography.bodyMedium)
}
PullToRefreshBox( PullToRefreshBox(
isRefreshing = refreshing && !loading, isRefreshing = refreshing && !loading,
onRefresh = onRefresh, onRefresh = onRefresh,
@@ -643,7 +641,13 @@ private fun MailInboxScreen(
} }
return@Box return@Box
} }
if (messages.isEmpty()) { if (messages.isEmpty() && !error.isNullOrBlank()) {
F7ErrorState(
message = error,
onRetry = onRefresh,
modifier = Modifier.fillMaxSize(),
)
} else if (messages.isEmpty()) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@@ -306,6 +306,7 @@ fun TalkScreen(
modifier = modifier, modifier = modifier,
loading = state.loading && state.rooms.isEmpty(), loading = state.loading && state.rooms.isEmpty(),
error = state.error, error = state.error,
onErrorRetry = { vm.load(session) },
) { ) {
RoomListContent( RoomListContent(
session = session, session = session,