diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt new file mode 100644 index 000000000..259768254 --- /dev/null +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt @@ -0,0 +1,31 @@ +package io.github.kdroidfilter.nucleus.darkmodedetector + +import io.github.kdroidfilter.nucleus.core.runtime.Platform +import io.github.kdroidfilter.nucleus.darkmodedetector.linux.LinuxPortalThemeDetector +import io.github.kdroidfilter.nucleus.darkmodedetector.mac.MacOSThemeDetector +import io.github.kdroidfilter.nucleus.darkmodedetector.windows.WindowsThemeDetector +import java.util.function.Consumer + +interface IDarkModeDetector { + fun isDark(): Boolean + + fun registerListener(listener: Consumer) + + fun removeListener(listener: Consumer) +} + +object NoopDarkModeDetector : IDarkModeDetector { + override fun isDark(): Boolean = false + + override fun registerListener(listener: Consumer) = Unit + + override fun removeListener(listener: Consumer) = Unit +} + +public fun getPlatformDarkModeDetector(): IDarkModeDetector = + when (Platform.Current) { + Platform.MacOS -> MacOSThemeDetector + Platform.Windows -> WindowsThemeDetector + Platform.Linux -> LinuxPortalThemeDetector + else -> NoopDarkModeDetector + } diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt index a1f669a1c..65aa4b33b 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt @@ -2,11 +2,11 @@ package io.github.kdroidfilter.nucleus.darkmodedetector import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalInspectionMode -import io.github.kdroidfilter.nucleus.core.runtime.Platform -import io.github.kdroidfilter.nucleus.darkmodedetector.linux.isLinuxInDarkMode -import io.github.kdroidfilter.nucleus.darkmodedetector.mac.isMacOsInDarkMode -import io.github.kdroidfilter.nucleus.darkmodedetector.windows.isWindowsInDarkMode +import java.util.function.Consumer /** * Composable function that returns whether the system is in dark mode. @@ -18,11 +18,26 @@ fun isSystemInDarkMode(): Boolean { if (isInPreview) { return isSystemInDarkTheme() } + val darkModeDetector = remember { getPlatformDarkModeDetector() } + val darkModeState = remember { mutableStateOf(darkModeDetector.isDark()) } - return when (Platform.Current) { - Platform.MacOS -> isMacOsInDarkMode() - Platform.Windows -> isWindowsInDarkMode() - Platform.Linux -> isLinuxInDarkMode() - else -> false + DisposableEffect(Unit) { + debugln(TAG) { "Registering OS dark mode listener in Compose" } + val listener = + Consumer { newValue -> + debugln(TAG) { "OS dark mode updated: $newValue" } + darkModeState.value = newValue + } + + darkModeDetector.registerListener(listener) + + onDispose { + debugln(TAG) { "Removing OS dark mode listener in Compose" } + darkModeDetector.removeListener(listener) + } } + + return darkModeState.value } + +private const val TAG = "PlatformThemeDetectorCompose" diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt index 789c3b15c..a97d485b4 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt @@ -1,9 +1,6 @@ package io.github.kdroidfilter.nucleus.darkmodedetector.linux -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector import io.github.kdroidfilter.nucleus.darkmodedetector.debugln import java.util.function.Consumer @@ -18,42 +15,19 @@ private const val TAG = "LinuxPortalThemeDetector" * The detector also monitors for SettingChanged signals in real-time via a background * D-Bus dispatch thread. */ -internal object LinuxPortalThemeDetector { +internal object LinuxPortalThemeDetector : IDarkModeDetector { init { debugln(TAG) { "Initializing Linux portal theme observer via JNI" } NativeLinuxBridge.nativeStartObserving() } - fun isDark(): Boolean = NativeLinuxBridge.nativeIsDark() + override fun isDark(): Boolean = NativeLinuxBridge.nativeIsDark() - fun registerListener(listener: Consumer) { + override fun registerListener(listener: Consumer) { NativeLinuxBridge.registerListener(listener) } - fun removeListener(listener: Consumer) { + override fun removeListener(listener: Consumer) { NativeLinuxBridge.removeListener(listener) } } - -/** - * A helper composable function that returns the current Linux dark mode state - * via the XDG Desktop Portal, updating automatically when the system theme changes. - */ -@Composable -fun isLinuxInDarkMode(): Boolean { - val darkModeState = remember { mutableStateOf(LinuxPortalThemeDetector.isDark()) } - DisposableEffect(Unit) { - debugln(TAG) { "Registering Linux portal dark mode listener in Compose" } - val listener = - Consumer { newValue -> - debugln(TAG) { "Compose Linux portal dark mode updated: $newValue" } - darkModeState.value = newValue - } - LinuxPortalThemeDetector.registerListener(listener) - onDispose { - debugln(TAG) { "Removing Linux portal dark mode listener in Compose" } - LinuxPortalThemeDetector.removeListener(listener) - } - } - return darkModeState.value -} diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt index 491afef23..6cd6e9580 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt @@ -1,9 +1,6 @@ package io.github.kdroidfilter.nucleus.darkmodedetector.mac -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector import io.github.kdroidfilter.nucleus.darkmodedetector.debugln import java.util.function.Consumer @@ -14,42 +11,19 @@ private const val TAG = "MacOSThemeDetector" * to detect theme changes in macOS. It reads the system preference "AppleInterfaceStyle" * (which is "Dark" when in dark mode) from NSUserDefaults. */ -internal object MacOSThemeDetector { +internal object MacOSThemeDetector : IDarkModeDetector { init { debugln(TAG) { "Initializing macOS theme observer via JNI" } NativeDarkModeBridge.nativeStartObserving() } - fun isDark(): Boolean = NativeDarkModeBridge.nativeIsDark() + override fun isDark(): Boolean = NativeDarkModeBridge.nativeIsDark() - fun registerListener(listener: Consumer) { + override fun registerListener(listener: Consumer) { NativeDarkModeBridge.registerListener(listener) } - fun removeListener(listener: Consumer) { + override fun removeListener(listener: Consumer) { NativeDarkModeBridge.removeListener(listener) } } - -/** - * A helper composable function that returns the current macOS dark mode state, - * updating automatically when the system theme changes. - */ -@Composable -internal fun isMacOsInDarkMode(): Boolean { - val darkModeState = remember { mutableStateOf(MacOSThemeDetector.isDark()) } - DisposableEffect(Unit) { - debugln(TAG) { "Registering macOS dark mode listener in Compose" } - val listener = - Consumer { newValue -> - debugln(TAG) { "Compose macOS dark mode updated: $newValue" } - darkModeState.value = newValue - } - MacOSThemeDetector.registerListener(listener) - onDispose { - debugln(TAG) { "Removing macOS dark mode listener in Compose" } - MacOSThemeDetector.removeListener(listener) - } - } - return darkModeState.value -} diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt index 9dbe098ed..33689c14e 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt @@ -1,9 +1,6 @@ package io.github.kdroidfilter.nucleus.darkmodedetector.windows -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector import io.github.kdroidfilter.nucleus.darkmodedetector.debugln import java.util.function.Consumer @@ -18,45 +15,19 @@ private const val TAG = "WindowsThemeDetector" * The detector monitors the registry for changes in real-time via a native * background thread using RegNotifyChangeKeyValue in async mode. */ -internal object WindowsThemeDetector { +internal object WindowsThemeDetector : IDarkModeDetector { init { debugln(TAG) { "Initializing Windows theme observer via JNI" } NativeWindowsBridge.nativeStartObserving() } - fun isDark(): Boolean = NativeWindowsBridge.nativeIsDark() + override fun isDark(): Boolean = NativeWindowsBridge.nativeIsDark() - fun registerListener(listener: Consumer) { + override fun registerListener(listener: Consumer) { NativeWindowsBridge.registerListener(listener) } - fun removeListener(listener: Consumer) { + override fun removeListener(listener: Consumer) { NativeWindowsBridge.removeListener(listener) } } - -/** - * Composable function that returns whether Windows is currently in dark mode. - */ -@Composable -internal fun isWindowsInDarkMode(): Boolean { - val darkModeState = remember { mutableStateOf(WindowsThemeDetector.isDark()) } - - DisposableEffect(Unit) { - debugln(TAG) { "Registering Windows dark mode listener in Compose" } - val listener = - Consumer { newValue -> - debugln(TAG) { "Windows dark mode updated: $newValue" } - darkModeState.value = newValue - } - - WindowsThemeDetector.registerListener(listener) - - onDispose { - debugln(TAG) { "Removing Windows dark mode listener in Compose" } - WindowsThemeDetector.removeListener(listener) - } - } - - return darkModeState.value -} diff --git a/docs/runtime/darkmode-detector.md b/docs/runtime/darkmode-detector.md index 9fbe5901f..8f1542438 100644 --- a/docs/runtime/darkmode-detector.md +++ b/docs/runtime/darkmode-detector.md @@ -14,6 +14,40 @@ dependencies { ## Usage +### Manual usage + +This is the core functionality - it’s totally up to you how you want to use it. Just don’t forget to dispose its callback (`removeListener`) when you don’t need it anymore. + +You can use Compose states, RxJava/Kotlin, or anything else you prefer. + +Here we’re using a coroutines Flow wrapper just for demonstration. + +```kt +val isSystemDarkFlow: Flow = callbackFlow { + val listener: Consumer = { isDark: Boolean -> + trySend(isDark) + } + + val darkModeDetector = getPlatformDarkModeDetector() + + // emit initial value + trySend(darkModeDetector.isDark()) + + // listen to dark mode change events + darkModeDetector.registerListener(listener) + awaitClose { + darkModeDetector.removeListener(listener) + } +} +isSystemDarkFlow.collect { + println("dark mode changes, isDark: $it") +} +``` + +### In compose + +There is already a composable function available out of the box, which you can use it in your compose applications. + ```kotlin @Composable fun App() {