diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index cc87bc5..4f368d4 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -2,6 +2,6 @@
-
+
\ No newline at end of file
diff --git a/app/android/build.gradle.kts b/app/android/build.gradle.kts
index 804b8c1..8424b14 100644
--- a/app/android/build.gradle.kts
+++ b/app/android/build.gradle.kts
@@ -56,7 +56,11 @@ val fixComposeResourcesStructure = tasks.register("fixCompose
extensions.configure {
namespace = "ru.fromchat"
- compileSdk = 37
+ compileSdk {
+ version = release(37) {
+ minorApiLevel = 2
+ }
+ }
defaultConfig {
applicationId = "ru.fromchat"
diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts
index 51824a8..8119be5 100644
--- a/app/desktop/build.gradle.kts
+++ b/app/desktop/build.gradle.kts
@@ -1,13 +1,13 @@
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.util.zip.ZipFile
+
plugins {
kotlin("jvm")
alias(libs.plugins.compose.multiplatform)
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 javafxClassifier = openjfxClassifier()
val javafxModules = listOf("base", "graphics", "controls", "media", "web", "swing")
@@ -21,13 +21,13 @@ dependencies {
} else {
implementation(compose.desktop.currentOs)
}
- implementation(compose.material3)
- implementation(compose.materialIconsExtended)
+ implementation(libs.compose.material3)
+ implementation(libs.compose.materialIconsExtended)
implementation(libs.compose.components.resources)
implementation(project(":app:shared"))
implementation(project(":utils:shared"))
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
// with the host classifier and exclude transitive stubs.
javafxModules.forEach { module ->
@@ -618,7 +618,7 @@ val exportDmgBackground = tasks.register("exportDmgBackground") {
)
}
-fun org.gradle.api.tasks.TaskContainer.registerPackageDmgTask(
+fun TaskContainer.registerPackageDmgTask(
name: String,
appBundle: Provider,
dmgOutput: Provider,
@@ -644,37 +644,37 @@ fun org.gradle.api.tasks.TaskContainer.registerPackageDmgTask(
val script = dmgScript.get().asFile
script.parentFile.mkdirs()
script.writeText(
- """
+ $$"""
#!/usr/bin/env bash
set -euo pipefail
- POSITIONS='${positions.absolutePath}'
- APP='${app.absolutePath}'
- OUT='${dmgOutput.get().asFile.absolutePath}'
- DIST='${dmgDistDir.get().asFile.absolutePath}'
- STAGING="${'$'}{TMPDIR:-/tmp}/fromchat-dmg-staging-${'$'}RANDOM"
- rm -rf "${'$'}OUT" "${'$'}STAGING"
- mkdir -p "${'$'}STAGING"
- cp -R "${'$'}APP" "${'$'}STAGING/"
- WINDOW_SIZE=($(node -e "const c=require('${'$'}POSITIONS').createDmg; console.log(c.windowSize.join(' '))"))
- 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])"))
- APPS_POS=($(node -e "const i=require('${'$'}POSITIONS').createDmg.icons.find(x=>x[0]==='Applications'); console.log(i[1], i[2])"))
+ POSITIONS='$${positions.absolutePath}'
+ APP='$${app.absolutePath}'
+ OUT='$${dmgOutput.get().asFile.absolutePath}'
+ DIST='$${dmgDistDir.get().asFile.absolutePath}'
+ STAGING="${TMPDIR:-/tmp}/fromchat-dmg-staging-$RANDOM"
+ rm -rf "$OUT" "$STAGING"
+ mkdir -p "$STAGING"
+ cp -R "$APP" "$STAGING/"
+ WINDOW_SIZE=($(node -e "const c=require('$POSITIONS').createDmg; console.log(c.windowSize.join(' '))"))
+ 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])"))
+ 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 \
--volname "FromChat" \
- --window-size "${'$'}{WINDOW_SIZE[0]}" "${'$'}{WINDOW_SIZE[1]}" \
- --icon-size "${'$'}ICON_SIZE" \
- --icon "FromChat.app" "${'$'}{APP_POS[0]}" "${'$'}{APP_POS[1]}" \
+ --window-size "${WINDOW_SIZE[0]}" "${WINDOW_SIZE[1]}" \
+ --icon-size "$ICON_SIZE" \
+ --icon "FromChat.app" "${APP_POS[0]}" "${APP_POS[1]}" \
--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 "Программы" \
--text-size 14 \
--background "dmg-background@2x.png" \
- "${'$'}OUT" \
- "${'$'}STAGING"
+ "$OUT" \
+ "$STAGING"
)
- rm -rf "${'$'}STAGING"
+ rm -rf "$STAGING"
""".trimIndent() + "\n",
)
script.setExecutable(true)
@@ -770,10 +770,10 @@ val packageLinuxAppImage = tasks.register("packageLinuxAppImage") {
val appRun = stage.resolve("AppRun")
val rel = stage.toPath().relativize(nestedExe.toPath()).toString()
appRun.writeText(
- """
+ $$"""
#!/bin/sh
- SELF="${'$'}(dirname "${'$'}(readlink -f "${'$'}0")")"
- exec "${'$'}SELF/$rel" "${'$'}@"
+ SELF="$(dirname "$(readlink -f "$0")")"
+ exec "$SELF/$$rel" "$@"
""".trimIndent() + "\n",
)
appRun.setExecutable(true)
@@ -795,16 +795,16 @@ val packageLinuxAppImage = tasks.register("packageLinuxAppImage") {
commandLine(
"bash",
"-lc",
- """
+ $$"""
set -euo pipefail
- TOOL="${'$'}{APPIMAGETOOL:-appimagetool}"
- if ! command -v "${'$'}TOOL" >/dev/null 2>&1; then
+ TOOL="${APPIMAGETOOL:-appimagetool}"
+ if ! command -v "$TOOL" >/dev/null 2>&1; then
echo "appimagetool not found. Install it or set APPIMAGETOOL." >&2
exit 1
fi
- ARCH="${'$'}(uname -m)"
+ ARCH="$(uname -m)"
export ARCH
- "${'$'}TOOL" "${stage.absolutePath}" "${out.absolutePath}"
+ "$TOOL" "$${stage.absolutePath}" "$${out.absolutePath}"
""".trimIndent(),
)
}
@@ -925,8 +925,8 @@ val windowsPrebuiltX64AppImage = layout.buildDirectory.dir("prebuilt/windows-x64
val windowsPrebuiltArm64AppImage = layout.buildDirectory.dir("prebuilt/windows-arm64/app/FromChat")
fun Exec.configureWindowsPackTask(
- appImageDir: org.gradle.api.provider.Provider?,
- setupOutput: org.gradle.api.provider.Provider,
+ appImageDir: Provider?,
+ setupOutput: Provider,
registrationId: String = "FromChat",
prebuiltOnly: Boolean = false,
) {
diff --git a/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopTrayMenu.kt b/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopTrayMenu.kt
index a1f3cba..1fe106f 100644
--- a/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopTrayMenu.kt
+++ b/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopTrayMenu.kt
@@ -1,20 +1,35 @@
package ru.fromchat.desktop
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.layout.size
+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.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -22,40 +37,58 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+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.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.rememberWindowState
import java.awt.AWTEvent
+import java.awt.Point
import java.awt.SystemTray
import java.awt.Toolkit
import java.awt.TrayIcon
import java.awt.event.AWTEventListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
+import java.awt.event.WindowEvent
import java.awt.image.BufferedImage
+import javax.swing.JFrame
import javax.swing.SwingUtilities
+import kotlin.math.roundToInt
+import ru.fromchat.ui.FromChatTheme
import ru.fromchat.ui.components.Text
-import ru.fromchat.ui.getColorScheme
import ru.fromchat.ui.isInsideWindow
+internal enum class TrayConnectionStatus {
+ Connected,
+ Connecting,
+ Disconnected,
+}
+
@Composable
internal fun DesktopTrayHost(
trayImage: BufferedImage,
tooltip: String,
statusLabel: String,
+ connectionStatus: TrayConnectionStatus,
showLabel: String,
aboutLabel: String,
quitLabel: String,
- darkTheme: Boolean,
onShow: () -> Unit,
onAbout: () -> Unit,
onQuit: () -> Unit,
) {
var menuOpen by remember { mutableStateOf(false) }
- var menuPosition by remember { mutableStateOf(WindowPosition(0.dp, 0.dp)) }
+ var menuAnchor by remember { mutableStateOf(null) }
val closeMenu = rememberUpdatedState { menuOpen = false }
val onShowState = rememberUpdatedState(onShow)
@@ -68,10 +101,7 @@ internal fun DesktopTrayHost(
fun openMenu(event: MouseEvent) {
val screen = event.locationOnScreen
SwingUtilities.invokeLater {
- menuPosition = WindowPosition(
- x = (screen.x - 220).coerceAtLeast(8).dp,
- y = (screen.y - 180).coerceAtLeast(8).dp,
- )
+ menuAnchor = screen
menuOpen = true
}
}
@@ -105,11 +135,15 @@ internal fun DesktopTrayHost(
}
if (menuOpen) {
+ val density = LocalDensity.current
+ var contentSizePx by remember { mutableStateOf(IntSize.Zero) }
+ var showWindow by remember { mutableStateOf(false) }
val windowState = rememberWindowState(
- position = menuPosition,
- width = 240.dp,
- height = 220.dp,
+ width = 220.dp,
+ height = 260.dp,
+ position = WindowPosition((-20_000).dp, (-20_000).dp),
)
+
Window(
onCloseRequest = { menuOpen = false },
state = windowState,
@@ -119,93 +153,240 @@ internal fun DesktopTrayHost(
resizable = false,
alwaysOnTop = 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)) {
- DisposableEffect(window) {
- val toolkit = Toolkit.getDefaultToolkit()
- var armed = false
- val armTimer = javax.swing.Timer(200) { armed = true }.apply {
- isRepeats = false
- start()
- }
- val dismissListener = AWTEventListener { event ->
- when (event.id) {
- MouseEvent.MOUSE_RELEASED -> armed = true
- MouseEvent.MOUSE_PRESSED -> {
- if (!armed) return@AWTEventListener
- val mouse = event as MouseEvent
- if (mouse.button != MouseEvent.BUTTON1) return@AWTEventListener
- if (mouse.isInsideWindow(window)) return@AWTEventListener
- SwingUtilities.invokeLater { closeMenu.value() }
- }
+ val awtWindow = 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()
+ var armed = false
+ val armTimer = javax.swing.Timer(200) { armed = true }.apply {
+ isRepeats = false
+ start()
+ }
+ val dismissListener = AWTEventListener { event ->
+ when (event.id) {
+ MouseEvent.MOUSE_RELEASED -> armed = true
+ MouseEvent.MOUSE_PRESSED -> {
+ if (!armed) return@AWTEventListener
+ val mouse = event as MouseEvent
+ if (mouse.button != MouseEvent.BUTTON1) return@AWTEventListener
+ if (mouse.isInsideWindow(awtWindow)) return@AWTEventListener
+ dismiss()
}
}
- toolkit.addAWTEventListener(dismissListener, AWTEvent.MOUSE_EVENT_MASK)
- onDispose {
- armTimer.stop()
- toolkit.removeAWTEventListener(dismissListener)
+ }
+ 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()
+ }
}
}
- Column(
- modifier = Modifier
- .shadow(12.dp, RoundedCornerShape(12.dp))
- .clip(RoundedCornerShape(12.dp))
- .background(MaterialTheme.colorScheme.surfaceContainer)
- .padding(vertical = 6.dp)
- .widthIn(min = 220.dp),
- ) {
- DesktopTrayMenuRow(
- text = statusLabel,
- enabled = false,
- onClick = {},
+ toolkit.addAWTEventListener(dismissListener, AWTEvent.MOUSE_EVENT_MASK)
+ awtWindow.addWindowFocusListener(focusListener)
+ onDispose {
+ armTimer.stop()
+ focusArmTimer.stop()
+ toolkit.removeAWTEventListener(dismissListener)
+ awtWindow.removeWindowFocusListener(focusListener)
+ }
+ }
+
+ 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,
)
- HorizontalDivider(
- modifier = Modifier.padding(vertical = 4.dp),
- color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f),
+ windowState.size = DpSize(
+ width = contentSizePx.width.toDp(),
+ height = contentSizePx.height.toDp(),
)
- DesktopTrayMenuRow(text = showLabel, onClick = {
- menuOpen = false
- onShow()
- })
- DesktopTrayMenuRow(text = aboutLabel, onClick = {
- menuOpen = false
- onAbout()
- })
- HorizontalDivider(
- modifier = Modifier.padding(vertical = 4.dp),
- color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f),
+ }
+ excludeFromWindowsTaskbar(awtWindow)
+ showWindow = true
+ awtWindow.isVisible = true
+ awtWindow.toFront()
+ awtWindow.requestFocus()
+ }
+
+ Box(
+ modifier = Modifier
+ .wrapContentSize(unbounded = true)
+ .onSizeChanged { size ->
+ if (size.width > 0 && size.height > 0) {
+ contentSizePx = size
+ }
+ },
+ ) {
+ FromChatTheme(darkTheme = desktopAppDarkTheme(), dynamicColor = false) {
+ val surfaceColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ SideEffect {
+ val awtColor = java.awt.Color(
+ surfaceColor.red,
+ surfaceColor.green,
+ surfaceColor.blue,
+ surfaceColor.alpha,
+ )
+ awtWindow.background = awtColor
+ (awtWindow as? JFrame)?.contentPane?.background = awtColor
+ }
+ DesktopTrayMenuContent(
+ statusLabel = statusLabel,
+ connectionStatus = connectionStatus,
+ showLabel = showLabel,
+ aboutLabel = aboutLabel,
+ quitLabel = quitLabel,
+ onShow = {
+ menuOpen = false
+ onShow()
+ },
+ onAbout = {
+ menuOpen = false
+ onAbout()
+ },
+ onQuit = {
+ menuOpen = false
+ onQuit()
+ },
)
- DesktopTrayMenuRow(text = quitLabel, onClick = {
- menuOpen = false
- 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
private fun DesktopTrayMenuRow(
text: String,
+ icon: ImageVector,
enabled: Boolean = true,
+ textColor: Color = MaterialTheme.colorScheme.onSurface,
+ iconTint: Color = textColor,
onClick: () -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
val hovered by interactionSource.collectIsHoveredAsState()
- val textColor = when {
- !enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f)
- else -> MaterialTheme.colorScheme.onSurface
+ val resolvedTextColor = when {
+ !enabled -> MaterialTheme.colorScheme.onSurfaceVariant
+ else -> textColor
+ }
+ val resolvedIconTint = when {
+ !enabled -> iconTint.copy(alpha = 0.7f)
+ else -> iconTint
}
Row(
modifier = Modifier
.fillMaxWidth()
- .clip(RoundedCornerShape(8.dp))
.background(
if (enabled && hovered) {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
} else {
- androidx.compose.ui.graphics.Color.Transparent
+ Color.Transparent
},
)
.hoverable(interactionSource, enabled = enabled)
@@ -215,13 +396,20 @@ private fun DesktopTrayMenuRow(
enabled = enabled,
onClick = onClick,
)
- .padding(horizontal = 14.dp, vertical = 10.dp),
+ .padding(horizontal = 10.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = resolvedIconTint,
+ )
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
- color = textColor,
+ color = resolvedTextColor,
)
}
}
diff --git a/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopWindowChrome.kt b/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopWindowChrome.kt
index 80d7ab5..64634d7 100644
--- a/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopWindowChrome.kt
+++ b/app/desktop/src/main/kotlin/ru/fromchat/desktop/DesktopWindowChrome.kt
@@ -1,5 +1,7 @@
package ru.fromchat.desktop
+import ru.fromchat.config.Settings
+import ru.fromchat.ui.Theme
import javax.swing.JRootPane
internal fun isMacOs(): Boolean =
@@ -14,6 +16,48 @@ internal fun isWindowsArm64(): Boolean {
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. */
internal fun applyDesktopEdgeToEdgeChrome(rootPane: JRootPane) {
if (!isMacOs()) return
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 112a40c..262a22a 100644
--- a/app/desktop/src/main/kotlin/ru/fromchat/desktop/Main.kt
+++ b/app/desktop/src/main/kotlin/ru/fromchat/desktop/Main.kt
@@ -357,12 +357,17 @@ fun main(args: Array) {
}
}
// Mirror ConnectionStateStore + WebSocketManager.isConnected (+ logged-out → disconnected).
- val wsStatusLabel = when {
+ val trayConnectionStatus = when {
connectionStatus == ConnectionStatus.CONNECTED ||
connectionStatus == ConnectionStatus.UPDATING ||
- wsLinked -> stringResource(Res.string.status_connected)
- ApiClient.token.isNullOrEmpty() -> stringResource(Res.string.status_disconnected)
- else -> stringResource(Res.string.status_connecting)
+ wsLinked -> TrayConnectionStatus.Connected
+ ApiClient.token.isNullOrEmpty() -> TrayConnectionStatus.Disconnected
+ 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() }
@@ -498,11 +503,6 @@ fun main(args: Array) {
}
if (traySupported) {
- val darkTheme = when (runCatching { Settings.theme }.getOrDefault(Theme.AsSystem)) {
- Theme.Dark -> true
- Theme.Light -> false
- Theme.AsSystem -> isSystemAppearanceDark()
- }
if (mac) {
Tray(
icon = trayIcon,
@@ -523,10 +523,10 @@ fun main(args: Array) {
trayImage = trayIconImage,
tooltip = appName,
statusLabel = wsStatusLabel,
+ connectionStatus = trayConnectionStatus,
showLabel = trayShow,
aboutLabel = aboutApp,
quitLabel = quit,
- darkTheme = darkTheme,
onShow = { showMainWindow() },
onAbout = { openAbout() },
onQuit = { exitApplication() },
@@ -690,12 +690,7 @@ fun main(args: Array) {
) {
App(onContentReady = { contentReady = true })
if (windows) {
- val darkTheme = when (runCatching { Settings.theme }.getOrDefault(Theme.AsSystem)) {
- Theme.Dark -> true
- Theme.Light -> false
- Theme.AsSystem -> isSystemAppearanceDark()
- }
- MaterialTheme(colorScheme = getColorScheme(darkTheme, dynamicColor = false)) {
+ MaterialTheme(colorScheme = getColorScheme(desktopAppDarkTheme(), dynamicColor = false)) {
WindowsDesktopTitleBar(
title = appName,
windowIcon = windowIcon,
@@ -841,29 +836,12 @@ private fun applyDesktopWindowChromeBackground() {
}
private fun desktopThemeBackgroundCompose() =
- if (
- when (runCatching { Settings.theme }.getOrDefault(Theme.AsSystem)) {
- Theme.Dark -> true
- Theme.Light -> false
- Theme.AsSystem -> isSystemAppearanceDark()
- }
- ) Color(0xFF1C1B1F) else Color(0xFFFFFBFE)
+ if (desktopAppDarkTheme()) Color(0xFF1C1B1F) else Color(0xFFFFFBFE)
private fun Color.toAwtColor() =
java.awt.Color(red, green, blue, alpha)
-private fun isSystemAppearanceDark() = runCatching {
- if (isMacOs()) {
- ProcessBuilder("defaults", "read", "-g", "AppleInterfaceStyle")
- .redirectErrorStream(true)
- .start()
- .inputStream
- .bufferedReader()
- .readText()
- .trim()
- .equals("Dark", ignoreCase = true)
- } else false
-}.getOrDefault(false)
+private fun isSystemAppearanceDark() = desktopSystemDarkTheme()
private fun applyDockIcon(image: BufferedImage?) {
if (image == null) {
diff --git a/app/desktop/src/main/kotlin/ru/fromchat/desktop/WindowsNativeChrome.kt b/app/desktop/src/main/kotlin/ru/fromchat/desktop/WindowsNativeChrome.kt
index 3d4c364..38cf794 100644
--- a/app/desktop/src/main/kotlin/ru/fromchat/desktop/WindowsNativeChrome.kt
+++ b/app/desktop/src/main/kotlin/ru/fromchat/desktop/WindowsNativeChrome.kt
@@ -51,6 +51,36 @@ internal fun updateWindowsNativeCaptionBackground(window: Window, background: Co
(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 {
val handle = (this as? ComposeWindow)?.windowHandle ?: 0L
return if (handle != 0L) {
diff --git a/app/shared/build.gradle.kts b/app/shared/build.gradle.kts
index 579e86d..f5283f0 100644
--- a/app/shared/build.gradle.kts
+++ b/app/shared/build.gradle.kts
@@ -68,7 +68,11 @@ kotlin {
android {
namespace = "ru.fromchat.shared"
minSdk = 24
- compileSdk = 37
+ compileSdk {
+ version = release(37) {
+ minorApiLevel = 2
+ }
+ }
}
jvmToolchain(17)
diff --git a/app/shared/src/androidMain/kotlin/ru/fromchat/ui/calls/CallMediaLayer.android.kt b/app/shared/src/androidMain/kotlin/ru/fromchat/ui/calls/CallMediaLayer.android.kt
index 68b4d17..288e850 100644
--- a/app/shared/src/androidMain/kotlin/ru/fromchat/ui/calls/CallMediaLayer.android.kt
+++ b/app/shared/src/androidMain/kotlin/ru/fromchat/ui/calls/CallMediaLayer.android.kt
@@ -92,10 +92,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
+import dev.chrisbanes.haze.HazeInput
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.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import io.livekit.android.RoomOptions
@@ -1221,16 +1221,11 @@ private fun PreviewTile(
innerClipShape = tileShape,
)
} else {
- val placeholderHazeStyle = HazeMaterials.thick()
Box(
Modifier
.fillMaxSize()
// Placeholder blur only for missing/disabled track.
- .hazeEffect(state = hazeState) {
- blurEffect {
- style = placeholderHazeStyle
- }
- }
+ .hazeBlur(input = HazeInput.Backdrop(hazeState), style = HazeMaterials.thick())
.background(MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = 0.55f)),
)
}
@@ -1329,16 +1324,11 @@ private fun CallInlineControlBar(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
- val controlsHazeStyle = HazeMaterials.thick()
Box(
modifier = Modifier
.fillMaxWidth(0.86f)
.clip(RoundedCornerShape(28.dp))
- .hazeEffect(state = hazeState) {
- blurEffect {
- style = controlsHazeStyle
- }
- }
+ .hazeBlur(input = HazeInput.Backdrop(hazeState), style = HazeMaterials.thick())
.background(MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.48f)),
) {
Row(
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/legal/DocumentScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/legal/DocumentScreen.kt
index 74e134d..c09af59 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/legal/DocumentScreen.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/legal/DocumentScreen.kt
@@ -54,8 +54,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.ToggleNavScrimEffect
-import dev.chrisbanes.haze.blur.blurEffect
-import dev.chrisbanes.haze.hazeEffect
+import dev.chrisbanes.haze.HazeInput
+import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import org.jetbrains.compose.resources.stringResource
@@ -118,7 +118,6 @@ fun DocumentScreen(
contentColor = MaterialTheme.colorScheme.onSurface,
contentWindowInsets = WindowInsets.navigationBars,
topBar = {
- val topBarHazeStyle = rememberChatSurfaceContainerHazeStyle()
MediumTopAppBar(
windowInsets = WindowInsets.extraStatusBars,
title = {
@@ -145,11 +144,10 @@ fun DocumentScreen(
),
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.91f))
- .hazeEffect(state = hazeState) {
- blurEffect {
- style = topBarHazeStyle
- }
- },
+ .hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = rememberChatSurfaceContainerHazeStyle(),
+ ),
)
},
) { innerPadding ->
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/AttachmentPreview.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/AttachmentPreview.kt
index dd1fe69..b053fa4 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/AttachmentPreview.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/AttachmentPreview.kt
@@ -66,9 +66,9 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
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.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.Dispatchers
@@ -784,11 +784,10 @@ private fun ChatImageTileContent(
Box(
modifier = Modifier
.fillMaxSize()
- .hazeEffect(state = thumbHazeState) {
- blurEffect {
- style = thumbOverlayHazeStyle
- }
- },
+ .hazeBlur(
+ input = HazeInput.Backdrop(thumbHazeState),
+ style = thumbOverlayHazeStyle,
+ ),
)
}
}
@@ -917,14 +916,9 @@ private fun DownloadCancelledImageOverlay(
modifier: Modifier = Modifier,
) {
val scrim = MaterialTheme.colorScheme.scrim.copy(alpha = 0.38f)
- val overlayHazeStyle = HazeMaterials.thin()
Box(
modifier = modifier
- .hazeEffect {
- blurEffect {
- style = overlayHazeStyle
- }
- }
+ .hazeBlur(input = HazeInput.Content, style = HazeMaterials.thin())
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.12f)),
contentAlignment = Alignment.Center,
) {
@@ -964,17 +958,12 @@ private fun UploadingImageOverlay(
onCancelUpload: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
- val overlayHazeStyle = HazeMaterials.thin()
Box(modifier = modifier) {
Box(
modifier = Modifier
.matchParentSize()
.clip(clipShape)
- .hazeEffect {
- blurEffect {
- style = overlayHazeStyle
- }
- }
+ .hazeBlur(input = HazeInput.Content, style = HazeMaterials.thin())
) {
when {
previewBitmap != null -> {
@@ -1259,15 +1248,10 @@ private fun CorruptedImagePlaceholder(
modifier: Modifier = Modifier,
clipShape: RoundedCornerShape = attachmentImageCornerShape(isAuthor = false),
) {
- val overlayHazeStyle = HazeMaterials.thin()
Box(
modifier = modifier
.clip(clipShape)
- .hazeEffect {
- blurEffect {
- style = overlayHazeStyle
- }
- },
+ .hazeBlur(input = HazeInput.Content, style = HazeMaterials.thin()),
contentAlignment = Alignment.Center,
) {
Icon(
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatInput.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatInput.kt
index 9e6b15c..6079c1a 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatInput.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatInput.kt
@@ -93,9 +93,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.pr0gramm3r101.utils.conditional
+import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeState
-import dev.chrisbanes.haze.blur.blurEffect
-import dev.chrisbanes.haze.hazeEffect
+import dev.chrisbanes.haze.blur.hazeBlur
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
@@ -323,7 +323,7 @@ fun ChatInput(
attachmentDropBridge: AttachmentDropBridge,
pendingDropUris: List = emptyList(),
) {
- val composerHazeStyle = rememberChatSurfaceContainerHazeStyle()
+ val composerHazeStyle = rememberChatSurfaceContainerHazeStyle().then { blurEnabled(hazeBlurEnabled) }
val dropScrimColor = lerp(MaterialTheme.colorScheme.primary, Color.Black, 0.62f)
val attachmentDropEnabled = supportsAttachments && !isReadOnly
val dropHighlightActive =
@@ -450,12 +450,7 @@ fun ChatInput(
modifier = Modifier
.fillMaxWidth()
.clip(pillShape)
- .hazeEffect(state = hazeState) {
- blurEffect {
- blurEnabled = hazeBlurEnabled
- style = composerHazeStyle
- }
- }
+ .hazeBlur(input = HazeInput.Backdrop(hazeState), style = composerHazeStyle)
.clickable(enabled = true) { onReadOnlyMessageClick() },
) {
Text(
@@ -499,12 +494,7 @@ fun ChatInput(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainer, pillShape)
- .hazeEffect(state = hazeState) {
- blurEffect {
- blurEnabled = hazeBlurEnabled
- style = composerHazeStyle
- }
- }
+ .hazeBlur(input = HazeInput.Backdrop(hazeState), style = composerHazeStyle)
.conditional(dropBlurRadius > 0.dp) {
blur(dropBlurRadius, BlurredEdgeTreatment.Unbounded)
},
@@ -753,15 +743,12 @@ fun ChatInput(
modifier = Modifier
.fillMaxSize()
.background(actionBackground)
- .hazeEffect(state = hazeState) {
- blurEffect {
- // Keep the node in the tree in both directions; fading alpha avoids the
- // frosted layer popping on over the color when send -> voice.
- blurEnabled = hazeBlurEnabled
- style = composerHazeStyle
- alpha = hazeOverlayAlpha
- }
- },
+ .hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ // Keep the node in the tree in both directions; fading alpha avoids the
+ // frosted layer popping on over the color when send -> voice.
+ style = composerHazeStyle.then { alpha(hazeOverlayAlpha) },
+ ),
)
AnimatedContent(
targetState = canSend,
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt
index a61d618..3aa635b 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt
@@ -81,11 +81,11 @@ import ru.fromchat.chat_date_yesterday
import ru.fromchat.utils.rememberRegistrationDateFormatStrings
import com.pr0gramm3r101.utils.resetFocus
import com.pr0gramm3r101.utils.supportClipboardManagerImpl
+import dev.chrisbanes.haze.HazeInput
+import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
-import dev.chrisbanes.haze.blur.HazeProgressive
-import dev.chrisbanes.haze.blur.blurEffect
+import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials
-import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
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”.
contentWindowInsets = WindowInsets.navigationBars,
bottomBar = {
- val bottomBarHazeStyle = HazeMaterials.thin()
Column(
modifier = Modifier
.windowInsetsPadding(WindowInsets.ime)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainer)
- .hazeEffect(state = hazeState) {
- blurEffect {
- blurEnabled = hazeBlurEnabled
- style = bottomBarHazeStyle
- progressive = HazeProgressive.verticalGradient(
- startIntensity = 0f,
- endIntensity = 1f,
+ .hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = HazeMaterials.thin().then {
+ blurEnabled(hazeBlurEnabled)
+ progressive(
+ HazeProgressive.verticalGradient(
+ startIntensity = 0f,
+ endIntensity = 1f,
+ ),
)
- }
- }
+ },
+ )
) {
if (peerDeleted && dmRecipientId != null) {
Box(
@@ -1726,7 +1727,6 @@ private fun ChatScrollToBottomButton(
modifier: Modifier = Modifier,
hazeBlurEnabled: Boolean = true,
) {
- val hazeStyle = rememberChatSurfaceContainerHazeStyle()
Box(
modifier = modifier
.size(ChatScrollToBottomButtonSize)
@@ -1740,12 +1740,10 @@ private fun ChatScrollToBottomButton(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.91f))
- .hazeEffect(state = hazeState) {
- blurEffect {
- blurEnabled = hazeBlurEnabled
- style = hazeStyle
- }
- },
+ .hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = rememberChatSurfaceContainerHazeStyle().then { blurEnabled(hazeBlurEnabled) },
+ ),
)
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
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 434ca09..ff55f93 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
@@ -62,13 +62,13 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.conditional
+import dev.chrisbanes.haze.HazeInput
+import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
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.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials
-import dev.chrisbanes.haze.hazeEffect
import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res
import ru.fromchat.chat_members_count
@@ -397,7 +397,6 @@ fun ChatTopBar(
val layoutHeight = topBarPlaceable.height + arcExtentPx
val bgPlaceable = subcompose("background") {
- val hazeStyle = rememberChatSurfaceContainerHazeStyle()
Box(
modifier = Modifier
.fillMaxSize()
@@ -407,12 +406,10 @@ fun ChatTopBar(
}
)
.background(MaterialTheme.colorScheme.surfaceContainer)
- .hazeEffect(state = hazeState) {
- blurEffect {
- blurEnabled = hazeBlurEnabled
- style = hazeStyle
- }
- },
+ .hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = rememberChatSurfaceContainerHazeStyle().then { blurEnabled(hazeBlurEnabled) },
+ ),
)
}.first().measure(Constraints.fixed(layoutWidth, layoutHeight))
@@ -443,8 +440,7 @@ private fun ChatTopBarPill(
hazeBlurEnabled: Boolean = true,
showBackButton: Boolean = true,
) {
- val hazeStyle = rememberChatSurfaceContainerHazeStyle()
- val progressiveStripHazeStyle = HazeMaterials.thin()
+ val hazeStyle = rememberChatSurfaceContainerHazeStyle().then { blurEnabled(hazeBlurEnabled) }
val chromeColor = MaterialTheme.colorScheme.surfaceContainer
val pillShape = RoundedCornerShape(28.dp)
@@ -453,16 +449,18 @@ private fun ChatTopBarPill(
Box(
modifier = Modifier
.matchParentSize()
- .hazeEffect(state = hazeState) {
- blurEffect {
- blurEnabled = hazeBlurEnabled
- style = progressiveStripHazeStyle
- progressive = HazeProgressive.verticalGradient(
- startIntensity = 1f,
- endIntensity = 0f,
+ .hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = HazeMaterials.thin().then {
+ blurEnabled(hazeBlurEnabled)
+ progressive(
+ HazeProgressive.verticalGradient(
+ startIntensity = 1f,
+ endIntensity = 0f,
+ ),
)
- }
- },
+ },
+ ),
)
Row(
modifier = Modifier
@@ -478,12 +476,7 @@ private fun ChatTopBarPill(
.size(48.dp)
.clip(CircleShape)
.background(chromeColor)
- .hazeEffect(state = hazeState) {
- blurEffect {
- blurEnabled = hazeBlurEnabled
- style = hazeStyle
- }
- },
+ .hazeBlur(input = HazeInput.Backdrop(hazeState), style = hazeStyle),
contentAlignment = Alignment.Center,
) {
IconButton(onClick = onBack) {
@@ -499,12 +492,7 @@ private fun ChatTopBarPill(
.weight(1f)
.background(chromeColor, pillShape)
.clip(pillShape)
- .hazeEffect(state = hazeState) {
- blurEffect {
- blurEnabled = hazeBlurEnabled
- style = hazeStyle
- }
- }
+ .hazeBlur(input = HazeInput.Backdrop(hazeState), style = hazeStyle)
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -592,13 +580,17 @@ fun rememberChatSurfaceContainerHazeStyle(): HazeBlurStyle {
val surface = MaterialTheme.colorScheme.surfaceContainer
return remember(surface) {
- HazeBlurStyle(
- blurRadius = 24.dp,
- backgroundColor = surface,
- colorEffect = HazeColorEffect.tint(
- surface.copy(alpha = if (surface.luminance() >= 0.5f) 0.74f else 0.79f),
- ),
- )
+ HazeBlurStyle {
+ blurRadius(24.dp)
+ backgroundColor(surface)
+ colorEffects(
+ listOf(
+ HazeColorEffect.tint(
+ surface.copy(alpha = if (surface.luminance() >= 0.5f) 0.74f else 0.79f),
+ ),
+ ),
+ )
+ }
}
}
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/ExpressiveStepFlow.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/ExpressiveStepFlow.kt
index c689882..238103e 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/ExpressiveStepFlow.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/ExpressiveStepFlow.kt
@@ -83,11 +83,11 @@ import androidx.compose.ui.zIndex
import androidx.graphics.shapes.Morph
import androidx.graphics.shapes.RoundedPolygon
import com.pr0gramm3r101.utils.LastAnchoredBottomArrangement
+import dev.chrisbanes.haze.HazeInput
+import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
-import dev.chrisbanes.haze.blur.HazeProgressive
-import dev.chrisbanes.haze.blur.blurEffect
+import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials
-import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.CoroutineScope
@@ -551,7 +551,6 @@ fun ExpressiveStepFlowScaffold(
@Composable
fun HazeTopBar(hazeState: HazeState) {
- val topBarHazeStyle = HazeMaterials.thin()
TopAppBar(
windowInsets = topBarWindowInsets,
title = {},
@@ -570,15 +569,17 @@ fun ExpressiveStepFlowScaffold(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
),
- modifier = Modifier.hazeEffect(state = hazeState) {
- blurEffect {
- style = topBarHazeStyle
- progressive = HazeProgressive.verticalGradient(
- startIntensity = 1f,
- endIntensity = 0f,
+ modifier = Modifier.hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = HazeMaterials.thin().then {
+ progressive(
+ HazeProgressive.verticalGradient(
+ startIntensity = 1f,
+ endIntensity = 0f,
+ ),
)
- }
- },
+ },
+ ),
)
}
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/HazeActionButton.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/HazeActionButton.kt
index 4961a5e..3315954 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/HazeActionButton.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/HazeActionButton.kt
@@ -15,12 +15,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import dev.chrisbanes.haze.HazeInput
+import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeBlurStyle
-import dev.chrisbanes.haze.blur.HazeProgressive
-import dev.chrisbanes.haze.blur.blurEffect
+import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials
-import dev.chrisbanes.haze.hazeEffect
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
@Composable
@@ -31,28 +31,17 @@ fun HazeBottomBar(
baseColor: Color? = null,
content: @Composable () -> Unit,
) {
- val resolvedStyle = hazeStyle ?: HazeMaterials.thin()
- val effectModifier = if (hazeStyle != null) {
- Modifier.hazeEffect(state = hazeState) {
- blurEffect {
- style = resolvedStyle
- progressive = HazeProgressive.verticalGradient(
+ val effectModifier = Modifier.hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = (hazeStyle ?: HazeMaterials.thin()).then {
+ progressive(
+ HazeProgressive.verticalGradient(
startIntensity = 0f,
endIntensity = 1f,
- )
- }
- }
- } else {
- Modifier.hazeEffect(state = hazeState) {
- blurEffect {
- style = resolvedStyle
- progressive = HazeProgressive.verticalGradient(
- startIntensity = 0f,
- endIntensity = 1f,
- )
- }
- }
- }
+ ),
+ )
+ },
+ )
Column(
modifier = Modifier
.windowInsetsPadding(WindowInsets.ime)
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 4c52ba3..42f0e44 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
@@ -51,11 +51,11 @@ import androidx.navigation.NavController
import com.pr0gramm3r101.utils.WindowWidthSizeClass
import com.pr0gramm3r101.utils.currentWindowAdaptiveInfo
import com.pr0gramm3r101.utils.widthSizeClass
+import dev.chrisbanes.haze.HazeInput
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeBlurStyle
import dev.chrisbanes.haze.blur.HazeColorEffect
-import dev.chrisbanes.haze.blur.blurEffect
-import dev.chrisbanes.haze.hazeEffect
+import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.CoroutineScope
@@ -343,11 +343,7 @@ fun MainScreen(
}
}
.background(MaterialTheme.colorScheme.surfaceContainer)
- .hazeEffect(state = navBarHazeState) {
- blurEffect {
- style = navBarHazeStyle
- }
- },
+ .hazeBlur(input = HazeInput.Backdrop(navBarHazeState), style = navBarHazeStyle),
) {
NavigationBar(
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
// the sharp row/menu, blur them, and intercept dismiss taps.
val surface = MaterialTheme.colorScheme.surfaceContainerLowest
+ val tint = HazeColorEffect.tint(surface.copy(alpha = 0.4f * progress))
Box(
modifier = modifier
.fillMaxSize()
- .hazeEffect(state = hazeState) {
- blurEffect {
- style = HazeBlurStyle(
- blurRadius = blurRadius,
- backgroundColor = surface,
- colorEffects = listOf(
- HazeColorEffect.tint(surface.copy(alpha = 0.4f * progress)),
- ),
- noiseFactor = 0f,
- fallbackColorEffect = HazeColorEffect.tint(surface.copy(alpha = 0.4f * progress)),
- )
- }
- },
+ .hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = HazeBlurStyle {
+ blurRadius(blurRadius)
+ backgroundColor(surface)
+ colorEffects(listOf(tint))
+ noiseFactor(0f)
+ fallbackColorEffect(tint)
+ },
+ ),
)
}
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 33c193b..fa92495 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
@@ -58,10 +58,10 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.ui.graphics.Color
+import dev.chrisbanes.haze.HazeInput
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.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import androidx.compose.runtime.DisposableEffect
@@ -360,16 +360,13 @@ private fun ChatsTopBarHazeBackdrop(
blurEnabled: Boolean,
modifier: Modifier = Modifier,
) {
- val topBarHazeStyle = HazeMaterials.thin()
Box(
modifier = modifier
.graphicsLayer { alpha = blurReveal.coerceIn(0f, 1f) }
- .hazeEffect(state = hazeState) {
- blurEffect {
- this.blurEnabled = blurEnabled
- style = topBarHazeStyle
- }
- },
+ .hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = HazeMaterials.thin().then { blurEnabled(blurEnabled) },
+ ),
)
}
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/DevicesScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/DevicesScreen.kt
index 14fee7e..bffe195 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/DevicesScreen.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/DevicesScreen.kt
@@ -70,10 +70,10 @@ import org.jetbrains.compose.resources.stringResource
import com.pr0gramm3r101.components.Category
import com.pr0gramm3r101.components.ListItem
import com.pr0gramm3r101.utils.currentDeviceInfo
-import dev.chrisbanes.haze.blur.HazeProgressive
-import dev.chrisbanes.haze.blur.blurEffect
+import dev.chrisbanes.haze.HazeInput
+import dev.chrisbanes.haze.HazeProgressive
+import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials
-import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import io.ktor.client.network.sockets.ConnectTimeoutException
@@ -435,7 +435,6 @@ fun DevicesScreen(onBack: () -> Unit) {
contentWindowInsets = WindowInsets.navigationBars,
snackbarHost = { FromChatSnackbarHost(hostState = snackbarHostState) },
topBar = {
- val topBarHazeStyle = HazeMaterials.thin()
TopAppBar(
windowInsets = settingsDetailWindowInsets(),
title = {},
@@ -450,15 +449,17 @@ fun DevicesScreen(onBack: () -> Unit) {
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
),
- modifier = Modifier.hazeEffect(state = hazeState) {
- blurEffect {
- style = topBarHazeStyle
- progressive = HazeProgressive.verticalGradient(
- startIntensity = 1f,
- endIntensity = 0f,
+ modifier = Modifier.hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = HazeMaterials.thin().then {
+ progressive(
+ HazeProgressive.verticalGradient(
+ startIntensity = 1f,
+ endIntensity = 0f,
+ ),
)
- }
- }
+ },
+ ),
)
},
bottomBar = {
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/server/ServerConfigScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/server/ServerConfigScreen.kt
index 7d84a83..0df4a7e 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/server/ServerConfigScreen.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/server/ServerConfigScreen.kt
@@ -67,10 +67,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.navigateAndWipeBackStack
import com.pr0gramm3r101.utils.toDp
-import dev.chrisbanes.haze.blur.HazeProgressive
-import dev.chrisbanes.haze.blur.blurEffect
+import dev.chrisbanes.haze.HazeInput
+import dev.chrisbanes.haze.HazeProgressive
+import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials
-import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.Dispatchers
@@ -339,7 +339,6 @@ fun ServerConfigScreen() {
}
},
topBar = {
- val topBarHazeStyle = HazeMaterials.thin()
TopAppBar(
windowInsets = settingsDetailWindowInsets(),
title = {},
@@ -357,15 +356,17 @@ fun ServerConfigScreen() {
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
),
- modifier = Modifier.hazeEffect(state = actionHazeState) {
- blurEffect {
- style = topBarHazeStyle
- progressive = HazeProgressive.verticalGradient(
- startIntensity = 1f,
- endIntensity = 0f,
+ modifier = Modifier.hazeBlur(
+ input = HazeInput.Backdrop(actionHazeState),
+ style = HazeMaterials.thin().then {
+ progressive(
+ HazeProgressive.verticalGradient(
+ startIntensity = 1f,
+ endIntensity = 0f,
+ ),
)
- }
- },
+ },
+ ),
)
}
) { innerPadding ->
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/profile/EditProfileScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/profile/EditProfileScreen.kt
index a799067..ad8f0e1 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/profile/EditProfileScreen.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/profile/EditProfileScreen.kt
@@ -47,10 +47,10 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
-import dev.chrisbanes.haze.blur.HazeProgressive
-import dev.chrisbanes.haze.blur.blurEffect
+import dev.chrisbanes.haze.HazeInput
+import dev.chrisbanes.haze.HazeProgressive
+import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials
-import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import io.ktor.client.call.body
@@ -339,7 +339,6 @@ fun EditProfileScreen(
}
},
topBar = {
- val topBarHazeStyle = HazeMaterials.thin()
TopAppBar(
title = { Text(stringResource(Res.string.profile_edit_title)) },
navigationIcon = {
@@ -367,15 +366,17 @@ fun EditProfileScreen(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
),
- modifier = Modifier.hazeEffect(state = hazeState) {
- blurEffect {
- style = topBarHazeStyle
- progressive = HazeProgressive.verticalGradient(
- startIntensity = 1f,
- endIntensity = 0f,
+ modifier = Modifier.hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = HazeMaterials.thin().then {
+ progressive(
+ HazeProgressive.verticalGradient(
+ startIntensity = 1f,
+ endIntensity = 0f,
+ ),
)
- }
- },
+ },
+ ),
)
},
) { innerPadding ->
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/profile/ProfileScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/profile/ProfileScreen.kt
index d81820a..634d4b9 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/profile/ProfileScreen.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/profile/ProfileScreen.kt
@@ -37,11 +37,11 @@ import androidx.compose.ui.unit.Dp
import androidx.navigation.NavController
import com.pr0gramm3r101.components.ListItemPosition
import com.pr0gramm3r101.utils.SupportClipboardManager
+import dev.chrisbanes.haze.HazeInput
+import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
-import dev.chrisbanes.haze.blur.HazeProgressive
-import dev.chrisbanes.haze.blur.blurEffect
+import dev.chrisbanes.haze.blur.hazeBlur
import dev.chrisbanes.haze.blur.materials.HazeMaterials
-import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.CoroutineScope
@@ -2058,21 +2058,22 @@ private fun ProfileFloatingBackBar(
) {
if (!visible) return
- val backBarHazeStyle = HazeMaterials.thin()
Box(modifier = modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(blurHeight)
- .hazeEffect(state = hazeState) {
- blurEffect {
- style = backBarHazeStyle
- progressive = HazeProgressive.verticalGradient(
- startIntensity = 1f,
- endIntensity = 0f,
+ .hazeBlur(
+ input = HazeInput.Backdrop(hazeState),
+ style = HazeMaterials.thin().then {
+ progressive(
+ HazeProgressive.verticalGradient(
+ startIntensity = 1f,
+ endIntensity = 0f,
+ ),
)
- }
- },
+ },
+ ),
)
IconButton(
onClick = onBack,
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4516d29..f9b1a71 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,19 +1,20 @@
[versions]
-agp = "9.3.1"
+agp = "9.4.0"
androidx-activityCompose = "1.13.0"
-androidx-appcompat = "1.7.1"
+androidx-appcompat = "1.8.0"
androidx-core-ktx = "1.19.0"
androidx-exifinterface = "1.4.2"
-coilCompose = "3.5.0"
-compose-multiplatform = "1.12.0-beta02"
+coilCompose = "3.6.2"
+compose-multiplatform = "1.12.0"
#noinspection NewerVersionAvailable
constraintlayout = "0.8.0-shaded"
coreSplashscreen = "1.2.0"
-firebaseMessaging = "25.1.1"
+firebaseMessaging = "25.1.3"
googleServices = "4.5.0"
-haze = "2.0.0-alpha03"
-kotlin = "2.4.10"
-adaptiveAndroid = "1.2.0"
+haze = "2.0.0-beta03"
+jnaPlatform = "5.19.1"
+kotlin = "2.4.20"
+adaptiveAndroid = "1.3.0"
biometric = "1.4.0-alpha07"
gson = "2.14.0"
kotlinxIoBytestring = "0.9.1"
@@ -21,33 +22,34 @@ kotlinxCoroutinesCore = "1.11.0"
kotlinxIoCore = "0.9.1"
multiplatformCryptoLibsodiumBindings = "0.9.5"
multiplatformSettings = "1.3.0"
-serialization = "2.4.10"
+serialization = "2.4.20"
serialization-json = "1.11.0"
material = "1.14.0"
activityKtx = "1.13.0"
navigationCompose = "2.9.2"
datastore = "1.2.1"
security-crypto = "1.1.0"
-ktor = "3.5.1"
+ktor = "3.5.2"
slf4j = "1.7.36"
kotlinxDatetime = "0.8.0"
lifecycleRuntimeKtx = "2.11.0"
-composeBom = "2026.06.01"
+composeBom = "2026.09.00"
composeMaterialIconsExtended = "1.7.3"
composeMaterial3 = "1.12.0-alpha03"
-composeComponents = "1.12.0-beta02"
-playServicesBase = "18.10.0"
+composeComponents = "1.12.0"
+playServicesBase = "18.10.1"
tweetnaclJava = "1.1.3"
androidxWork = "2.11.2"
cryptography-kotlin = "0.6.0"
krypto = "4.0.10"
sqldelight = "2.3.2"
-livekitAndroid = "2.27.0"
-livekitAndroidComposeComponents = "2.4.0"
-markdownRendererM3 = "0.43.0"
-bouncycastle = "1.85"
-webkit = "1.16.0"
-openjfx = "21.0.6"
+livekitAndroid = "2.28.2"
+livekitAndroidComposeComponents = "2.4.2"
+markdownRendererM3 = "0.45.0"
+bouncycastle = "1.85.2"
+webkit = "1.17.0"
+openjfx = "22.0.2"
+
[libraries]
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" }
@@ -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-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" }
+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" }
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" }
@@ -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-darwin = { module = "io.ktor:ktor-client-darwin", 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" }
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" }
@@ -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" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" }
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
+#noinspection UnusedVersionCatalogEntry
openjfx-controls = { module = "org.openjfx:javafx-controls", version.ref = "openjfx" }
+#noinspection UnusedVersionCatalogEntry
openjfx-web = { module = "org.openjfx:javafx-web", version.ref = "openjfx" }
+#noinspection UnusedVersionCatalogEntry
openjfx-swing = { module = "org.openjfx:javafx-swing", version.ref = "openjfx" }
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" }
sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
diff --git a/utils/shared/build.gradle.kts b/utils/shared/build.gradle.kts
index a3bbb6f..1521dde 100644
--- a/utils/shared/build.gradle.kts
+++ b/utils/shared/build.gradle.kts
@@ -14,7 +14,11 @@ kotlin {
android {
namespace = "com.pr0gramm3r101.utils"
- compileSdk = 37
+ compileSdk {
+ version = release(37) {
+ minorApiLevel = 2
+ }
+ }
minSdk = 24
}