From 332c5a6600f0e284911bd4ed08c9cf166c8ac895 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Fri, 11 Sep 2026 16:30:33 +0300 Subject: [PATCH] Fix attachments and tray menu on Windows --- .idea/kotlinc.xml | 2 +- app/android/build.gradle.kts | 6 +- app/desktop/build.gradle.kts | 78 ++-- .../ru/fromchat/desktop/DesktopTrayMenu.kt | 334 ++++++++++++++---- .../fromchat/desktop/DesktopWindowChrome.kt | 44 +++ .../main/kotlin/ru/fromchat/desktop/Main.kt | 48 +-- .../fromchat/desktop/WindowsNativeChrome.kt | 30 ++ app/shared/build.gradle.kts | 6 +- .../ui/calls/CallMediaLayer.android.kt | 18 +- .../ru/fromchat/legal/DocumentScreen.kt | 14 +- .../ru/fromchat/ui/chat/AttachmentPreview.kt | 34 +- .../kotlin/ru/fromchat/ui/chat/ChatInput.kt | 35 +- .../kotlin/ru/fromchat/ui/chat/ChatScreen.kt | 38 +- .../kotlin/ru/fromchat/ui/chat/ChatTopBar.kt | 72 ++-- .../ui/components/ExpressiveStepFlow.kt | 25 +- .../ui/components/HazeActionButton.kt | 35 +- .../kotlin/ru/fromchat/ui/main/MainScreen.kt | 34 +- .../ru/fromchat/ui/main/chats/ChatsTab.kt | 15 +- .../ui/main/settings/DevicesScreen.kt | 25 +- .../settings/server/ServerConfigScreen.kt | 25 +- .../fromchat/ui/profile/EditProfileScreen.kt | 25 +- .../ru/fromchat/ui/profile/ProfileScreen.kt | 25 +- gradle/libs.versions.toml | 45 +-- utils/shared/build.gradle.kts | 6 +- 24 files changed, 605 insertions(+), 414 deletions(-) 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 }