mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-27 10:11:48 -04:00
feat: enhance desktop notification styling and taskbar integration
- Implement native taskbar and dock icon support using the AWT `Taskbar` API in the desktop entry point. - Refactor notification icon resolution to use URI strings directly, removing the need to copy resources to temporary files. - Introduce a notification styling mechanism that automatically prefixes notification titles based on their category (e.g., "Message", "Node", "Battery"). - Update `DesktopSystemNotifier` to provide both small and large icons to `knotify` for improved native presentation. - Ensure the `AppConfig` for notifications is initialized with the resolved application icon path. - Update project documentation to reflect the move toward native default styling for notifications.
This commit is contained in:
@@ -38,7 +38,7 @@ The module depends on the JVM variants of KMP modules:
|
||||
|
||||
**Tray:** Uses `ComposeNativeTray` (`io.github.kdroidfilter:composenativetray`) for native tray rendering and menu interactions across Linux, macOS, and Windows.
|
||||
|
||||
**Notifications:** `DesktopNotificationManager` uses DesktopNotifyKT (`io.github.kdroidfilter:knotify`) for desktop notifications. If delivery fails at runtime, the notifier logs the failure as a non-blocking fallback.
|
||||
**Notifications:** `DesktopNotificationManager` uses DesktopNotifyKT (`io.github.kdroidfilter:knotify`) for desktop notifications with native default styling. If delivery fails at runtime, the notifier logs the failure as a non-blocking fallback.
|
||||
|
||||
**Localization:** Desktop exposes a language picker, persisting the selected BCP-47 tag in `UiPreferencesDataSource.locale`. `Main.kt` applies the override to the JVM default `Locale` and uses a `staticCompositionLocalOf`-backed recomposition trigger so Compose Multiplatform `stringResource()` calls update immediately without recreating the Navigation 3 backstack.
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ import io.github.kdroidfilter.knotify.builder.NotificationInitializer
|
||||
import io.github.kdroidfilter.knotify.builder.notification
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.Notification
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
interface DesktopSystemNotifier {
|
||||
fun show(notification: Notification)
|
||||
@@ -59,10 +57,11 @@ class DesktopSystemNotifierImpl(
|
||||
|
||||
private companion object {
|
||||
const val APP_NAME = "Meshtastic"
|
||||
private const val APP_ICON_RESOURCE = "icon.png"
|
||||
const val NOTIFICATION_ICON_RESOURCE = "icon.png"
|
||||
|
||||
@Volatile private var knotifyConfigured = false
|
||||
@Volatile private var notificationIconPath: String? = null
|
||||
|
||||
private val notificationIconPath: String? by lazy { resolveResourcePath(NOTIFICATION_ICON_RESOURCE) }
|
||||
|
||||
private fun configureKnotifyIfNeeded() {
|
||||
if (knotifyConfigured) return
|
||||
@@ -70,11 +69,11 @@ class DesktopSystemNotifierImpl(
|
||||
synchronized(this) {
|
||||
if (knotifyConfigured) return
|
||||
|
||||
notificationIconPath = resolveResourceToTempPath(APP_ICON_RESOURCE)
|
||||
runCatching {
|
||||
NotificationInitializer.configure(
|
||||
AppConfig(
|
||||
appName = APP_NAME,
|
||||
smallIcon = notificationIconPath,
|
||||
),
|
||||
)
|
||||
knotifyConfigured = true
|
||||
@@ -83,34 +82,52 @@ class DesktopSystemNotifierImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveResourceToTempPath(resourcePath: String): String? =
|
||||
runCatching {
|
||||
val stream = Thread.currentThread().contextClassLoader?.getResourceAsStream(resourcePath)
|
||||
stream?.use { input ->
|
||||
val suffix = "." + resourcePath.substringAfterLast('.', "png")
|
||||
val tempFile = Files.createTempFile("meshtastic-knotify-icon-", suffix)
|
||||
Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING)
|
||||
tempFile.toFile().deleteOnExit()
|
||||
tempFile.toAbsolutePath().toString()
|
||||
}
|
||||
}
|
||||
.onFailure { Logger.w(it) { "Failed to resolve DesktopNotifyKT icon resource: $resourcePath" } }
|
||||
.getOrNull()
|
||||
|
||||
@OptIn(ExperimentalNotificationsApi::class)
|
||||
private fun sendWithKnotify(notification: Notification): Boolean =
|
||||
runCatching {
|
||||
private fun sendWithKnotify(notification: Notification): Boolean {
|
||||
val styled = style(notification)
|
||||
return runCatching {
|
||||
notification(
|
||||
title = notification.title,
|
||||
message = notification.message,
|
||||
largeIcon = notificationIconPath,
|
||||
title = styled.title,
|
||||
message = styled.message,
|
||||
smallIcon = notificationIconPath,
|
||||
largeIcon = notificationIconPath,
|
||||
onFailed = { Logger.w { "DesktopNotifyKT failed to display notification" } },
|
||||
).send()
|
||||
true
|
||||
}
|
||||
.onFailure { Logger.w(it) { "DesktopNotifyKT threw while sending notification" } }
|
||||
.getOrDefault(false)
|
||||
}
|
||||
|
||||
private fun style(notification: Notification): Notification {
|
||||
val prefix =
|
||||
when (notification.category) {
|
||||
Notification.Category.Message -> "Message"
|
||||
Notification.Category.NodeEvent -> "Node"
|
||||
Notification.Category.Battery -> "Battery"
|
||||
Notification.Category.Alert -> "Alert"
|
||||
Notification.Category.Service -> "Service"
|
||||
}
|
||||
|
||||
val title =
|
||||
when {
|
||||
notification.title.startsWith("$prefix: ") -> notification.title
|
||||
else -> "$prefix: ${notification.title}"
|
||||
}
|
||||
|
||||
return notification.copy(title = title)
|
||||
}
|
||||
|
||||
private fun resolveResourcePath(resourceName: String): String? {
|
||||
return runCatching {
|
||||
Thread.currentThread()
|
||||
.contextClassLoader
|
||||
?.getResource(resourceName)
|
||||
?.toURI()
|
||||
?.toString()
|
||||
}
|
||||
.onFailure { Logger.w(it) { "Unable to resolve notification icon resource: $resourceName" } }
|
||||
.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,10 @@ import org.meshtastic.desktop.di.desktopModule
|
||||
import org.meshtastic.desktop.di.desktopPlatformModule
|
||||
import org.meshtastic.desktop.ui.DesktopMainScreen
|
||||
import java.awt.Desktop
|
||||
import java.awt.Taskbar
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.Locale
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
/**
|
||||
* Meshtastic Desktop — the first non-Android target for the shared KMP module graph.
|
||||
@@ -115,6 +118,8 @@ private fun classpathPainterResource(path: String): Painter {
|
||||
fun main(args: Array<String>) = application(exitProcessOnExit = false) {
|
||||
Logger.i { "Meshtastic Desktop — Starting" }
|
||||
|
||||
LaunchedEffect(Unit) { setTaskbarDockIcon("icon.png") }
|
||||
|
||||
val koinApp = remember { startKoin { modules(desktopPlatformModule(), desktopModule()) } }
|
||||
val systemLocale = remember { Locale.getDefault() }
|
||||
val uiViewModel = remember { koinApp.koin.get<UIViewModel>() }
|
||||
@@ -299,6 +304,22 @@ fun main(args: Array<String>) = application(exitProcessOnExit = false) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTaskbarDockIcon(resourcePath: String) {
|
||||
if (!Taskbar.isTaskbarSupported()) return
|
||||
|
||||
val taskbar = Taskbar.getTaskbar()
|
||||
if (!taskbar.isSupported(Taskbar.Feature.ICON_IMAGE)) return
|
||||
|
||||
runCatching {
|
||||
val stream = Thread.currentThread().contextClassLoader?.getResourceAsStream(resourcePath) ?: return
|
||||
stream.use {
|
||||
val image = ImageIO.read(ByteArrayInputStream(it.readAllBytes())) ?: return
|
||||
taskbar.iconImage = image
|
||||
}
|
||||
}
|
||||
.onFailure { Logger.w(it) { "Unable to set dock/taskbar icon from $resourcePath" } }
|
||||
}
|
||||
|
||||
/** Replaces the backstack with a single top-level destination route. */
|
||||
private fun navigateTopLevel(backStack: MutableList<NavKey>, route: NavKey) {
|
||||
backStack.add(route)
|
||||
|
||||
Reference in New Issue
Block a user