ux: единый экран ошибки F7ErrorState + retry (Этап 2 п.12)
Новый F7ErrorState: центрированная иконка + сообщение + «Повторить» вместо россыпи красного текста. F7ModuleScreen при ошибке без данных показывает его (+ новый onErrorRetry — retry без хедер-кнопки ↻). Подключено: Контакты/Конференции/Карточки/Поддержка (через F7ModuleScreen), Файлы (onErrorRetry=refreshCurrent), Почта (пустой список + ошибка → F7ErrorState, убран верхний баннер-текст). Retry везде дергает load/refresh.
This commit is contained in:
+59
-2
@@ -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,8 +175,15 @@ 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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -135,6 +191,7 @@ fun F7ModuleScreen(
|
|||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user