Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/build-natives.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,21 @@ jobs:
name: linux-natives-${{ matrix.arch }}
path: darkmode-detector/src/main/resources/nucleus/native/linux-*/
retention-days: 1

- name: Build nucleus-notification Linux native shared library
run: bash nucleus-notification/src/main/native/linux/build.sh
env:
CI: true

- name: Verify nucleus-notification Linux native
run: |
f="nucleus-notification/src/main/resources/nucleus/native/linux-${{ matrix.arch }}/libnucleus_notification.so"
if [ ! -f "$f" ]; then echo "MISSING: $f" >&2; exit 1; fi
echo "OK: $f ($(wc -c < "$f") bytes)"

- name: Upload nucleus-notification Linux shared library
uses: actions/upload-artifact@v4
with:
name: linux-natives-notification-${{ matrix.arch }}
path: nucleus-notification/src/main/resources/nucleus/native/linux-*/
retention-days: 1
16 changes: 16 additions & 0 deletions .github/workflows/publish-maven.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ jobs:
pattern: 'decorated-window-*'
merge-multiple: true

- name: Download nucleus-notification x64 artifacts
uses: actions/download-artifact@v4
with:
path: nucleus-notification/src/main/resources/nucleus/native/
pattern: 'linux-natives-notification-x64'
merge-multiple: true

- name: Download nucleus-notification ARM64 artifacts
uses: actions/download-artifact@v4
with:
path: nucleus-notification/src/main/resources/nucleus/native/
pattern: 'linux-natives-notification-aarch64'
merge-multiple: true

- name: Verify all natives present
run: |
EXPECTED=(
Expand All @@ -53,6 +67,8 @@ jobs:
"native-ssl/src/main/resources/nucleus/native/win32-aarch64/nucleus_ssl.dll"
"decorated-window/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_macos.dylib"
"decorated-window/src/main/resources/nucleus/native/darwin-x64/libnucleus_macos.dylib"
"nucleus-notification/src/main/resources/nucleus/native/linux-x64/libnucleus_notification.so"
"nucleus-notification/src/main/resources/nucleus/native/linux-aarch64/libnucleus_notification.so"
)
MISSING=0
for f in "${EXPECTED[@]}"; do
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ site
decorated-window/src/main/resources/nucleus/native/
darkmode-detector/src/main/resources/nucleus/native/
native-ssl/src/main/resources/nucleus/native/
nucleus-notification/src/main/resources/nucleus/native/

# MSVC build artifacts
*.obj
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,31 +49,31 @@ private val timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS

private fun getCurrentTimestamp(): String = LocalDateTime.now().format(timeFormatter)

