mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Fix blur and make window size be remembered
This commit is contained in:
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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<String>) {
|
||||
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<String>) {
|
||||
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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -268,7 +268,6 @@ internal fun ChatConversationsList(
|
||||
publicChatSelected: Boolean,
|
||||
selectedOtherUserIds: Set<Int>,
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -681,7 +681,12 @@ fun ListItem(
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
leadingContent()
|
||||
// Material list leadingIconColor default: onSurfaceVariant
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
) {
|
||||
leadingContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user