feat(stage-pocket): allow to set transparent background

This commit is contained in:
LemonNeko
2026-04-10 17:51:09 +08:00
parent fb3f54699b
commit c8a01e7a3b
10 changed files with 139 additions and 15 deletions
@@ -1,5 +1,6 @@
package ai.moeru.airi_pocket
import android.graphics.Color
import android.net.Uri
import android.net.http.SslError
import android.webkit.SslErrorHandler
@@ -29,6 +30,7 @@ class MainActivity : BridgeActivity() {
super.load()
val bridge = bridge ?: return
bridge.webView.setBackgroundColor(Color.TRANSPARENT)
installWebSocketBridge(bridge)
if (!bridge.isDevMode) {
@@ -4,9 +4,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:background="@android:color/transparent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -14,6 +14,7 @@ class DevBridgeViewController: CAPBridgeViewController {
override func capacitorDidLoad() {
super.capacitorDidLoad()
configureTransparentBackground()
webView?.allowsBackForwardNavigationGestures = true
installWebSocketBridge()
}
@@ -44,6 +45,20 @@ class DevBridgeViewController: CAPBridgeViewController {
webView.configuration.userContentController.add(hostBridgeMessageHandler, name: hostBridgeName)
}
private func configureTransparentBackground() {
view.isOpaque = false
view.backgroundColor = .clear
guard let webView = bridge?.webView else {
return
}
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.backgroundColor = .clear
webView.superview?.backgroundColor = .clear
}
private func dispatchWebSocketBridgeEvent(_ payload: String) {
guard let webView = bridge?.webView else {
return
+6
View File
@@ -32,6 +32,12 @@ html {
transition: all 0.3s ease-in-out;
}
html:has(.airi-native-transparent-surface),
body:has(.airi-native-transparent-surface),
#app:has(.airi-native-transparent-surface) {
background: transparent !important;
}
html.dark {
--bg-color: var(--bg-color-dark);
color-scheme: dark;
@@ -20,11 +20,15 @@ defineExpose({
</script>
<template>
<div ref="containerRef" class="customized-background relative min-h-100dvh w-full overflow-hidden">
<div
ref="containerRef"
class="customized-background relative min-h-100dvh w-full overflow-hidden"
:class="[background.kind === BackgroundKind.Transparent ? 'airi-native-transparent-surface' : '']"
>
<!-- Background layers -->
<div
class="absolute inset-0 z-0 transition-all duration-300"
:class="[(background.blur && background.kind !== BackgroundKind.Wave) ? 'blur-md scale-110' : '']"
:class="[(background.blur && background.kind === BackgroundKind.Image) ? 'blur-md scale-110' : '']"
>
<template v-if="background.kind === BackgroundKind.Wave">
<DefaultBackground class="h-full w-full" />
@@ -37,13 +41,16 @@ defineExpose({
decoding="async"
>
</template>
<template v-else-if="background.kind === BackgroundKind.Transparent">
<div class="h-full w-full bg-transparent" />
</template>
<template v-else>
<div class="h-full w-full bg-neutral-950" />
</template>
</div>
<!-- Overlay (not for wave) -->
<BackgroundGradientOverlay v-if="background.kind !== BackgroundKind.Wave" :color="topColor" />
<BackgroundGradientOverlay v-if="background.kind === BackgroundKind.Image" :color="topColor" />
<!-- Content layer (kept mounted during background switches) -->
<div class="relative z-10 h-full w-full">
@@ -2,3 +2,4 @@ export { default as DefaultBackground } from './index.vue'
export { default as PartAnimatedWave } from './part-animated-wave.vue'
export { default as PatternCross } from './pattern-cross.vue'
export { default as DefaultBackgroundPreview } from './preview.vue'
export { default as TransparentBackgroundPreview } from './transparent-preview.vue'
@@ -0,0 +1,56 @@
<template>
<div class="transparent-preview">
<div class="transparent-preview__label">
Transparent
</div>
</div>
</template>
<style scoped>
.transparent-preview {
display: flex;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
background-color: rgb(255 255 255 / 0.92);
background-image:
linear-gradient(45deg, rgb(226 232 240 / 0.95) 25%, transparent 25%),
linear-gradient(-45deg, rgb(226 232 240 / 0.95) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgb(226 232 240 / 0.95) 75%),
linear-gradient(-45deg, transparent 75%, rgb(226 232 240 / 0.95) 75%);
background-position:
0 0,
0 8px,
8px -8px,
-8px 0;
background-size: 16px 16px;
}
.transparent-preview__label {
border: 1px solid rgb(255 255 255 / 0.7);
border-radius: 999px;
background: rgb(255 255 255 / 0.78);
padding: 0.35rem 0.75rem;
color: rgb(51 65 85 / 0.9);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.02em;
backdrop-filter: blur(10px);
}
:global(.dark) .transparent-preview {
background-color: rgb(23 23 23 / 0.92);
background-image:
linear-gradient(45deg, rgb(64 64 64 / 0.9) 25%, transparent 25%),
linear-gradient(-45deg, rgb(64 64 64 / 0.9) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgb(64 64 64 / 0.9) 75%),
linear-gradient(-45deg, transparent 75%, rgb(64 64 64 / 0.9) 75%);
}
:global(.dark) .transparent-preview__label {
border-color: rgb(64 64 64 / 0.85);
background: rgb(23 23 23 / 0.72);
color: rgb(245 245 245 / 0.9);
}
</style>
@@ -79,6 +79,10 @@ export function useBackgroundThemeColor({
return waveThemeColor()
}
if (selectedOption.value?.kind === BackgroundKind.Transparent) {
return isDark.value ? 'rgb(18 18 18 / 0)' : 'rgb(255 255 255 / 0)'
}
return sampledColor.value
})
@@ -86,11 +90,11 @@ export function useBackgroundThemeColor({
const { pause, resume } = useIntervalFn(() => {
if (visibility.value !== 'visible')
return
if (selectedOption.value?.kind === BackgroundKind.Wave && themeColorsHueDynamic)
if (selectedOption.value?.kind === BackgroundKind.Wave && themeColorsHueDynamic.value)
void updateThemeColor()
}, 250, { immediate: false })
watch([() => selectedOption.value?.kind, () => themeColorsHueDynamic], ([kind, dynamic]) => {
watch([() => selectedOption.value?.kind, () => themeColorsHueDynamic.value], ([kind, dynamic]) => {
if (kind === BackgroundKind.Wave && dynamic) {
void updateThemeColor()
resume()
@@ -120,6 +124,12 @@ export function useBackgroundThemeColor({
return
}
if (selectedOption.value?.kind === BackgroundKind.Transparent) {
sampledColor.value = 'transparent'
await updateThemeColor()
return
}
const el = backgroundSurface.value?.surfaceEl
if (!el)
return
@@ -7,11 +7,12 @@ import { useLocalStorage, useObjectUrl } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, markRaw, onScopeDispose, ref, shallowRef, watch } from 'vue'
import { DefaultBackgroundPreview } from '../components/Backgrounds/default'
import { DefaultBackgroundPreview, TransparentBackgroundPreview } from '../components/Backgrounds/default'
export enum BackgroundKind {
Wave = 'wave',
Image = 'image',
Transparent = 'transparent',
}
export interface BackgroundItem extends BackgroundOption {
@@ -36,6 +37,13 @@ export const useBackgroundStore = defineStore('background', () => {
kind: BackgroundKind.Wave,
component: markRaw(DefaultBackgroundPreview),
},
{
id: 'transparent',
label: 'Transparent',
description: 'Reveal the native background behind the WebView',
kind: BackgroundKind.Transparent,
component: markRaw(TransparentBackgroundPreview),
},
]
const loading = ref(false)
@@ -143,9 +151,11 @@ export const useBackgroundStore = defineStore('background', () => {
async function applyPickerSelection(payload: { option: BackgroundOption, color?: string }) {
const kind = payload.option.kind === BackgroundKind.Wave
? BackgroundKind.Wave
: payload.option.kind === BackgroundKind.Image
? BackgroundKind.Image
: BackgroundKind.Image
: payload.option.kind === BackgroundKind.Transparent
? BackgroundKind.Transparent
: payload.option.kind === BackgroundKind.Image
? BackgroundKind.Image
: BackgroundKind.Image
const selection: BackgroundItem = {
...payload.option,
@@ -103,6 +103,10 @@ watch(selectedOption, async (option) => {
if (option?.kind === 'wave') {
previewColor.value = themeColorsHue.toString()
}
else if (option?.kind === 'transparent') {
enableBlur.value = false
previewColor.value = 'transparent'
}
else if (option) {
await waitForPreviewReady()
const result = await colorFromElement(previewRef.value!, {
@@ -170,10 +174,21 @@ async function applySelection(isImport = false) {
busy.value = true
try {
const blur = selectedOption.value.kind === 'image' ? enableBlur.value : false
if (selectedOption.value.kind === 'wave') {
const color = themeColorsHue.toString()
const payload = { option: { ...selectedOption.value, blur: enableBlur.value }, color }
const payload = { option: { ...selectedOption.value, blur }, color }
if (isImport)
(emit as any)('import', payload) // TODO: so ugly, need to fix the typing of emit
else
(emit as any)('apply', payload)
return
}
if (selectedOption.value.kind === 'transparent') {
const payload = { option: { ...selectedOption.value, blur }, color: 'transparent' }
if (isImport)
(emit as any)('import', payload)
else
@@ -190,7 +205,7 @@ async function applySelection(isImport = false) {
await new Promise(resolve => setTimeout(resolve, 300))
}
const payload = { option: { ...selectedOption.value, blur: enableBlur.value }, color: previewColor.value }
const payload = { option: { ...selectedOption.value, blur }, color: previewColor.value }
if (isImport)
(emit as any)('import', payload)
else
@@ -267,7 +282,7 @@ async function applySelection(isImport = false) {
Preview
</p>
<label
v-if="selectedOption?.kind !== 'wave'"
v-if="selectedOption?.kind === 'image'"
class="flex items-center gap-2 pb-2 text-sm text-neutral-700 dark:text-neutral-200"
>
<input v-model="enableBlur" type="checkbox" class="accent-primary-500">
@@ -279,7 +294,7 @@ async function applySelection(isImport = false) {
>
<div
class="h-full w-full transition-all duration-300"
:class="[(enableBlur && selectedOption?.kind !== 'wave') ? 'blur-md scale-110' : '']"
:class="[(enableBlur && selectedOption?.kind === 'image') ? 'blur-md scale-110' : '']"
>
<component
:is="selectedOption?.component"
@@ -295,7 +310,7 @@ async function applySelection(isImport = false) {
Select a background
</div>
</div>
<BackgroundGradientOverlay v-if="(selectedOption as any)?.kind !== 'wave'" :color="previewColor" />
<BackgroundGradientOverlay v-if="selectedOption?.kind === 'image'" :color="previewColor" />
</div>
</div>
</div>