├── .idea ├── .name ├── vcs.xml ├── ktfmt.xml ├── kotlinc.xml ├── .gitignore ├── AndroidProjectSystem.xml ├── artifacts │ ├── composenotification_js_0_2_0.xml │ ├── composenotification_jvm_0_2_0.xml │ ├── composenotification_wasm_js_0_2_0.xml │ ├── demo_wasm_js_1_0_0.xml │ ├── knotify_js_0_2_0.xml │ ├── knotify_jvm_0_2_0.xml │ ├── demo_desktop_1_0_0.xml │ ├── knotify_wasm_js_0_2_0.xml │ ├── composeApp_jvm_1_0_0.xml │ ├── composeApp_wasm_js_1_0_0.xml │ └── composeApp_desktop_1_0_0.xml ├── compiler.xml ├── misc.xml ├── gradle.xml └── runConfigurations.xml ├── img.png ├── assets ├── header.png └── screenshots │ ├── KDE.png │ ├── Gnome.png │ ├── MacOS.png │ └── Windows.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .github ├── dependabot.yml └── workflows │ └── publish-on-maven.yml ├── knotify ├── src │ ├── jvmMain │ │ ├── resources │ │ │ ├── win32-arm64 │ │ │ │ └── wintoastlibc.dll │ │ │ ├── win32-x86-64 │ │ │ │ └── wintoastlibc.dll │ │ │ ├── linux-x86-64 │ │ │ │ └── libnotification.so │ │ │ ├── darwin-aarch64 │ │ │ │ └── libMacNotification.dylib │ │ │ └── darwin-x86-64 │ │ │ │ └── libMacNotification.dylib │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── knotify │ │ │ ├── platform │ │ │ ├── windows │ │ │ │ ├── types │ │ │ │ │ └── TypeAliases.kt │ │ │ │ ├── callbacks │ │ │ │ │ └── StdCallCallback.kt │ │ │ │ ├── nativeintegration │ │ │ │ │ ├── ExtendedUser32.kt │ │ │ │ │ └── WinToastLibC.kt │ │ │ │ ├── utils │ │ │ │ │ └── Utils.kt │ │ │ │ ├── constants │ │ │ │ │ └── Constants.kt │ │ │ │ └── provider │ │ │ │ │ └── WindowsNotificationProvider.kt │ │ │ ├── linux │ │ │ │ ├── LinuxNativeNotificationIntegration.kt │ │ │ │ └── LinuxNotificationProvider.kt │ │ │ └── mac │ │ │ │ ├── MacNativeNotificationIntegration.kt │ │ │ │ └── MacNotificationProvider.kt │ │ │ ├── utils │ │ │ ├── WindowUtils.kt │ │ │ ├── RuntimeMode.kt │ │ │ └── JarResourceExtractor.kt │ │ │ └── builder │ │ │ ├── NotificationInitializer.jvm.kt │ │ │ └── NotificationBuilder.jvm.kt │ └── commonMain │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── kdroidfilter │ │ └── knotify │ │ ├── model │ │ ├── Button.kt │ │ └── DismissalReason.kt │ │ ├── enums │ │ └── NotificationDuration.kt │ │ └── builder │ │ ├── NotificationProvider.kt │ │ └── NotificationBuilder.kt └── build.gradle.kts ├── demo └── composeApp │ ├── src │ ├── commonMain │ │ ├── composeResources │ │ │ └── drawable │ │ │ │ ├── kdroid.png │ │ │ │ └── compose.png │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── knotify │ │ │ └── demo │ │ │ ├── App.kt │ │ │ ├── ScreenOne.kt │ │ │ ├── ScreenTwo.kt │ │ │ └── ScreenThree.kt │ └── jvmMain │ │ └── kotlin │ │ └── Main.kt │ └── build.gradle.kts ├── .gitmodules ├── linuxlib ├── build.sh ├── CMakeLists.txt ├── linux_notification_library.h └── linux_notification_library.c ├── gradle.properties ├── .gitignore ├── knotify-compose ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── knotify │ │ │ └── compose │ │ │ ├── utils │ │ │ ├── ComposableIconRenderer.kt │ │ │ └── IconRenderProperties.kt │ │ │ └── builder │ │ │ └── ComposeNotificationBuilder.kt │ └── jvmMain │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── kdroidfilter │ │ └── knotify │ │ └── compose │ │ └── utils │ │ ├── ComposableIconRenderer.kt │ │ └── ComposableIconUtils.kt └── build.gradle.kts ├── settings.gradle.kts ├── LICENSE ├── maclib ├── build.sh ├── README.md └── mac_notification_library.swift ├── AGENTS.md ├── gradlew.bat ├── README.MD └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | K-Notify -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/img.png -------------------------------------------------------------------------------- /assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/assets/header.png -------------------------------------------------------------------------------- /assets/screenshots/KDE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/assets/screenshots/KDE.png -------------------------------------------------------------------------------- /assets/screenshots/Gnome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/assets/screenshots/Gnome.png -------------------------------------------------------------------------------- /assets/screenshots/MacOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/assets/screenshots/MacOS.png -------------------------------------------------------------------------------- /assets/screenshots/Windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/assets/screenshots/Windows.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/resources/win32-arm64/wintoastlibc.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/knotify/src/jvmMain/resources/win32-arm64/wintoastlibc.dll -------------------------------------------------------------------------------- /knotify/src/jvmMain/resources/win32-x86-64/wintoastlibc.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/knotify/src/jvmMain/resources/win32-x86-64/wintoastlibc.dll -------------------------------------------------------------------------------- /knotify/src/jvmMain/resources/linux-x86-64/libnotification.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/knotify/src/jvmMain/resources/linux-x86-64/libnotification.so -------------------------------------------------------------------------------- /demo/composeApp/src/commonMain/composeResources/drawable/kdroid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/demo/composeApp/src/commonMain/composeResources/drawable/kdroid.png -------------------------------------------------------------------------------- /knotify/src/commonMain/kotlin/io/github/kdroidfilter/knotify/model/Button.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.model 2 | 3 | data class Button(val label: String, val onClick: () -> Unit) 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demo/composeApp/src/commonMain/composeResources/drawable/compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/demo/composeApp/src/commonMain/composeResources/drawable/compose.png -------------------------------------------------------------------------------- /knotify/src/jvmMain/resources/darwin-aarch64/libMacNotification.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/knotify/src/jvmMain/resources/darwin-aarch64/libMacNotification.dylib -------------------------------------------------------------------------------- /knotify/src/jvmMain/resources/darwin-x86-64/libMacNotification.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/DesktopNotifyKT/HEAD/knotify/src/jvmMain/resources/darwin-x86-64/libMacNotification.dylib -------------------------------------------------------------------------------- /.idea/ktfmt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /knotify/src/commonMain/kotlin/io/github/kdroidfilter/knotify/model/DismissalReason.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.model 2 | 3 | enum class DismissalReason { 4 | UserCanceled, 5 | ApplicationHidden, 6 | TimedOut, 7 | Unknown 8 | } 9 | -------------------------------------------------------------------------------- /knotify/src/commonMain/kotlin/io/github/kdroidfilter/knotify/enums/NotificationDuration.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.enums 2 | 3 | /** 4 | * Enum class representing notification duration options. 5 | */ 6 | enum class NotificationDuration { 7 | SHORT, LONG 8 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "composenotification/src/jvmMain/c/WintoastLibC"] 2 | path = composenotification/src/jvmMain/c/WintoastLibC 3 | url = https://github.com/kdroidFilter/WinToastLibC 4 | [submodule "winlib/WinToastLibC"] 5 | path = winlib/WinToastLibC 6 | url = https://github.com/kdroidFilter/WinToastLibC.git 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.idea/artifacts/composenotification_js_0_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/composenotification/build/libs 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/artifacts/composenotification_jvm_0_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/composenotification/build/libs 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/artifacts/composenotification_wasm_js_0_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/composenotification/build/libs 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/artifacts/demo_wasm_js_1_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/demo/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/knotify_js_0_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/knotify/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/knotify_jvm_0_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/knotify/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/demo_desktop_1_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/demo/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/knotify_wasm_js_0_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/knotify/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/composeApp_jvm_1_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/demo/composeApp/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /linuxlib/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | echo "Building LinuxNotification library..." 7 | 8 | # Build the shared library using gcc 9 | gcc -shared -o ../knotify/src/jvmMain/resources/linux-x86-64/libnotification.so -fPIC linux_notification_library.c $(pkg-config --cflags --libs libnotify glib-2.0 gdk-pixbuf-2.0) 10 | 11 | echo "Build completed successfully." -------------------------------------------------------------------------------- /.idea/artifacts/composeApp_wasm_js_1_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/demo/composeApp/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/composeApp_desktop_1_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/demo/composeApp/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4G 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.daemon=true 6 | org.gradle.parallel=true 7 | 8 | #Kotlin 9 | kotlin.code.style=official 10 | kotlin.daemon.jvmargs=-Xmx4G 11 | 12 | #Android 13 | android.useAndroidX=true 14 | android.nonTransitiveRClass=true 15 | 16 | org.jetbrains.compose.experimental.macos.enabled=true -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/platform/windows/types/TypeAliases.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.platform.windows.types 2 | 3 | import com.sun.jna.Pointer 4 | 5 | internal typealias WTLC_Instance = Pointer 6 | internal typealias WTLC_Template = Pointer 7 | internal typealias WTLC_Error = Int 8 | internal typealias WTLC_DismissalReason = Int 9 | internal typealias WTLC_TextField = Int 10 | internal typealias WTLC_TemplateType = Int 11 | internal typealias WTLC_AudioOption = Int -------------------------------------------------------------------------------- /linuxlib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | project(notification C) 4 | 5 | set(CMAKE_C_STANDARD 11) 6 | 7 | find_package(PkgConfig REQUIRED) 8 | pkg_check_modules(LIBNOTIFY REQUIRED libnotify) 9 | 10 | include_directories(${LIBNOTIFY_INCLUDE_DIRS}) 11 | link_directories(${LIBNOTIFY_LIBRARY_DIRS}) 12 | 13 | add_library(notification SHARED 14 | linux_notification_library.c 15 | linux_notification_library.h) 16 | 17 | target_link_libraries(notification ${LIBNOTIFY_LIBRARIES}) 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.iml 3 | .gradle 4 | .kotlin 5 | .DS_Store 6 | build 7 | */build 8 | captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | xcuserdata/ 13 | Pods/ 14 | *.jks 15 | *yarn.lock 16 | /linuxlib/.idea 17 | /linuxlib/cmake-build-release 18 | .cache 19 | .idea 20 | /knotify/src/jvmMain/resources/win32-arm64/wintoastlibc_lazy.dll 21 | /knotify/src/jvmMain/resources/win32-arm64/wintoastlibc_static.lib 22 | /knotify/src/jvmMain/resources/win32-x86-64/wintoastlibc_lazy.dll 23 | /knotify/src/jvmMain/resources/win32-x86-64/wintoastlibc_static.lib -------------------------------------------------------------------------------- /knotify/src/commonMain/kotlin/io/github/kdroidfilter/knotify/builder/NotificationProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.builder 2 | 3 | interface NotificationProvider { 4 | 5 | /** 6 | * Sends a notification based on the properties and callbacks defined in the [NotificationBuilder]. 7 | * 8 | * @param builder The builder containing the notification properties and callbacks. 9 | */ 10 | fun sendNotification(builder: NotificationBuilder) 11 | 12 | /** 13 | * Hides a notification that was previously sent. 14 | * 15 | * @param builder The builder containing the notification properties and callbacks. 16 | */ 17 | fun hideNotification(builder: NotificationBuilder) 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/utils/WindowUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.utils 2 | 3 | import javax.swing.JFrame 4 | import java.awt.Frame 5 | 6 | /** 7 | * Utility functions for window-related operations. 8 | */ 9 | object WindowUtils { 10 | /** 11 | * Gets the title of the first JFrame window. 12 | * This is used to determine the application name for notifications. 13 | * 14 | * @return The title of the first JFrame window, or "Application" if no JFrame is found. 15 | */ 16 | fun getWindowsTitle(): String { 17 | return Frame.getFrames() 18 | .filterIsInstance() 19 | .map { it.title } 20 | .firstOrNull()?.takeIf { it.isNotEmpty() } ?: "Application" 21 | } 22 | } -------------------------------------------------------------------------------- /knotify-compose/src/commonMain/kotlin/io/github/kdroidfilter/knotify/compose/utils/ComposableIconRenderer.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.compose.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | /** 6 | * Interface for rendering Composable content to image files. 7 | */ 8 | expect object ComposableIconRenderer { 9 | /** 10 | * Renders a Composable to a PNG file and returns the path to the file. 11 | * 12 | * @param iconRenderProperties Properties for rendering the icon 13 | * @param content The Composable content to render 14 | * @return Path to the generated PNG file 15 | */ 16 | fun renderComposableToPngFile( 17 | iconRenderProperties: IconRenderProperties, 18 | content: @Composable () -> Unit 19 | ): String 20 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/platform/windows/callbacks/StdCallCallback.kt: -------------------------------------------------------------------------------- 1 | 2 | package io.github.kdroidfilter.knotify.platform.windows.callbacks 3 | 4 | import com.sun.jna.Pointer 5 | import com.sun.jna.win32.StdCallLibrary 6 | 7 | /** 8 | * Callback interfaces for Windows Toast notifications 9 | */ 10 | internal interface ToastActivatedCallback : StdCallLibrary.StdCallCallback { 11 | fun invoke(userData: Pointer?) 12 | } 13 | 14 | internal interface ToastActivatedActionCallback : StdCallLibrary.StdCallCallback { 15 | fun invoke(userData: Pointer?, actionIndex: Int) 16 | } 17 | 18 | internal interface ToastDismissedCallback : StdCallLibrary.StdCallCallback { 19 | fun invoke(userData: Pointer?, state: Int) 20 | } 21 | 22 | internal interface ToastFailedCallback : StdCallLibrary.StdCallCallback { 23 | fun invoke(userData: Pointer?) 24 | } 25 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/utils/RuntimeMode.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.utils 2 | 3 | /** 4 | * Enum representing the runtime mode of the application. 5 | * DEV: Development mode (running from IDE or gradlew run) 6 | * DIST: Distribution mode (running from installed package or gradlew runDistributable) 7 | */ 8 | enum class RuntimeMode { 9 | DEV, 10 | DIST 11 | } 12 | 13 | /** 14 | * Detects the current runtime mode of the application. 15 | * 16 | * @return RuntimeMode.DEV if running in development mode, RuntimeMode.DIST if running in distribution mode 17 | */ 18 | fun detectRuntimeMode(): RuntimeMode = 19 | if (System.getProperty("compose.application.resources.dir") == null) 20 | RuntimeMode.DEV // gradlew run / IDE 21 | else 22 | RuntimeMode.DIST // gradlew runDistributable ou package installé -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/builder/NotificationInitializer.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.builder 2 | 3 | import io.github.kdroidfilter.knotify.utils.extractToTempIfDifferent 4 | 5 | 6 | data class AppConfig( 7 | val appName: String? = null, 8 | val smallIcon: String? = null, 9 | ) 10 | 11 | object NotificationInitializer { 12 | var appConfiguration: AppConfig = AppConfig() 13 | private set 14 | 15 | fun configure(config: AppConfig) { 16 | if (config.appName != null) 17 | require(config.appName.isNotEmpty()) { "App name must not be empty" } 18 | 19 | val pngPath = config.smallIcon 20 | val extractedPngPath = pngPath?.let { extractToTempIfDifferent(it)?.absolutePath } 21 | 22 | 23 | val newConfig = config.copy(smallIcon = extractedPngPath) 24 | appConfiguration = newConfig 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/platform/windows/nativeintegration/ExtendedUser32.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.platform.windows.nativeintegration 2 | 3 | import com.sun.jna.Native 4 | import com.sun.jna.Pointer 5 | import com.sun.jna.platform.win32.User32 6 | import com.sun.jna.win32.W32APIOptions 7 | 8 | internal interface ExtendedUser32 : User32 { 9 | companion object { 10 | val INSTANCE: ExtendedUser32 = Native.load( 11 | "user32", 12 | ExtendedUser32::class.java, 13 | W32APIOptions.DEFAULT_OPTIONS // Use default options 14 | ) 15 | 16 | } 17 | 18 | // Definition of MsgWaitForMultipleObjects 19 | fun MsgWaitForMultipleObjects( 20 | nCount: Int, 21 | pHandles: Pointer, 22 | bWaitAll: Boolean, 23 | dwMilliseconds: Int, 24 | dwWakeMask: Int 25 | ): Int 26 | 27 | 28 | } -------------------------------------------------------------------------------- /knotify-compose/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/compose/utils/ComposableIconRenderer.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.compose.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | /** 6 | * JVM implementation of ComposableIconRenderer that delegates to ComposableIconUtils. 7 | */ 8 | actual object ComposableIconRenderer { 9 | /** 10 | * Renders a Composable to a PNG file and returns the path to the file. 11 | * 12 | * @param iconRenderProperties Properties for rendering the icon 13 | * @param content The Composable content to render 14 | * @return Path to the generated PNG file 15 | */ 16 | actual fun renderComposableToPngFile( 17 | iconRenderProperties: IconRenderProperties, 18 | content: @Composable () -> Unit 19 | ): String { 20 | return ComposableIconUtils.renderComposableToPngFile(iconRenderProperties, content) 21 | } 22 | } -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/builder/NotificationBuilder.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.builder 2 | 3 | import io.github.kdroidfilter.knotify.platform.linux.LinuxNotificationProvider 4 | import io.github.kdroidfilter.knotify.platform.mac.MacNotificationProvider 5 | import io.github.kdroidfilter.knotify.platform.windows.provider.WindowsNotificationProvider 6 | import io.github.kdroidfilter.platformtools.OperatingSystem 7 | import io.github.kdroidfilter.platformtools.getOperatingSystem 8 | 9 | actual fun getNotificationProvider(): NotificationProvider { 10 | val os = getOperatingSystem() 11 | return when (os) { 12 | OperatingSystem.LINUX -> LinuxNotificationProvider() 13 | OperatingSystem.WINDOWS -> WindowsNotificationProvider() 14 | OperatingSystem.MACOS -> MacNotificationProvider() 15 | else -> throw UnsupportedOperationException("Unsupported OS") 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /demo/composeApp/src/jvmMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.demo 2 | 3 | import androidx.compose.ui.window.Window 4 | import androidx.compose.ui.window.application 5 | import io.github.kdroidfilter.knotify.builder.AppConfig 6 | import io.github.kdroidfilter.knotify.builder.NotificationInitializer 7 | import io.github.kdroidfilter.knotify.demo.composeapp.generated.resources.Res 8 | import io.github.kdroidfilter.knotify.demo.composeapp.generated.resources.compose 9 | import org.jetbrains.compose.resources.ExperimentalResourceApi 10 | import org.jetbrains.compose.resources.painterResource 11 | 12 | @OptIn(ExperimentalResourceApi::class) 13 | fun main() = application { 14 | NotificationInitializer.configure( // Optional 15 | AppConfig("My Application"), 16 | ) 17 | Window(onCloseRequest = ::exitApplication, title = "Compose Native Notification Demo", icon = painterResource(Res.drawable.compose)) { 18 | App() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "K-Notify" 2 | 3 | pluginManagement { 4 | repositories { 5 | google { 6 | content { 7 | includeGroupByRegex("com\\.android.*") 8 | includeGroupByRegex("com\\.google.*") 9 | includeGroupByRegex("androidx.*") 10 | includeGroupByRegex("android.*") 11 | } 12 | } 13 | gradlePluginPortal() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google { 21 | content { 22 | includeGroupByRegex("com\\.android.*") 23 | includeGroupByRegex("com\\.google.*") 24 | includeGroupByRegex("androidx.*") 25 | includeGroupByRegex("android.*") 26 | } 27 | } 28 | mavenCentral() 29 | mavenLocal() 30 | } 31 | } 32 | 33 | include(":knotify") 34 | include(":knotify-compose") 35 | include(":demo:composeApp") 36 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/publish-on-maven.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven Central 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | publish: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up JDK 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | 22 | - name: Set up Publish to Maven Central 23 | run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache 24 | env: 25 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVENCENTRALUSERNAME }} 26 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVENCENTRALPASSWORD }} 27 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNINGINMEMORYKEY }} 28 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNINGKEYID }} 29 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNINGPASSWORD }} 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Elie G. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/platform/windows/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.platform.windows.utils 2 | 3 | import co.touchlab.kermit.Logger 4 | import com.sun.jna.platform.win32.Advapi32Util 5 | import com.sun.jna.platform.win32.WinReg 6 | 7 | private val logger = Logger.withTag("WindowsUtils") 8 | 9 | internal fun registerBasicAUMID(aumid: String, displayName: String, iconUri: String): Boolean { 10 | val rootKeyPath = "Software\\Classes\\AppUserModelId" 11 | val aumidKeyPath = "$rootKeyPath\\$aumid" 12 | 13 | try { 14 | // Create or open the root key 15 | Advapi32Util.registryCreateKey(WinReg.HKEY_CURRENT_USER, rootKeyPath) 16 | // Create or open the AUMID key 17 | Advapi32Util.registryCreateKey(WinReg.HKEY_CURRENT_USER, aumidKeyPath) 18 | // Set the DisplayName value 19 | Advapi32Util.registrySetStringValue(WinReg.HKEY_CURRENT_USER, aumidKeyPath, "DisplayName", displayName) 20 | // Set the IconUri value if provided 21 | if (iconUri.isNotEmpty()) { 22 | Advapi32Util.registrySetStringValue(WinReg.HKEY_CURRENT_USER, aumidKeyPath, "IconUri", iconUri) 23 | } 24 | return true 25 | } catch (e: Exception) { 26 | logger.e { "Exception : ${e.message}" } 27 | return false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/platform/windows/constants/Constants.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.platform.windows.constants 2 | 3 | internal object WTLC_TextField_Constants { 4 | const val FirstLine = 0 5 | const val SecondLine = 1 6 | const val ThirdLine = 2 7 | } 8 | 9 | internal object WTLC_TemplateType_Constants { 10 | const val ImageAndText02 = 1 11 | const val Text02 = 2 12 | } 13 | 14 | internal object WTLC_AudioOption_Constants { 15 | const val Default = 0 16 | } 17 | 18 | internal object WTLC_DismissalReason_Constants { 19 | const val UserCanceled = 0 20 | const val ApplicationHidden = 1 21 | const val TimedOut = 2 22 | } 23 | 24 | internal object WTLC_ShortcutPolicy_Constants { 25 | const val IGNORE = 0 26 | } 27 | 28 | internal const val PM_REMOVE = 0x0001 29 | internal const val QS_KEY = 0x0001 30 | internal const val QS_MOUSEMOVE = 0x0002 31 | internal const val QS_MOUSEBUTTON = 0x0004 32 | internal const val QS_POSTMESSAGE = 0x0008 33 | internal const val QS_TIMER = 0x0010 34 | internal const val QS_PAINT = 0x0020 35 | internal const val QS_SENDMESSAGE = 0x0040 36 | internal const val QS_HOTKEY = 0x0080 37 | internal const val QS_ALLPOSTMESSAGE = 0x0100 38 | internal const val QS_RAWINPUT = 0x0400 39 | internal const val QS_MOUSE = QS_MOUSEMOVE or QS_MOUSEBUTTON 40 | internal const val QS_INPUT = QS_MOUSE or QS_KEY or QS_RAWINPUT 41 | internal const val QS_ALLEVENTS = QS_INPUT or QS_POSTMESSAGE or QS_TIMER or QS_PAINT or QS_HOTKEY 42 | internal const val QS_ALLINPUT = QS_INPUT or QS_POSTMESSAGE or QS_TIMER or QS_PAINT or QS_HOTKEY or QS_SENDMESSAGE -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | kermit = "2.0.8" 4 | jna = "5.18.1" 5 | kotlin = "2.2.20" 6 | agp = "8.9.2" 7 | kotlinx-coroutines = "1.10.2" 8 | compose-plugin = "1.9.1" 9 | platformtoolsCore = "0.7.1" 10 | runtime = "1.9.0" 11 | vanniktech = "0.33.0" 12 | 13 | [libraries] 14 | 15 | kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } 16 | jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } 17 | jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" } 18 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 19 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 20 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } 21 | 22 | platformtools-core = { module = "io.github.kdroidfilter:platformtools.core", version.ref = "platformtoolsCore" } 23 | runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "runtime" } 24 | 25 | [plugins] 26 | 27 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 28 | android-library = { id = "com.android.library", version.ref = "agp" } 29 | androidApplication = { id = "com.android.application", version.ref = "agp" } 30 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 31 | vannitktech-maven-publish = {id = "com.vanniktech.maven.publish", version.ref = "vanniktech"} 32 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 33 | -------------------------------------------------------------------------------- /demo/composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig 4 | 5 | plugins { 6 | alias(libs.plugins.multiplatform) 7 | alias(libs.plugins.jetbrainsCompose) 8 | alias(libs.plugins.compose.compiler) 9 | 10 | } 11 | 12 | val appVersion = "1.0.0" 13 | val appPackageName = "io.github.kdroidfilter.knotify.demo" 14 | 15 | group = appPackageName 16 | version = appVersion 17 | 18 | kotlin { 19 | jvmToolchain(17) 20 | jvm() 21 | 22 | sourceSets { 23 | commonMain.dependencies { 24 | implementation(project(":knotify")) 25 | implementation(project(":knotify-compose")) 26 | implementation(compose.runtime) 27 | implementation(compose.foundation) 28 | implementation(compose.material3) 29 | implementation(compose.ui) 30 | implementation(libs.kermit) 31 | implementation(compose.components.resources) 32 | } 33 | 34 | jvmMain.dependencies { 35 | implementation(compose.desktop.currentOs) 36 | } 37 | 38 | } 39 | } 40 | 41 | 42 | 43 | compose.desktop { 44 | application { 45 | mainClass = "io.github.kdroidfilter.knotify.demo.MainKt" 46 | 47 | nativeDistributions { 48 | targetFormats(TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Dmg) 49 | packageName = "KnotifyDemo" 50 | packageVersion = "1.0.0" 51 | description = "DesktopNotify-KT Sample" 52 | copyright = "© 2024 KdroidFilter. All rights reserved." 53 | 54 | } 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /demo/composeApp/src/commonMain/kotlin/io/github/kdroidfilter/knotify/demo/App.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalResourceApi::class) 2 | 3 | package io.github.kdroidfilter.knotify.demo 4 | 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Modifier 10 | import org.jetbrains.compose.resources.ExperimentalResourceApi 11 | 12 | @Composable 13 | fun App() { 14 | var currentScreen by remember { mutableStateOf(Screen.Screen1) } 15 | var notificationMessage by remember { mutableStateOf(null) } 16 | 17 | MaterialTheme { 18 | Surface(modifier = Modifier.fillMaxSize()) { 19 | when (currentScreen) { 20 | Screen.Screen1 -> ScreenOne( 21 | onNavigate = { currentScreen = Screen.Screen2 }, 22 | onNavigateToComposeDemo = { currentScreen = Screen.Screen3 }, 23 | notificationMessage = notificationMessage, 24 | onShowMessage = { message -> notificationMessage = message } 25 | ) 26 | 27 | Screen.Screen2 -> ScreenTwo( 28 | onNavigate = { currentScreen = Screen.Screen1 }, 29 | onNavigateToComposeDemo = { currentScreen = Screen.Screen3 }, 30 | notificationMessage = notificationMessage, 31 | onShowMessage = { message -> notificationMessage = message } 32 | ) 33 | 34 | Screen.Screen3 -> ScreenThree( 35 | onNavigateBack = { currentScreen = Screen.Screen1 }, 36 | notificationMessage = notificationMessage, 37 | onShowMessage = { message -> notificationMessage = message } 38 | ) 39 | } 40 | } 41 | } 42 | } 43 | 44 | enum class Screen { 45 | Screen1, 46 | Screen2, 47 | Screen3 48 | } -------------------------------------------------------------------------------- /knotify-compose/src/commonMain/kotlin/io/github/kdroidfilter/knotify/compose/utils/IconRenderProperties.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.compose.utils 2 | 3 | import androidx.compose.ui.unit.Density 4 | import io.github.kdroidfilter.platformtools.OperatingSystem 5 | import io.github.kdroidfilter.platformtools.getOperatingSystem 6 | 7 | 8 | /** 9 | * Properties for rendering a Composable icon. 10 | * 11 | * @property sceneWidth Width of the [androidx.compose.ui.ImageComposeScene] in pixels 12 | * @property sceneHeight Height of the [androidx.compose.ui.ImageComposeScene] in pixels 13 | * @property sceneDensity Density for [androidx.compose.ui.ImageComposeScene] 14 | * @property targetWidth Width of the rendered icon in pixels 15 | * @property targetHeight Height of the rendered icon in pixels 16 | */ 17 | data class IconRenderProperties( 18 | val sceneWidth: Int = 192, 19 | val sceneHeight: Int = 192, 20 | val sceneDensity: Density = Density(2f), 21 | val targetWidth: Int = 192, 22 | val targetHeight: Int = 192 23 | ) { 24 | val requiresScaling = sceneWidth != targetWidth || sceneHeight != targetHeight 25 | 26 | companion object { 27 | 28 | /** 29 | * Provides an [IconRenderProperties] configured with settings that don't force icon scaling and aliasing. 30 | * 31 | * @param sceneWidth Width of the [androidx.compose.ui.ImageComposeScene] in pixels. 32 | * @param sceneHeight Height of the [androidx.compose.ui.ImageComposeScene] in pixels. 33 | * @param density Density of the [androidx.compose.ui.ImageComposeScene]. 34 | * @return An instance of [IconRenderProperties] with the appropriate target width and height based on the operating system. 35 | */ 36 | fun withoutScalingAndAliasing( 37 | sceneWidth: Int = 192, 38 | sceneHeight: Int = 192, 39 | density: Density = Density(2f) 40 | ) = IconRenderProperties( 41 | sceneWidth = sceneWidth, 42 | sceneHeight = sceneHeight, 43 | sceneDensity = density, 44 | targetWidth = sceneWidth, 45 | targetHeight = sceneHeight 46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /maclib/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | echo "Building MacNotification library..." 7 | 8 | # Create Info.plist file for bundle identification 9 | cat > Info.plist << EOF 10 | 11 | 12 | 13 | 14 | CFBundleIdentifier 15 | io.github.kdroidfilter.knotify 16 | CFBundleName 17 | KNotify 18 | CFBundleVersion 19 | 1.0 20 | CFBundleShortVersionString 21 | 1.0 22 | NSHumanReadableCopyright 23 | Copyright © 2023 KDroidFilter. All rights reserved. 24 | 25 | 26 | EOF 27 | 28 | echo "Building for ARM64..." 29 | 30 | # Compile Swift file directly to dylib with bundle info for ARM64 (Apple Silicon) 31 | swiftc -emit-library -o ../knotify/src/jvmMain/resources/darwin-aarch64/libMacNotification.dylib \ 32 | -module-name MacNotification \ 33 | -swift-version 5 \ 34 | -O -whole-module-optimization \ 35 | -framework Foundation \ 36 | -framework UserNotifications \ 37 | -Xlinker -rpath -Xlinker @executable_path/../Frameworks \ 38 | -Xlinker -rpath -Xlinker @loader_path/Frameworks \ 39 | -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist \ 40 | mac_notification_library.swift 41 | 42 | echo "Building for x86_64..." 43 | 44 | # Compile Swift file directly to dylib with bundle info for x86_64 (Intel) 45 | swiftc -emit-library -o ../knotify/src/jvmMain/resources/darwin-x86-64/libMacNotification.dylib \ 46 | -module-name MacNotification \ 47 | -swift-version 5 \ 48 | -target x86_64-apple-macosx10.14 \ 49 | -O -whole-module-optimization \ 50 | -framework Foundation \ 51 | -framework UserNotifications \ 52 | -Xlinker -rpath -Xlinker @executable_path/../Frameworks \ 53 | -Xlinker -rpath -Xlinker @loader_path/Frameworks \ 54 | -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist \ 55 | mac_notification_library.swift 56 | 57 | # Clean up temporary files 58 | rm -f Info.plist 59 | 60 | echo "Build completed successfully." 61 | -------------------------------------------------------------------------------- /linuxlib/linux_notification_library.h: -------------------------------------------------------------------------------- 1 | #ifndef NOTIFICATION_LIBRARY_H 2 | #define NOTIFICATION_LIBRARY_H 3 | 4 | #include 5 | #include // Include GdkPixbuf 6 | 7 | // Global debug flag 8 | extern int debug_mode; 9 | 10 | // Set debug mode (0 = disabled, 1 = enabled) 11 | void set_debug_mode(int enable); 12 | 13 | // Type definition for notification 14 | typedef NotifyNotification Notification; 15 | 16 | // Type definition for action callbacks 17 | typedef void (*NotifyActionCallback)(NotifyNotification *notification, char *action, gpointer user_data); 18 | 19 | // Type definition for closed callbacks 20 | typedef void (*NotifyClosedCallback)(NotifyNotification *notification, gpointer user_data); 21 | 22 | // Initialize notification library with application name 23 | int my_notify_init(const char *app_name); 24 | 25 | // Create a new notification with an icon 26 | Notification *create_notification(const char *summary, const char *body, const char *icon_path); 27 | 28 | // Create a new notification with a GdkPixbuf image 29 | Notification *create_notification_with_pixbuf(const char *summary, const char *body, const char *image_path); 30 | 31 | // Add an optional button to the notification 32 | void add_button_to_notification(Notification *notification, const char *button_id, const char *button_label, NotifyActionCallback callback, gpointer user_data); 33 | 34 | // Send the notification 35 | int send_notification(Notification *notification); 36 | 37 | // Add a callback for notification click 38 | void set_notification_clicked_callback(Notification *notification, NotifyActionCallback callback, gpointer user_data); 39 | 40 | // Add a callback for notification close 41 | void set_notification_closed_callback(Notification *notification, NotifyClosedCallback callback, gpointer user_data); 42 | 43 | // Set image from GdkPixbuf 44 | void set_image_from_pixbuf(Notification *notification, GdkPixbuf *pixbuf); 45 | 46 | // Load a GdkPixbuf from a file 47 | GdkPixbuf *load_pixbuf_from_file(const char *image_path); 48 | 49 | // Start the main loop to handle events 50 | void run_main_loop(); 51 | 52 | // Stop the main loop 53 | void quit_main_loop(); 54 | 55 | // Clean up resources 56 | void cleanup_notification(); 57 | 58 | // Close/hide a notification 59 | int close_notification(Notification *notification); 60 | 61 | 62 | 63 | #endif // NOTIFICATION_LIBRARY_H 64 | -------------------------------------------------------------------------------- /maclib/README.md: -------------------------------------------------------------------------------- 1 | # MacNotification Library 2 | 3 | This library provides native macOS notifications for the Compose Native Notification project using Swift and JNA. 4 | 5 | ## Requirements 6 | 7 | - macOS 10.14 or later 8 | - Xcode 11 or later with Swift 5.0 9 | 10 | ## Building the Library 11 | 12 | 1. Navigate to the `src` directory: 13 | ``` 14 | cd src 15 | ``` 16 | 17 | 2. Make the build script executable: 18 | ``` 19 | chmod +x build.sh 20 | ``` 21 | 22 | 3. Run the build script: 23 | ``` 24 | ./build.sh 25 | ``` 26 | 27 | 4. The library will be built in the `lib` directory as `libMacNotification.dylib`. 28 | 29 | ## Integration with JNA 30 | 31 | The library is designed to be used with JNA (Java Native Access). The JNA interface is defined in the `MacNativeNotificationIntegration.kt` file. 32 | 33 | To use the library: 34 | 35 | 1. Make sure the `libMacNotification.dylib` file is in a directory that is in the Java library path. 36 | 37 | 2. You can set the library path when running your application: 38 | ``` 39 | java -Djna.library.path=/path/to/lib -jar your-application.jar 40 | ``` 41 | 42 | 3. Alternatively, you can copy the library to a standard library location like `/usr/local/lib`. 43 | 44 | ## API Reference 45 | 46 | The library provides the following functions: 47 | 48 | - `create_notification(title, body, iconPath)`: Creates a notification with the specified title, body, and icon. 49 | - `add_button_to_notification(notification, buttonId, buttonLabel, callback, userData)`: Adds a button to the notification. 50 | - `set_notification_clicked_callback(notification, callback, userData)`: Sets a callback for when the notification is clicked. 51 | - `set_notification_closed_callback(notification, callback, userData)`: Sets a callback for when the notification is closed. 52 | - `set_notification_image(notification, imagePath)`: Sets an image for the notification. 53 | - `send_notification(notification)`: Sends the notification. 54 | - `hide_notification(notification)`: Hides/removes the notification. 55 | - `cleanup_notification(notification)`: Cleans up resources associated with the notification. 56 | 57 | ## Troubleshooting 58 | 59 | If you encounter issues with the library: 60 | 61 | 1. Make sure you have the required macOS version and Xcode installed. 62 | 2. Check that the library is in the correct location and accessible to your application. 63 | 3. Look for error messages in the application logs. 64 | 4. If the library fails to load, the application will fall back to a dummy implementation that logs errors but doesn't crash. 65 | 66 | ## License 67 | 68 | This library is part of the Compose Native Notification project and is licensed under the MIT License. 69 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Repository Guidelines 2 | 3 | ## Project Structure & Module Organization 4 | DesktopNotify-KT is a multiplatform Gradle build with three active modules. `knotify` hosts the core API and native bridges in `knotify/src/commonMain/kotlin` and `knotify/src/jvmMain/kotlin`, with shared assets in `knotify/src/jvmMain/resources`. `knotify-compose` extends the core module to render Compose UI into notifications. The sample app lives in `demo/composeApp` and is the right place for manual experiments. Native helper sources and build scripts sit under `linuxlib`, `maclib`, and `winlib`, while marketing assets stay in `assets/`. 5 | 6 | ## Build, Test, and Development Commands 7 | - `./gradlew build` compiles every module and runs available unit tests. 8 | - `./gradlew :knotify:buildNativeLibraries` rebuilds the platform binaries; invoke on the host OS before publishing. 9 | - `./gradlew :knotify:check` focuses checks on the core library when iterating locally. 10 | - `./gradlew :demo:composeApp:run` launches the demo client for interactive verification. 11 | - `./gradlew publishToMavenLocal` installs both artifacts for downstream integration testing. 12 | 13 | ## Coding Style & Naming Conventions 14 | Follow the official Kotlin code style (4-space indents, trailing commas on multiline lists, explicit visibility on public APIs). Public classes, DSL builders, and Compose components use PascalCase; functions, properties, and lambda receivers use camelCase; callback lambdas start with `on`. Keep DSL functions fluent and side-effect free, prefer extension functions over utility singletons, and isolate platform-specific code inside the JVM source set. Run Android Studio or IntelliJ “Reformat Code” using Kotlin defaults before committing. 15 | 16 | ## Testing Guidelines 17 | Place multiplatform tests in `knotify/src/commonTest/kotlin`; JVM-only scenarios can use `knotify/src/jvmTest/kotlin`. Use `kotlin.test` assertions and the coroutine test dispatcher from `kotlinx.coroutines.test`. Name tests after the behavior under validation (`functionName_condition_expectedResult`). Execute `./gradlew :knotify:check` ahead of every pull request and add regression coverage whenever fixing bugs or changing native bindings. 18 | 19 | ## Commit & Pull Request Guidelines 20 | Keep commits small, focused, and written in the imperative mood (e.g., `Add Windows toast scheduler guard`). Prefix with the module when it clarifies scope (`knotify-compose: ...`). Pull requests should summarize intent, list platform verification (Windows/macOS/Linux), and link related issues. Capture screenshots or logs when altering notification visuals or interaction flows, and update README/demo snippets when public APIs change. 21 | 22 | ## Native Library Notes 23 | Platform bridges are produced by the scripts in `linuxlib/build.sh`, `maclib/build.sh`, and `winlib/WinToastLibC/build.bat`. Run the matching script on its native host so the resulting binaries align with toolchain expectations. When changing headers or C++ sources, rebuild, verify loading through the demo app on each platform, and bump embedded library versions if distribution artifacts depend on the output. 24 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/platform/windows/nativeintegration/WinToastLibC.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.platform.windows.nativeintegration 2 | 3 | import io.github.kdroidfilter.knotify.platform.windows.callbacks.ToastActivatedActionCallback 4 | import io.github.kdroidfilter.knotify.platform.windows.callbacks.ToastActivatedCallback 5 | import io.github.kdroidfilter.knotify.platform.windows.callbacks.ToastDismissedCallback 6 | import io.github.kdroidfilter.knotify.platform.windows.callbacks.ToastFailedCallback 7 | import com.sun.jna.Library 8 | import com.sun.jna.Native 9 | import com.sun.jna.Pointer 10 | import com.sun.jna.WString 11 | import com.sun.jna.ptr.IntByReference 12 | import com.sun.jna.win32.W32APIOptions 13 | import io.github.kdroidfilter.knotify.platform.windows.types.WTLC_AudioOption 14 | import io.github.kdroidfilter.knotify.platform.windows.types.WTLC_Error 15 | import io.github.kdroidfilter.knotify.platform.windows.types.WTLC_Instance 16 | import io.github.kdroidfilter.knotify.platform.windows.types.WTLC_Template 17 | import io.github.kdroidfilter.knotify.platform.windows.types.WTLC_TemplateType 18 | import io.github.kdroidfilter.knotify.platform.windows.types.WTLC_TextField 19 | 20 | internal interface WinToastLibC : Library { 21 | companion object { 22 | val INSTANCE: WinToastLibC = Native.load( 23 | "wintoastlibc", 24 | WinToastLibC::class.java, 25 | W32APIOptions.UNICODE_OPTIONS 26 | ) 27 | } 28 | 29 | fun WTLC_isCompatible(): Boolean 30 | fun WTLC_Instance_Create(): WTLC_Instance? 31 | fun WTLC_Instance_Destroy(instance: WTLC_Instance) 32 | 33 | fun WTLC_setAppName(instance: WTLC_Instance, appName: WString) 34 | fun WTLC_setAppUserModelId(instance: WTLC_Instance, aumi: WString) 35 | fun WTLC_setShortcutPolicy(instance: WTLC_Instance, policy: Int) 36 | 37 | fun WTLC_initialize(instance: WTLC_Instance, error: IntByReference): Boolean 38 | fun WTLC_strerror(error: WTLC_Error): WString 39 | 40 | fun WTLC_hideToast(instance: WTLC_Instance, id: Long): Boolean 41 | 42 | fun WTLC_Template_Create(templateType: WTLC_TemplateType): WTLC_Template? 43 | fun WTLC_Template_Destroy(template: WTLC_Template) 44 | 45 | fun WTLC_Template_setTextField(template: WTLC_Template, text: WString, field: WTLC_TextField) 46 | fun WTLC_Template_setAudioOption(template: WTLC_Template, option: WTLC_AudioOption) 47 | fun WTLC_Template_setExpiration(template: WTLC_Template, milliseconds: Long) 48 | fun WTLC_Template_setImagePath(template: WTLC_Template, imagePath: WString) 49 | fun WTLC_Template_addAction(template: WTLC_Template, label: WString) 50 | 51 | fun WTLC_showToast( 52 | instance: WTLC_Instance, 53 | template: WTLC_Template, 54 | userData: Pointer?, 55 | activatedCallback: ToastActivatedCallback?, 56 | activatedActionCallback: ToastActivatedActionCallback?, 57 | dismissedCallback: ToastDismissedCallback?, 58 | failedCallback: ToastFailedCallback?, 59 | error: IntByReference 60 | ): Long 61 | } 62 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /knotify-compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalWasmDsl::class) 2 | 3 | import com.vanniktech.maven.publish.SonatypeHost 4 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 5 | 6 | plugins { 7 | alias(libs.plugins.multiplatform) 8 | alias(libs.plugins.vannitktech.maven.publish) 9 | alias(libs.plugins.jetbrainsCompose) 10 | alias(libs.plugins.compose.compiler) 11 | } 12 | 13 | group = "io.github.kdroidfilter.knotify" 14 | val ref = System.getenv("GITHUB_REF") ?: "" 15 | val version = if (ref.startsWith("refs/tags/")) { 16 | val tag = ref.removePrefix("refs/tags/") 17 | if (tag.startsWith("v")) tag.substring(1) else tag 18 | } else "dev" 19 | 20 | kotlin { 21 | jvmToolchain(17) 22 | 23 | jvm() 24 | 25 | sourceSets { 26 | commonMain.dependencies { 27 | implementation(project(":knotify")) 28 | implementation(libs.kotlinx.coroutines.core) 29 | implementation(libs.kotlinx.coroutines.test) 30 | implementation(libs.kermit) 31 | implementation(libs.runtime) 32 | implementation(libs.platformtools.core) 33 | 34 | // Compose dependencies 35 | implementation(compose.runtime) 36 | implementation(compose.ui) 37 | } 38 | 39 | commonTest.dependencies { 40 | implementation(kotlin("test")) 41 | } 42 | 43 | jvmMain.dependencies { 44 | implementation(libs.kotlinx.coroutines.swing) 45 | implementation(compose.desktop.currentOs) 46 | } 47 | } 48 | 49 | //https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers 50 | targets.withType { 51 | compilations["main"].compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc") 52 | } 53 | } 54 | 55 | mavenPublishing { 56 | coordinates( 57 | groupId = "io.github.kdroidfilter", 58 | artifactId = "knotify-compose", 59 | version = version.toString() 60 | ) 61 | 62 | // Configure POM metadata for the published artifact 63 | pom { 64 | name.set("DesktopNotify-KT-Compose") 65 | description.set("Compose integration for DesktopNotify-KT, enabling the use of Composable UI in desktop notifications.") 66 | inceptionYear.set("2024") 67 | url.set("https://github.com/kdroidFilter/ComposeNativeNotification") 68 | 69 | licenses { 70 | license { 71 | name.set("MIT") 72 | url.set("https://opensource.org/licenses/MIT") 73 | } 74 | } 75 | 76 | // Specify developers information 77 | developers { 78 | developer { 79 | id.set("kdroidfilter") 80 | name.set("Elie Gambache") 81 | email.set("elyahou.hadass@gmail.com") 82 | } 83 | } 84 | 85 | // Specify SCM information 86 | scm { 87 | url.set("https://github.com/kdroidFilter/ComposeNativeNotification") 88 | } 89 | } 90 | 91 | // Configure publishing to Maven Central 92 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 93 | 94 | // Enable GPG signing for all publications 95 | signAllPublications() 96 | } 97 | 98 | task("testClasses") {} -------------------------------------------------------------------------------- /knotify/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalWasmDsl::class) 2 | 3 | import com.vanniktech.maven.publish.SonatypeHost 4 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 5 | 6 | plugins { 7 | alias(libs.plugins.multiplatform) 8 | alias(libs.plugins.vannitktech.maven.publish) 9 | } 10 | 11 | group = "io.github.kdroidfilter.knotify" 12 | val ref = System.getenv("GITHUB_REF") ?: "" 13 | val version = if (ref.startsWith("refs/tags/")) { 14 | val tag = ref.removePrefix("refs/tags/") 15 | if (tag.startsWith("v")) tag.substring(1) else tag 16 | } else "dev" 17 | 18 | kotlin { 19 | jvmToolchain(17) 20 | 21 | jvm() 22 | 23 | sourceSets { 24 | commonMain.dependencies { 25 | implementation(libs.kotlinx.coroutines.core) 26 | implementation(libs.kotlinx.coroutines.test) 27 | implementation(libs.kermit) 28 | implementation(libs.platformtools.core) 29 | } 30 | 31 | commonTest.dependencies { 32 | implementation(kotlin("test")) 33 | } 34 | 35 | 36 | jvmMain.dependencies { 37 | 38 | implementation(libs.kotlinx.coroutines.swing) 39 | implementation(libs.jna) 40 | implementation(libs.jna.platform) 41 | } 42 | } 43 | 44 | //https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers 45 | targets.withType { 46 | compilations["main"].compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc") 47 | } 48 | 49 | } 50 | 51 | 52 | val buildNativeMac: TaskProvider = tasks.register("buildNativeMac") { 53 | onlyIf { System.getProperty("os.name").startsWith("Mac") } 54 | workingDir(rootDir.resolve("maclib")) 55 | commandLine("./build.sh") 56 | } 57 | 58 | val buildNativeWin: TaskProvider = tasks.register("buildNativeWin") { 59 | onlyIf { System.getProperty("os.name").startsWith("Windows") } 60 | workingDir(rootDir.resolve("winlib/WinToastLibC")) 61 | commandLine("cmd", "/c", "build.bat") 62 | } 63 | 64 | val buildNativeLinux: TaskProvider = tasks.register("buildNativeLinux") { 65 | onlyIf { System.getProperty("os.name").startsWith("Linux") } 66 | workingDir(rootDir.resolve("linuxlib")) 67 | commandLine("./build.sh") 68 | } 69 | 70 | tasks.register("buildNativeLibraries") { 71 | dependsOn( buildNativeMac, buildNativeWin, buildNativeLinux) 72 | } 73 | 74 | mavenPublishing { 75 | coordinates( 76 | groupId = "io.github.kdroidfilter", 77 | artifactId = "knotify", 78 | version = version.toString() 79 | ) 80 | 81 | // Configure POM metadata for the published artifact 82 | pom { 83 | name.set("DesktopNotify-KT") 84 | description.set("The DesktopNotify-KT is a Kotlin JVM library that enables developers to add notifications to their desktop applications in a unified way across different platforms, including Windows, macOS and Linux.") 85 | inceptionYear.set("2024") 86 | url.set("https://github.com/kdroidFilter/ComposeNativeNotification") 87 | 88 | licenses { 89 | license { 90 | name.set("MIT") 91 | url.set("https://opensource.org/licenses/MIT") 92 | } 93 | } 94 | 95 | // Specify developers information 96 | developers { 97 | developer { 98 | id.set("kdroidfilter") 99 | name.set("Elie Gambache") 100 | email.set("elyahou.hadass@gmail.com") 101 | } 102 | } 103 | 104 | // Specify SCM information 105 | scm { 106 | url.set("https://github.com/kdroidFilter/ComposeNativeNotification") 107 | } 108 | } 109 | 110 | // Configure publishing to Maven Central 111 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 112 | 113 | 114 | // Enable GPG signing for all publications 115 | signAllPublications() 116 | } 117 | 118 | 119 | task("testClasses") {} 120 | -------------------------------------------------------------------------------- /demo/composeApp/src/commonMain/kotlin/io/github/kdroidfilter/knotify/demo/ScreenOne.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalResourceApi::class) 2 | 3 | package io.github.kdroidfilter.knotify.demo 4 | 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.Button 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | import io.github.kdroidfilter.knotify.builder.ExperimentalNotificationsApi 16 | import io.github.kdroidfilter.knotify.builder.notification 17 | import co.touchlab.kermit.Logger 18 | import io.github.kdroidfilter.knotify.demo.composeapp.generated.resources.Res 19 | import org.jetbrains.compose.resources.ExperimentalResourceApi 20 | 21 | // Reference to the shared logger 22 | private val logger = Logger.withTag("NotificationDemo") 23 | 24 | @OptIn(ExperimentalResourceApi::class, ExperimentalNotificationsApi::class) 25 | @Composable 26 | fun ScreenOne( 27 | onNavigate: () -> Unit, 28 | onNavigateToComposeDemo: () -> Unit, 29 | notificationMessage: String?, 30 | onShowMessage: (String?) -> Unit 31 | ) { 32 | val myNotification = notification( 33 | title = "Notification from Screen 1", 34 | message = "This is a test notification from Screen 1", 35 | largeIcon = Res.getUri("drawable/kdroid.png"), 36 | smallIcon = Res.getUri("drawable/compose.png"), 37 | onActivated = { logger.d { "Notification 1 activated" } }, 38 | onDismissed = { reason -> logger.d { "Notification 1 dismissed: $reason" } }, 39 | onFailed = { logger.d { "Notification 1 failed" } } 40 | ) { 41 | button(title = "Show Message from Button 1") { 42 | logger.d { "Button 1 from Screen 1 clicked" } 43 | onShowMessage("Button 1 clicked from Screen 1's notification") 44 | } 45 | button(title = "Hide Message from Button 2") { 46 | logger.d { "Button 2 from Screen 1 clicked" } 47 | onShowMessage("Button 2 clicked from Screen 1's notification") 48 | } 49 | } 50 | Column( 51 | modifier = Modifier 52 | .fillMaxSize() 53 | .padding(16.dp), 54 | verticalArrangement = Arrangement.Center, 55 | horizontalAlignment = Alignment.CenterHorizontally 56 | ) { 57 | Text( 58 | text = "Screen 1", 59 | style = MaterialTheme.typography.bodyLarge.copy(fontSize = 28.sp), 60 | color = MaterialTheme.colorScheme.primary 61 | ) 62 | Spacer(modifier = Modifier.height(24.dp)) 63 | Button( 64 | onClick = onNavigate, 65 | shape = RoundedCornerShape(8.dp), 66 | modifier = Modifier.fillMaxWidth(0.6f) 67 | ) { 68 | Text("Go to Screen 2") 69 | } 70 | Spacer(modifier = Modifier.height(16.dp)) 71 | Button( 72 | onClick = { 73 | myNotification.send() 74 | }, 75 | shape = RoundedCornerShape(8.dp), 76 | modifier = Modifier.fillMaxWidth(0.6f) 77 | ) { 78 | Text("Send notification from Screen 1") 79 | } 80 | 81 | Spacer(modifier = Modifier.height(16.dp)) 82 | Button( 83 | onClick = { 84 | // hide it 85 | myNotification.hide() 86 | }, 87 | shape = RoundedCornerShape(8.dp), 88 | modifier = Modifier.fillMaxWidth(0.6f) 89 | ) { 90 | Text("Hide notification from Screen 1") 91 | } 92 | 93 | Spacer(modifier = Modifier.height(16.dp)) 94 | Button( 95 | onClick = onNavigateToComposeDemo, 96 | shape = RoundedCornerShape(8.dp), 97 | modifier = Modifier.fillMaxWidth(0.6f) 98 | ) { 99 | Text("Go to Compose Notification Demo") 100 | } 101 | 102 | notificationMessage?.let { 103 | Spacer(modifier = Modifier.height(16.dp)) 104 | Text(it, fontSize = 20.sp, color = MaterialTheme.colorScheme.secondary) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /demo/composeApp/src/commonMain/kotlin/io/github/kdroidfilter/knotify/demo/ScreenTwo.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalResourceApi::class) 2 | 3 | package io.github.kdroidfilter.knotify.demo 4 | 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.Button 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | import io.github.kdroidfilter.knotify.builder.ExperimentalNotificationsApi 16 | import io.github.kdroidfilter.knotify.builder.notification 17 | import co.touchlab.kermit.Logger 18 | import io.github.kdroidfilter.knotify.demo.composeapp.generated.resources.Res 19 | import org.jetbrains.compose.resources.ExperimentalResourceApi 20 | 21 | // Reference to the shared logger 22 | private val logger = Logger.withTag("NotificationDemo") 23 | 24 | @OptIn(ExperimentalNotificationsApi::class) 25 | @Composable 26 | fun ScreenTwo( 27 | onNavigate: () -> Unit, 28 | onNavigateToComposeDemo: () -> Unit, 29 | notificationMessage: String?, 30 | onShowMessage: (String?) -> Unit 31 | ) { 32 | Column( 33 | modifier = Modifier 34 | .fillMaxSize() 35 | .padding(16.dp), 36 | verticalArrangement = Arrangement.Center, 37 | horizontalAlignment = Alignment.CenterHorizontally 38 | ) { 39 | Text( 40 | text = "Screen 2", 41 | style = MaterialTheme.typography.bodyLarge.copy(fontSize = 28.sp), 42 | color = MaterialTheme.colorScheme.primary 43 | ) 44 | Spacer(modifier = Modifier.height(24.dp)) 45 | Button( 46 | onClick = onNavigate, 47 | shape = RoundedCornerShape(8.dp), 48 | modifier = Modifier.fillMaxWidth(0.6f) 49 | ) { 50 | Text("Go back to Screen 1") 51 | } 52 | Spacer(modifier = Modifier.height(16.dp)) 53 | 54 | 55 | // Store the notification in a remember variable so we can reference it later 56 | val myNotification = remember { 57 | notification( 58 | largeIcon = Res.getUri("drawable/compose.png"), 59 | title = "Notification from Screen 2", 60 | message = "This is a test notification from Screen 2", 61 | onActivated = { logger.d { "Notification activated" } }, 62 | onDismissed = { reason -> logger.d { "Notification dismissed: $reason" } }, 63 | onFailed = { logger.d { "Notification failed" } } 64 | ) { 65 | button(title = "Show Message from Button 1") { 66 | logger.d { "Button 1 from Screen 2 clicked" } 67 | onShowMessage("Button 1 clicked from Screen 2's notification") 68 | } 69 | button(title = "Hide Message from Button 2") { 70 | logger.d { "Button 2 from Screen 2 clicked" } 71 | onShowMessage("Button 2 clicked from Screen 2's notification") 72 | } 73 | } 74 | } 75 | 76 | Button( 77 | onClick = { 78 | myNotification.send() 79 | }, 80 | shape = RoundedCornerShape(8.dp), 81 | modifier = Modifier.fillMaxWidth(0.6f) 82 | ) { 83 | Text("Send notification from Screen 2") 84 | } 85 | 86 | Spacer(modifier = Modifier.height(16.dp)) 87 | Button( 88 | onClick = { 89 | // hide it 90 | myNotification.hide() 91 | }, 92 | shape = RoundedCornerShape(8.dp), 93 | modifier = Modifier.fillMaxWidth(0.6f) 94 | ) { 95 | Text("Hide notification from Screen 2") 96 | } 97 | 98 | Spacer(modifier = Modifier.height(16.dp)) 99 | Button( 100 | onClick = onNavigateToComposeDemo, 101 | shape = RoundedCornerShape(8.dp), 102 | modifier = Modifier.fillMaxWidth(0.6f) 103 | ) { 104 | Text("Go to Compose Notification Demo") 105 | } 106 | 107 | notificationMessage?.let { 108 | Spacer(modifier = Modifier.height(16.dp)) 109 | Text(it, fontSize = 20.sp, color = MaterialTheme.colorScheme.secondary) 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/platform/linux/LinuxNativeNotificationIntegration.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.platform.linux 2 | 3 | import com.sun.jna.Callback 4 | import com.sun.jna.Library 5 | import com.sun.jna.Native 6 | import com.sun.jna.Pointer 7 | 8 | /** 9 | * Interface for integrating native Linux notification functionalities. 10 | */ 11 | interface LinuxNativeNotificationIntegration : Library { 12 | companion object { 13 | val INSTANCE: LinuxNativeNotificationIntegration = Native.load("notification", LinuxNativeNotificationIntegration::class.java) 14 | } 15 | 16 | /** 17 | * Set debug mode for the native library 18 | * @param enable 1 to enable debug logs, 0 to disable 19 | */ 20 | fun set_debug_mode(enable: Int) 21 | 22 | /** 23 | * Initialize the notification library with the application name 24 | * @param app_name The name of the application 25 | * @return 1 if initialization was successful, 0 otherwise 26 | */ 27 | fun my_notify_init(app_name: String): Int 28 | 29 | /** 30 | * Create a new notification with an icon 31 | * @param summary The notification title 32 | * @param body The notification message 33 | * @param icon_path Path to the icon file 34 | * @return Pointer to the created notification or null if failed 35 | */ 36 | fun create_notification(summary: String, body: String, icon_path: String): Pointer? 37 | 38 | /** 39 | * Add a button to the notification 40 | * @param notification Pointer to the notification 41 | * @param button_id ID of the button 42 | * @param button_label Label of the button 43 | * @param callback Callback to be called when the button is clicked 44 | * @param user_data User data to be passed to the callback 45 | */ 46 | fun add_button_to_notification(notification: Pointer?, button_id: String, button_label: String, callback: NotifyActionCallback?, user_data: Pointer?) 47 | 48 | /** 49 | * Send the notification 50 | * @param notification Pointer to the notification 51 | * @return 0 if successful, non-zero otherwise 52 | */ 53 | fun send_notification(notification: Pointer?): Int 54 | 55 | /** 56 | * Set image from GdkPixbuf 57 | * @param notification Pointer to the notification 58 | * @param pixbuf Pointer to the GdkPixbuf 59 | */ 60 | fun set_image_from_pixbuf(notification: Pointer?, pixbuf: Pointer?) 61 | 62 | /** 63 | * Load a GdkPixbuf from a file 64 | * @param image_path Path to the image file 65 | * @return Pointer to the loaded GdkPixbuf or null if failed 66 | */ 67 | fun load_pixbuf_from_file(image_path: String): Pointer? 68 | 69 | /** 70 | * Set callback for notification close 71 | * @param notification Pointer to the notification 72 | * @param callback Callback to be called when the notification is closed 73 | * @param user_data User data to be passed to the callback 74 | */ 75 | fun set_notification_closed_callback(notification: Pointer?, callback: NotifyClosedCallback, user_data: Pointer?) 76 | 77 | /** 78 | * Set callback for notification click 79 | * @param notification Pointer to the notification 80 | * @param callback Callback to be called when the notification is clicked 81 | * @param user_data User data to be passed to the callback 82 | */ 83 | fun set_notification_clicked_callback(notification: Pointer?, callback: NotifyActionCallback, user_data: Pointer?) 84 | 85 | /** 86 | * Clean up resources 87 | */ 88 | fun cleanup_notification() 89 | 90 | /** 91 | * Close/hide a notification 92 | * @param notification Pointer to the notification 93 | * @return 0 if successful, non-zero otherwise 94 | */ 95 | fun close_notification(notification: Pointer?): Int 96 | 97 | /** 98 | * Start the main loop to handle events 99 | */ 100 | fun run_main_loop() 101 | 102 | /** 103 | * Stop the main loop 104 | */ 105 | fun quit_main_loop() 106 | } 107 | 108 | /** 109 | * Callback interface for notification actions (button clicks or notification clicks) 110 | */ 111 | @FunctionalInterface 112 | fun interface NotifyActionCallback : Callback { 113 | fun invoke(notification: Pointer?, action: String?, user_data: Pointer?) 114 | } 115 | 116 | /** 117 | * Callback interface for notification closure 118 | */ 119 | @FunctionalInterface 120 | fun interface NotifyClosedCallback : Callback { 121 | fun invoke(notification: Pointer?, user_data: Pointer?) 122 | } 123 | -------------------------------------------------------------------------------- /knotify-compose/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/compose/utils/ComposableIconUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.compose.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.ImageComposeScene 5 | import androidx.compose.ui.use 6 | import kotlinx.coroutines.Dispatchers 7 | import org.jetbrains.skia.Bitmap 8 | import org.jetbrains.skia.EncodedImageFormat 9 | import org.jetbrains.skia.FilterMipmap 10 | import org.jetbrains.skia.FilterMode 11 | import org.jetbrains.skia.Image 12 | import org.jetbrains.skia.MipmapMode 13 | import java.io.File 14 | import java.util.zip.CRC32 15 | import java.awt.image.BufferedImage 16 | import java.io.ByteArrayOutputStream 17 | import javax.imageio.ImageIO 18 | import kotlin.use 19 | 20 | /** 21 | * Utility functions for rendering Composable icons to image files for use in system notifications. 22 | */ 23 | object ComposableIconUtils { 24 | 25 | /** 26 | * Renders a Composable to a PNG file and returns the path to the file. 27 | * 28 | * @param iconRenderProperties Properties for rendering the icon 29 | * @param content The Composable content to render 30 | * @return Path to the generated PNG file 31 | */ 32 | fun renderComposableToPngFile( 33 | iconRenderProperties: IconRenderProperties, 34 | content: @Composable () -> Unit 35 | ): String { 36 | val tempFile = createTempFile(suffix = ".png") 37 | val pngData = renderComposableToPngBytes(iconRenderProperties, content) 38 | tempFile.writeBytes(pngData) 39 | return tempFile.absolutePath 40 | } 41 | 42 | /** 43 | * Renders a Composable to a PNG image and returns the result as a byte array. 44 | * 45 | * This function creates an [ImageComposeScene] based on the provided [IconRenderProperties], 46 | * renders the Composable content, and encodes the output into PNG format. 47 | * If scaling is required based on the [IconRenderProperties], the rendered content is scaled before encoding. 48 | * 49 | * @param iconRenderProperties Properties for rendering the icon 50 | * @param content The Composable content to render 51 | * @return A byte array containing the rendered PNG image data. 52 | */ 53 | fun renderComposableToPngBytes( 54 | iconRenderProperties: IconRenderProperties, 55 | content: @Composable () -> Unit 56 | ): ByteArray { 57 | val scene = ImageComposeScene( 58 | width = iconRenderProperties.sceneWidth, 59 | height = iconRenderProperties.sceneHeight, 60 | density = iconRenderProperties.sceneDensity, 61 | coroutineContext = Dispatchers.Unconfined 62 | ) { 63 | content() 64 | } 65 | 66 | val renderedIcon = scene.use { it.render() } 67 | 68 | val iconData = if (iconRenderProperties.requiresScaling) { 69 | val scaledIcon = Bitmap().apply { 70 | allocN32Pixels(iconRenderProperties.targetWidth, iconRenderProperties.targetHeight) 71 | } 72 | renderedIcon.use { 73 | it.scalePixels(scaledIcon.peekPixels()!!, FilterMipmap(FilterMode.LINEAR, MipmapMode.LINEAR), true) 74 | } 75 | scaledIcon.use { bitmap -> 76 | Image.makeFromBitmap(bitmap).use { image -> 77 | image.encodeToData(EncodedImageFormat.PNG)!! 78 | } 79 | } 80 | } else { 81 | renderedIcon.use { image -> 82 | image.encodeToData(EncodedImageFormat.PNG)!! 83 | } 84 | } 85 | 86 | return iconData.bytes 87 | } 88 | 89 | /** 90 | * Creates a temporary file that will be deleted when the JVM exits. 91 | */ 92 | private fun createTempFile(prefix: String = "notification_icon_", suffix: String): File { 93 | val tempFile = File.createTempFile(prefix, suffix) 94 | tempFile.deleteOnExit() 95 | return tempFile 96 | } 97 | 98 | /** 99 | * Calculates a hash value for the rendered composable content. 100 | * This can be used to detect changes in the composable content without requiring an explicit key. 101 | * 102 | * @param iconRenderProperties Properties for rendering the icon 103 | * @param content The Composable content to render 104 | * @return A hash value representing the current state of the composable content 105 | */ 106 | fun calculateContentHash( 107 | iconRenderProperties: IconRenderProperties, 108 | content: @Composable () -> Unit 109 | ): Long { 110 | // Render the composable to PNG bytes 111 | val pngBytes = renderComposableToPngBytes(iconRenderProperties, content) 112 | 113 | // Calculate CRC32 hash of the PNG bytes 114 | val crc = CRC32() 115 | crc.update(pngBytes) 116 | return crc.value 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /demo/composeApp/src/commonMain/kotlin/io/github/kdroidfilter/knotify/demo/ScreenThree.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalResourceApi::class) 2 | 3 | package io.github.kdroidfilter.knotify.demo 4 | 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.unit.sp 18 | import co.touchlab.kermit.Logger 19 | import io.github.kdroidfilter.knotify.builder.ExperimentalNotificationsApi 20 | import io.github.kdroidfilter.knotify.compose.builder.notification 21 | import io.github.kdroidfilter.knotify.demo.composeapp.generated.resources.Res 22 | import io.github.kdroidfilter.knotify.demo.composeapp.generated.resources.compose 23 | import org.jetbrains.compose.resources.ExperimentalResourceApi 24 | import org.jetbrains.compose.resources.painterResource 25 | 26 | // Reference to the shared logger 27 | private val logger = Logger.withTag("NotificationDemo") 28 | 29 | @OptIn(ExperimentalNotificationsApi::class) 30 | @Composable 31 | fun ScreenThree( 32 | onNavigateBack: () -> Unit, notificationMessage: String?, onShowMessage: (String?) -> Unit 33 | ) { 34 | Column( 35 | modifier = Modifier.fillMaxSize().padding(16.dp), 36 | verticalArrangement = Arrangement.Center, 37 | horizontalAlignment = Alignment.CenterHorizontally 38 | ) { 39 | Text( 40 | text = "Compose Notification Demo", 41 | style = MaterialTheme.typography.bodyLarge.copy(fontSize = 28.sp), 42 | color = MaterialTheme.colorScheme.primary 43 | ) 44 | 45 | Spacer(modifier = Modifier.height(24.dp)) 46 | 47 | // Preview of the notification logo 48 | Box( 49 | modifier = Modifier.size(120.dp).clip(RoundedCornerShape(8.dp)) 50 | ) { 51 | NotificationLogo() 52 | } 53 | 54 | Spacer(modifier = Modifier.height(24.dp)) 55 | 56 | Text( 57 | text = "This screen demonstrates using a Composable UI as a notification image", 58 | fontSize = 16.sp, 59 | textAlign = TextAlign.Center, 60 | modifier = Modifier.padding(horizontal = 16.dp) 61 | ) 62 | 63 | Spacer(modifier = Modifier.height(24.dp)) 64 | 65 | val composeNotif = notification( 66 | title = "Compose Notification", 67 | message = "This notification uses a Composable UI as its image", 68 | largeIcon = { NotificationLogo() }, 69 | smallIcon = { NotificationLogo() }, 70 | onActivated = { logger.d { "Compose notification activated" } }, 71 | onDismissed = { reason -> logger.d { "Compose notification dismissed: $reason" } }, 72 | onFailed = { logger.d { "Compose notification failed" } }) { 73 | button(title = "Show Message") { 74 | logger.d { "Button clicked from Compose notification" } 75 | onShowMessage("Button clicked from Compose notification") 76 | } 77 | } 78 | 79 | 80 | Button( 81 | onClick = { 82 | composeNotif.send() 83 | }, shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth(0.6f) 84 | ) { 85 | Text("Send Compose Notification") 86 | } 87 | 88 | Spacer(modifier = Modifier.height(16.dp)) 89 | 90 | Button( 91 | onClick = { 92 | composeNotif.hide() 93 | }, shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth(0.6f) 94 | ) { 95 | Text("Hide Compose Notification") 96 | } 97 | 98 | Spacer(modifier = Modifier.height(24.dp)) 99 | 100 | Button( 101 | onClick = onNavigateBack, shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth(0.6f) 102 | ) { 103 | Text("Go back to Screen 1") 104 | } 105 | 106 | notificationMessage?.let { 107 | Spacer(modifier = Modifier.height(16.dp)) 108 | Text(it, fontSize = 20.sp, color = MaterialTheme.colorScheme.secondary) 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * A Composable function that creates a simple logo for use in 115 | * notifications. This demonstrates how to create a custom UI for 116 | * notifications using Compose. 117 | */ 118 | @Composable 119 | fun NotificationLogo() { 120 | Box( 121 | modifier = Modifier.fillMaxSize() 122 | ) { 123 | Image( 124 | painter = painterResource(Res.drawable.compose), 125 | contentDescription = "Notification Logo", 126 | modifier = Modifier.align(Alignment.Center) 127 | ) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /knotify/src/jvmMain/kotlin/io/github/kdroidfilter/knotify/utils/JarResourceExtractor.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.utils 2 | 3 | import co.touchlab.kermit.Logger 4 | import java.io.File 5 | import java.io.FileNotFoundException 6 | import java.io.InputStream 7 | import java.net.URI 8 | import java.net.URLDecoder 9 | import java.nio.file.Files 10 | import java.nio.file.StandardCopyOption 11 | import java.security.MessageDigest 12 | import java.util.jar.JarFile 13 | 14 | // Initialize Kermit logger 15 | private val logger = Logger.withTag("JarResourceExtractor") 16 | 17 | 18 | fun extractToTempIfDifferent(jarPath: String): File? { 19 | // Check if the path is a regular file path or a JAR path 20 | if (!jarPath.startsWith("jar:file:") && !jarPath.contains("!")) { 21 | // This is a regular file path, not a JAR path 22 | val file = File(jarPath) 23 | if (file.exists()) { 24 | return file 25 | } else { 26 | throw FileNotFoundException("File does not exist: $jarPath") 27 | } 28 | } 29 | 30 | // Analyze the path to get the file path and the entry path 31 | val correctedJarFilePath = URLDecoder.decode(jarPath.substringAfter("jar:file:").substringBefore("!"), Charsets.UTF_8.name()) 32 | 33 | // Encode special characters to be URI compatible 34 | val encodedJarFilePath = correctedJarFilePath.replace(" ", "%20") 35 | 36 | // Convert the path to File via URI 37 | val jarFile = try { 38 | File(URI("file:" + encodedJarFilePath.replace("\\", "/"))) 39 | } catch (e: IllegalArgumentException) { 40 | File(correctedJarFilePath.removePrefix("file:")) 41 | } 42 | 43 | // Check if the file exists 44 | if (!jarFile.exists()) { 45 | throw FileNotFoundException("File does not exist: $correctedJarFilePath") 46 | } 47 | 48 | val entryPath = jarPath.substringAfter("!").trimStart('/') 49 | 50 | // Extract file extension from the original path 51 | val fileExtension = getFileExtension(jarFile.name) 52 | logger.d { "Original file extension: $fileExtension" } 53 | 54 | // Logging to verify paths 55 | logger.d { "Corrected jarFilePath: $correctedJarFilePath" } 56 | logger.d { "Encoded jarFilePath: $encodedJarFilePath" } 57 | logger.d { "Entry path: $entryPath" } 58 | 59 | 60 | // If the file is not a JAR, handle it differently 61 | if (!correctedJarFilePath.endsWith(".jar")) { 62 | logger.d { "The file is not a JAR. Direct copy." } 63 | val tempFile = createTempFile("extracted_", fileExtension, File(System.getProperty("java.io.tmpdir"))).apply { 64 | deleteOnExit() 65 | } 66 | 67 | // Copy the file directly if it is not a JAR 68 | Files.copy(jarFile.toPath(), tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING) 69 | return tempFile 70 | } 71 | 72 | // Open the JAR from the absolute path 73 | JarFile(jarFile).use { jar -> 74 | val entry = jar.getJarEntry(entryPath) ?: return null 75 | 76 | // Extract file extension from the entry name 77 | val fileExtension = getFileExtension(entryPath) 78 | logger.d { "JAR entry file extension: $fileExtension" } 79 | 80 | // Create a temporary file to store the extracted resource 81 | val tempFile = createTempFile("extracted_", fileExtension, File(System.getProperty("java.io.tmpdir"))).apply { 82 | deleteOnExit() 83 | } 84 | 85 | // Check if the temporary file already exists and compare the hash 86 | if (tempFile.exists()) { 87 | val tempFileHash = tempFile.sha256() 88 | jar.getInputStream(entry).use { input -> 89 | val jarEntryHash = input.sha256() 90 | // If the hash is identical, no need to copy again 91 | if (tempFileHash == jarEntryHash) { 92 | return tempFile 93 | } 94 | } 95 | } 96 | 97 | // Copy the content of the JAR entry to the temporary file 98 | jar.getInputStream(entry).use { input -> 99 | Files.copy(input, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING) 100 | } 101 | 102 | return tempFile 103 | } 104 | } 105 | 106 | 107 | // Extension to calculate SHA-256 of a file 108 | fun File.sha256(): String = inputStream().use { it.sha256() } 109 | 110 | // Extension to calculate SHA-256 of an InputStream 111 | fun InputStream.sha256(): String { 112 | val digest = MessageDigest.getInstance("SHA-256") 113 | val buffer = ByteArray(1024) 114 | var bytesRead: Int 115 | while (this.read(buffer).also { bytesRead = it } != -1) { 116 | digest.update(buffer, 0, bytesRead) 117 | } 118 | return digest.digest().joinToString("") { "%02x".format(it) } 119 | } 120 | 121 | // Helper function to extract file extension from a path 122 | private fun getFileExtension(path: String): String { 123 | val lastDotIndex = path.lastIndexOf('.') 124 | return if (lastDotIndex > 0) { 125 | // Return the extension with the dot 126 | path.substring(lastDotIndex) 127 | } else { 128 | // Default to .tmp if no extension is found 129 | ".tmp" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /knotify/src/commonMain/kotlin/io/github/kdroidfilter/knotify/builder/NotificationBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.knotify.builder 2 | 3 | import io.github.kdroidfilter.knotify.model.Button 4 | import io.github.kdroidfilter.knotify.model.DismissalReason 5 | 6 | 7 | /** 8 | * Marks the notifications API as experimental and subject to change in future releases. 9 | */ 10 | @Suppress("ExperimentalAnnotationRetention") 11 | @RequiresOptIn( 12 | level = RequiresOptIn.Level.WARNING, 13 | message = "This notifications API is experimental and may change in the future." 14 | ) 15 | @Retention(AnnotationRetention.BINARY) 16 | annotation class ExperimentalNotificationsApi 17 | 18 | /** 19 | * Creates a notification with customizable settings. The notification can have an app name, 20 | * icon, title, message, and a large image. Additionally, various actions can be added to the 21 | * notification using a builder-style DSL. 22 | * 23 | * @param title The title of the notification. Defaults to an empty string. 24 | * @param message The message of the notification. Defaults to an empty string. 25 | * @param largeIcon The file path to a large image to be displayed within the notification. Can be null. 26 | * @param onActivated Callback that is invoked when the notification is activated. 27 | * @param onDismissed Callback that is invoked when the notification is dismissed. 28 | * @param onFailed Callback that is invoked when the notification fails to display. 29 | * @param builderAction A DSL block that customizes the notification options and actions. 30 | * @return A Notification object that can be sent or hidden. 31 | */ 32 | @ExperimentalNotificationsApi 33 | fun notification( 34 | title: String = "", 35 | message: String = "", 36 | largeIcon: String? = null, 37 | smallIcon: String? = null, 38 | onActivated: (() -> Unit)? = null, 39 | onDismissed: ((DismissalReason) -> Unit)? = null, 40 | onFailed: (() -> Unit)? = null, 41 | builderAction: NotificationBuilder.() -> Unit = {} 42 | ): Notification { 43 | val builder = NotificationBuilder(title, message, largeIcon, smallIcon, onActivated, onDismissed, onFailed) 44 | builder.builderAction() 45 | return Notification(builder) 46 | } 47 | 48 | /** 49 | * Creates and immediately sends a notification with customizable settings. 50 | * 51 | * @param title The title of the notification. Defaults to an empty string. 52 | * @param message The message of the notification. Defaults to an empty string. 53 | * @param largeImage The file path to a large image to be displayed within the notification. Can be null. 54 | * @param onActivated Callback that is invoked when the notification is activated. 55 | * @param onDismissed Callback that is invoked when the notification is dismissed. 56 | * @param onFailed Callback that is invoked when the notification fails to display. 57 | * @param builderAction A DSL block that customizes the notification options and actions. 58 | * @return A Notification object that can be hidden or manipulated later. 59 | */ 60 | @ExperimentalNotificationsApi 61 | suspend fun sendNotification( 62 | title: String = "", 63 | message: String = "", 64 | largeImage: String? = null, 65 | smallIcon: String? = null, 66 | onActivated: (() -> Unit)? = null, 67 | onDismissed: ((DismissalReason) -> Unit)? = null, 68 | onFailed: (() -> Unit)? = null, 69 | builderAction: NotificationBuilder.() -> Unit = {} 70 | ): Notification { 71 | val notification = notification(title, message, largeImage, smallIcon, onActivated, onDismissed, onFailed, builderAction) 72 | notification.send() 73 | return notification 74 | } 75 | 76 | /** 77 | * A notification that can be sent or hidden. 78 | */ 79 | class Notification internal constructor(private val builder: NotificationBuilder) { 80 | /** 81 | * Sends the notification. 82 | */ 83 | fun send() { 84 | val notificationProvider = getNotificationProvider() 85 | notificationProvider.sendNotification(builder) 86 | } 87 | 88 | /** 89 | * Hides the notification. 90 | */ 91 | fun hide() { 92 | val notificationProvider = getNotificationProvider() 93 | notificationProvider.hideNotification(builder) 94 | } 95 | } 96 | 97 | class NotificationBuilder( 98 | var title: String = "", 99 | var message: String = "", 100 | var largeImagePath: String?, 101 | var smallIconPath: String? = null, 102 | var onActivated: (() -> Unit)? = null, 103 | var onDismissed: ((DismissalReason) -> Unit)? = null, 104 | var onFailed: (() -> Unit)? = null, 105 | ) { 106 | internal val buttons = mutableListOf