diff --git a/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopWindowPrefs.kt b/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopWindowPrefs.kt new file mode 100644 index 0000000..161f2bb --- /dev/null +++ b/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopWindowPrefs.kt @@ -0,0 +1,35 @@ +package ru.fromchat.desktop + +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import java.util.prefs.Preferences + +/** + * Desktop main-window size prefs. + * + * Stored in the same JVM prefs node as PlatformSettings (`ru.fromchat.settings`), + * keys `desktop_window_width` / `desktop_window_height` (float dp). + */ +internal object DesktopWindowPrefs { + private const val WIDTH_KEY = "desktop_window_width" + private const val HEIGHT_KEY = "desktop_window_height" + + private val prefs: Preferences = + Preferences.userRoot().node("ru.fromchat.settings") + + fun loadSize(): DpSize { + val width = prefs.getFloat(WIDTH_KEY, 0f) + val height = prefs.getFloat(HEIGHT_KEY, 0f) + return if (width <= 0f || height <= 0f) { + DpSize(800.dp, 600.dp) + } else { + DpSize(width.dp, height.dp) + } + } + + fun saveSize(size: DpSize) { + prefs.putFloat(WIDTH_KEY, size.width.value) + prefs.putFloat(HEIGHT_KEY, size.height.value) + runCatching { prefs.flush() } + } +} diff --git a/app/desktop/src/main/kotlin/ru/fromchat/desktop/Main.kt b/app/desktop/src/main/kotlin/ru/fromchat/desktop/Main.kt index ca2286c..988d292 100644 --- a/app/desktop/src/main/kotlin/ru/fromchat/desktop/Main.kt +++ b/app/desktop/src/main/kotlin/ru/fromchat/desktop/Main.kt @@ -52,6 +52,7 @@ import kotlin.concurrent.thread import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.jetbrains.skia.Image @@ -232,7 +233,7 @@ fun main(args: Array) { UtilsLibrary.init() DesktopApplicationBootstrap.launchOnApplicationStart() - val windowState = rememberWindowState() + val windowState = rememberWindowState(size = remember { DesktopWindowPrefs.loadSize() }) val aboutWindowState = rememberWindowState( position = WindowPosition.Aligned(Alignment.Center), size = DpSize(440.dp, 640.dp), @@ -249,6 +250,11 @@ fun main(args: Array) { val trayShow = stringResource(Res.string.desktop_tray_show) val quit = stringResource(Res.string.desktop_quit) + LaunchedEffect(windowState.size) { + delay(3_000) + DesktopWindowPrefs.saveSize(windowState.size) + } + DisposableEffect(trayState) { DesktopNotifier.sink = { title, body -> trayState.sendNotification( diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatTopBar.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatTopBar.kt index 2b1a682..7fbd709 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatTopBar.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatTopBar.kt @@ -67,6 +67,7 @@ import dev.chrisbanes.haze.blur.HazeBlurStyle import dev.chrisbanes.haze.blur.HazeColorEffect import dev.chrisbanes.haze.blur.HazeProgressive import dev.chrisbanes.haze.blur.blurEffect +import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.hazeEffect import org.jetbrains.compose.resources.stringResource import ru.fromchat.Res @@ -425,8 +426,9 @@ fun ChatTopBar( /** * Two-pane chat chrome: separate back pill + title pill. Matches [ChatInput] composer chrome * ([rememberChatSurfaceContainerHazeStyle] + [androidx.compose.material3.ColorScheme.surfaceContainer]). - * Full-bleed progressive haze strip behind the pills (high at top → fade toward bottom); pills keep - * their own surfaceContainer + [hazeEffect]. Horizontal inset matches [ChatInput]. + * Full-bleed progressive haze strip behind the pills uses [HazeMaterials.thin] (high at top → fade + * toward bottom); pills keep surfaceContainer + [rememberChatSurfaceContainerHazeStyle]. + * Horizontal inset matches [ChatInput]. */ @Composable private fun ChatTopBarPill( @@ -441,6 +443,7 @@ private fun ChatTopBarPill( hazeBlurEnabled: Boolean = true, ) { val hazeStyle = rememberChatSurfaceContainerHazeStyle() + val progressiveStripHazeStyle = HazeMaterials.thin() val chromeColor = MaterialTheme.colorScheme.surfaceContainer val pillShape = RoundedCornerShape(28.dp) @@ -452,7 +455,7 @@ private fun ChatTopBarPill( .hazeEffect(state = hazeState) { blurEffect { blurEnabled = hazeBlurEnabled - style = hazeStyle + style = progressiveStripHazeStyle progressive = HazeProgressive.verticalGradient( startIntensity = 1f, endIntensity = 0f, diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/MainScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/MainScreen.kt index 2b11b26..a5f8228 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/MainScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/MainScreen.kt @@ -442,11 +442,15 @@ private fun ChatContextMenuBlurLayer( blurProgress: Float, modifier: Modifier = Modifier, ) { - val blurRadius = 12.dp * blurProgress + val progress = blurProgress.coerceIn(0f, 1f) + val blurRadius = 12.dp * progress if (blurRadius <= 0.dp) return + // Opaque enough background so rounded chrome corners don't stay razor-sharp while + // interiors look frosted (Transparent + no tint left original AA edges unblurred). // In-tree under the overlay (zIndex). Never use a Popup here: platform popups stack above // the sharp row/menu, blur them, and intercept dismiss taps. + val surface = MaterialTheme.colorScheme.surfaceContainerLowest Box( modifier = modifier .fillMaxSize() @@ -454,10 +458,12 @@ private fun ChatContextMenuBlurLayer( blurEffect { style = HazeBlurStyle( blurRadius = blurRadius, - colorEffects = emptyList(), - backgroundColor = Color.Transparent, + backgroundColor = surface, + colorEffects = listOf( + HazeColorEffect.tint(surface.copy(alpha = 0.4f * progress)), + ), noiseFactor = 0f, - fallbackColorEffect = HazeColorEffect.tint(Color.Transparent), + fallbackColorEffect = HazeColorEffect.tint(surface.copy(alpha = 0.4f * progress)), ) } }, diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatListShared.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatListShared.kt index 5c8d898..56af0c7 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatListShared.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatListShared.kt @@ -268,7 +268,6 @@ internal fun ChatConversationsList( publicChatSelected: Boolean, selectedOtherUserIds: Set, contextMenuState: ChatContextMenuState, - overlayCloneReady: Boolean, rowRevealProgress: Float, listContentPadding: PaddingValues = PaddingValues(0.dp), showSearchBar: Boolean = false, @@ -380,11 +379,11 @@ internal fun ChatConversationsList( selectionTransitionProgress = selectionTransitionProgress, isSelected = publicChatSelected, isHiddenForOverlay = contextMenuState.listIndex == ChatListLayout.PUBLIC_CHAT_ROW && - contextMenuState.isOverlayReplicaActive && - overlayCloneReady, + contextMenuState.isOverlayReplicaActive, isPressingForContextMenu = contextMenuState.listIndex == ChatListLayout.PUBLIC_CHAT_ROW && contextMenuState.phase == ChatContextMenuPhase.Pressing, - contextMenuPressScaleActive = contextMenuState.phase == ChatContextMenuPhase.Animating && + contextMenuPressScaleActive = contextMenuState.listIndex == ChatListLayout.PUBLIC_CHAT_ROW && + contextMenuState.phase == ChatContextMenuPhase.Animating && !contextMenuState.animatingOut, rowRevealProgress = if (contextMenuState.listIndex == ChatListLayout.PUBLIC_CHAT_ROW) { rowRevealProgress @@ -461,11 +460,11 @@ internal fun ChatConversationsList( selectionTransitionProgress = selectionTransitionProgress, isSelected = conversation.otherUserId in selectedOtherUserIds, isHiddenForOverlay = contextMenuState.listIndex == lazyIndex && - contextMenuState.isOverlayReplicaActive && - overlayCloneReady, + contextMenuState.isOverlayReplicaActive, isPressingForContextMenu = contextMenuState.listIndex == lazyIndex && contextMenuState.phase == ChatContextMenuPhase.Pressing, - contextMenuPressScaleActive = contextMenuState.phase == ChatContextMenuPhase.Animating && + contextMenuPressScaleActive = contextMenuState.listIndex == lazyIndex && + contextMenuState.phase == ChatContextMenuPhase.Animating && !contextMenuState.animatingOut, rowRevealProgress = if (contextMenuState.listIndex == lazyIndex) { rowRevealProgress diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatsSelectionState.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatsSelectionState.kt index 06ee68e..3e0b1de 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatsSelectionState.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatsSelectionState.kt @@ -103,8 +103,6 @@ class ChatContextMenuOverlayController { var blurProgress by mutableFloatStateOf(0f) /** Shared row scale progress (0 = pressed, 1 = full) for overlay ↔ list handoff. */ var rowRevealProgress by mutableFloatStateOf(0f) - /** True once the overlay row clone has been composed and positioned. */ - var overlayCloneReady by mutableStateOf(false) var onStateChange: (ChatContextMenuState) -> Unit = {} var onDismiss: () -> Unit = {} var onMessage: () -> Unit = {} @@ -114,12 +112,10 @@ class ChatContextMenuOverlayController { var onMarkPublicRead: () -> Unit = {} var onDelete: (Int) -> Unit = {} var onSelect: () -> Unit = {} - var onOverlayCloneReady: () -> Unit = {} fun clear() { uiState = null blurProgress = 0f rowRevealProgress = 0f - overlayCloneReady = false } } diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatsTab.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatsTab.kt index d55526b..6c9aa3a 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatsTab.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/chats/ChatsTab.kt @@ -829,7 +829,6 @@ fun ChatsTab( ), ) val rowRevealProgress = chatContextMenuOverlay.rowRevealProgress - val overlayCloneReady = chatContextMenuOverlay.overlayCloneReady val callsEnabled = ServerConfig.callsEnabled fun markSelectedChatsRead() { @@ -968,7 +967,6 @@ fun ChatsTab( publicChatSelected = publicChatSelected, selectedOtherUserIds = selectedOtherUserIds, contextMenuState = contextMenuState, - overlayCloneReady = overlayCloneReady, rowRevealProgress = rowRevealProgress, listContentPadding = PaddingValues(top = fixedTopBarHeight), showSearchBar = true, @@ -1033,7 +1031,6 @@ fun ChatsTab( return@ChatConversationsList } haptic(HapticFeedbackEvent.ContextMenuOpened) - chatContextMenuOverlay.overlayCloneReady = false contextMenuState = ChatContextMenuState( phase = ChatContextMenuPhase.Animating, target = target, @@ -1049,7 +1046,6 @@ fun ChatsTab( if (suspensionState.isSuspended) return@ChatConversationsList haptic(HapticFeedbackEvent.ContextMenuOpened) avatarPressMark = null - chatContextMenuOverlay.overlayCloneReady = false contextMenuState = ChatContextMenuState( phase = ChatContextMenuPhase.Animating, target = target, @@ -1185,9 +1181,6 @@ fun ChatsTab( animatingOut = true, ) } - chatContextMenuOverlay.onOverlayCloneReady = { - chatContextMenuOverlay.overlayCloneReady = true - } val shouldPublishOverlay = isVisible && contextMenuState.isOverlayReplicaActive && @@ -1215,19 +1208,10 @@ fun ChatsTab( null } if (chatContextMenuOverlay.uiState != overlayUiState) { - val previousOverlay = chatContextMenuOverlay.uiState chatContextMenuOverlay.uiState = overlayUiState if (overlayUiState == null) { chatContextMenuOverlay.blurProgress = 0f chatContextMenuOverlay.rowRevealProgress = 0f - chatContextMenuOverlay.overlayCloneReady = false - } else { - val isNewOverlaySession = previousOverlay == null || - previousOverlay.contextMenuState.listIndex != overlayUiState.contextMenuState.listIndex || - !previousOverlay.contextMenuState.isOverlayReplicaActive - if (isNewOverlaySession) { - chatContextMenuOverlay.overlayCloneReady = false - } } } @@ -1277,7 +1261,6 @@ internal fun ChatContextMenuOverlayHost( uiState.contextMenuState.otherUserId?.let(controller.onDelete) }, onSelect = controller.onSelect, - onOverlayCloneReady = controller.onOverlayCloneReady, modifier = modifier, ) } @@ -1297,7 +1280,6 @@ private fun ChatContextMenuOverlay( onMarkRead: () -> Unit, onDelete: () -> Unit, onSelect: () -> Unit, - onOverlayCloneReady: () -> Unit, modifier: Modifier = Modifier, ) { val contextMenuState = uiState.contextMenuState @@ -1503,17 +1485,6 @@ private fun ChatContextMenuOverlay( } } - LaunchedEffect( - layoutReady, - contextMenuState.listIndex, - contextMenuState.target, - contextMenuState.otherUserId, - ) { - if (layoutReady && contextMenuState.isOverlayReplicaActive) { - onOverlayCloneReady() - } - } - fun requestDismiss() { when (contextMenuState.phase) { ChatContextMenuPhase.Open -> { diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/SettingsTab.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/SettingsTab.kt index 7a2a7e6..a766b16 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/SettingsTab.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/SettingsTab.kt @@ -1,23 +1,26 @@ package ru.fromchat.ui.main.settings +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi +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.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Devices import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.filled.Palette +import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Storage +import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import ru.fromchat.ui.components.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -31,12 +34,17 @@ import com.pr0gramm3r101.utils.WindowWidthSizeClass import com.pr0gramm3r101.utils.currentWindowAdaptiveInfo import com.pr0gramm3r101.utils.verticalScroll import com.pr0gramm3r101.utils.widthSizeClass +import dev.chrisbanes.haze.blur.blurEffect +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.rememberHazeState import org.jetbrains.compose.resources.stringResource import ru.fromchat.Res import ru.fromchat.about import ru.fromchat.api.ApiClient import ru.fromchat.change_server import ru.fromchat.change_server_d +import ru.fromchat.logs_title import ru.fromchat.profile import ru.fromchat.settings import ru.fromchat.settings_category_account @@ -47,11 +55,12 @@ import ru.fromchat.settings_category_devices import ru.fromchat.settings_category_devices_d import ru.fromchat.settings_category_notifications import ru.fromchat.settings_category_notifications_d -import ru.fromchat.logs_title import ru.fromchat.settings_hub_about_sub import ru.fromchat.settings_hub_logs_sub import ru.fromchat.settings_hub_profile_sub import ru.fromchat.ui.LocalNavController +import ru.fromchat.ui.chat.rememberChatSurfaceContainerHazeStyle +import ru.fromchat.ui.components.Text import ru.fromchat.ui.extraStatusBars import ru.fromchat.ui.main.LocalMainChromeInsets import ru.fromchat.ui.main.mainPagerBottomInset @@ -64,6 +73,9 @@ val SettingsStepHorizontalPadding = 24.dp fun SettingsTab() { val navController = LocalNavController.current val isTwoPane = currentWindowAdaptiveInfo().widthSizeClass != WindowWidthSizeClass.COMPACT + val topChromeHazeState = rememberHazeState() + // Same material as MainScreen bottom nav (not HazeMaterials.thin). + val topChromeHazeStyle = rememberChatSurfaceContainerHazeStyle() fun openDetail(route: String) { navController.navigateReplacingMainDetail( @@ -75,6 +87,7 @@ fun SettingsTab() { Scaffold( modifier = Modifier.fillMaxSize(), containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { TopAppBar( @@ -89,16 +102,27 @@ fun SettingsTab() { colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent, ), + // Match MainScreen bottom chrome: surfaceContainer + hazeEffect/blurEffect. + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceContainer) + .hazeEffect(state = topChromeHazeState) { + blurEffect { + style = topChromeHazeStyle + } + }, ) } ) { innerPadding -> Column( Modifier .fillMaxSize() + .hazeSource(topChromeHazeState) .verticalScroll() .mainPagerBottomInset() - .padding(innerPadding) ) { + // Scrollable top inset so rows can pass under the frosted top chrome. + Spacer(Modifier.height(innerPadding.calculateTopPadding())) + if (isTwoPane) { Category(Modifier.padding(top = 16.dp)) { ListItem( diff --git a/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/components/Components.kt b/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/components/Components.kt index 28d55ae..fd38a44 100644 --- a/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/components/Components.kt +++ b/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/components/Components.kt @@ -681,7 +681,12 @@ fun ListItem( ), contentAlignment = Alignment.Center ) { - leadingContent() + // Material list leadingIconColor default: onSurfaceVariant + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant, + ) { + leadingContent() + } } }