fix: tag A2UI platform and boost Android canvas
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
- macOS Talk Mode: orb overlay refresh, ElevenLabs request logging, API key status in settings, and auto-select first voice when none is configured.
|
- macOS Talk Mode: orb overlay refresh, ElevenLabs request logging, API key status in settings, and auto-select first voice when none is configured.
|
||||||
- Talk Mode: wait for chat history to surface the assistant reply before starting TTS (macOS/iOS/Android).
|
- Talk Mode: wait for chat history to surface the assistant reply before starting TTS (macOS/iOS/Android).
|
||||||
- Gateway config: inject `talk.apiKey` from `ELEVENLABS_API_KEY`/shell profile so nodes can fetch it on demand.
|
- Gateway config: inject `talk.apiKey` from `ELEVENLABS_API_KEY`/shell profile so nodes can fetch it on demand.
|
||||||
|
- Canvas A2UI: tag requests with `platform=android|ios|macos` and boost Android canvas background contrast.
|
||||||
- iOS/Android nodes: enable scrolling for loaded web pages in the Canvas WebView (default scaffold stays touch-first).
|
- iOS/Android nodes: enable scrolling for loaded web pages in the Canvas WebView (default scaffold stays touch-first).
|
||||||
- macOS menu: device list now uses `node.list` (devices only; no agent/tool presence entries).
|
- macOS menu: device list now uses `node.list` (devices only; no agent/tool presence entries).
|
||||||
- macOS menu: device list now shows connected nodes only.
|
- macOS menu: device list now shows connected nodes only.
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ dependencies {
|
|||||||
implementation("androidx.core:core-ktx:1.17.0")
|
implementation("androidx.core:core-ktx:1.17.0")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
|
||||||
implementation("androidx.activity:activity-compose:1.12.2")
|
implementation("androidx.activity:activity-compose:1.12.2")
|
||||||
|
implementation("androidx.webkit:webkit:1.14.0")
|
||||||
|
|
||||||
implementation("androidx.compose.ui:ui")
|
implementation("androidx.compose.ui:ui")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
|
|||||||
@@ -815,7 +815,7 @@ class NodeRuntime(context: Context) {
|
|||||||
val raw = session.currentCanvasHostUrl()?.trim().orEmpty()
|
val raw = session.currentCanvasHostUrl()?.trim().orEmpty()
|
||||||
if (raw.isBlank()) return null
|
if (raw.isBlank()) return null
|
||||||
val base = raw.trimEnd('/')
|
val base = raw.trimEnd('/')
|
||||||
return "${base}/__clawdis__/a2ui/"
|
return "${base}/__clawdis__/a2ui/?platform=android"
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ensureA2uiReady(a2uiUrl: String): Boolean {
|
private suspend fun ensureA2uiReady(a2uiUrl: String): Boolean {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import com.steipete.clawdis.node.BuildConfig
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@@ -23,6 +24,7 @@ import java.io.BufferedWriter
|
|||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.URI
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
@@ -213,7 +215,14 @@ class BridgeSession(
|
|||||||
when (first["type"].asStringOrNull()) {
|
when (first["type"].asStringOrNull()) {
|
||||||
"hello-ok" -> {
|
"hello-ok" -> {
|
||||||
val name = first["serverName"].asStringOrNull() ?: "Bridge"
|
val name = first["serverName"].asStringOrNull() ?: "Bridge"
|
||||||
canvasHostUrl = first["canvasHostUrl"].asStringOrNull()?.trim()?.ifEmpty { null }
|
val rawCanvasUrl = first["canvasHostUrl"].asStringOrNull()?.trim()?.ifEmpty { null }
|
||||||
|
canvasHostUrl = normalizeCanvasHostUrl(rawCanvasUrl, endpoint)
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
android.util.Log.d(
|
||||||
|
"ClawdisBridge",
|
||||||
|
"canvasHostUrl resolved=${canvasHostUrl ?: "none"} (raw=${rawCanvasUrl ?: "none"})",
|
||||||
|
)
|
||||||
|
}
|
||||||
onConnected(name, conn.remoteAddress)
|
onConnected(name, conn.remoteAddress)
|
||||||
}
|
}
|
||||||
"error" -> {
|
"error" -> {
|
||||||
@@ -292,6 +301,37 @@ class BridgeSession(
|
|||||||
conn.closeQuietly()
|
conn.closeQuietly()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun normalizeCanvasHostUrl(raw: String?, endpoint: BridgeEndpoint): String? {
|
||||||
|
val trimmed = raw?.trim().orEmpty()
|
||||||
|
val parsed = trimmed.takeIf { it.isNotBlank() }?.let { runCatching { URI(it) }.getOrNull() }
|
||||||
|
val host = parsed?.host?.trim().orEmpty()
|
||||||
|
val port = parsed?.port ?: -1
|
||||||
|
val scheme = parsed?.scheme?.trim().orEmpty().ifBlank { "http" }
|
||||||
|
|
||||||
|
if (trimmed.isNotBlank() && !isLoopbackHost(host)) {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
val fallbackHost =
|
||||||
|
endpoint.tailnetDns?.trim().takeIf { !it.isNullOrEmpty() }
|
||||||
|
?: endpoint.lanHost?.trim().takeIf { !it.isNullOrEmpty() }
|
||||||
|
?: endpoint.host.trim()
|
||||||
|
if (fallbackHost.isEmpty()) return trimmed.ifBlank { null }
|
||||||
|
|
||||||
|
val fallbackPort = endpoint.canvasPort ?: if (port > 0) port else 18793
|
||||||
|
val formattedHost = if (fallbackHost.contains(":")) "[${fallbackHost}]" else fallbackHost
|
||||||
|
return "$scheme://$formattedHost:$fallbackPort"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isLoopbackHost(raw: String?): Boolean {
|
||||||
|
val host = raw?.trim()?.lowercase().orEmpty()
|
||||||
|
if (host.isEmpty()) return false
|
||||||
|
if (host == "localhost") return true
|
||||||
|
if (host == "::1") return true
|
||||||
|
if (host == "0.0.0.0" || host == "::") return true
|
||||||
|
return host.startsWith("127.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject
|
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.steipete.clawdis.node.node
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.core.graphics.scale
|
import androidx.core.graphics.scale
|
||||||
@@ -16,6 +17,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import com.steipete.clawdis.node.BuildConfig
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class CanvasController {
|
class CanvasController {
|
||||||
@@ -81,8 +83,14 @@ class CanvasController {
|
|||||||
val currentUrl = url
|
val currentUrl = url
|
||||||
withWebViewOnMain { wv ->
|
withWebViewOnMain { wv ->
|
||||||
if (currentUrl == null) {
|
if (currentUrl == null) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d("ClawdisCanvas", "load scaffold: $scaffoldAssetUrl")
|
||||||
|
}
|
||||||
wv.loadUrl(scaffoldAssetUrl)
|
wv.loadUrl(scaffoldAssetUrl)
|
||||||
} else {
|
} else {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d("ClawdisCanvas", "load url: $currentUrl")
|
||||||
|
}
|
||||||
wv.loadUrl(currentUrl)
|
wv.loadUrl(currentUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import android.graphics.Color
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.ConsoleMessage
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebResourceError
|
import android.webkit.WebResourceError
|
||||||
@@ -15,6 +17,8 @@ import android.webkit.WebResourceResponse
|
|||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.webkit.WebSettingsCompat
|
||||||
|
import androidx.webkit.WebViewFeature
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -301,6 +305,15 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
|
|||||||
// Some embedded web UIs (incl. the "background website") use localStorage/sessionStorage.
|
// Some embedded web UIs (incl. the "background website") use localStorage/sessionStorage.
|
||||||
settings.domStorageEnabled = true
|
settings.domStorageEnabled = true
|
||||||
settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
|
settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
|
||||||
|
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
|
||||||
|
WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_OFF)
|
||||||
|
}
|
||||||
|
if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
|
||||||
|
WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false)
|
||||||
|
}
|
||||||
|
if (isDebuggable) {
|
||||||
|
Log.d("ClawdisWebView", "userAgent: ${settings.userAgentString}")
|
||||||
|
}
|
||||||
isScrollContainer = true
|
isScrollContainer = true
|
||||||
overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS
|
overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||||
isVerticalScrollBarEnabled = true
|
isVerticalScrollBarEnabled = true
|
||||||
@@ -331,11 +344,38 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView, url: String?) {
|
override fun onPageFinished(view: WebView, url: String?) {
|
||||||
|
if (isDebuggable) {
|
||||||
|
Log.d("ClawdisWebView", "onPageFinished: $url")
|
||||||
|
}
|
||||||
viewModel.canvas.onPageFinished()
|
viewModel.canvas.onPageFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRenderProcessGone(
|
||||||
|
view: WebView,
|
||||||
|
detail: android.webkit.RenderProcessGoneDetail,
|
||||||
|
): Boolean {
|
||||||
|
if (isDebuggable) {
|
||||||
|
Log.e(
|
||||||
|
"ClawdisWebView",
|
||||||
|
"onRenderProcessGone didCrash=${detail.didCrash()} priorityAtExit=${detail.rendererPriorityAtExit()}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setBackgroundColor(Color.BLACK)
|
webChromeClient =
|
||||||
setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
object : WebChromeClient() {
|
||||||
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||||
|
if (!isDebuggable) return false
|
||||||
|
val msg = consoleMessage ?: return false
|
||||||
|
Log.d(
|
||||||
|
"ClawdisWebView",
|
||||||
|
"console ${msg.messageLevel()} @ ${msg.sourceId()}:${msg.lineNumber()} ${msg.message()}",
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use default layer/background; avoid forcing a black fill over WebView content.
|
||||||
|
|
||||||
val a2uiBridge =
|
val a2uiBridge =
|
||||||
CanvasA2UIActionBridge { payload ->
|
CanvasA2UIActionBridge { payload ->
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ final class NodeAppModel {
|
|||||||
guard let raw = await self.bridge.currentCanvasHostUrl() else { return nil }
|
guard let raw = await self.bridge.currentCanvasHostUrl() else { return nil }
|
||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
|
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
|
||||||
return base.appendingPathComponent("__clawdis__/a2ui/").absoluteString
|
return base.appendingPathComponent("__clawdis__/a2ui/").absoluteString + "?platform=ios"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showA2UIOnConnectIfNeeded() async {
|
private func showA2UIOnConnectIfNeeded() async {
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ final class CanvasManager {
|
|||||||
private static func resolveA2UIHostUrl(from raw: String?) -> String? {
|
private static func resolveA2UIHostUrl(from raw: String?) -> String? {
|
||||||
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
|
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
|
||||||
return base.appendingPathComponent("__clawdis__/a2ui/").absoluteString
|
return base.appendingPathComponent("__clawdis__/a2ui/").absoluteString + "?platform=macos"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Anchoring
|
// MARK: - Anchoring
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ actor MacNodeRuntime {
|
|||||||
guard let raw = await GatewayConnection.shared.canvasHostUrl() else { return nil }
|
guard let raw = await GatewayConnection.shared.canvasHostUrl() else { return nil }
|
||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !trimmed.isEmpty, let baseUrl = URL(string: trimmed) else { return nil }
|
guard !trimmed.isEmpty, let baseUrl = URL(string: trimmed) else { return nil }
|
||||||
return baseUrl.appendingPathComponent("__clawdis__/a2ui/").absoluteString
|
return baseUrl.appendingPathComponent("__clawdis__/a2ui/").absoluteString + "?platform=macos"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isA2UIReady(poll: Bool = false) async -> Bool {
|
private func isA2UIReady(poll: Bool = false) async -> Bool {
|
||||||
|
|||||||
@@ -4,6 +4,21 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<title>Canvas</title>
|
<title>Canvas</title>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const platform = (params.get('platform') || '').trim().toLowerCase();
|
||||||
|
if (platform) {
|
||||||
|
document.documentElement.dataset.platform = platform;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (/android/i.test(navigator.userAgent || '')) {
|
||||||
|
document.documentElement.dataset.platform = 'android';
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
:root { color-scheme: dark; }
|
:root { color-scheme: dark; }
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
@@ -18,6 +33,13 @@
|
|||||||
#000;
|
#000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
:root[data-platform="android"] body {
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 900px at 15% 20%, rgba(42, 113, 255, 0.38), rgba(0,0,0,0) 55%),
|
||||||
|
radial-gradient(900px 700px at 85% 30%, rgba(255, 0, 138, 0.30), rgba(0,0,0,0) 60%),
|
||||||
|
radial-gradient(1000px 900px at 60% 90%, rgba(0, 209, 255, 0.28), rgba(0,0,0,0) 60%),
|
||||||
|
#07090f;
|
||||||
|
}
|
||||||
body::before {
|
body::before {
|
||||||
content:"";
|
content:"";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -35,6 +57,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: clawdis-grid-drift 140s ease-in-out infinite alternate;
|
animation: clawdis-grid-drift 140s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
:root[data-platform="android"] body::before { opacity: 0.60; }
|
||||||
body::after {
|
body::after {
|
||||||
content:"";
|
content:"";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -52,6 +75,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: clawdis-glow-drift 110s ease-in-out infinite alternate;
|
animation: clawdis-glow-drift 110s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
:root[data-platform="android"] body::after { opacity: 0.70; }
|
||||||
@supports (mix-blend-mode: screen) {
|
@supports (mix-blend-mode: screen) {
|
||||||
body::after { mix-blend-mode: screen; }
|
body::after { mix-blend-mode: screen; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,21 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Clawdis Canvas</title>
|
<title>Clawdis Canvas</title>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const platform = (params.get('platform') || '').trim().toLowerCase();
|
||||||
|
if (platform) {
|
||||||
|
document.documentElement.dataset.platform = platform;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (/android/i.test(navigator.userAgent || '')) {
|
||||||
|
document.documentElement.dataset.platform = 'android';
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
:root { color-scheme: dark; }
|
:root { color-scheme: dark; }
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
@@ -20,6 +35,13 @@
|
|||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
:root[data-platform="android"] body {
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 900px at 15% 20%, rgba(42, 113, 255, 0.38), rgba(0,0,0,0) 55%),
|
||||||
|
radial-gradient(900px 700px at 85% 30%, rgba(255, 0, 138, 0.30), rgba(0,0,0,0) 60%),
|
||||||
|
radial-gradient(1000px 900px at 60% 90%, rgba(0, 209, 255, 0.28), rgba(0,0,0,0) 60%),
|
||||||
|
#07090f;
|
||||||
|
}
|
||||||
body::before {
|
body::before {
|
||||||
content:"";
|
content:"";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -37,6 +59,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: clawdis-grid-drift 140s ease-in-out infinite alternate;
|
animation: clawdis-grid-drift 140s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
:root[data-platform="android"] body::before { opacity: 0.60; }
|
||||||
body::after {
|
body::after {
|
||||||
content:"";
|
content:"";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -54,6 +77,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: clawdis-glow-drift 110s ease-in-out infinite alternate;
|
animation: clawdis-glow-drift 110s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
:root[data-platform="android"] body::after { opacity: 0.70; }
|
||||||
@supports (mix-blend-mode: screen) {
|
@supports (mix-blend-mode: screen) {
|
||||||
body::after { mix-blend-mode: screen; }
|
body::after { mix-blend-mode: screen; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user