internal fun debugln(message: () -> String) {
public fun debugln(message: () -> String) {
if (allowNucleusRuntimeLogging && composeNativeTrayLoggingLevel <= DEBUG) {
println("[${getCurrentTimestamp()}] ${message()}")
}
}

internal fun verboseln(message: () -> String) {
public fun verboseln(message: () -> String) {
if (allowNucleusRuntimeLogging && composeNativeTrayLoggingLevel <= VERBOSE) {
println("[${getCurrentTimestamp()}] ${message()}", COLOR_LIGHT_GRAY)
}
}

internal fun infoln(message: () -> String) {
public fun infoln(message: () -> String) {
if (allowNucleusRuntimeLogging && composeNativeTrayLoggingLevel <= INFO) {
println("[${getCurrentTimestamp()}] ${message()}", COLOR_AQUA)
}
}

internal fun warnln(message: () -> String) {
public fun warnln(message: () -> String) {
if (allowNucleusRuntimeLogging && composeNativeTrayLoggingLevel <= WARN) {
println("[${getCurrentTimestamp()}] ${message()}", COLOR_ORANGE)
}
}

internal fun errorln(message: () -> String) {
public fun errorln(message: () -> String) {
if (allowNucleusRuntimeLogging && composeNativeTrayLoggingLevel <= ERROR) {
println("[${getCurrentTimestamp()}] ${message()}", COLOR_RED)
}
Expand Down
3 changes: 2 additions & 1 deletion example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
implementation(project(":updater-runtime"))
implementation(project(":darkmode-detector"))
implementation(project(":decorated-window-material"))
implementation(project(":nucleus-notification"))
}

val releaseVersion =
Expand Down Expand Up @@ -65,7 +66,7 @@ nucleus.application {

// --- Native libs handling ---
cleanupNativeLibs = true // Auto cleanup native libraries
enableAotCache = true // Enable AOT compilation cache
enableAotCache = false // Enable AOT compilation cache
splashImage = "splash.png" // Splash screen image file
homepage = "https://github.com/KdroidFilter/NucleusDemo"

Expand Down
8 changes: 8 additions & 0 deletions example/config/ktlint/baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<baseline version="1.0">
<file name="src/main/kotlin/com/example/demo/Main.kt">
<error line="61" column="1" source="standard:no-unused-imports" />
<error line="324" column="63" source="standard:trailing-comma-on-call-site" />
<error line="329" column="10" source="standard:trailing-comma-on-call-site" />
</file>
</baseline>
7 changes: 7 additions & 0 deletions example/detekt-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>FunctionNaming:Main.kt$@OptIn(ExperimentalNotificationsApi::class) @Composable private fun NotificationButton()</ID>
</CurrentIssues>
</SmellBaseline>
32 changes: 32 additions & 0 deletions example/src/main/kotlin/com/example/demo/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ import io.github.kdroidfilter.nucleus.core.runtime.DeepLinkHandler
import io.github.kdroidfilter.nucleus.core.runtime.Platform
import io.github.kdroidfilter.nucleus.core.runtime.SingleInstanceManager
import io.github.kdroidfilter.nucleus.darkmodedetector.isSystemInDarkMode
import io.github.kdroidfilter.nucleus.notification.builder.ExperimentalNotificationsApi
import io.github.kdroidfilter.nucleus.notification.builder.notification
import io.github.kdroidfilter.nucleus.notification.builder.sendNotification
import io.github.kdroidfilter.nucleus.updater.NucleusUpdater
import io.github.kdroidfilter.nucleus.updater.UpdateResult
import io.github.kdroidfilter.nucleus.updater.provider.GitHubProvider
Expand Down Expand Up @@ -253,6 +256,10 @@ fun app() {
) {
NucleusAtom(atomSize = 200.dp)

Spacer(modifier = Modifier.height(24.dp))

NotificationButton()

if (currentDeepLink != null) {
Spacer(modifier = Modifier.height(16.dp))
Text(
Expand Down Expand Up @@ -300,6 +307,31 @@ fun app() {
}
}

@OptIn(ExperimentalNotificationsApi::class)
@Composable
private fun NotificationButton() {
var notificationSent by remember { mutableStateOf(false) }

Button(
onClick = {
notificationSent = true
notification(
title = "Nucleus Demo",
smallIcon = "dialog-information",
message = "This is a test notification from Nucleus!",
onActivated = { println("Notification activated!") },
onDismissed = { reason -> println("Notification dismissed: $reason") },
onFailed = { println("Notification failed!") }
) {
button("Reply") { println("Reply button clicked!") }
button("Dismiss") { println("Dismiss button clicked!") }
}.send()
}
) {
Text(if (notificationSent) "Notification Sent!" else "Send Notification")
}
}

private enum class ThemeMode {
System,
Dark,
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jbrApi = "1.10.1"
jna = "5.17.0"
okhttp = "4.12.0"
ktor = "3.4.0"
kotlinxCoroutines = "1.10.2"

[plugins]
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt"}
Expand Down Expand Up @@ -41,3 +42,4 @@ ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-apache5 = { module = "io.ktor:ktor-client-apache5", version.ref = "ktor" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
145 changes: 145 additions & 0 deletions nucleus-notification/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import org.apache.tools.ant.taskdefs.condition.Os
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
kotlin("jvm")
alias(libs.plugins.vanniktechMavenPublish)
}

val publishVersion =
providers
.environmentVariable("GITHUB_REF")
.orNull
?.removePrefix("refs/tags/v")
?: "1.0.0"

dependencies {
compileOnly(project(":core-runtime"))
implementation(libs.kotlinx.coroutines.core)
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}

val nativeResourceDir = layout.projectDirectory.dir("src/main/resources/nucleus/native")

val buildNativeMacOs by tasks.registering(Exec::class) {
description = "Compiles the Objective-C JNI bridge into macOS dylibs (arm64 + x64)"
group = "build"
val hasPrebuilt =
nativeResourceDir
.dir("darwin-aarch64")
.file("libnucleus_notification.dylib")
.asFile
.exists()
enabled = Os.isFamily(Os.FAMILY_MAC) && !hasPrebuilt

val nativeDir = layout.projectDirectory.dir("src/main/native/macos")
inputs.dir(nativeDir)
outputs.dir(nativeResourceDir)
workingDir(nativeDir)
commandLine("bash", "build.sh")
}

val buildNativeLinux by tasks.registering(Exec::class) {
description = "Compiles the C JNI bridge into Linux shared library (x64 + aarch64)"
group = "build"
val hasPrebuiltX64 = nativeResourceDir
.dir("linux-x64")
.file("libnucleus_notification.so")
.asFile
.exists()
// Build if x64 is missing (local dev) or if CI is detected (for ARM64)
val isCI = System.getenv("CI") != null
enabled = (Os.isFamily(Os.FAMILY_UNIX) && !Os.isFamily(Os.FAMILY_MAC)) && (!hasPrebuiltX64 || isCI)

val nativeDir = layout.projectDirectory.dir("src/main/native/linux")
inputs.dir(nativeDir)
outputs.dir(nativeResourceDir)
workingDir(nativeDir)
commandLine("bash", "build.sh")
}

// Task to ensure ARM64 is built before publishing (CI only)
val buildNativeLinuxArm64ForPublish by tasks.registering(Exec::class) {
description = "Cross-compiles ARM64 Linux native library for publishing"
group = "publish"
val isCI = System.getenv("CI") != null
val hasArm64 = nativeResourceDir
.dir("linux-aarch64")
.file("libnucleus_notification.so")
.asFile
.exists()

// Only run in CI when publishing and ARM64 not already built
enabled = isCI && !hasArm64

val nativeDir = layout.projectDirectory.dir("src/main/native/linux")
inputs.dir(nativeDir)
outputs.dir(nativeResourceDir.dir("linux-aarch64"))
workingDir(nativeDir)

// Set CI flag to force ARM64 build
environment("CI", "true")
commandLine("bash", "build.sh")
}

tasks.processResources {
dependsOn(buildNativeMacOs)
dependsOn(buildNativeLinux)
}

tasks.configureEach {
if (name == "sourcesJar") {
dependsOn(buildNativeMacOs)
dependsOn(buildNativeLinux)
}
// Ensure ARM64 is built before publishing
if (name.startsWith("publish") || name.startsWith("publishTo")) {
dependsOn(buildNativeLinuxArm64ForPublish)
}
}

mavenPublishing {
coordinates("io.github.kdroidfilter", "nucleus.notification", publishVersion)

pom {
name.set("Nucleus Notification")
description.set("Desktop notifications for Compose Desktop across Windows, macOS and Linux")
url.set("https://github.com/kdroidFilter/Nucleus")

licenses {
license {
name.set("MIT License")
url.set("https://opensource.org/licenses/MIT")
}
}

developers {
developer {
id.set("kdroidfilter")
name.set("kdroidFilter")
url.set("https://github.com/kdroidFilter")
}
}

scm {
url.set("https://github.com/kdroidFilter/Nucleus")
connection.set("scm:git:git://github.com/kdroidFilter/Nucleus.git")
developerConnection.set("scm:git:ssh://git@github.com/kdroidFilter/Nucleus.git")
}
}

publishToMavenCentral()
if (project.hasProperty("signingInMemoryKey")) {
signAllPublications()
}
}
19 changes: 19 additions & 0 deletions nucleus-notification/detekt-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>EmptyFunctionBlock:NoOpNotificationProvider.kt$NoOpNotificationProvider${ }</ID>
<ID>LongParameterList:NotificationBuilder.kt$( title: String = "", message: String = "", largeIcon: String? = null, smallIcon: String? = null, onActivated: (() -&gt; Unit)? = null, onDismissed: ((DismissalReason) -&gt; Unit)? = null, onFailed: (() -&gt; Unit)? = null, builderAction: NotificationBuilder.() -&gt; Unit = {} )</ID>
<ID>LongParameterList:NotificationBuilder.kt$( title: String = "", message: String = "", largeImage: String? = null, smallIcon: String? = null, onActivated: (() -&gt; Unit)? = null, onDismissed: ((DismissalReason) -&gt; Unit)? = null, onFailed: (() -&gt; Unit)? = null, builderAction: NotificationBuilder.() -&gt; Unit = {} )</ID>
<ID>LongParameterList:NotificationBuilder.kt$NotificationBuilder$( var title: String = "", var message: String = "", var largeImagePath: String?, var smallIconPath: String? = null, var onActivated: (() -&gt; Unit)? = null, var onDismissed: ((DismissalReason) -&gt; Unit)? = null, var onFailed: (() -&gt; Unit)? = null, )</ID>
<ID>MagicNumber:MacNotificationProvider.kt$MacNotificationProvider$100</ID>
<ID>MaxLineLength:LinuxNotificationProvider.kt$LinuxNotificationProvider.&lt;no name provided&gt;$infoln { "LinuxNotificationProvider: Closed by user interaction - NOT calling onDismissed" }</ID>
<ID>MaxLineLength:LinuxNotificationProvider.kt$LinuxNotificationProvider.&lt;no name provided&gt;$warnln { "LinuxNotificationProvider: Could not find builder ID for notification ptr: $notificationPtr" }</ID>
<ID>SwallowedException:LinuxNotificationProvider.kt$LinuxNotificationProvider$e: Exception</ID>
<ID>SwallowedException:MacNotificationProvider.kt$MacNotificationProvider$e: Exception</ID>
<ID>TooGenericExceptionCaught:LinuxNotificationProvider.kt$LinuxNotificationProvider$e: Exception</ID>
<ID>TooGenericExceptionCaught:MacNotificationProvider.kt$MacNotificationProvider$e: Exception</ID>
<ID>TooGenericExceptionCaught:NativeNotificationBridge.kt$NativeNotificationBridge$e: Exception</ID>
<ID>TooManyFunctions:NativeNotificationBridge.kt$NativeNotificationBridge</ID>
</CurrentIssues>
</SmellBaseline>
Loading
Loading