Fix attachments and tray menu on Windows

This commit is contained in:
2026-09-11 16:30:33 +03:00
Unverified
parent cf8ea8bbce
commit 332c5a6600
24 changed files with 605 additions and 414 deletions
+1 -1
View File
@@ -2,6 +2,6 @@
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.4.10" />
<option name="version" value="2.4.20" />
</component>
</project>
+5 -1
View File
@@ -56,7 +56,11 @@ val fixComposeResourcesStructure = tasks.register<FixComposeResTask>("fixCompose
extensions.configure<ApplicationExtension> {
namespace = "ru.fromchat"
compileSdk = 37
compileSdk {
version = release(37) {
minorApiLevel = 2
}
}
defaultConfig {
applicationId = "ru.fromchat"
+39 -39
View File
@@ -1,13 +1,13 @@
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.zip.ZipFile
plugins {
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<Exec>("exportDmgBackground") {
)
}
fun org.gradle.api.tasks.TaskContainer.registerPackageDmgTask(
fun TaskContainer.registerPackageDmgTask(
name: String,
appBundle: Provider<Directory>,
dmgOutput: Provider<RegularFile>,
@@ -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<Exec>("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<Exec>("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<org.gradle.api.file.Directory>?,
setupOutput: org.gradle.api.provider.Provider<org.gradle.api.file.RegularFile>,
appImageDir: Provider<Directory>?,
setupOutput: Provider<RegularFile>,
registrationId: String = "FromChat",
prebuiltOnly: Boolean = false,
) {
@@ -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<Point?>(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,9 +153,29 @@ 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 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 {
@@ -135,77 +189,204 @@ internal fun DesktopTrayHost(
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() }
if (mouse.isInsideWindow(awtWindow)) return@AWTEventListener
dismiss()
}
}
}
var focusDismissArmed = false
val focusArmTimer = javax.swing.Timer(250) { focusDismissArmed = true }.apply {
isRepeats = false
start()
}
val focusListener = object : java.awt.event.WindowFocusListener {
override fun windowGainedFocus(event: WindowEvent) = Unit
override fun windowLostFocus(event: WindowEvent) {
if (focusDismissArmed) {
dismiss()
}
}
}
toolkit.addAWTEventListener(dismissListener, AWTEvent.MOUSE_EVENT_MASK)
awtWindow.addWindowFocusListener(focusListener)
onDispose {
armTimer.stop()
focusArmTimer.stop()
toolkit.removeAWTEventListener(dismissListener)
awtWindow.removeWindowFocusListener(focusListener)
}
}
Column(
LaunchedEffect(contentSizePx, menuAnchor) {
val anchor = menuAnchor
if (anchor == null || contentSizePx.width < 80 || contentSizePx.height < 40) {
showWindow = false
awtWindow.isVisible = false
return@LaunchedEffect
}
val scale = density.density
val awtWidth = (contentSizePx.width / scale).roundToInt().coerceAtLeast(1)
val awtHeight = (contentSizePx.height / scale).roundToInt().coerceAtLeast(1)
val screen = awtWindow.graphicsConfiguration.bounds
val x = (anchor.x - awtWidth).coerceIn(
screen.x,
(screen.x + screen.width - awtWidth).coerceAtLeast(screen.x),
)
val y = (anchor.y - awtHeight).coerceIn(
screen.y,
(screen.y + screen.height - awtHeight).coerceAtLeast(screen.y),
)
awtWindow.setSize(awtWidth, awtHeight)
awtWindow.setLocation(x, y)
with(density) {
windowState.position = WindowPosition(
x = (x / density.density).dp,
y = (y / density.density).dp,
)
windowState.size = DpSize(
width = contentSizePx.width.toDp(),
height = contentSizePx.height.toDp(),
)
}
excludeFromWindowsTaskbar(awtWindow)
showWindow = true
awtWindow.isVisible = true
awtWindow.toFront()
awtWindow.requestFocus()
}
Box(
modifier = Modifier
.shadow(12.dp, RoundedCornerShape(12.dp))
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(vertical = 6.dp)
.widthIn(min = 220.dp),
.wrapContentSize(unbounded = true)
.onSizeChanged { size ->
if (size.width > 0 && size.height > 0) {
contentSizePx = size
}
},
) {
DesktopTrayMenuRow(
text = statusLabel,
enabled = false,
onClick = {},
FromChatTheme(darkTheme = desktopAppDarkTheme(), dynamicColor = false) {
val surfaceColor = MaterialTheme.colorScheme.surfaceContainerHigh
SideEffect {
val awtColor = java.awt.Color(
surfaceColor.red,
surfaceColor.green,
surfaceColor.blue,
surfaceColor.alpha,
)
HorizontalDivider(
modifier = Modifier.padding(vertical = 4.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f),
)
DesktopTrayMenuRow(text = showLabel, onClick = {
awtWindow.background = awtColor
(awtWindow as? JFrame)?.contentPane?.background = awtColor
}
DesktopTrayMenuContent(
statusLabel = statusLabel,
connectionStatus = connectionStatus,
showLabel = showLabel,
aboutLabel = aboutLabel,
quitLabel = quitLabel,
onShow = {
menuOpen = false
onShow()
})
DesktopTrayMenuRow(text = aboutLabel, onClick = {
},
onAbout = {
menuOpen = false
onAbout()
})
HorizontalDivider(
modifier = Modifier.padding(vertical = 4.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f),
)
DesktopTrayMenuRow(text = quitLabel, onClick = {
},
onQuit = {
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,
)
}
}
@@ -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
@@ -357,12 +357,17 @@ fun main(args: Array<String>) {
}
}
// 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<String>) {
}
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<String>) {
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<String>) {
) {
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) {
@@ -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) {
+5 -1
View File
@@ -68,7 +68,11 @@ kotlin {
android {
namespace = "ru.fromchat.shared"
minSdk = 24
compileSdk = 37
compileSdk {
version = release(37) {
minorApiLevel = 2
}
}
}
jvmToolchain(17)
@@ -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(
@@ -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 ->
@@ -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(
@@ -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<String> = 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 {
.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.
blurEnabled = hazeBlurEnabled
style = composerHazeStyle
alpha = hazeOverlayAlpha
}
},
style = composerHazeStyle.then { alpha(hazeOverlayAlpha) },
),
)
AnimatedContent(
targetState = canSend,
@@ -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(
.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,
@@ -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(
.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,14 +580,18 @@ fun rememberChatSurfaceContainerHazeStyle(): HazeBlurStyle {
val surface = MaterialTheme.colorScheme.surfaceContainer
return remember(surface) {
HazeBlurStyle(
blurRadius = 24.dp,
backgroundColor = surface,
colorEffect = HazeColorEffect.tint(
HazeBlurStyle {
blurRadius(24.dp)
backgroundColor(surface)
colorEffects(
listOf(
HazeColorEffect.tint(
surface.copy(alpha = if (surface.luminance() >= 0.5f) 0.74f else 0.79f),
),
),
)
}
}
}
/**
@@ -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(
modifier = Modifier.hazeBlur(
input = HazeInput.Backdrop(hazeState),
style = HazeMaterials.thin().then {
progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 0f,
),
)
}
},
),
)
}
@@ -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)
@@ -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)
},
),
)
}
@@ -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) },
),
)
}
@@ -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(
modifier = Modifier.hazeBlur(
input = HazeInput.Backdrop(hazeState),
style = HazeMaterials.thin().then {
progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 0f,
),
)
}
}
},
),
)
},
bottomBar = {
@@ -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(
modifier = Modifier.hazeBlur(
input = HazeInput.Backdrop(actionHazeState),
style = HazeMaterials.thin().then {
progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 0f,
),
)
}
},
),
)
}
) { innerPadding ->
@@ -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(
modifier = Modifier.hazeBlur(
input = HazeInput.Backdrop(hazeState),
style = HazeMaterials.thin().then {
progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 0f,
),
)
}
},
),
)
},
) { innerPadding ->
@@ -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(
.hazeBlur(
input = HazeInput.Backdrop(hazeState),
style = HazeMaterials.thin().then {
progressive(
HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 0f,
),
)
}
},
),
)
IconButton(
onClick = onBack,
+25 -20
View File
@@ -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" }
+5 -1
View File
@@ -14,7 +14,11 @@ kotlin {
android {
namespace = "com.pr0gramm3r101.utils"
compileSdk = 37
compileSdk {
version = release(37) {
minorApiLevel = 2
}
}
minSdk = 24
}