├── .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 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
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