From 036aeba245645dc073a6a4a3374a341234939b09 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 15 Aug 2026 08:30:39 +0300 Subject: [PATCH] add web appbar color customization --- .../data/sync/strategy/SearchSyncStrategy.kt | 1 - .../vbox/shared/presentation/Navigation.kt | 7 +++--- .../kotlin/ru/shadowsparky/vbox/Main.kt | 7 +++++- .../backend/data/TokenVerifier.kt | 3 ++- .../shadowsparky/ui/components/Expressive.kt | 4 +++ .../kotlin/ru/shadowsparky/ui/nav/Nav.kt | 3 ++- .../ru/shadowsparky/ui/PageLifecycle.kt | 25 +++++++++++++++++++ 7 files changed, 43 insertions(+), 7 deletions(-) diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/SearchSyncStrategy.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/SearchSyncStrategy.kt index d56956d..02bb568 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/SearchSyncStrategy.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/sync/strategy/SearchSyncStrategy.kt @@ -38,4 +38,3 @@ class SearchSyncStrategy( } } } - diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/Navigation.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/Navigation.kt index bdb2637..f3098f5 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/Navigation.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/Navigation.kt @@ -39,14 +39,14 @@ fun Modifier.imePaddingInternal(): Modifier { } @Composable -fun Nav(root: VBoxRootComponent) { +fun Nav(root: VBoxRootComponent, onAttach: @Composable () -> Unit = {}) { CompositionLocalProvider( LocalImageLoader provides remember { root.imageLoaderProvider.provide() } - ) { NavInternal(root) } + ) { NavInternal(root, onAttach) } } @Composable -private fun NavInternal(root: VBoxRootComponent) { +private fun NavInternal(root: VBoxRootComponent, onAttach: @Composable () -> Unit) { Nav(root) { child, route -> when (child) { is Child.PlayerChild -> PlayerScreen(child.component) @@ -76,6 +76,7 @@ private fun NavInternal(root: VBoxRootComponent) { } } } + onAttach() UpdateDialog(root.updateComponent) } } diff --git a/apps/vbox/client/web/src/wasmJsMain/kotlin/ru/shadowsparky/vbox/Main.kt b/apps/vbox/client/web/src/wasmJsMain/kotlin/ru/shadowsparky/vbox/Main.kt index f3e4f65..21be40c 100644 --- a/apps/vbox/client/web/src/wasmJsMain/kotlin/ru/shadowsparky/vbox/Main.kt +++ b/apps/vbox/client/web/src/wasmJsMain/kotlin/ru/shadowsparky/vbox/Main.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -26,6 +27,7 @@ import ru.shadowsparky.domain.AppConfig import ru.shadowsparky.koin import ru.shadowsparky.ui.hideElementById import ru.shadowsparky.ui.hookPageVisibility +import ru.shadowsparky.ui.updateBrowserAppBarColor import ru.shadowsparky.vbox.shared.presentation.Nav import ru.shadowsparky.vbox.shared.presentation.RootFactory import ru.shadowsparky.vbox.shared.presentation.UrlRoutes @@ -71,7 +73,10 @@ fun mainInternal() { root.onFinish.collect { root.navReplace(Route.Loading()) } } Box(Modifier.fillMaxSize().padding(WindowInsets.safeDrawing.asPaddingValues())) { - Nav(root) + Nav(root) { + val color = MaterialTheme.colorScheme.background + LaunchedEffect(color) { updateBrowserAppBarColor(color) } + } } } } diff --git a/libs/backend-base/src/main/kotlin/ru/shadowsparky/backend/data/TokenVerifier.kt b/libs/backend-base/src/main/kotlin/ru/shadowsparky/backend/data/TokenVerifier.kt index 006cbb3..0f447ae 100644 --- a/libs/backend-base/src/main/kotlin/ru/shadowsparky/backend/data/TokenVerifier.kt +++ b/libs/backend-base/src/main/kotlin/ru/shadowsparky/backend/data/TokenVerifier.kt @@ -1,6 +1,7 @@ package ru.shadowsparky.backend.data import com.auth0.jwt.JWT +import com.auth0.jwt.JWTVerifier import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.interfaces.DecodedJWT import org.koin.core.annotation.Factory @@ -14,7 +15,7 @@ class TokenVerifier( private val loginVerifier: LoginVerifier ) { - val verifier = JWT.require(Algorithm.HMAC512(jwtInfo.secret)) + val verifier: JWTVerifier = JWT.require(Algorithm.HMAC512(jwtInfo.secret)) .withAudience(jwtInfo.audience) .withIssuer(jwtInfo.issuer) .build() diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt index d41d2b6..a79ba01 100644 --- a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt +++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -89,11 +91,13 @@ fun ExpressiveLazyColumn( reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, + state: LazyListState = rememberLazyListState(), content: ExpressiveLazyListScope.() -> Unit ) { val scope = ExpressiveLazyListScope().apply(content) LazyColumn( + state = state, modifier = modifier, contentPadding = contentPadding, reverseLayout = reverseLayout, diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/nav/Nav.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/nav/Nav.kt index dbc84ed..a1f7152 100644 --- a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/nav/Nav.kt +++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/nav/Nav.kt @@ -9,6 +9,7 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.fade import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.androidPredictiveBackAnimatableV2 import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import ru.shadowsparky.ui.theme.getColorScheme @OptIn(ExperimentalDecomposeApi::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -16,7 +17,7 @@ fun Nav( rootComponent: RootComponent<*>, process: @Composable (child: Child, route: Route) -> Unit ) { - MaterialExpressiveTheme { + MaterialExpressiveTheme(getColorScheme()) { Children( stack = rootComponent.valueChildStack, animation = predictiveBackAnimation( diff --git a/libs/ui/src/wasmJsMain/kotlin/ru/shadowsparky/ui/PageLifecycle.kt b/libs/ui/src/wasmJsMain/kotlin/ru/shadowsparky/ui/PageLifecycle.kt index 6f3e108..775e7a5 100644 --- a/libs/ui/src/wasmJsMain/kotlin/ru/shadowsparky/ui/PageLifecycle.kt +++ b/libs/ui/src/wasmJsMain/kotlin/ru/shadowsparky/ui/PageLifecycle.kt @@ -1,5 +1,6 @@ package ru.shadowsparky.ui +import androidx.compose.ui.graphics.Color import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.essenty.lifecycle.create @@ -8,6 +9,7 @@ import com.arkivanov.essenty.lifecycle.resume import com.arkivanov.essenty.lifecycle.start import com.arkivanov.essenty.lifecycle.stop import kotlinx.browser.document +import org.w3c.dom.HTMLMetaElement import org.w3c.dom.events.Event @JsFun( @@ -80,3 +82,26 @@ fun hookPageVisibility(lifecycle: LifecycleRegistry) { document.addEventListener("pagehide", { onPageHideShow(true) }) document.addEventListener("pageshow", { onPageHideShow(false) }) } + +fun updateBrowserAppBarColor(color: Color) { + var metaElement = document.querySelector("meta[name='theme-color']") as? HTMLMetaElement + if (metaElement == null) { + val meta = document.createElement("meta") as HTMLMetaElement + meta.setAttribute("name", "theme-color") + document.head?.appendChild(meta) + metaElement = meta + } + metaElement.setAttribute("content", color.toHexStr()) +} + +private fun Color.toHexStr(): String { + val r = (this.red * 255).toInt() + val g = (this.green * 255).toInt() + val b = (this.blue * 255).toInt() + return "#${r.toHexString()}${g.toHexString()}${b.toHexString()}" +} + +private fun Int.toHexString(): String { + val hex = this.toString(16) + return if (hex.length == 1) "0$hex" else hex +}