From 49bd33c53eae19abe24704b282c3a3f14e110fcf Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:28:33 -0500 Subject: [PATCH] feat(ui): enable wasmJs for the shared Compose design-system module core:ui is the first UI-layer module in this effort -- 204 Kotlin files, the substrate every feature module depends on. Every one of its own dependencies (core/*, coil3, jetbrains:markdown, qrcode-kotlin, every compose-multiplatform-*/jetbrains-* artifact) already publishes a wasmJs variant, so this was mostly a platform-seam problem, not a dependency problem: 6 commonMain files carry expect/actual declarations (an initial survey found 3; a second sweep before writing any code found 3 more), and the other 198 files needed zero changes. PlatformUtils.kt's ~23 members are mostly Android-system-settings concepts (NFC/Bluetooth/Wi-Fi/location/app settings deep-links, ahead-of-time permission-state checks, native file/document pickers) with no browser equivalent -- these mirror iOS's own NoopStubs.kt exactly, the established precedent in this codebase for "the platform doesn't have this concept, don't block the UI on it." Two exceptions get real implementations instead: rememberOpenUrl opens the URL via window.open, and KeepScreenOn is backed by the real Screen Wake Lock API (hand-declared external binding, kotlinx-browser has none) with its acquire/release lifecycle kept in one coroutine via awaitCancellation()/finally so a fast dispose can never race ahead of an in-flight request and leak the lock. createClipEntry (ClipboardUtils.kt) upgrades to ClipEntry.withPlainText -- verified against Compose Multiplatform's own wasmJs klib metadata that this is the only factory available on this target, with the actual Clipboard API access handled inside Compose Multiplatform's own setClipEntry() implementation, not something this module needs to wire itself. annotatedStringFromHtml (HtmlUtils.kt) keeps the plain-text fallback that the JVM/desktop actual already uses -- matching this codebase's own established fidelity bar for HTML rendering off Android, not a regression introduced here. dynamicColorScheme, SetScreenBrightness, and DropDownPreference's enum-reflection helpers (found in the second sweep) all get honest no-ops matching the JVM/iOS actuals already in place, each documented with why no better browser answer exists. 3 of 27 commonTest files (the ones depending on core:testing, which has no wasmJs target -- the same gap every prior module's test suite has hit) move to a new nonWebTest source set; a vestigial, actually-unused libs.junit dependency moves with them. Co-Authored-By: Claude Sonnet 5 --- core/ui/build.gradle.kts | 54 ++++++- .../ui/share/SharedContactViewModelTest.kt | 0 .../core/ui/util/ProtoExtensionsTest.kt | 0 .../ui/viewmodel/ConnectionsViewModelTest.kt | 0 .../ui/component/EnumReflection.wasmJs.kt | 30 ++++ .../core/ui/theme/DynamicColorScheme.kt | 23 +++ .../meshtastic/core/ui/util/ClipboardUtils.kt | 33 ++++ .../org/meshtastic/core/ui/util/HtmlUtils.kt | 30 ++++ .../meshtastic/core/ui/util/PlatformUtils.kt | 144 ++++++++++++++++++ .../meshtastic/core/ui/util/ScreenUtils.kt | 29 ++++ 10 files changed, 339 insertions(+), 4 deletions(-) rename core/ui/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt (100%) rename core/ui/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt (100%) rename core/ui/src/{commonTest => nonWebTest}/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt (100%) create mode 100644 core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/component/EnumReflection.wasmJs.kt create mode 100644 core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/theme/DynamicColorScheme.kt create mode 100644 core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/ClipboardUtils.kt create mode 100644 core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/HtmlUtils.kt create mode 100644 core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt create mode 100644 core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/ScreenUtils.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index c60358c184..1183bbbb38 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -28,6 +28,19 @@ kotlin { // Without this, Res.readBytes() throws MissingResourceException at runtime. android { androidResources.enable = true } + // Library module: bare wasmJs(), no browser() (that's for the eventual webApp executable). No custom + // hierarchy group is needed for MAIN, same shape as core:common/core:model/core:repository/core:service: + // every one of this module's own dependencies (core/*, coil, jetbrains-markdown, qrcode-kotlin, every + // compose-multiplatform-*/jetbrains-* artifact) is confirmed to publish a wasmJs variant, and the only + // three commonMain files with `expect`/`actual` (PlatformUtils.kt, ClipboardUtils.kt, HtmlUtils.kt) plus + // three more found while verifying this pass (ScreenUtils.kt, DynamicColorScheme.kt, + // DropDownPreference.kt's enumEntriesOf/isDeprecatedEnumEntry) all gained real wasmJsMain actuals below — + // none of them needed a native-only dependency hidden from wasmJs, so no `applyHierarchyTemplate` call is + // needed here (calling it a second time, after `meshtastic.kmp.jvm.android`'s own, would fail — Gradle + // only allows one per project — which is why core:common/core:model keep the plugin and skip the call too). + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs() + sourceSets { commonMain.dependencies { implementation(projects.core.common) @@ -73,11 +86,44 @@ kotlin { androidMain.dependencies { implementation(libs.androidx.activity.compose) } - commonTest.dependencies { - implementation(projects.core.testing) - implementation(libs.junit) - implementation(libs.compose.multiplatform.ui.test) + // `rememberOpenUrl`'s wasmJs actual (`window.open`) is the only wasmJsMain file needing a JS-facing + // dependency; everything else in wasmJsMain is either a pure Kotlin no-op or (createClipEntry, + // KeepScreenOn) uses only Compose Multiplatform's own API / a hand-declared `external` (matching + // core:ble's `WebBluetoothApi.kt` idiom for the Screen Wake Lock API kotlinx-browser doesn't bind). + wasmJsMain.dependencies { implementation(libs.kotlinx.browser) } + + commonTest.dependencies { implementation(libs.compose.multiplatform.ui.test) } + + // TEST only: core:testing has no wasmJs target (same gap core:ble/core:database/core:network/ + // core:data/core:service all hit) — confirmed via grep that exactly 3 of this module's 27 commonTest + // files import org.meshtastic.core.testing (ConnectionsViewModelTest.kt, ProtoExtensionsTest.kt, + // SharedContactViewModelTest.kt); those 3 move to nonWebTest, the other 24 (including every + // runComposeUiTest-based UI test — compose-multiplatform-ui-test itself does publish a wasmJs variant) + // stay in commonTest. `libs.junit` also moves here: grep found zero `org.junit.*` usage anywhere in + // this module's tests (vestigial, presumably pre-dating the migration to `kotlin.test`), and it is a + // plain non-KMP jar with no wasmJs metadata at all, so leaving it in commonTest risked being a second, + // indistinguishable cause of any wasmJs test-resolution failure. + // + // No applyHierarchyTemplate call happens in this script (see the MAIN comment above), so the default + // template's shared intermediate test source sets are not synchronously available by name this early + // (confirmed: core:ui has no `android { withHostTest {} }` call at all, so `androidHostTest` doesn't + // even exist — unlike core:service/core:data, which all call `withHostTest {}` and could `getByName` + // it directly). `matching{}.configureEach{}` degrades to a harmless no-op for whichever of these leaf + // test source sets isn't registered, instead of throwing. + val nonWebTest by creating { + dependsOn(commonTest.get()) + dependencies { + implementation(projects.core.testing) + implementation(libs.junit) + } } + matching { + it.name == "jvmTest" || + it.name == "androidHostTest" || + it.name == "iosArm64Test" || + it.name == "iosSimulatorArm64Test" + } + .configureEach { dependsOn(nonWebTest) } jvmTest.dependencies { implementation(compose.desktop.currentOs) } } diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt b/core/ui/src/nonWebTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt similarity index 100% rename from core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt rename to core/ui/src/nonWebTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt b/core/ui/src/nonWebTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt similarity index 100% rename from core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt rename to core/ui/src/nonWebTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt b/core/ui/src/nonWebTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt similarity index 100% rename from core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt rename to core/ui/src/nonWebTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt diff --git a/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/component/EnumReflection.wasmJs.kt b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/component/EnumReflection.wasmJs.kt new file mode 100644 index 0000000000..04717112a7 --- /dev/null +++ b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/component/EnumReflection.wasmJs.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.component + +// jvmAndroidMain's actuals use `java.lang.Class` reflection (`declaringJavaClass.enumConstants`, `getField(...)`) to +// enumerate an enum's entries and check for `@Deprecated` from just an instance, with no reified type parameter to +// call `enumValues()`/`enumEntries()` instead. Kotlin/Wasm has no `java.lang.Class`-equivalent reflection +// surface at all (same gap iOS's Kotlin/Native actual has, which is why it takes the identical fallback below) — and +// changing `DropDownPreference`'s public signature to take a reified type parameter instead of an instance is a +// commonMain API change well outside this pass's scope. Matches iOS exactly: `DropDownPreference`'s enum-instance +// overload renders with no selectable items on this target, same as on iOS today — a pre-existing gap this pass +// inherits rather than introduces. Callers who need reliable enum options in a dropdown can use the `List>` or `List>` overloads instead, which don't call through this expect at all. +internal actual fun > enumEntriesOf(selectedItem: T): List = emptyList() + +internal actual fun Enum<*>.isDeprecatedEnumEntry(): Boolean = false diff --git a/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/theme/DynamicColorScheme.kt b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/theme/DynamicColorScheme.kt new file mode 100644 index 0000000000..b99c1fb0b7 --- /dev/null +++ b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/theme/DynamicColorScheme.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +/** Web — dynamic (Material You / wallpaper-derived) color is an Android System-only concept. Matches the JVM actual. */ +@Composable actual fun dynamicColorScheme(darkTheme: Boolean): ColorScheme? = null diff --git a/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/ClipboardUtils.kt b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/ClipboardUtils.kt new file mode 100644 index 0000000000..7e1b50bf7e --- /dev/null +++ b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/ClipboardUtils.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.util + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.ClipEntry + +// Web has no native clip-data object comparable to Android's `ClipData`/AWT's `StringSelection` — there is no +// `ClipEntry(nativeType)` constructor overload for wasmJs at all (confirmed against Compose Multiplatform's own +// wasmJs `ui` klib: only the common `ClipEntry.withPlainText` factory is present in that target's metadata). That +// factory is real, not a fallback: writing the returned `ClipEntry` to the system clipboard still goes through the +// genuine `navigator.clipboard` Async Clipboard API — that's handled inside Compose Multiplatform's own +// `Clipboard.setClipEntry()` implementation for this target, which every call site here already goes through (see +// `CopyIconButton.kt`, `QrDialog.kt`, etc.) — so no extra `navigator.clipboard.writeText` interop is needed here. +// `label`/`sensitive` have no browser equivalent (there is no OS-level "don't show a paste-preview toast" concept, nor +// a "mark as secret" clipboard flag in the Clipboard API), so — like the JVM/AWT actual's `sensitive` — both are +// accepted and ignored. +@OptIn(ExperimentalComposeUiApi::class) +actual fun createClipEntry(text: String, label: String, sensitive: Boolean): ClipEntry = ClipEntry.withPlainText(text) diff --git a/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/HtmlUtils.kt b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/HtmlUtils.kt new file mode 100644 index 0000000000..442077580d --- /dev/null +++ b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/HtmlUtils.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.util + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLinkStyles + +// androidMain's actual (`AnnotatedString.fromHtml`) is a genuinely Android-specific real HTML renderer — it isn't a +// common Compose Multiplatform API, so it isn't available here. The desktop JVM actual, despite running on a full +// desktop JVM with far more capability than a browser sandbox, *also* takes the plain-text shortcut below rather than +// pulling in an HTML parser — that's the established bar for "good enough" fidelity on every non-Android platform in +// this codebase today, so wasmJs matches it rather than reaching for `org.jetbrains:markdown`-style HTML parsing +// (this module already depends on that library for actual Markdown, but it doesn't parse HTML) or hand-rolling a +// `dangerouslySetInnerHTML`-style DOM injection, which would also sidestep Compose's own text layout/selection. +/** Web stub — returns the raw HTML as plain text (no HTML rendering), matching the JVM/Desktop and iOS actuals. */ +actual fun annotatedStringFromHtml(html: String, linkStyles: TextLinkStyles?): AnnotatedString = AnnotatedString(html) diff --git a/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt new file mode 100644 index 0000000000..b315bac263 --- /dev/null +++ b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +@file:Suppress("TooManyFunctions") + +package org.meshtastic.core.ui.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import kotlinx.browser.window +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.await +import kotlinx.coroutines.awaitCancellation +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.common.util.CommonUri +import kotlin.js.Promise + +// Everything below except `rememberOpenUrl` and `KeepScreenOn` mirrors iosMain/NoopStubs.kt's exact choices — same +// reasoning as iOS: these are Android-system-settings concepts (NFC/Bluetooth/Wi-Fi/location/app settings, ahead-of- +// time permission-state checks) with no browser equivalent, or platform features (toasts, native file/document +// pickers) whose nearest browser analog (a File System Access API picker, gated behind a user gesture and Chromium- +// only) isn't a drop-in replacement for the contract these expects describe. `bleScanRequiresLocationServices` and the +// four `is*Disabled`/`is*Unavailable` checks are `false` for the same reason they are on iOS: the concept of an ahead- +// of-time "is Bluetooth/GPS/Wi-Fi disabled at the system level" query doesn't exist for a sandboxed browser tab. + +@Composable actual fun rememberOpenNfcSettings(): () -> Unit = {} + +@Composable actual fun rememberShowToast(): suspend (String) -> Unit = { _ -> } + +@Composable actual fun rememberShowToastResource(): suspend (StringResource) -> Unit = { _ -> } + +@Composable actual fun rememberOpenMap(): (latitude: Double, longitude: Double, label: String) -> Unit = { _, _, _ -> } + +/** Web — opens the URL in a new browser tab via `window.open`, so the running app's own tab/state is left intact. */ +@Composable actual fun rememberOpenUrl(): (url: String) -> Unit = { url -> window.open(url, "_blank", "") } + +@Composable +actual fun rememberSaveFileLauncher( + onUriReceived: (CommonUri) -> Unit, +): (defaultFilename: String, mimeType: String) -> Unit = { _, _ -> } + +@Composable +actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeType: String) -> Unit = { _ -> } + +@Composable actual fun rememberOpenDocumentTreeLauncher(onTreeUriSelect: (CommonUri?) -> Unit): () -> Unit = {} + +@Composable actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? = { _, _ -> null } + +/** + * Web — backed by the real, documented Screen Wake Lock API + * (https://www.w3.org/TR/screen-wake-lock/#the-wakelock-interface). `kotlinx-browser` has no binding for it (checked: + * absent from its generated `org.w3c.dom` sources), so it's declared fresh below, following the same `external + * interface : JsAny` + private `external val navigator` idiom `core:ble`'s `WebBluetoothApi.kt` established for + * `navigator.bluetooth`. + * + * Requesting the lock requires a secure context (HTTPS) and a visible document; a request that is rejected for any + * reason (unsupported browser, insecure origin, tab not visible yet) is swallowed here — the screen simply behaves as + * if `KeepScreenOn` had never been called, the same no-op fallback posture every platform gap in this file takes. + * + * Known gap, not fixed here: per spec the browser releases the lock automatically the instant the document is hidden + * (tab backgrounded/minimized), and this does not re-request it when the tab regains visibility — a deliberate scope + * cut for this pass (re-acquiring correctly needs a `visibilitychange` listener, which `kotlinx-browser` also doesn't + * bind), not an oversight. Android's own `view.keepScreenOn` is similarly revocable by the OS outside this code's + * control, so this is a difference of degree, not of kind. + */ +@Suppress("TooGenericExceptionCaught") +@Composable +actual fun KeepScreenOn(enabled: Boolean) { + LaunchedEffect(enabled) { + if (!enabled) return@LaunchedEffect + // The request and the eventual release live in the same coroutine (rather than a DisposableEffect firing a + // separate `launch` for the request) so a fast dispose can never race ahead of an in-flight `request()` and + // leak the lock — `awaitCancellation()` blocks this coroutine open until the effect leaves composition or + // `enabled` flips, and `finally` then runs release synchronously on that same cancellation, never orphaned. + val sentinel = + try { + navigator.wakeLock?.request("screen")?.await() + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + // See kdoc: unsupported/insecure-context/not-yet-visible rejection is an expected, silent no-op. + null + } + try { + awaitCancellation() + } finally { + sentinel?.release() + } + } +} + +@Composable actual fun rememberOpenLocationSettings(): () -> Unit = {} + +@Composable actual fun rememberOpenBluetoothSettings(): () -> Unit = {} + +@Composable actual fun rememberOpenWifiSettings(): () -> Unit = {} + +actual val bleScanRequiresLocationServices: Boolean = false + +@Composable actual fun isGpsDisabled(): Boolean = false + +@Composable actual fun isBluetoothDisabled(): Boolean = false + +@Composable actual fun isWifiUnavailable(): Boolean = false + +@Composable actual fun rememberOpenAppSettings(): () -> Unit = {} + +@Composable actual fun rememberLocationPermissionState(): PermissionUiState = grantedPermissionUiState() + +@Composable actual fun rememberBluetoothPermissionState(): PermissionUiState = grantedPermissionUiState() + +@Composable actual fun rememberNotificationPermissionState(): PermissionUiState = grantedPermissionUiState() + +@Composable actual fun rememberLocalNetworkPermissionState(): PermissionUiState = grantedPermissionUiState() + +@Composable actual fun rememberCameraPermissionState(): PermissionUiState = grantedPermissionUiState() + +private external interface JsWakeLockSentinel : JsAny { + fun release(): Promise +} + +private external interface JsWakeLock : JsAny { + fun request(type: String): Promise +} + +private external interface JsNavigatorWakeLock : JsAny { + val wakeLock: JsWakeLock? +} + +/** Access to `navigator.wakeLock`, isolated here — see the `KeepScreenOn` kdoc above. */ +private external val navigator: JsNavigatorWakeLock diff --git a/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/ScreenUtils.kt b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/ScreenUtils.kt new file mode 100644 index 0000000000..2677f00bd7 --- /dev/null +++ b/core/ui/src/wasmJsMain/kotlin/org/meshtastic/core/ui/util/ScreenUtils.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.util + +import androidx.compose.runtime.Composable + +/** + * Web no-op — unlike Screen Wake Lock, there is no browser API to read or set the physical display's brightness at all + * (a page can only fake it by dimming its own content with an overlay, which is a UI concern, not a platform primitive + * this function should reach for). Matches the JVM/Desktop actual. + */ +@Composable +actual fun SetScreenBrightness(brightness: Float) { + // No-op on Web — no browser API for physical display brightness exists. +}