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 widthSizeClass = currentWindowAdaptiveInfo().widthSizeClass
val isDesktopListDetail = widthSizeClass != WindowWidthSizeClass.COMPACT val isDesktopListDetail = widthSizeClass != WindowWidthSizeClass.COMPACT
var pendingMainTab by remember { mutableIntStateOf(MAIN_PAGE_CHATS) } var pendingMainTab by remember { mutableIntStateOf(MAIN_PAGE_CHATS) }
var pendingPreAuthSettingsDetailRoute by remember { mutableStateOf<String?>(null) }
// Handle startup/deep-link navigation targets (profile links) // Handle startup/deep-link navigation targets (profile links)
LaunchedEffect( LaunchedEffect(
@@ -504,21 +503,19 @@ fun App(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
DesktopMenuCommands.commands.collect { command -> DesktopMenuCommands.commands.collect { command ->
if (command != DesktopMenuCommand.OpenAbout) return@collect if (command != DesktopMenuCommand.OpenAbout) return@collect
// Logged-out: same root About as Welcome/auth overflow — never mount
// the empty chat listdetail shell.
if (ApiClient.token.isNullOrBlank()) {
navController.navigate(SettingsRoutes.About) {
launchSingleTop = true
}
return@collect
}
pendingMainTab = MAIN_PAGE_SETTINGS pendingMainTab = MAIN_PAGE_SETTINGS
if (isDesktopListDetail) { if (isDesktopListDetail) {
if (ApiClient.token.isNullOrBlank()) { settingsDetailNavController.navigateReplacingMainDetail(
// Pre-auth: the settings detail NavHost isn't mounted until the root route = SettingsRoutes.About,
// shell is visible, so we defer settings navigation until `currentRoute` )
// becomes `chat`.
pendingPreAuthSettingsDetailRoute = SettingsRoutes.About
navController.navigate("chat") {
launchSingleTop = true
}
} else {
settingsDetailNavController.navigateReplacingMainDetail(
route = SettingsRoutes.About,
)
}
} else { } else {
navController.navigateReplacingMainDetail( navController.navigateReplacingMainDetail(
route = SettingsRoutes.About, 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 { ScreenSurface {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
@Composable @Composable
@@ -4,36 +4,25 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.widthIn 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.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -41,20 +30,14 @@ import coil3.compose.AsyncImage
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res import ru.fromchat.Res
import ru.fromchat.api.ApiClient import ru.fromchat.api.ApiClient
import ru.fromchat.about
import ru.fromchat.auth_get_started import ru.fromchat.auth_get_started
import ru.fromchat.auth_welcome_tagline import ru.fromchat.auth_welcome_tagline
import ru.fromchat.auth_welcome_title import ru.fromchat.auth_welcome_title
import ru.fromchat.logs_title import ru.fromchat.ui.auth.PreAuthOverflowMenu
import ru.fromchat.more
import ru.fromchat.ui.components.ActionButton import ru.fromchat.ui.components.ActionButton
import ru.fromchat.ui.components.Text import ru.fromchat.ui.components.Text
import ru.fromchat.ui.main.settings.SettingsRoutes
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
private val AUTH_CONTENT_MAX_WIDTH = 600.dp
private val AUTH_CONTENT_MAX_HEIGHT = 800.dp
@Composable @Composable
fun WelcomeScreen( fun WelcomeScreen(
onGetStarted: () -> Unit, onGetStarted: () -> Unit,
@@ -67,103 +50,69 @@ fun WelcomeScreen(
} }
Scaffold( Scaffold(
containerColor = Color.Transparent,
contentWindowInsets = WindowInsets.safeDrawing.union(WindowInsets.extraStatusBars), contentWindowInsets = WindowInsets.safeDrawing.union(WindowInsets.extraStatusBars),
) { innerPadding -> ) { innerPadding ->
val navController = LocalNavController.current
var menuExpanded by remember { mutableStateOf(false) }
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding), .padding(innerPadding),
) { ) {
Box(modifier = Modifier.align(Alignment.TopEnd)) { Box(modifier = Modifier.align(Alignment.TopEnd)) {
IconButton(onClick = { menuExpanded = true }) { PreAuthOverflowMenu()
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)
},
)
}
} }
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { BoxWithConstraints(Modifier.fillMaxSize()) {
val constrainWide = maxWidth > AUTH_CONTENT_MAX_WIDTH val wide = maxWidth > 600.dp
Box( Column(
modifier = modifier = Modifier
if (constrainWide) { .align(if (wide) Alignment.Center else Alignment.TopCenter)
Modifier .then(
.align(Alignment.Center) if (wide) {
.widthIn(max = AUTH_CONTENT_MAX_WIDTH) Modifier
.heightIn(max = AUTH_CONTENT_MAX_HEIGHT) .widthIn(max = 600.dp)
} else { .heightIn(max = 800.dp)
Modifier.fillMaxSize() } else {
}, Modifier.fillMaxSize()
},
)
.navigationBarsPadding()
.padding(horizontal = SettingsStepHorizontalPadding)
.padding(bottom = 16.dp),
verticalArrangement = if (wide) Arrangement.Center else Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Column( AsyncImage(
model = Res.getUri("drawable/logo_square.png"),
contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxSize() .size(112.dp)
.navigationBarsPadding() .clip(MaterialTheme.shapes.extraLarge),
.padding(horizontal = SettingsStepHorizontalPadding) contentScale = ContentScale.Fit,
.padding(bottom = 16.dp), )
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
AsyncImage(
model = Res.getUri("drawable/logo_square.png"),
contentDescription = null,
modifier = Modifier
.size(112.dp)
.clip(MaterialTheme.shapes.extraLarge),
contentScale = ContentScale.Fit,
)
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Text( Text(
text = stringResource(Res.string.auth_welcome_title), text = stringResource(Res.string.auth_welcome_title),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface,
) textAlign = TextAlign.Center,
)
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Text( Text(
text = stringResource(Res.string.auth_welcome_tagline), text = stringResource(Res.string.auth_welcome_tagline),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(40.dp))
ActionButton(onClick = onGetStarted) { ActionButton(onClick = onGetStarted) {
Text(stringResource(Res.string.auth_get_started)) Text(stringResource(Res.string.auth_get_started))
}
} }
} }
} }
@@ -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 package ru.fromchat.ui.auth
import androidx.compose.foundation.layout.Box 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.fillMaxSize
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.filled.Storage import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -18,7 +20,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret 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.TextCta
import ru.fromchat.ui.components.rememberExpressiveStepFlow import ru.fromchat.ui.components.rememberExpressiveStepFlow
import ru.fromchat.ui.components.showLoggedSnackbar import ru.fromchat.ui.components.showLoggedSnackbar
import ru.fromchat.ui.extraStatusBars
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
import kotlin.time.Duration.Companion.milliseconds 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 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 { internal suspend fun probeCurrentServer() = runCatching {
withTimeout(AUTH_SERVER_PROBE_TIMEOUT_MS.milliseconds) { withTimeout(AUTH_SERVER_PROBE_TIMEOUT_MS.milliseconds) {
probeServer(Settings.readServerConfig()) is ServerProbeResult.Supported probeServer(Settings.readServerConfig()) is ServerProbeResult.Supported
@@ -443,19 +442,13 @@ fun AuthScreen(
} }
val yandexStep = yandexParams val yandexStep = yandexParams
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { // Clear title bar / system bars outside the rounded panel (same as WelcomeScreen).
val constrainWide = maxWidth > AUTH_CONTENT_MAX_WIDTH Box(
Box( Modifier
modifier = .fillMaxSize()
if (constrainWide) { .windowInsetsPadding(WindowInsets.safeDrawing.union(WindowInsets.extraStatusBars)),
Modifier ) {
.align(Alignment.Center) AuthContentFrame { _ ->
.widthIn(max = AUTH_CONTENT_MAX_WIDTH)
.heightIn(max = AUTH_CONTENT_MAX_HEIGHT)
} else {
Modifier.fillMaxSize()
},
) {
ExpressiveStepFlowScaffold( ExpressiveStepFlowScaffold(
flowState = flowState, flowState = flowState,
pages = listOf( pages = listOf(
@@ -566,6 +559,9 @@ fun AuthScreen(
), ),
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
onBackAtFirstPage = wrappedBackToWelcome, onBackAtFirstPage = wrappedBackToWelcome,
trailingTopBarActions = { PreAuthOverflowMenu() },
contentBackground = MaterialTheme.colorScheme.background,
topBarWindowInsets = WindowInsets(0, 0, 0, 0),
) )
} }
} }
@@ -343,6 +343,8 @@ fun ExpressiveStepFlowScaffold(
onBackAtFirstPage: () -> Unit, onBackAtFirstPage: () -> Unit,
trailingTopBarActions: @Composable (() -> Unit)? = null, trailingTopBarActions: @Composable (() -> Unit)? = null,
hazeScaffold: Boolean = true, hazeScaffold: Boolean = true,
contentBackground: Color = MaterialTheme.colorScheme.background,
topBarWindowInsets: WindowInsets = WindowInsets.extraStatusBars,
) { ) {
val pagerState = flowState.pagerState val pagerState = flowState.pagerState
val scope = flowState.scope val scope = flowState.scope
@@ -551,7 +553,7 @@ fun ExpressiveStepFlowScaffold(
fun HazeTopBar(hazeState: HazeState) { fun HazeTopBar(hazeState: HazeState) {
val topBarHazeStyle = HazeMaterials.thin() val topBarHazeStyle = HazeMaterials.thin()
TopAppBar( TopAppBar(
windowInsets = WindowInsets.extraStatusBars, windowInsets = topBarWindowInsets,
title = {}, title = {},
navigationIcon = { navigationIcon = {
IconButton(onClick = navigateBack) { IconButton(onClick = navigateBack) {
@@ -585,7 +587,7 @@ fun ExpressiveStepFlowScaffold(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.windowInsetsPadding(WindowInsets.extraStatusBars) .windowInsetsPadding(topBarWindowInsets)
.padding(horizontal = 4.dp), .padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@@ -648,7 +650,7 @@ fun ExpressiveStepFlowScaffold(
state = listState, state = listState,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(scheme.background) .background(contentBackground)
.hazeSource(hazeState) .hazeSource(hazeState)
.onGloballyPositioned { .onGloballyPositioned {
listViewportBounds = it.boundsInWindow() listViewportBounds = it.boundsInWindow()
@@ -1,6 +1,8 @@
package ru.fromchat.ui.components package ru.fromchat.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets 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.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeBlurStyle import dev.chrisbanes.haze.blur.HazeBlurStyle
@@ -25,6 +28,7 @@ fun HazeBottomBar(
hazeState: HazeState, hazeState: HazeState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
hazeStyle: HazeBlurStyle? = null, hazeStyle: HazeBlurStyle? = null,
baseColor: Color? = null,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val resolvedStyle = hazeStyle ?: HazeMaterials.thin() val resolvedStyle = hazeStyle ?: HazeMaterials.thin()
@@ -32,6 +36,10 @@ fun HazeBottomBar(
Modifier.hazeEffect(state = hazeState) { Modifier.hazeEffect(state = hazeState) {
blurEffect { blurEffect {
style = resolvedStyle style = resolvedStyle
progressive = HazeProgressive.verticalGradient(
startIntensity = 0f,
endIntensity = 1f,
)
} }
} }
} else { } else {
@@ -49,17 +57,30 @@ fun HazeBottomBar(
modifier = Modifier modifier = Modifier
.windowInsetsPadding(WindowInsets.ime) .windowInsetsPadding(WindowInsets.ime)
.fillMaxWidth() .fillMaxWidth()
.then(effectModifier)
.then(modifier), .then(modifier),
) { ) {
Column( Box(Modifier.fillMaxWidth()) {
modifier = Modifier if (baseColor != null) {
.fillMaxWidth() Box(
.navigationBarsPadding() modifier = Modifier
.padding(horizontal = SettingsStepHorizontalPadding) .matchParentSize()
.padding(top = 0.dp, bottom = 16.dp), .background(baseColor),
) { )
content() }
Box(
modifier = Modifier
.matchParentSize()
.then(effectModifier),
)
Column(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(horizontal = SettingsStepHorizontalPadding)
.padding(top = 0.dp, bottom = 16.dp),
) {
content()
}
} }
} }
} }
@@ -71,12 +92,18 @@ fun HazeActionButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
innerModifier: Modifier = Modifier, innerModifier: Modifier = Modifier,
hazeStyle: HazeBlurStyle? = null, hazeStyle: HazeBlurStyle? = null,
baseColor: Color? = null,
enabled: Boolean = true, enabled: Boolean = true,
loading: Boolean = false, loading: Boolean = false,
interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null,
content: @Composable (RowScope.() -> Unit) content: @Composable (RowScope.() -> Unit)
) { ) {
HazeBottomBar(hazeState = hazeState, modifier = modifier, hazeStyle = hazeStyle) { HazeBottomBar(
hazeState = hazeState,
modifier = modifier,
hazeStyle = hazeStyle,
baseColor = baseColor,
) {
ActionButton( ActionButton(
onClick = onClick, onClick = onClick,
modifier = innerModifier, modifier = innerModifier,
@@ -2,6 +2,7 @@ package ru.fromchat.ui.main.settings
import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.animate import androidx.compose.animation.core.animate
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -32,6 +33,8 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res import ru.fromchat.Res
import ru.fromchat.back import ru.fromchat.back
import ru.fromchat.ui.extraStatusBars
import ru.fromchat.ui.main.LocalConversationListDetailActive
import ru.fromchat.ui.main.detailPaneShowBackButton import ru.fromchat.ui.main.detailPaneShowBackButton
/** Idle gap after mouse-wheel / trackpad deltas before snapping the collapsing bar. */ /** 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 // `surfaceContainerLowest` — that token reads as a separate pane fill in
// two-pane against the listdetail shell. // two-pane against the listdetail shell.
val paneColor = MaterialTheme.colorScheme.background 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()) { if (settingsDetailUseCollapsingTopBar()) {
MediumTopAppBar( MediumTopAppBar(
title = title, title = title,
navigationIcon = navigationIcon, navigationIcon = navigationIcon,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
windowInsets = topBarWindowInsets,
// Unscrolled: pane. Collapsed: default scrolledContainerColor (elevation). // Unscrolled: pane. Collapsed: default scrolledContainerColor (elevation).
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = paneColor, containerColor = paneColor,
@@ -100,6 +111,7 @@ fun SettingsDetailTopBar(
TopAppBar( TopAppBar(
title = title, title = title,
navigationIcon = navigationIcon, navigationIcon = navigationIcon,
windowInsets = topBarWindowInsets,
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = paneColor, containerColor = paneColor,
scrolledContainerColor = paneColor, scrolledContainerColor = paneColor,
@@ -19,7 +19,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.statusBars 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.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions 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_hint
import ru.fromchat.server_ip_label import ru.fromchat.server_ip_label
import ru.fromchat.ui.LocalNavController 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.CtaShape
import ru.fromchat.ui.components.DisabledBringIntoViewSpec import ru.fromchat.ui.components.DisabledBringIntoViewSpec
import ru.fromchat.ui.components.ExpressiveIconFrame import ru.fromchat.ui.components.ExpressiveIconFrame
@@ -231,7 +236,12 @@ fun ServerConfigScreen() {
disabledContainerColor = Color.Transparent, disabledContainerColor = Color.Transparent,
) )
Box(Modifier.fillMaxSize()) { val preAuth = ApiClient.token.isNullOrBlank()
@Composable
fun ServerConfigContent() {
val listBackground = scheme.background
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.navigationBars, contentWindowInsets = WindowInsets.navigationBars,
@@ -333,31 +343,31 @@ fun ServerConfigScreen() {
topBar = { topBar = {
val topBarHazeStyle = HazeMaterials.thin() val topBarHazeStyle = HazeMaterials.thin()
TopAppBar( TopAppBar(
title = {}, title = {},
navigationIcon = { navigationIcon = {
if (settingsDetailShowBackButton()) { if (settingsDetailShowBackButton()) {
IconButton(onClick = navController::navigateUp) { IconButton(onClick = navController::navigateUp) {
Icon( Icon(
Icons.AutoMirrored.Filled.ArrowBack, Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(Res.string.back) contentDescription = stringResource(Res.string.back)
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
),
modifier = Modifier.hazeEffect(state = actionHazeState) {
blurEffect {
style = topBarHazeStyle
progressive = HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 0f,
) )
} }
} },
}, )
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
),
modifier = Modifier.hazeEffect(state = actionHazeState) {
blurEffect {
style = topBarHazeStyle
progressive = HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 0f,
)
}
}
)
} }
) { innerPadding -> ) { innerPadding ->
val floatingHeaderClearance = WindowInsets.statusBars.getTop(density).toDp(density) + 68.dp val floatingHeaderClearance = WindowInsets.statusBars.getTop(density).toDp(density) + 68.dp
@@ -379,7 +389,7 @@ fun ServerConfigScreen() {
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.consumeWindowInsets(innerPadding) .consumeWindowInsets(innerPadding)
.background(MaterialTheme.colorScheme.background) .background(listBackground)
.hazeSource(actionHazeState) .hazeSource(actionHazeState)
.onGloballyPositioned { listViewportBounds = it.boundsInWindow() }, .onGloballyPositioned { listViewportBounds = it.boundsInWindow() },
contentPadding = PaddingValues(bottom = bottomInsetPadding), contentPadding = PaddingValues(bottom = bottomInsetPadding),
@@ -422,6 +432,7 @@ fun ServerConfigScreen() {
Text( Text(
text = stringResource(Res.string.server_config_title), text = stringResource(Res.string.server_config_title),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
color = scheme.onSurface,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
@@ -568,4 +579,18 @@ fun ServerConfigScreen() {
} }
} }
} }
if (preAuth) {
Box(
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing.union(WindowInsets.extraStatusBars)),
) {
AuthContentFrame { _ ->
ServerConfigContent()
}
}
} else {
ServerConfigContent()
}
} }