Fix attachments and tray menu on Windows

This commit is contained in:
2026-09-11 16:30:33 +03:00
Unverified
parent cf8ea8bbce
commit 332c5a6600
24 changed files with 605 additions and 414 deletions
+1 -1
View File
@@ -2,6 +2,6 @@
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" /> <option name="externalSystemId" value="Gradle" />
<option name="version" value="2.4.10" /> <option name="version" value="2.4.20" />
</component> </component>
</project> </project>
+5 -1
View File
@@ -56,7 +56,11 @@ val fixComposeResourcesStructure = tasks.register<FixComposeResTask>("fixCompose
extensions.configure<ApplicationExtension> { extensions.configure<ApplicationExtension> {
namespace = "ru.fromchat" namespace = "ru.fromchat"
compileSdk = 37 compileSdk {
version = release(37) {
minorApiLevel = 2
}
}
defaultConfig { defaultConfig {
applicationId = "ru.fromchat" applicationId = "ru.fromchat"
+39 -39
View File
@@ -1,13 +1,13 @@
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.zip.ZipFile
plugins { plugins {
kotlin("jvm") kotlin("jvm")
alias(libs.plugins.compose.multiplatform) alias(libs.plugins.compose.multiplatform)
alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.compiler)
} }
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.zip.ZipFile
val javafxVersion = libs.versions.openjfx.get() val javafxVersion = libs.versions.openjfx.get()
val javafxClassifier = openjfxClassifier() val javafxClassifier = openjfxClassifier()
val javafxModules = listOf("base", "graphics", "controls", "media", "web", "swing") val javafxModules = listOf("base", "graphics", "controls", "media", "web", "swing")
@@ -21,13 +21,13 @@ dependencies {
} else { } else {
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
} }
implementation(compose.material3) implementation(libs.compose.material3)
implementation(compose.materialIconsExtended) implementation(libs.compose.materialIconsExtended)
implementation(libs.compose.components.resources) implementation(libs.compose.components.resources)
implementation(project(":app:shared")) implementation(project(":app:shared"))
implementation(project(":utils:shared")) implementation(project(":utils:shared"))
implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.coroutines.swing)
implementation("net.java.dev.jna:jna-platform:5.15.0") implementation(libs.jna.platform)
// OpenJFX publishes empty jars without a classifier; declare every module // OpenJFX publishes empty jars without a classifier; declare every module
// with the host classifier and exclude transitive stubs. // with the host classifier and exclude transitive stubs.
javafxModules.forEach { module -> javafxModules.forEach { module ->
@@ -618,7 +618,7 @@ val exportDmgBackground = tasks.register<Exec>("exportDmgBackground") {
) )
} }
fun org.gradle.api.tasks.TaskContainer.registerPackageDmgTask( fun TaskContainer.registerPackageDmgTask(
name: String, name: String,
appBundle: Provider<Directory>, appBundle: Provider<Directory>,
dmgOutput: Provider<RegularFile>, dmgOutput: Provider<RegularFile>,
@@ -644,37 +644,37 @@ fun org.gradle.api.tasks.TaskContainer.registerPackageDmgTask(
val script = dmgScript.get().asFile val script = dmgScript.get().asFile
script.parentFile.mkdirs() script.parentFile.mkdirs()
script.writeText( script.writeText(
""" $$"""
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
POSITIONS='${positions.absolutePath}' POSITIONS='$${positions.absolutePath}'
APP='${app.absolutePath}' APP='$${app.absolutePath}'
OUT='${dmgOutput.get().asFile.absolutePath}' OUT='$${dmgOutput.get().asFile.absolutePath}'
DIST='${dmgDistDir.get().asFile.absolutePath}' DIST='$${dmgDistDir.get().asFile.absolutePath}'
STAGING="${'$'}{TMPDIR:-/tmp}/fromchat-dmg-staging-${'$'}RANDOM" STAGING="${TMPDIR:-/tmp}/fromchat-dmg-staging-$RANDOM"
rm -rf "${'$'}OUT" "${'$'}STAGING" rm -rf "$OUT" "$STAGING"
mkdir -p "${'$'}STAGING" mkdir -p "$STAGING"
cp -R "${'$'}APP" "${'$'}STAGING/" cp -R "$APP" "$STAGING/"
WINDOW_SIZE=($(node -e "const c=require('${'$'}POSITIONS').createDmg; console.log(c.windowSize.join(' '))")) WINDOW_SIZE=($(node -e "const c=require('$POSITIONS').createDmg; console.log(c.windowSize.join(' '))"))
ICON_SIZE=$(node -e "console.log(require('${'$'}POSITIONS').createDmg.iconSize)") ICON_SIZE=$(node -e "console.log(require('$POSITIONS').createDmg.iconSize)")
APP_POS=($(node -e "const i=require('${'$'}POSITIONS').createDmg.icons.find(x=>x[0]==='FromChat.app'); console.log(i[1], i[2])")) APP_POS=($(node -e "const i=require('$POSITIONS').createDmg.icons.find(x=>x[0]==='FromChat.app'); console.log(i[1], i[2])"))
APPS_POS=($(node -e "const i=require('${'$'}POSITIONS').createDmg.icons.find(x=>x[0]==='Applications'); console.log(i[1], i[2])")) APPS_POS=($(node -e "const i=require('$POSITIONS').createDmg.icons.find(x=>x[0]==='Applications'); console.log(i[1], i[2])"))
( (
cd "${'$'}DIST" cd "$DIST"
create-dmg \ create-dmg \
--volname "FromChat" \ --volname "FromChat" \
--window-size "${'$'}{WINDOW_SIZE[0]}" "${'$'}{WINDOW_SIZE[1]}" \ --window-size "${WINDOW_SIZE[0]}" "${WINDOW_SIZE[1]}" \
--icon-size "${'$'}ICON_SIZE" \ --icon-size "$ICON_SIZE" \
--icon "FromChat.app" "${'$'}{APP_POS[0]}" "${'$'}{APP_POS[1]}" \ --icon "FromChat.app" "${APP_POS[0]}" "${APP_POS[1]}" \
--hide-extension "FromChat.app" \ --hide-extension "FromChat.app" \
--app-drop-link "${'$'}{APPS_POS[0]}" "${'$'}{APPS_POS[1]}" \ --app-drop-link "${APPS_POS[0]}" "${APPS_POS[1]}" \
--app-drop-link-name "Программы" \ --app-drop-link-name "Программы" \
--text-size 14 \ --text-size 14 \
--background "dmg-background@2x.png" \ --background "dmg-background@2x.png" \
"${'$'}OUT" \ "$OUT" \
"${'$'}STAGING" "$STAGING"
) )
rm -rf "${'$'}STAGING" rm -rf "$STAGING"
""".trimIndent() + "\n", """.trimIndent() + "\n",
) )
script.setExecutable(true) script.setExecutable(true)
@@ -770,10 +770,10 @@ val packageLinuxAppImage = tasks.register<Exec>("packageLinuxAppImage") {
val appRun = stage.resolve("AppRun") val appRun = stage.resolve("AppRun")
val rel = stage.toPath().relativize(nestedExe.toPath()).toString() val rel = stage.toPath().relativize(nestedExe.toPath()).toString()
appRun.writeText( appRun.writeText(
""" $$"""
#!/bin/sh #!/bin/sh
SELF="${'$'}(dirname "${'$'}(readlink -f "${'$'}0")")" SELF="$(dirname "$(readlink -f "$0")")"
exec "${'$'}SELF/$rel" "${'$'}@" exec "$SELF/$$rel" "$@"
""".trimIndent() + "\n", """.trimIndent() + "\n",
) )
appRun.setExecutable(true) appRun.setExecutable(true)
@@ -795,16 +795,16 @@ val packageLinuxAppImage = tasks.register<Exec>("packageLinuxAppImage") {
commandLine( commandLine(
"bash", "bash",
"-lc", "-lc",
""" $$"""
set -euo pipefail set -euo pipefail
TOOL="${'$'}{APPIMAGETOOL:-appimagetool}" TOOL="${APPIMAGETOOL:-appimagetool}"
if ! command -v "${'$'}TOOL" >/dev/null 2>&1; then if ! command -v "$TOOL" >/dev/null 2>&1; then
echo "appimagetool not found. Install it or set APPIMAGETOOL." >&2 echo "appimagetool not found. Install it or set APPIMAGETOOL." >&2
exit 1 exit 1
fi fi
ARCH="${'$'}(uname -m)" ARCH="$(uname -m)"
export ARCH export ARCH
"${'$'}TOOL" "${stage.absolutePath}" "${out.absolutePath}" "$TOOL" "$${stage.absolutePath}" "$${out.absolutePath}"
""".trimIndent(), """.trimIndent(),
) )
} }
@@ -925,8 +925,8 @@ val windowsPrebuiltX64AppImage = layout.buildDirectory.dir("prebuilt/windows-x64
val windowsPrebuiltArm64AppImage = layout.buildDirectory.dir("prebuilt/windows-arm64/app/FromChat") val windowsPrebuiltArm64AppImage = layout.buildDirectory.dir("prebuilt/windows-arm64/app/FromChat")
fun Exec.configureWindowsPackTask( fun Exec.configureWindowsPackTask(
appImageDir: org.gradle.api.provider.Provider<org.gradle.api.file.Directory>?, appImageDir: Provider<Directory>?,
setupOutput: org.gradle.api.provider.Provider<org.gradle.api.file.RegularFile>, setupOutput: Provider<RegularFile>,
registrationId: String = "FromChat", registrationId: String = "FromChat",
prebuiltOnly: Boolean = false, prebuiltOnly: Boolean = false,
) { ) {
@@ -1,20 +1,35 @@
package ru.fromchat.desktop package ru.fromchat.desktop
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.hoverable import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.CloudDone
import androidx.compose.material.icons.filled.CloudOff
import androidx.compose.material.icons.filled.CloudSync
import androidx.compose.material.icons.filled.DesktopWindows
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -22,40 +37,58 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue 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.graphics.Color
import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import java.awt.AWTEvent import java.awt.AWTEvent
import java.awt.Point
import java.awt.SystemTray import java.awt.SystemTray
import java.awt.Toolkit import java.awt.Toolkit
import java.awt.TrayIcon import java.awt.TrayIcon
import java.awt.event.AWTEventListener import java.awt.event.AWTEventListener
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.awt.event.WindowEvent
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import javax.swing.JFrame
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import kotlin.math.roundToInt
import ru.fromchat.ui.FromChatTheme
import ru.fromchat.ui.components.Text import ru.fromchat.ui.components.Text
import ru.fromchat.ui.getColorScheme
import ru.fromchat.ui.isInsideWindow import ru.fromchat.ui.isInsideWindow
internal enum class TrayConnectionStatus {
Connected,
Connecting,
Disconnected,
}
@Composable @Composable
internal fun DesktopTrayHost( internal fun DesktopTrayHost(
trayImage: BufferedImage, trayImage: BufferedImage,
tooltip: String, tooltip: String,
statusLabel: String, statusLabel: String,
connectionStatus: TrayConnectionStatus,
showLabel: String, showLabel: String,
aboutLabel: String, aboutLabel: String,
quitLabel: String, quitLabel: String,
darkTheme: Boolean,
onShow: () -> Unit, onShow: () -> Unit,
onAbout: () -> Unit, onAbout: () -> Unit,
onQuit: () -> Unit, onQuit: () -> Unit,
) { ) {
var menuOpen by remember { mutableStateOf(false) } var menuOpen by remember { mutableStateOf(false) }
var menuPosition by remember { mutableStateOf(WindowPosition(0.dp, 0.dp)) } var menuAnchor by remember { mutableStateOf<Point?>(null) }
val closeMenu = rememberUpdatedState { menuOpen = false } val closeMenu = rememberUpdatedState { menuOpen = false }
val onShowState = rememberUpdatedState(onShow) val onShowState = rememberUpdatedState(onShow)
@@ -68,10 +101,7 @@ internal fun DesktopTrayHost(
fun openMenu(event: MouseEvent) { fun openMenu(event: MouseEvent) {
val screen = event.locationOnScreen val screen = event.locationOnScreen
SwingUtilities.invokeLater { SwingUtilities.invokeLater {
menuPosition = WindowPosition( menuAnchor = screen
x = (screen.x - 220).coerceAtLeast(8).dp,
y = (screen.y - 180).coerceAtLeast(8).dp,
)
menuOpen = true menuOpen = true
} }
} }
@@ -105,11 +135,15 @@ internal fun DesktopTrayHost(
} }
if (menuOpen) { if (menuOpen) {
val density = LocalDensity.current
var contentSizePx by remember { mutableStateOf(IntSize.Zero) }
var showWindow by remember { mutableStateOf(false) }
val windowState = rememberWindowState( val windowState = rememberWindowState(
position = menuPosition, width = 220.dp,
width = 240.dp, height = 260.dp,
height = 220.dp, position = WindowPosition((-20_000).dp, (-20_000).dp),
) )
Window( Window(
onCloseRequest = { menuOpen = false }, onCloseRequest = { menuOpen = false },
state = windowState, state = windowState,
@@ -119,9 +153,29 @@ internal fun DesktopTrayHost(
resizable = false, resizable = false,
alwaysOnTop = true, alwaysOnTop = true,
focusable = true, focusable = true,
onPreviewKeyEvent = { event ->
if (event.type == KeyEventType.KeyDown && event.key == Key.Escape) {
menuOpen = false
true
} else {
false
}
},
) { ) {
MaterialTheme(colorScheme = getColorScheme(darkTheme, dynamicColor = false)) { val awtWindow = window
DisposableEffect(window) {
SideEffect {
if (!showWindow) {
awtWindow.isVisible = false
}
}
DisposableEffect(awtWindow) {
showWindow = false
awtWindow.isVisible = false
awtWindow.setLocation(-20_000, -20_000)
val dismiss = { SwingUtilities.invokeLater { closeMenu.value() } }
val toolkit = Toolkit.getDefaultToolkit() val toolkit = Toolkit.getDefaultToolkit()
var armed = false var armed = false
val armTimer = javax.swing.Timer(200) { armed = true }.apply { val armTimer = javax.swing.Timer(200) { armed = true }.apply {
@@ -135,77 +189,204 @@ internal fun DesktopTrayHost(
if (!armed) return@AWTEventListener if (!armed) return@AWTEventListener
val mouse = event as MouseEvent val mouse = event as MouseEvent
if (mouse.button != MouseEvent.BUTTON1) return@AWTEventListener if (mouse.button != MouseEvent.BUTTON1) return@AWTEventListener
if (mouse.isInsideWindow(window)) return@AWTEventListener if (mouse.isInsideWindow(awtWindow)) return@AWTEventListener
SwingUtilities.invokeLater { closeMenu.value() } dismiss()
}
}
}
var focusDismissArmed = false
val focusArmTimer = javax.swing.Timer(250) { focusDismissArmed = true }.apply {
isRepeats = false
start()
}
val focusListener = object : java.awt.event.WindowFocusListener {
override fun windowGainedFocus(event: WindowEvent) = Unit
override fun windowLostFocus(event: WindowEvent) {
if (focusDismissArmed) {
dismiss()
} }
} }
} }
toolkit.addAWTEventListener(dismissListener, AWTEvent.MOUSE_EVENT_MASK) toolkit.addAWTEventListener(dismissListener, AWTEvent.MOUSE_EVENT_MASK)
awtWindow.addWindowFocusListener(focusListener)
onDispose { onDispose {
armTimer.stop() armTimer.stop()
focusArmTimer.stop()
toolkit.removeAWTEventListener(dismissListener) toolkit.removeAWTEventListener(dismissListener)
awtWindow.removeWindowFocusListener(focusListener)
} }
} }
Column(
LaunchedEffect(contentSizePx, menuAnchor) {
val anchor = menuAnchor
if (anchor == null || contentSizePx.width < 80 || contentSizePx.height < 40) {
showWindow = false
awtWindow.isVisible = false
return@LaunchedEffect
}
val scale = density.density
val awtWidth = (contentSizePx.width / scale).roundToInt().coerceAtLeast(1)
val awtHeight = (contentSizePx.height / scale).roundToInt().coerceAtLeast(1)
val screen = awtWindow.graphicsConfiguration.bounds
val x = (anchor.x - awtWidth).coerceIn(
screen.x,
(screen.x + screen.width - awtWidth).coerceAtLeast(screen.x),
)
val y = (anchor.y - awtHeight).coerceIn(
screen.y,
(screen.y + screen.height - awtHeight).coerceAtLeast(screen.y),
)
awtWindow.setSize(awtWidth, awtHeight)
awtWindow.setLocation(x, y)
with(density) {
windowState.position = WindowPosition(
x = (x / density.density).dp,
y = (y / density.density).dp,
)
windowState.size = DpSize(
width = contentSizePx.width.toDp(),
height = contentSizePx.height.toDp(),
)
}
excludeFromWindowsTaskbar(awtWindow)
showWindow = true
awtWindow.isVisible = true
awtWindow.toFront()
awtWindow.requestFocus()
}
Box(
modifier = Modifier modifier = Modifier
.shadow(12.dp, RoundedCornerShape(12.dp)) .wrapContentSize(unbounded = true)
.clip(RoundedCornerShape(12.dp)) .onSizeChanged { size ->
.background(MaterialTheme.colorScheme.surfaceContainer) if (size.width > 0 && size.height > 0) {
.padding(vertical = 6.dp) contentSizePx = size
.widthIn(min = 220.dp), }
},
) { ) {
DesktopTrayMenuRow( FromChatTheme(darkTheme = desktopAppDarkTheme(), dynamicColor = false) {
text = statusLabel, val surfaceColor = MaterialTheme.colorScheme.surfaceContainerHigh
enabled = false, SideEffect {
onClick = {}, val awtColor = java.awt.Color(
surfaceColor.red,
surfaceColor.green,
surfaceColor.blue,
surfaceColor.alpha,
) )
HorizontalDivider( awtWindow.background = awtColor
modifier = Modifier.padding(vertical = 4.dp), (awtWindow as? JFrame)?.contentPane?.background = awtColor
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f), }
) DesktopTrayMenuContent(
DesktopTrayMenuRow(text = showLabel, onClick = { statusLabel = statusLabel,
connectionStatus = connectionStatus,
showLabel = showLabel,
aboutLabel = aboutLabel,
quitLabel = quitLabel,
onShow = {
menuOpen = false menuOpen = false
onShow() onShow()
}) },
DesktopTrayMenuRow(text = aboutLabel, onClick = { onAbout = {
menuOpen = false menuOpen = false
onAbout() onAbout()
}) },
HorizontalDivider( onQuit = {
modifier = Modifier.padding(vertical = 4.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f),
)
DesktopTrayMenuRow(text = quitLabel, onClick = {
menuOpen = false menuOpen = false
onQuit() onQuit()
}) },
)
} }
} }
} }
} }
} }
@Composable
private fun DesktopTrayMenuContent(
statusLabel: String,
connectionStatus: TrayConnectionStatus,
showLabel: String,
aboutLabel: String,
quitLabel: String,
onShow: () -> Unit,
onAbout: () -> Unit,
onQuit: () -> Unit,
) {
val statusIcon = when (connectionStatus) {
TrayConnectionStatus.Connected -> Icons.Filled.CloudDone
TrayConnectionStatus.Connecting -> Icons.Filled.CloudSync
TrayConnectionStatus.Disconnected -> Icons.Filled.CloudOff
}
val statusTint = when (connectionStatus) {
TrayConnectionStatus.Connected -> MaterialTheme.colorScheme.primary
TrayConnectionStatus.Connecting -> MaterialTheme.colorScheme.onSurfaceVariant
TrayConnectionStatus.Disconnected -> MaterialTheme.colorScheme.error
}
Column(
modifier = Modifier
.width(IntrinsicSize.Max)
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
.border(1.dp, MaterialTheme.colorScheme.outlineVariant)
.padding(vertical = 2.dp),
) {
DesktopTrayMenuRow(
text = statusLabel,
icon = statusIcon,
iconTint = statusTint,
enabled = false,
onClick = {},
)
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
DesktopTrayMenuRow(
text = showLabel,
icon = Icons.Filled.DesktopWindows,
onClick = onShow,
)
DesktopTrayMenuRow(
text = aboutLabel,
icon = Icons.Filled.Info,
onClick = onAbout,
)
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
DesktopTrayMenuRow(
text = quitLabel,
icon = Icons.AutoMirrored.Filled.Logout,
textColor = MaterialTheme.colorScheme.error,
iconTint = MaterialTheme.colorScheme.error,
onClick = onQuit,
)
}
}
@Composable @Composable
private fun DesktopTrayMenuRow( private fun DesktopTrayMenuRow(
text: String, text: String,
icon: ImageVector,
enabled: Boolean = true, enabled: Boolean = true,
textColor: Color = MaterialTheme.colorScheme.onSurface,
iconTint: Color = textColor,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val hovered by interactionSource.collectIsHoveredAsState() val hovered by interactionSource.collectIsHoveredAsState()
val textColor = when { val resolvedTextColor = when {
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f) !enabled -> MaterialTheme.colorScheme.onSurfaceVariant
else -> MaterialTheme.colorScheme.onSurface else -> textColor
}
val resolvedIconTint = when {
!enabled -> iconTint.copy(alpha = 0.7f)
else -> iconTint
} }
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background( .background(
if (enabled && hovered) { if (enabled && hovered) {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
} else { } else {
androidx.compose.ui.graphics.Color.Transparent Color.Transparent
}, },
) )
.hoverable(interactionSource, enabled = enabled) .hoverable(interactionSource, enabled = enabled)
@@ -215,13 +396,20 @@ private fun DesktopTrayMenuRow(
enabled = enabled, enabled = enabled,
onClick = onClick, onClick = onClick,
) )
.padding(horizontal = 14.dp, vertical = 10.dp), .padding(horizontal = 10.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = resolvedIconTint,
)
Text( Text(
text = text, text = text,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = textColor, color = resolvedTextColor,
) )
} }
} }
@@ -1,5 +1,7 @@
package ru.fromchat.desktop package ru.fromchat.desktop
import ru.fromchat.config.Settings
import ru.fromchat.ui.Theme
import javax.swing.JRootPane import javax.swing.JRootPane
internal fun isMacOs(): Boolean = internal fun isMacOs(): Boolean =
@@ -14,6 +16,48 @@ internal fun isWindowsArm64(): Boolean {
return arch.contains("aarch64") || arch.contains("arm64") return arch.contains("aarch64") || arch.contains("arm64")
} }
internal fun desktopAppDarkTheme(): Boolean =
when (runCatching { Settings.theme }.getOrDefault(Theme.AsSystem)) {
Theme.Dark -> true
Theme.Light -> false
Theme.AsSystem -> desktopSystemDarkTheme()
}
internal fun desktopSystemDarkTheme(): Boolean = runCatching {
when {
isMacOs() ->
ProcessBuilder("defaults", "read", "-g", "AppleInterfaceStyle")
.redirectErrorStream(true)
.start()
.inputStream
.bufferedReader()
.readText()
.trim()
.equals("Dark", ignoreCase = true)
isWindowsOs() -> isWindowsAppsDarkTheme()
else -> false
}
}.getOrDefault(false)
private fun isWindowsAppsDarkTheme(): Boolean {
val process = ProcessBuilder(
"reg",
"query",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
"/v",
"AppsUseLightTheme",
)
.redirectErrorStream(true)
.start()
val output = process.inputStream.bufferedReader().readText()
process.waitFor()
return when {
"0x0" in output -> true
"0x1" in output -> false
else -> false
}
}
/** Enables drawing under the system title bar on macOS. */ /** Enables drawing under the system title bar on macOS. */
internal fun applyDesktopEdgeToEdgeChrome(rootPane: JRootPane) { internal fun applyDesktopEdgeToEdgeChrome(rootPane: JRootPane) {
if (!isMacOs()) return if (!isMacOs()) return
@@ -357,12 +357,17 @@ fun main(args: Array<String>) {
} }
} }
// Mirror ConnectionStateStore + WebSocketManager.isConnected (+ logged-out → disconnected). // Mirror ConnectionStateStore + WebSocketManager.isConnected (+ logged-out → disconnected).
val wsStatusLabel = when { val trayConnectionStatus = when {
connectionStatus == ConnectionStatus.CONNECTED || connectionStatus == ConnectionStatus.CONNECTED ||
connectionStatus == ConnectionStatus.UPDATING || connectionStatus == ConnectionStatus.UPDATING ||
wsLinked -> stringResource(Res.string.status_connected) wsLinked -> TrayConnectionStatus.Connected
ApiClient.token.isNullOrEmpty() -> stringResource(Res.string.status_disconnected) ApiClient.token.isNullOrEmpty() -> TrayConnectionStatus.Disconnected
else -> stringResource(Res.string.status_connecting) else -> TrayConnectionStatus.Connecting
}
val wsStatusLabel = when (trayConnectionStatus) {
TrayConnectionStatus.Connected -> stringResource(Res.string.status_connected)
TrayConnectionStatus.Disconnected -> stringResource(Res.string.status_disconnected)
TrayConnectionStatus.Connecting -> stringResource(Res.string.status_connecting)
} }
val windowChrome = remember { desktopThemeBackgroundCompose() } val windowChrome = remember { desktopThemeBackgroundCompose() }
@@ -498,11 +503,6 @@ fun main(args: Array<String>) {
} }
if (traySupported) { if (traySupported) {
val darkTheme = when (runCatching { Settings.theme }.getOrDefault(Theme.AsSystem)) {
Theme.Dark -> true
Theme.Light -> false
Theme.AsSystem -> isSystemAppearanceDark()
}
if (mac) { if (mac) {
Tray( Tray(
icon = trayIcon, icon = trayIcon,
@@ -523,10 +523,10 @@ fun main(args: Array<String>) {
trayImage = trayIconImage, trayImage = trayIconImage,
tooltip = appName, tooltip = appName,
statusLabel = wsStatusLabel, statusLabel = wsStatusLabel,
connectionStatus = trayConnectionStatus,
showLabel = trayShow, showLabel = trayShow,
aboutLabel = aboutApp, aboutLabel = aboutApp,
quitLabel = quit, quitLabel = quit,
darkTheme = darkTheme,
onShow = { showMainWindow() }, onShow = { showMainWindow() },
onAbout = { openAbout() }, onAbout = { openAbout() },
onQuit = { exitApplication() }, onQuit = { exitApplication() },
@@ -690,12 +690,7 @@ fun main(args: Array<String>) {
) { ) {
App(onContentReady = { contentReady = true }) App(onContentReady = { contentReady = true })
if (windows) { if (windows) {
val darkTheme = when (runCatching { Settings.theme }.getOrDefault(Theme.AsSystem)) { MaterialTheme(colorScheme = getColorScheme(desktopAppDarkTheme(), dynamicColor = false)) {
Theme.Dark -> true
Theme.Light -> false
Theme.AsSystem -> isSystemAppearanceDark()
}
MaterialTheme(colorScheme = getColorScheme(darkTheme, dynamicColor = false)) {
WindowsDesktopTitleBar( WindowsDesktopTitleBar(
title = appName, title = appName,
windowIcon = windowIcon, windowIcon = windowIcon,
@@ -841,29 +836,12 @@ private fun applyDesktopWindowChromeBackground() {
} }
private fun desktopThemeBackgroundCompose() = private fun desktopThemeBackgroundCompose() =
if ( if (desktopAppDarkTheme()) Color(0xFF1C1B1F) else Color(0xFFFFFBFE)
when (runCatching { Settings.theme }.getOrDefault(Theme.AsSystem)) {
Theme.Dark -> true
Theme.Light -> false
Theme.AsSystem -> isSystemAppearanceDark()
}
) Color(0xFF1C1B1F) else Color(0xFFFFFBFE)
private fun Color.toAwtColor() = private fun Color.toAwtColor() =
java.awt.Color(red, green, blue, alpha) java.awt.Color(red, green, blue, alpha)
private fun isSystemAppearanceDark() = runCatching { private fun isSystemAppearanceDark() = desktopSystemDarkTheme()
if (isMacOs()) {
ProcessBuilder("defaults", "read", "-g", "AppleInterfaceStyle")
.redirectErrorStream(true)
.start()
.inputStream
.bufferedReader()
.readText()
.trim()
.equals("Dark", ignoreCase = true)
} else false
}.getOrDefault(false)
private fun applyDockIcon(image: BufferedImage?) { private fun applyDockIcon(image: BufferedImage?) {
if (image == null) { if (image == null) {
@@ -51,6 +51,36 @@ internal fun updateWindowsNativeCaptionBackground(window: Window, background: Co
(frame.rootPane.getClientProperty(ChromeKey) as? WindowsCaptionWndProc)?.background = background (frame.rootPane.getClientProperty(ChromeKey) as? WindowsCaptionWndProc)?.background = background
} }
private const val GwlExStyle = -20
private const val WsExToolWindow = 0x00000080
private const val WsExAppWindow = 0x00040000
/** Keeps transient popups (tray menu, etc.) out of the Windows taskbar and Alt+Tab. */
internal fun excludeFromWindowsTaskbar(window: Window) {
if (!isWindowsOs()) return
val hwnd = window.windowsHwnd()
if (hwnd.pointer == null) return
val exStyle = User32.INSTANCE.GetWindowLong(hwnd, GwlExStyle)
User32.INSTANCE.SetWindowLong(
hwnd,
GwlExStyle,
(exStyle or WsExToolWindow) and WsExAppWindow.inv(),
)
User32.INSTANCE.SetWindowPos(
hwnd,
null,
0,
0,
0,
0,
WinUser.SWP_NOMOVE or
WinUser.SWP_NOSIZE or
WinUser.SWP_NOZORDER or
WinUser.SWP_NOACTIVATE or
WinUser.SWP_FRAMECHANGED,
)
}
internal fun Window.windowsHwnd(): HWND { internal fun Window.windowsHwnd(): HWND {
val handle = (this as? ComposeWindow)?.windowHandle ?: 0L val handle = (this as? ComposeWindow)?.windowHandle ?: 0L
return if (handle != 0L) { return if (handle != 0L) {
+5 -1
View File
@@ -68,7 +68,11 @@ kotlin {
android { android {
namespace = "ru.fromchat.shared" namespace = "ru.fromchat.shared"
minSdk = 24 minSdk = 24
compileSdk = 37 compileSdk {
version = release(37) {
minorApiLevel = 2
}
}
} }
jvmToolchain(17) jvmToolchain(17)
@@ -92,10 +92,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import io.livekit.android.RoomOptions import io.livekit.android.RoomOptions
@@ -1221,16 +1221,11 @@ private fun PreviewTile(
innerClipShape = tileShape, innerClipShape = tileShape,
) )
} else { } else {
val placeholderHazeStyle = HazeMaterials.thick()
Box( Box(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
// Placeholder blur only for missing/disabled track. // Placeholder blur only for missing/disabled track.
.hazeEffect(state = hazeState) { .hazeBlur(input = HazeInput.Backdrop(hazeState), style = HazeMaterials.thick())
blurEffect {
style = placeholderHazeStyle
}
}
.background(MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = 0.55f)), .background(MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = 0.55f)),
) )
} }
@@ -1329,16 +1324,11 @@ private fun CallInlineControlBar(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
val controlsHazeStyle = HazeMaterials.thick()
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.86f) .fillMaxWidth(0.86f)
.clip(RoundedCornerShape(28.dp)) .clip(RoundedCornerShape(28.dp))
.hazeEffect(state = hazeState) { .hazeBlur(input = HazeInput.Backdrop(hazeState), style = HazeMaterials.thick())
blurEffect {
style = controlsHazeStyle
}
}
.background(MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.48f)), .background(MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.48f)),
) { ) {
Row( Row(
@@ -54,8 +54,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.ToggleNavScrimEffect import com.pr0gramm3r101.utils.ToggleNavScrimEffect
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -118,7 +118,6 @@ fun DocumentScreen(
contentColor = MaterialTheme.colorScheme.onSurface, contentColor = MaterialTheme.colorScheme.onSurface,
contentWindowInsets = WindowInsets.navigationBars, contentWindowInsets = WindowInsets.navigationBars,
topBar = { topBar = {
val topBarHazeStyle = rememberChatSurfaceContainerHazeStyle()
MediumTopAppBar( MediumTopAppBar(
windowInsets = WindowInsets.extraStatusBars, windowInsets = WindowInsets.extraStatusBars,
title = { title = {
@@ -145,11 +144,10 @@ fun DocumentScreen(
), ),
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.91f)) .background(MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.91f))
.hazeEffect(state = hazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
style = topBarHazeStyle style = rememberChatSurfaceContainerHazeStyle(),
} ),
},
) )
}, },
) { innerPadding -> ) { innerPadding ->
@@ -66,9 +66,9 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.pr0gramm3r101.utils.crypto.Base64 import com.pr0gramm3r101.utils.crypto.Base64
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -784,11 +784,10 @@ private fun ChatImageTileContent(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.hazeEffect(state = thumbHazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(thumbHazeState),
style = thumbOverlayHazeStyle style = thumbOverlayHazeStyle,
} ),
},
) )
} }
} }
@@ -917,14 +916,9 @@ private fun DownloadCancelledImageOverlay(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scrim = MaterialTheme.colorScheme.scrim.copy(alpha = 0.38f) val scrim = MaterialTheme.colorScheme.scrim.copy(alpha = 0.38f)
val overlayHazeStyle = HazeMaterials.thin()
Box( Box(
modifier = modifier modifier = modifier
.hazeEffect { .hazeBlur(input = HazeInput.Content, style = HazeMaterials.thin())
blurEffect {
style = overlayHazeStyle
}
}
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.12f)), .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.12f)),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
@@ -964,17 +958,12 @@ private fun UploadingImageOverlay(
onCancelUpload: (() -> Unit)? = null, onCancelUpload: (() -> Unit)? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val overlayHazeStyle = HazeMaterials.thin()
Box(modifier = modifier) { Box(modifier = modifier) {
Box( Box(
modifier = Modifier modifier = Modifier
.matchParentSize() .matchParentSize()
.clip(clipShape) .clip(clipShape)
.hazeEffect { .hazeBlur(input = HazeInput.Content, style = HazeMaterials.thin())
blurEffect {
style = overlayHazeStyle
}
}
) { ) {
when { when {
previewBitmap != null -> { previewBitmap != null -> {
@@ -1259,15 +1248,10 @@ private fun CorruptedImagePlaceholder(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
clipShape: RoundedCornerShape = attachmentImageCornerShape(isAuthor = false), clipShape: RoundedCornerShape = attachmentImageCornerShape(isAuthor = false),
) { ) {
val overlayHazeStyle = HazeMaterials.thin()
Box( Box(
modifier = modifier modifier = modifier
.clip(clipShape) .clip(clipShape)
.hazeEffect { .hazeBlur(input = HazeInput.Content, style = HazeMaterials.thin()),
blurEffect {
style = overlayHazeStyle
}
},
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Icon( Icon(
@@ -93,9 +93,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.pr0gramm3r101.utils.conditional import com.pr0gramm3r101.utils.conditional
import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.hazeEffect
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -323,7 +323,7 @@ fun ChatInput(
attachmentDropBridge: AttachmentDropBridge, attachmentDropBridge: AttachmentDropBridge,
pendingDropUris: List<String> = emptyList(), pendingDropUris: List<String> = emptyList(),
) { ) {
val composerHazeStyle = rememberChatSurfaceContainerHazeStyle() val composerHazeStyle = rememberChatSurfaceContainerHazeStyle().then { blurEnabled(hazeBlurEnabled) }
val dropScrimColor = lerp(MaterialTheme.colorScheme.primary, Color.Black, 0.62f) val dropScrimColor = lerp(MaterialTheme.colorScheme.primary, Color.Black, 0.62f)
val attachmentDropEnabled = supportsAttachments && !isReadOnly val attachmentDropEnabled = supportsAttachments && !isReadOnly
val dropHighlightActive = val dropHighlightActive =
@@ -450,12 +450,7 @@ fun ChatInput(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(pillShape) .clip(pillShape)
.hazeEffect(state = hazeState) { .hazeBlur(input = HazeInput.Backdrop(hazeState), style = composerHazeStyle)
blurEffect {
blurEnabled = hazeBlurEnabled
style = composerHazeStyle
}
}
.clickable(enabled = true) { onReadOnlyMessageClick() }, .clickable(enabled = true) { onReadOnlyMessageClick() },
) { ) {
Text( Text(
@@ -499,12 +494,7 @@ fun ChatInput(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainer, pillShape) .background(MaterialTheme.colorScheme.surfaceContainer, pillShape)
.hazeEffect(state = hazeState) { .hazeBlur(input = HazeInput.Backdrop(hazeState), style = composerHazeStyle)
blurEffect {
blurEnabled = hazeBlurEnabled
style = composerHazeStyle
}
}
.conditional(dropBlurRadius > 0.dp) { .conditional(dropBlurRadius > 0.dp) {
blur(dropBlurRadius, BlurredEdgeTreatment.Unbounded) blur(dropBlurRadius, BlurredEdgeTreatment.Unbounded)
}, },
@@ -753,15 +743,12 @@ fun ChatInput(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(actionBackground) .background(actionBackground)
.hazeEffect(state = hazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
// Keep the node in the tree in both directions; fading alpha avoids the // Keep the node in the tree in both directions; fading alpha avoids the
// frosted layer popping on over the color when send -> voice. // frosted layer popping on over the color when send -> voice.
blurEnabled = hazeBlurEnabled style = composerHazeStyle.then { alpha(hazeOverlayAlpha) },
style = composerHazeStyle ),
alpha = hazeOverlayAlpha
}
},
) )
AnimatedContent( AnimatedContent(
targetState = canSend, targetState = canSend,
@@ -81,11 +81,11 @@ import ru.fromchat.chat_date_yesterday
import ru.fromchat.utils.rememberRegistrationDateFormatStrings import ru.fromchat.utils.rememberRegistrationDateFormatStrings
import com.pr0gramm3r101.utils.resetFocus import com.pr0gramm3r101.utils.resetFocus
import com.pr0gramm3r101.utils.supportClipboardManagerImpl import com.pr0gramm3r101.utils.supportClipboardManagerImpl
import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeProgressive import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.blurEffect
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -876,22 +876,23 @@ fun ChatScreen(
// floating header Box only; avoids extra top inset on the fade / list and duplicate “padding”. // floating header Box only; avoids extra top inset on the fade / list and duplicate “padding”.
contentWindowInsets = WindowInsets.navigationBars, contentWindowInsets = WindowInsets.navigationBars,
bottomBar = { bottomBar = {
val bottomBarHazeStyle = HazeMaterials.thin()
Column( Column(
modifier = Modifier modifier = Modifier
.windowInsetsPadding(WindowInsets.ime) .windowInsetsPadding(WindowInsets.ime)
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainer) .background(MaterialTheme.colorScheme.surfaceContainer)
.hazeEffect(state = hazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
blurEnabled = hazeBlurEnabled style = HazeMaterials.thin().then {
style = bottomBarHazeStyle blurEnabled(hazeBlurEnabled)
progressive = HazeProgressive.verticalGradient( progressive(
HazeProgressive.verticalGradient(
startIntensity = 0f, startIntensity = 0f,
endIntensity = 1f, endIntensity = 1f,
),
)
},
) )
}
}
) { ) {
if (peerDeleted && dmRecipientId != null) { if (peerDeleted && dmRecipientId != null) {
Box( Box(
@@ -1726,7 +1727,6 @@ private fun ChatScrollToBottomButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
hazeBlurEnabled: Boolean = true, hazeBlurEnabled: Boolean = true,
) { ) {
val hazeStyle = rememberChatSurfaceContainerHazeStyle()
Box( Box(
modifier = modifier modifier = modifier
.size(ChatScrollToBottomButtonSize) .size(ChatScrollToBottomButtonSize)
@@ -1740,12 +1740,10 @@ private fun ChatScrollToBottomButton(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.91f)) .background(MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.91f))
.hazeEffect(state = hazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
blurEnabled = hazeBlurEnabled style = rememberChatSurfaceContainerHazeStyle().then { blurEnabled(hazeBlurEnabled) },
style = hazeStyle ),
}
},
) )
Icon( Icon(
imageVector = Icons.Rounded.KeyboardArrowDown, imageVector = Icons.Rounded.KeyboardArrowDown,
@@ -62,13 +62,13 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.conditional import com.pr0gramm3r101.utils.conditional
import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeBlurStyle import dev.chrisbanes.haze.blur.HazeBlurStyle
import dev.chrisbanes.haze.blur.HazeColorEffect import dev.chrisbanes.haze.blur.HazeColorEffect
import dev.chrisbanes.haze.blur.HazeProgressive import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.blurEffect
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res import ru.fromchat.Res
import ru.fromchat.chat_members_count import ru.fromchat.chat_members_count
@@ -397,7 +397,6 @@ fun ChatTopBar(
val layoutHeight = topBarPlaceable.height + arcExtentPx val layoutHeight = topBarPlaceable.height + arcExtentPx
val bgPlaceable = subcompose("background") { val bgPlaceable = subcompose("background") {
val hazeStyle = rememberChatSurfaceContainerHazeStyle()
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -407,12 +406,10 @@ fun ChatTopBar(
} }
) )
.background(MaterialTheme.colorScheme.surfaceContainer) .background(MaterialTheme.colorScheme.surfaceContainer)
.hazeEffect(state = hazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
blurEnabled = hazeBlurEnabled style = rememberChatSurfaceContainerHazeStyle().then { blurEnabled(hazeBlurEnabled) },
style = hazeStyle ),
}
},
) )
}.first().measure(Constraints.fixed(layoutWidth, layoutHeight)) }.first().measure(Constraints.fixed(layoutWidth, layoutHeight))
@@ -443,8 +440,7 @@ private fun ChatTopBarPill(
hazeBlurEnabled: Boolean = true, hazeBlurEnabled: Boolean = true,
showBackButton: Boolean = true, showBackButton: Boolean = true,
) { ) {
val hazeStyle = rememberChatSurfaceContainerHazeStyle() val hazeStyle = rememberChatSurfaceContainerHazeStyle().then { blurEnabled(hazeBlurEnabled) }
val progressiveStripHazeStyle = HazeMaterials.thin()
val chromeColor = MaterialTheme.colorScheme.surfaceContainer val chromeColor = MaterialTheme.colorScheme.surfaceContainer
val pillShape = RoundedCornerShape(28.dp) val pillShape = RoundedCornerShape(28.dp)
@@ -453,16 +449,18 @@ private fun ChatTopBarPill(
Box( Box(
modifier = Modifier modifier = Modifier
.matchParentSize() .matchParentSize()
.hazeEffect(state = hazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
blurEnabled = hazeBlurEnabled style = HazeMaterials.thin().then {
style = progressiveStripHazeStyle blurEnabled(hazeBlurEnabled)
progressive = HazeProgressive.verticalGradient( progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f, startIntensity = 1f,
endIntensity = 0f, endIntensity = 0f,
),
) )
}
}, },
),
) )
Row( Row(
modifier = Modifier modifier = Modifier
@@ -478,12 +476,7 @@ private fun ChatTopBarPill(
.size(48.dp) .size(48.dp)
.clip(CircleShape) .clip(CircleShape)
.background(chromeColor) .background(chromeColor)
.hazeEffect(state = hazeState) { .hazeBlur(input = HazeInput.Backdrop(hazeState), style = hazeStyle),
blurEffect {
blurEnabled = hazeBlurEnabled
style = hazeStyle
}
},
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
@@ -499,12 +492,7 @@ private fun ChatTopBarPill(
.weight(1f) .weight(1f)
.background(chromeColor, pillShape) .background(chromeColor, pillShape)
.clip(pillShape) .clip(pillShape)
.hazeEffect(state = hazeState) { .hazeBlur(input = HazeInput.Backdrop(hazeState), style = hazeStyle)
blurEffect {
blurEnabled = hazeBlurEnabled
style = hazeStyle
}
}
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), .padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@@ -592,15 +580,19 @@ fun rememberChatSurfaceContainerHazeStyle(): HazeBlurStyle {
val surface = MaterialTheme.colorScheme.surfaceContainer val surface = MaterialTheme.colorScheme.surfaceContainer
return remember(surface) { return remember(surface) {
HazeBlurStyle( HazeBlurStyle {
blurRadius = 24.dp, blurRadius(24.dp)
backgroundColor = surface, backgroundColor(surface)
colorEffect = HazeColorEffect.tint( colorEffects(
listOf(
HazeColorEffect.tint(
surface.copy(alpha = if (surface.luminance() >= 0.5f) 0.74f else 0.79f), surface.copy(alpha = if (surface.luminance() >= 0.5f) 0.74f else 0.79f),
), ),
),
) )
} }
} }
}
/** /**
* Single-flight gate for chat title/avatar navigate vs back during shared-element transitions. * Single-flight gate for chat title/avatar navigate vs back during shared-element transitions.
@@ -83,11 +83,11 @@ import androidx.compose.ui.zIndex
import androidx.graphics.shapes.Morph import androidx.graphics.shapes.Morph
import androidx.graphics.shapes.RoundedPolygon import androidx.graphics.shapes.RoundedPolygon
import com.pr0gramm3r101.utils.LastAnchoredBottomArrangement import com.pr0gramm3r101.utils.LastAnchoredBottomArrangement
import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeProgressive import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.blurEffect
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -551,7 +551,6 @@ fun ExpressiveStepFlowScaffold(
@Composable @Composable
fun HazeTopBar(hazeState: HazeState) { fun HazeTopBar(hazeState: HazeState) {
val topBarHazeStyle = HazeMaterials.thin()
TopAppBar( TopAppBar(
windowInsets = topBarWindowInsets, windowInsets = topBarWindowInsets,
title = {}, title = {},
@@ -570,15 +569,17 @@ fun ExpressiveStepFlowScaffold(
containerColor = Color.Transparent, containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent, scrolledContainerColor = Color.Transparent,
), ),
modifier = Modifier.hazeEffect(state = hazeState) { modifier = Modifier.hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
style = topBarHazeStyle style = HazeMaterials.thin().then {
progressive = HazeProgressive.verticalGradient( progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f, startIntensity = 1f,
endIntensity = 0f, endIntensity = 0f,
),
) )
}
}, },
),
) )
} }
@@ -15,12 +15,12 @@ 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.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeBlurStyle import dev.chrisbanes.haze.blur.HazeBlurStyle
import dev.chrisbanes.haze.blur.HazeProgressive import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.blurEffect
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
@Composable @Composable
@@ -31,28 +31,17 @@ fun HazeBottomBar(
baseColor: Color? = null, baseColor: Color? = null,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val resolvedStyle = hazeStyle ?: HazeMaterials.thin() val effectModifier = Modifier.hazeBlur(
val effectModifier = if (hazeStyle != null) { input = HazeInput.Backdrop(hazeState),
Modifier.hazeEffect(state = hazeState) { style = (hazeStyle ?: HazeMaterials.thin()).then {
blurEffect { progressive(
style = resolvedStyle HazeProgressive.verticalGradient(
progressive = HazeProgressive.verticalGradient(
startIntensity = 0f, startIntensity = 0f,
endIntensity = 1f, endIntensity = 1f,
),
) )
} },
}
} else {
Modifier.hazeEffect(state = hazeState) {
blurEffect {
style = resolvedStyle
progressive = HazeProgressive.verticalGradient(
startIntensity = 0f,
endIntensity = 1f,
) )
}
}
}
Column( Column(
modifier = Modifier modifier = Modifier
.windowInsetsPadding(WindowInsets.ime) .windowInsetsPadding(WindowInsets.ime)
@@ -51,11 +51,11 @@ import androidx.navigation.NavController
import com.pr0gramm3r101.utils.WindowWidthSizeClass import com.pr0gramm3r101.utils.WindowWidthSizeClass
import com.pr0gramm3r101.utils.currentWindowAdaptiveInfo import com.pr0gramm3r101.utils.currentWindowAdaptiveInfo
import com.pr0gramm3r101.utils.widthSizeClass import com.pr0gramm3r101.utils.widthSizeClass
import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeBlurStyle import dev.chrisbanes.haze.blur.HazeBlurStyle
import dev.chrisbanes.haze.blur.HazeColorEffect import dev.chrisbanes.haze.blur.HazeColorEffect
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -343,11 +343,7 @@ fun MainScreen(
} }
} }
.background(MaterialTheme.colorScheme.surfaceContainer) .background(MaterialTheme.colorScheme.surfaceContainer)
.hazeEffect(state = navBarHazeState) { .hazeBlur(input = HazeInput.Backdrop(navBarHazeState), style = navBarHazeStyle),
blurEffect {
style = navBarHazeStyle
}
},
) { ) {
NavigationBar( NavigationBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -492,21 +488,19 @@ private fun ChatContextMenuBlurLayer(
// In-tree under the overlay (zIndex). Never use a Popup here: platform popups stack above // 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. // the sharp row/menu, blur them, and intercept dismiss taps.
val surface = MaterialTheme.colorScheme.surfaceContainerLowest val surface = MaterialTheme.colorScheme.surfaceContainerLowest
val tint = HazeColorEffect.tint(surface.copy(alpha = 0.4f * progress))
Box( Box(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.hazeEffect(state = hazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
style = HazeBlurStyle( style = HazeBlurStyle {
blurRadius = blurRadius, blurRadius(blurRadius)
backgroundColor = surface, backgroundColor(surface)
colorEffects = listOf( colorEffects(listOf(tint))
HazeColorEffect.tint(surface.copy(alpha = 0.4f * progress)), noiseFactor(0f)
), fallbackColorEffect(tint)
noiseFactor = 0f,
fallbackColorEffect = HazeColorEffect.tint(surface.copy(alpha = 0.4f * progress)),
)
}
}, },
),
) )
} }
@@ -58,10 +58,10 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -360,16 +360,13 @@ private fun ChatsTopBarHazeBackdrop(
blurEnabled: Boolean, blurEnabled: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val topBarHazeStyle = HazeMaterials.thin()
Box( Box(
modifier = modifier modifier = modifier
.graphicsLayer { alpha = blurReveal.coerceIn(0f, 1f) } .graphicsLayer { alpha = blurReveal.coerceIn(0f, 1f) }
.hazeEffect(state = hazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
this.blurEnabled = blurEnabled style = HazeMaterials.thin().then { blurEnabled(blurEnabled) },
style = topBarHazeStyle ),
}
},
) )
} }
@@ -70,10 +70,10 @@ import org.jetbrains.compose.resources.stringResource
import com.pr0gramm3r101.components.Category import com.pr0gramm3r101.components.Category
import com.pr0gramm3r101.components.ListItem import com.pr0gramm3r101.components.ListItem
import com.pr0gramm3r101.utils.currentDeviceInfo import com.pr0gramm3r101.utils.currentDeviceInfo
import dev.chrisbanes.haze.blur.HazeProgressive import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import io.ktor.client.network.sockets.ConnectTimeoutException import io.ktor.client.network.sockets.ConnectTimeoutException
@@ -435,7 +435,6 @@ fun DevicesScreen(onBack: () -> Unit) {
contentWindowInsets = WindowInsets.navigationBars, contentWindowInsets = WindowInsets.navigationBars,
snackbarHost = { FromChatSnackbarHost(hostState = snackbarHostState) }, snackbarHost = { FromChatSnackbarHost(hostState = snackbarHostState) },
topBar = { topBar = {
val topBarHazeStyle = HazeMaterials.thin()
TopAppBar( TopAppBar(
windowInsets = settingsDetailWindowInsets(), windowInsets = settingsDetailWindowInsets(),
title = {}, title = {},
@@ -450,15 +449,17 @@ fun DevicesScreen(onBack: () -> Unit) {
containerColor = Color.Transparent, containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent scrolledContainerColor = Color.Transparent
), ),
modifier = Modifier.hazeEffect(state = hazeState) { modifier = Modifier.hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
style = topBarHazeStyle style = HazeMaterials.thin().then {
progressive = HazeProgressive.verticalGradient( progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f, startIntensity = 1f,
endIntensity = 0f, endIntensity = 0f,
),
) )
} },
} ),
) )
}, },
bottomBar = { bottomBar = {
@@ -67,10 +67,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.navigateAndWipeBackStack import com.pr0gramm3r101.utils.navigateAndWipeBackStack
import com.pr0gramm3r101.utils.toDp import com.pr0gramm3r101.utils.toDp
import dev.chrisbanes.haze.blur.HazeProgressive import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -339,7 +339,6 @@ fun ServerConfigScreen() {
} }
}, },
topBar = { topBar = {
val topBarHazeStyle = HazeMaterials.thin()
TopAppBar( TopAppBar(
windowInsets = settingsDetailWindowInsets(), windowInsets = settingsDetailWindowInsets(),
title = {}, title = {},
@@ -357,15 +356,17 @@ fun ServerConfigScreen() {
containerColor = Color.Transparent, containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent, scrolledContainerColor = Color.Transparent,
), ),
modifier = Modifier.hazeEffect(state = actionHazeState) { modifier = Modifier.hazeBlur(
blurEffect { input = HazeInput.Backdrop(actionHazeState),
style = topBarHazeStyle style = HazeMaterials.thin().then {
progressive = HazeProgressive.verticalGradient( progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f, startIntensity = 1f,
endIntensity = 0f, endIntensity = 0f,
),
) )
}
}, },
),
) )
} }
) { innerPadding -> ) { innerPadding ->
@@ -47,10 +47,10 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.blur.HazeProgressive import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.blur.blurEffect import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import io.ktor.client.call.body import io.ktor.client.call.body
@@ -339,7 +339,6 @@ fun EditProfileScreen(
} }
}, },
topBar = { topBar = {
val topBarHazeStyle = HazeMaterials.thin()
TopAppBar( TopAppBar(
title = { Text(stringResource(Res.string.profile_edit_title)) }, title = { Text(stringResource(Res.string.profile_edit_title)) },
navigationIcon = { navigationIcon = {
@@ -367,15 +366,17 @@ fun EditProfileScreen(
containerColor = Color.Transparent, containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent, scrolledContainerColor = Color.Transparent,
), ),
modifier = Modifier.hazeEffect(state = hazeState) { modifier = Modifier.hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
style = topBarHazeStyle style = HazeMaterials.thin().then {
progressive = HazeProgressive.verticalGradient( progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f, startIntensity = 1f,
endIntensity = 0f, endIntensity = 0f,
),
) )
}
}, },
),
) )
}, },
) { innerPadding -> ) { innerPadding ->
@@ -37,11 +37,11 @@ import androidx.compose.ui.unit.Dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.pr0gramm3r101.components.ListItemPosition import com.pr0gramm3r101.components.ListItemPosition
import com.pr0gramm3r101.utils.SupportClipboardManager import com.pr0gramm3r101.utils.SupportClipboardManager
import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeProgressive import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.blurEffect
import dev.chrisbanes.haze.blur.materials.HazeMaterials import dev.chrisbanes.haze.blur.materials.HazeMaterials
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -2058,21 +2058,22 @@ private fun ProfileFloatingBackBar(
) { ) {
if (!visible) return if (!visible) return
val backBarHazeStyle = HazeMaterials.thin()
Box(modifier = modifier.fillMaxWidth()) { Box(modifier = modifier.fillMaxWidth()) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(blurHeight) .height(blurHeight)
.hazeEffect(state = hazeState) { .hazeBlur(
blurEffect { input = HazeInput.Backdrop(hazeState),
style = backBarHazeStyle style = HazeMaterials.thin().then {
progressive = HazeProgressive.verticalGradient( progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f, startIntensity = 1f,
endIntensity = 0f, endIntensity = 0f,
),
) )
}
}, },
),
) )
IconButton( IconButton(
onClick = onBack, onClick = onBack,
+25 -20
View File
@@ -1,19 +1,20 @@
[versions] [versions]
agp = "9.3.1" agp = "9.4.0"
androidx-activityCompose = "1.13.0" androidx-activityCompose = "1.13.0"
androidx-appcompat = "1.7.1" androidx-appcompat = "1.8.0"
androidx-core-ktx = "1.19.0" androidx-core-ktx = "1.19.0"
androidx-exifinterface = "1.4.2" androidx-exifinterface = "1.4.2"
coilCompose = "3.5.0" coilCompose = "3.6.2"
compose-multiplatform = "1.12.0-beta02" compose-multiplatform = "1.12.0"
#noinspection NewerVersionAvailable #noinspection NewerVersionAvailable
constraintlayout = "0.8.0-shaded" constraintlayout = "0.8.0-shaded"
coreSplashscreen = "1.2.0" coreSplashscreen = "1.2.0"
firebaseMessaging = "25.1.1" firebaseMessaging = "25.1.3"
googleServices = "4.5.0" googleServices = "4.5.0"
haze = "2.0.0-alpha03" haze = "2.0.0-beta03"
kotlin = "2.4.10" jnaPlatform = "5.19.1"
adaptiveAndroid = "1.2.0" kotlin = "2.4.20"
adaptiveAndroid = "1.3.0"
biometric = "1.4.0-alpha07" biometric = "1.4.0-alpha07"
gson = "2.14.0" gson = "2.14.0"
kotlinxIoBytestring = "0.9.1" kotlinxIoBytestring = "0.9.1"
@@ -21,33 +22,34 @@ kotlinxCoroutinesCore = "1.11.0"
kotlinxIoCore = "0.9.1" kotlinxIoCore = "0.9.1"
multiplatformCryptoLibsodiumBindings = "0.9.5" multiplatformCryptoLibsodiumBindings = "0.9.5"
multiplatformSettings = "1.3.0" multiplatformSettings = "1.3.0"
serialization = "2.4.10" serialization = "2.4.20"
serialization-json = "1.11.0" serialization-json = "1.11.0"
material = "1.14.0" material = "1.14.0"
activityKtx = "1.13.0" activityKtx = "1.13.0"
navigationCompose = "2.9.2" navigationCompose = "2.9.2"
datastore = "1.2.1" datastore = "1.2.1"
security-crypto = "1.1.0" security-crypto = "1.1.0"
ktor = "3.5.1" ktor = "3.5.2"
slf4j = "1.7.36" slf4j = "1.7.36"
kotlinxDatetime = "0.8.0" kotlinxDatetime = "0.8.0"
lifecycleRuntimeKtx = "2.11.0" lifecycleRuntimeKtx = "2.11.0"
composeBom = "2026.06.01" composeBom = "2026.09.00"
composeMaterialIconsExtended = "1.7.3" composeMaterialIconsExtended = "1.7.3"
composeMaterial3 = "1.12.0-alpha03" composeMaterial3 = "1.12.0-alpha03"
composeComponents = "1.12.0-beta02" composeComponents = "1.12.0"
playServicesBase = "18.10.0" playServicesBase = "18.10.1"
tweetnaclJava = "1.1.3" tweetnaclJava = "1.1.3"
androidxWork = "2.11.2" androidxWork = "2.11.2"
cryptography-kotlin = "0.6.0" cryptography-kotlin = "0.6.0"
krypto = "4.0.10" krypto = "4.0.10"
sqldelight = "2.3.2" sqldelight = "2.3.2"
livekitAndroid = "2.27.0" livekitAndroid = "2.28.2"
livekitAndroidComposeComponents = "2.4.0" livekitAndroidComposeComponents = "2.4.2"
markdownRendererM3 = "0.43.0" markdownRendererM3 = "0.45.0"
bouncycastle = "1.85" bouncycastle = "1.85.2"
webkit = "1.16.0" webkit = "1.17.0"
openjfx = "21.0.6" openjfx = "22.0.2"
[libraries] [libraries]
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
@@ -65,6 +67,7 @@ haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze-blur = { module = "dev.chrisbanes.haze:haze-blur", version.ref = "haze" } haze-blur = { module = "dev.chrisbanes.haze:haze-blur", version.ref = "haze" }
haze-blur-materials = { module = "dev.chrisbanes.haze:haze-blur-materials", version.ref = "haze" } haze-blur-materials = { module = "dev.chrisbanes.haze:haze-blur-materials", version.ref = "haze" }
jetbrains-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } jetbrains-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jnaPlatform" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesCore" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesCore" }
jetbrains-kotlinx-io-bytestring = { module = "org.jetbrains.kotlinx:kotlinx-io-bytestring", version.ref = "kotlinxIoBytestring" } jetbrains-kotlinx-io-bytestring = { module = "org.jetbrains.kotlinx:kotlinx-io-bytestring", version.ref = "kotlinxIoBytestring" }
androidx-adaptive-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-android", version.ref = "adaptiveAndroid" } androidx-adaptive-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-android", version.ref = "adaptiveAndroid" }
@@ -91,7 +94,6 @@ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "k
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4j" } slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4j" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
@@ -114,8 +116,11 @@ cryptography-core = { module = "dev.whyoleg.cryptography:cryptography-core", ver
cryptography-provider-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "cryptography-kotlin" } cryptography-provider-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "cryptography-kotlin" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" }
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
#noinspection UnusedVersionCatalogEntry
openjfx-controls = { module = "org.openjfx:javafx-controls", version.ref = "openjfx" } openjfx-controls = { module = "org.openjfx:javafx-controls", version.ref = "openjfx" }
#noinspection UnusedVersionCatalogEntry
openjfx-web = { module = "org.openjfx:javafx-web", version.ref = "openjfx" } openjfx-web = { module = "org.openjfx:javafx-web", version.ref = "openjfx" }
#noinspection UnusedVersionCatalogEntry
openjfx-swing = { module = "org.openjfx:javafx-swing", version.ref = "openjfx" } openjfx-swing = { module = "org.openjfx:javafx-swing", version.ref = "openjfx" }
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" } sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" }
sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
+5 -1
View File
@@ -14,7 +14,11 @@ kotlin {
android { android {
namespace = "com.pr0gramm3r101.utils" namespace = "com.pr0gramm3r101.utils"
compileSdk = 37 compileSdk {
version = release(37) {
minorApiLevel = 2
}
}
minSdk = 24 minSdk = 24
} }