Improve register and fix about app

Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
2026-07-30 20:57:16 +03:00
Unverified
parent 7916e16820
commit 4f9011e656
8 changed files with 285 additions and 179 deletions
@@ -358,7 +358,6 @@ fun App(
val widthSizeClass = currentWindowAdaptiveInfo().widthSizeClass
val isDesktopListDetail = widthSizeClass != WindowWidthSizeClass.COMPACT
var pendingMainTab by remember { mutableIntStateOf(MAIN_PAGE_CHATS) }
var pendingPreAuthSettingsDetailRoute by remember { mutableStateOf<String?>(null) }
// Handle startup/deep-link navigation targets (profile links)
LaunchedEffect(
@@ -504,21 +503,19 @@ fun App(
LaunchedEffect(Unit) {
DesktopMenuCommands.commands.collect { command ->
if (command != DesktopMenuCommand.OpenAbout) return@collect
pendingMainTab = MAIN_PAGE_SETTINGS
if (isDesktopListDetail) {
// Logged-out: same root About as Welcome/auth overflow — never mount
// the empty chat listdetail shell.
if (ApiClient.token.isNullOrBlank()) {
// Pre-auth: the settings detail NavHost isn't mounted until the root
// shell is visible, so we defer settings navigation until `currentRoute`
// becomes `chat`.
pendingPreAuthSettingsDetailRoute = SettingsRoutes.About
navController.navigate("chat") {
navController.navigate(SettingsRoutes.About) {
launchSingleTop = true
}
} else {
return@collect
}
pendingMainTab = MAIN_PAGE_SETTINGS
if (isDesktopListDetail) {
settingsDetailNavController.navigateReplacingMainDetail(
route = SettingsRoutes.About,
)
}
} else {
navController.navigateReplacingMainDetail(
route = SettingsRoutes.About,
@@ -527,14 +524,6 @@ fun App(
}
}
LaunchedEffect(currentRoute, pendingPreAuthSettingsDetailRoute) {
val route = pendingPreAuthSettingsDetailRoute ?: return@LaunchedEffect
if (!isDesktopListDetail) return@LaunchedEffect
if (currentRoute != "chat") return@LaunchedEffect
settingsDetailNavController.navigateReplacingMainDetail(route = route)
pendingPreAuthSettingsDetailRoute = null
}
ScreenSurface {
Box(Modifier.fillMaxSize()) {
@Composable
@@ -4,36 +4,25 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -41,20 +30,14 @@ import coil3.compose.AsyncImage
import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res
import ru.fromchat.api.ApiClient
import ru.fromchat.about
import ru.fromchat.auth_get_started
import ru.fromchat.auth_welcome_tagline
import ru.fromchat.auth_welcome_title
import ru.fromchat.logs_title
import ru.fromchat.more
import ru.fromchat.ui.auth.PreAuthOverflowMenu
import ru.fromchat.ui.components.ActionButton
import ru.fromchat.ui.components.Text
import ru.fromchat.ui.main.settings.SettingsRoutes
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
private val AUTH_CONTENT_MAX_WIDTH = 600.dp
private val AUTH_CONTENT_MAX_HEIGHT = 800.dp
@Composable
fun WelcomeScreen(
onGetStarted: () -> Unit,
@@ -67,70 +50,36 @@ fun WelcomeScreen(
}
Scaffold(
containerColor = Color.Transparent,
contentWindowInsets = WindowInsets.safeDrawing.union(WindowInsets.extraStatusBars),
) { innerPadding ->
val navController = LocalNavController.current
var menuExpanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
Box(modifier = Modifier.align(Alignment.TopEnd)) {
IconButton(onClick = { menuExpanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(Res.string.more),
)
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
) {
DropdownMenuItem(
text = { Text(stringResource(Res.string.about)) },
leadingIcon = {
Icon(Icons.Default.Info, contentDescription = null)
},
onClick = {
menuExpanded = false
navController.navigate(SettingsRoutes.About)
},
)
DropdownMenuItem(
text = { Text(stringResource(Res.string.logs_title)) },
leadingIcon = {
Icon(Icons.Outlined.BugReport, contentDescription = null)
},
onClick = {
menuExpanded = false
navController.navigate(SettingsRoutes.Logs)
},
)
}
PreAuthOverflowMenu()
}
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val constrainWide = maxWidth > AUTH_CONTENT_MAX_WIDTH
Box(
modifier =
if (constrainWide) {
BoxWithConstraints(Modifier.fillMaxSize()) {
val wide = maxWidth > 600.dp
Column(
modifier = Modifier
.align(if (wide) Alignment.Center else Alignment.TopCenter)
.then(
if (wide) {
Modifier
.align(Alignment.Center)
.widthIn(max = AUTH_CONTENT_MAX_WIDTH)
.heightIn(max = AUTH_CONTENT_MAX_HEIGHT)
.widthIn(max = 600.dp)
.heightIn(max = 800.dp)
} else {
Modifier.fillMaxSize()
},
) {
Column(
modifier = Modifier
.fillMaxSize()
)
.navigationBarsPadding()
.padding(horizontal = SettingsStepHorizontalPadding)
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.Center,
verticalArrangement = if (wide) Arrangement.Center else Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
AsyncImage(
@@ -147,6 +96,7 @@ fun WelcomeScreen(
Text(
text = stringResource(Res.string.auth_welcome_title),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
)
@@ -169,4 +119,3 @@ fun WelcomeScreen(
}
}
}
}
@@ -0,0 +1,106 @@
package ru.fromchat.ui.auth
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res
import ru.fromchat.about
import ru.fromchat.logs_title
import ru.fromchat.more
import ru.fromchat.ui.LocalNavController
import ru.fromchat.ui.components.Text
import ru.fromchat.ui.main.settings.SettingsRoutes
/** Margin between the window edge and the wide [AuthContentFrame] panel. */
val AuthWidePanelOuterPadding = 32.dp
/**
* Centers auth content on wide layouts (max 600×800.dp, [AuthWidePanelOuterPadding] margin).
* Narrow layouts fill the window ([widePanel] = false).
*/
@Composable
fun AuthContentFrame(
modifier: Modifier = Modifier,
content: @Composable BoxScope.(widePanel: Boolean) -> Unit,
) {
BoxWithConstraints(modifier.fillMaxSize()) {
val widePanel = maxWidth > 600.dp
if (widePanel) {
Box(
Modifier
.align(Alignment.Center)
.padding(AuthWidePanelOuterPadding)
.widthIn(max = 600.dp)
.heightIn(max = 800.dp),
) {
content(true)
}
} else {
Box(Modifier.fillMaxSize()) {
content(false)
}
}
}
}
/** Overflow menu shared by welcome and auth steps (About, Logs). */
@Composable
fun PreAuthOverflowMenu() {
val navController = LocalNavController.current
var menuExpanded by remember { mutableStateOf(false) }
Box {
IconButton(onClick = { menuExpanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(Res.string.more),
)
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
) {
DropdownMenuItem(
text = { Text(stringResource(Res.string.about)) },
leadingIcon = {
Icon(Icons.Default.Info, contentDescription = null)
},
onClick = {
menuExpanded = false
navController.navigate(SettingsRoutes.About)
},
)
DropdownMenuItem(
text = { Text(stringResource(Res.string.logs_title)) },
leadingIcon = {
Icon(Icons.Outlined.BugReport, contentDescription = null)
},
onClick = {
menuExpanded = false
navController.navigate(SettingsRoutes.Logs)
},
)
}
}
}
@@ -1,13 +1,15 @@
package ru.fromchat.ui.auth
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -18,7 +20,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
@@ -53,6 +54,7 @@ import ru.fromchat.ui.components.Text
import ru.fromchat.ui.components.TextCta
import ru.fromchat.ui.components.rememberExpressiveStepFlow
import ru.fromchat.ui.components.showLoggedSnackbar
import ru.fromchat.ui.extraStatusBars
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
import kotlin.time.Duration.Companion.milliseconds
@@ -85,9 +87,6 @@ internal sealed interface RegisterResult {
private const val AUTH_SERVER_PROBE_TIMEOUT_MS = 5_000L
private val AUTH_CONTENT_MAX_WIDTH = 600.dp
private val AUTH_CONTENT_MAX_HEIGHT = 800.dp
internal suspend fun probeCurrentServer() = runCatching {
withTimeout(AUTH_SERVER_PROBE_TIMEOUT_MS.milliseconds) {
probeServer(Settings.readServerConfig()) is ServerProbeResult.Supported
@@ -443,19 +442,13 @@ fun AuthScreen(
}
val yandexStep = yandexParams
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val constrainWide = maxWidth > AUTH_CONTENT_MAX_WIDTH
// Clear title bar / system bars outside the rounded panel (same as WelcomeScreen).
Box(
modifier =
if (constrainWide) {
Modifier
.align(Alignment.Center)
.widthIn(max = AUTH_CONTENT_MAX_WIDTH)
.heightIn(max = AUTH_CONTENT_MAX_HEIGHT)
} else {
Modifier.fillMaxSize()
},
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing.union(WindowInsets.extraStatusBars)),
) {
AuthContentFrame { _ ->
ExpressiveStepFlowScaffold(
flowState = flowState,
pages = listOf(
@@ -566,6 +559,9 @@ fun AuthScreen(
),
snackbarHostState = snackbarHostState,
onBackAtFirstPage = wrappedBackToWelcome,
trailingTopBarActions = { PreAuthOverflowMenu() },
contentBackground = MaterialTheme.colorScheme.background,
topBarWindowInsets = WindowInsets(0, 0, 0, 0),
)
}
}
@@ -343,6 +343,8 @@ fun ExpressiveStepFlowScaffold(
onBackAtFirstPage: () -> Unit,
trailingTopBarActions: @Composable (() -> Unit)? = null,
hazeScaffold: Boolean = true,
contentBackground: Color = MaterialTheme.colorScheme.background,
topBarWindowInsets: WindowInsets = WindowInsets.extraStatusBars,
) {
val pagerState = flowState.pagerState
val scope = flowState.scope
@@ -551,7 +553,7 @@ fun ExpressiveStepFlowScaffold(
fun HazeTopBar(hazeState: HazeState) {
val topBarHazeStyle = HazeMaterials.thin()
TopAppBar(
windowInsets = WindowInsets.extraStatusBars,
windowInsets = topBarWindowInsets,
title = {},
navigationIcon = {
IconButton(onClick = navigateBack) {
@@ -585,7 +587,7 @@ fun ExpressiveStepFlowScaffold(
Row(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.extraStatusBars)
.windowInsetsPadding(topBarWindowInsets)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -648,7 +650,7 @@ fun ExpressiveStepFlowScaffold(
state = listState,
modifier = Modifier
.fillMaxSize()
.background(scheme.background)
.background(contentBackground)
.hazeSource(hazeState)
.onGloballyPositioned {
listViewportBounds = it.boundsInWindow()
@@ -1,6 +1,8 @@
package ru.fromchat.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
@@ -11,6 +13,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeBlurStyle
@@ -25,6 +28,7 @@ fun HazeBottomBar(
hazeState: HazeState,
modifier: Modifier = Modifier,
hazeStyle: HazeBlurStyle? = null,
baseColor: Color? = null,
content: @Composable () -> Unit,
) {
val resolvedStyle = hazeStyle ?: HazeMaterials.thin()
@@ -32,6 +36,10 @@ fun HazeBottomBar(
Modifier.hazeEffect(state = hazeState) {
blurEffect {
style = resolvedStyle
progressive = HazeProgressive.verticalGradient(
startIntensity = 0f,
endIntensity = 1f,
)
}
}
} else {
@@ -49,9 +57,21 @@ fun HazeBottomBar(
modifier = Modifier
.windowInsetsPadding(WindowInsets.ime)
.fillMaxWidth()
.then(effectModifier)
.then(modifier),
) {
Box(Modifier.fillMaxWidth()) {
if (baseColor != null) {
Box(
modifier = Modifier
.matchParentSize()
.background(baseColor),
)
}
Box(
modifier = Modifier
.matchParentSize()
.then(effectModifier),
)
Column(
modifier = Modifier
.fillMaxWidth()
@@ -63,6 +83,7 @@ fun HazeBottomBar(
}
}
}
}
@Composable
fun HazeActionButton(
@@ -71,12 +92,18 @@ fun HazeActionButton(
modifier: Modifier = Modifier,
innerModifier: Modifier = Modifier,
hazeStyle: HazeBlurStyle? = null,
baseColor: Color? = null,
enabled: Boolean = true,
loading: Boolean = false,
interactionSource: MutableInteractionSource? = null,
content: @Composable (RowScope.() -> Unit)
) {
HazeBottomBar(hazeState = hazeState, modifier = modifier, hazeStyle = hazeStyle) {
HazeBottomBar(
hazeState = hazeState,
modifier = modifier,
hazeStyle = hazeStyle,
baseColor = baseColor,
) {
ActionButton(
onClick = onClick,
modifier = innerModifier,
@@ -2,6 +2,7 @@ package ru.fromchat.ui.main.settings
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.animate
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -32,6 +33,8 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res
import ru.fromchat.back
import ru.fromchat.ui.extraStatusBars
import ru.fromchat.ui.main.LocalConversationListDetailActive
import ru.fromchat.ui.main.detailPaneShowBackButton
/** Idle gap after mouse-wheel / trackpad deltas before snapping the collapsing bar. */
@@ -85,11 +88,19 @@ fun SettingsDetailTopBar(
// `surfaceContainerLowest` — that token reads as a separate pane fill in
// two-pane against the listdetail shell.
val paneColor = MaterialTheme.colorScheme.background
// Listdetail shell already pads/consumes extraStatusBars; full-screen settings
// (e.g. logged-out About) need the desktop title-bar inset explicitly.
val topBarWindowInsets = if (LocalConversationListDetailActive.current) {
WindowInsets(0, 0, 0, 0)
} else {
WindowInsets.extraStatusBars
}
if (settingsDetailUseCollapsingTopBar()) {
MediumTopAppBar(
title = title,
navigationIcon = navigationIcon,
scrollBehavior = scrollBehavior,
windowInsets = topBarWindowInsets,
// Unscrolled: pane. Collapsed: default scrolledContainerColor (elevation).
colors = TopAppBarDefaults.topAppBarColors(
containerColor = paneColor,
@@ -100,6 +111,7 @@ fun SettingsDetailTopBar(
TopAppBar(
title = title,
navigationIcon = navigationIcon,
windowInsets = topBarWindowInsets,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = paneColor,
scrolledContainerColor = paneColor,
@@ -19,7 +19,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions
@@ -108,6 +111,8 @@ import ru.fromchat.server_config_unsupported_no_instance_id
import ru.fromchat.server_ip_hint
import ru.fromchat.server_ip_label
import ru.fromchat.ui.LocalNavController
import ru.fromchat.ui.auth.AuthContentFrame
import ru.fromchat.ui.extraStatusBars
import ru.fromchat.ui.components.CtaShape
import ru.fromchat.ui.components.DisabledBringIntoViewSpec
import ru.fromchat.ui.components.ExpressiveIconFrame
@@ -231,7 +236,12 @@ fun ServerConfigScreen() {
disabledContainerColor = Color.Transparent,
)
Box(Modifier.fillMaxSize()) {
val preAuth = ApiClient.token.isNullOrBlank()
@Composable
fun ServerConfigContent() {
val listBackground = scheme.background
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.navigationBars,
@@ -346,7 +356,7 @@ fun ServerConfigScreen() {
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
scrolledContainerColor = Color.Transparent,
),
modifier = Modifier.hazeEffect(state = actionHazeState) {
blurEffect {
@@ -356,7 +366,7 @@ fun ServerConfigScreen() {
endIntensity = 0f,
)
}
}
},
)
}
) { innerPadding ->
@@ -379,7 +389,7 @@ fun ServerConfigScreen() {
modifier = Modifier
.fillMaxSize()
.consumeWindowInsets(innerPadding)
.background(MaterialTheme.colorScheme.background)
.background(listBackground)
.hazeSource(actionHazeState)
.onGloballyPositioned { listViewportBounds = it.boundsInWindow() },
contentPadding = PaddingValues(bottom = bottomInsetPadding),
@@ -422,6 +432,7 @@ fun ServerConfigScreen() {
Text(
text = stringResource(Res.string.server_config_title),
style = MaterialTheme.typography.headlineMedium,
color = scheme.onSurface,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
)
@@ -568,4 +579,18 @@ fun ServerConfigScreen() {
}
}
}
if (preAuth) {
Box(
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing.union(WindowInsets.extraStatusBars)),
) {
AuthContentFrame { _ ->
ServerConfigContent()
}
}
} else {
ServerConfigContent()
}
}