feat(stage-tamagotchi): move connection status into controls island (#2317)

This commit is contained in:
Neko
2026-08-19 03:58:18 +08:00
committed by GitHub
parent c595fd645c
commit 8322727f9b
3 changed files with 34 additions and 47 deletions
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { defineInvoke } from '@moeru/eventa'
import { useElectronEventaContext, useElectronEventaInvoke, useElectronMouseInElement } from '@proj-airi/electron-vueuse'
import { IS_DEV } from '@proj-airi/stage-shared'
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
import { useTheme } from '@proj-airi/ui'
import { refDebounced, useIntervalFn } from '@vueuse/core'
@@ -8,6 +9,7 @@ import { storeToRefs } from 'pinia'
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import StatusIsland from '../status-island/index.vue'
import ControlButtonTooltip from './control-button-tooltip.vue'
import ControlButton from './control-button.vue'
import ControlsIslandAuthButton from './controls-island-auth-button.vue'
@@ -303,6 +305,12 @@ function resetMainWindowPosition() {
</template>
</ControlButtonTooltip>
<StatusIsland
v-if="IS_DEV"
:button-style="adjustStyleClasses.button"
:icon-class="adjustStyleClasses.icon"
/>
<ControlButtonTooltip side="left">
<ControlButton
v-track-button="{ name: 'controls_island_action', action: 'toggle_chat' }"
@@ -12,37 +12,23 @@ import ControlButton from '../controls-island/control-button.vue'
import { electronOpenSettings } from '../../../../shared/eventa'
const props = defineProps<{
buttonStyle: string
iconClass: string
}>()
const { t } = useI18n()
const { connected } = storeToRefs(useModsServerChannelStore())
const openSettings = useElectronEventaInvoke(electronOpenSettings)
const { flickerStyle, onAnimationIteration } = useLampFlickerAnimation(() => !connected.value)
const statusIslandSize = {
border: 'border-2',
icon: 'size-6',
padding: 'p-2.5',
} as const
const buttonStyle = computed(() => {
return [
statusIslandSize.border,
statusIslandSize.padding,
'transition-all duration-300 ease-in-out',
connected.value
? 'border-emerald-200/60 bg-white/85 hover:bg-emerald-50/90 dark:border-emerald-400/15 dark:bg-neutral-900/75 dark:hover:bg-neutral-900/88'
: 'border-amber-300/80 bg-amber-50/90 hover:bg-amber-100/90 dark:border-amber-400/30 dark:bg-amber-950/25 dark:hover:bg-amber-950/38',
]
})
const iconClasses = computed(() => {
return [
connected.value ? 'i-ph:wifi-high' : `i-ph:wifi-slash ${lampFlickerAnimationClass}`,
statusIslandSize.icon,
'shrink-0 transition-colors duration-300 ease-in-out',
connected.value
? 'text-emerald-600 dark:text-emerald-300'
: 'text-amber-600 dark:text-amber-300',
'i-mingcute:link-3-line',
!connected.value && lampFlickerAnimationClass,
props.iconClass,
'shrink-0 text-neutral-800 transition-colors duration-300 ease-in-out dark:text-neutral-300',
]
})
@@ -58,19 +44,17 @@ const tooltipLabel = computed(() => {
</script>
<template>
<div fixed right-3 top-3 z-20>
<ControlButtonTooltip side="left">
<ControlButton
:button-style="buttonStyle.join(' ')"
:aria-label="tooltipLabel"
:title="tooltipLabel"
@click="openSettings({ route: '/settings/connection' })"
>
<div :class="iconClasses" :style="flickerStyle" @animationiteration="onAnimationIteration" />
</ControlButton>
<template #tooltip>
{{ tooltipLabel }}
</template>
</ControlButtonTooltip>
</div>
<ControlButtonTooltip side="left">
<ControlButton
:button-style="props.buttonStyle"
:aria-label="tooltipLabel"
:title="tooltipLabel"
@click="openSettings({ route: '/settings/connection' })"
>
<div :class="iconClasses" :style="flickerStyle" @animationiteration="onAnimationIteration" />
</ControlButton>
<template #tooltip>
{{ tooltipLabel }}
</template>
</ControlButtonTooltip>
</template>
@@ -14,7 +14,7 @@ import {
useElectronRelativeMouse,
} from '@proj-airi/electron-vueuse'
import { createTranscriptBuffer } from '@proj-airi/pipelines-audio'
import { hearingInputChannelName, IS_DEV } from '@proj-airi/stage-shared'
import { hearingInputChannelName } from '@proj-airi/stage-shared'
import { useModelStore, useThreeSceneIsTransparentAtPoint } from '@proj-airi/stage-ui-three'
import { HoloCoupon } from '@proj-airi/stage-ui/components'
import {
@@ -37,7 +37,6 @@ import { toast } from 'vue-sonner'
import ControlsIsland from '../components/stage-islands/controls-island/index.vue'
import ResourceStatusIsland from '../components/stage-islands/resource-status-island/index.vue'
import StatusIsland from '../components/stage-islands/status-island/index.vue'
import { electronOpenOnboarding } from '../../shared/eventa'
import { modelSettingsRuntimeSnapshotChannelName } from '../../shared/model-settings-runtime'
@@ -53,7 +52,6 @@ import {
} from '../utils/voice-input-suppression'
const controlsIslandRef = ref<InstanceType<typeof ControlsIsland>>()
const statusIslandRef = ref<InstanceType<typeof StatusIsland>>()
const widgetStageRef = ref<InstanceType<typeof WidgetStage>>()
const stageCanvas = toRef(() => widgetStageRef.value?.canvasElement())
const componentStateStage = ref<'pending' | 'loading' | 'mounted'>('pending')
@@ -68,9 +66,7 @@ const openOnboarding = useElectronEventaInvoke(electronOpenOnboarding)
const { isOutside: isOutsideWindow } = useElectronMouseInWindow()
const { isOutside } = useElectronMouseInElement(controlsIslandRef)
const { isOutside: isOutsideStatusIsland } = useElectronMouseInElement(statusIslandRef)
const isOutsideFor250Ms = refDebounced(isOutside, 250)
const isOutsideStatusIslandFor250Ms = refDebounced(isOutsideStatusIsland, 250)
const { x: relativeMouseX, y: relativeMouseY } = useElectronRelativeMouse()
// NOTICE: In real-world use cases of Fade on Hover feature, the cursor may move around the edge of the
// model rapidly, causing flickering effects when checking pixel transparency strictly.
@@ -245,7 +241,7 @@ const modelSettingsRuntimeSnapshot = computed<ModelSettingsRuntimeSnapshot>(() =
* -> {@link handleFadeOnHoverInteractionChange}
*
* Upstream:
* - {@link isOutsideFor250Ms}, {@link isOutsideStatusIslandFor250Ms}, and {@link isAroundWindowBorderFor250Ms}
* - {@link isOutsideFor250Ms} and {@link isAroundWindowBorderFor250Ms}
* - {@link isOutsideWindow}, {@link isTransparent}, and {@link isTransparentForMouseEvents}
* - {@link hearingDialogOpen}, {@link fadeOnHoverEnabled}, and {@link stagePaused}
*
@@ -269,7 +265,7 @@ function handleFadeOnHoverInteractionChange() {
return
}
const insideControls = !isOutsideFor250Ms.value || !isOutsideStatusIslandFor250Ms.value
const insideControls = !isOutsideFor250Ms.value
const nearBorder = isAroundWindowBorderFor250Ms.value
if (insideControls || nearBorder) {
@@ -293,7 +289,7 @@ function handleFadeOnHoverInteractionChange() {
}
watch(
[isOutsideFor250Ms, isOutsideStatusIslandFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, isTransparentForMouseEvents, hearingDialogOpen, fadeOnHoverEnabled, stagePaused],
[isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, isTransparentForMouseEvents, hearingDialogOpen, fadeOnHoverEnabled, stagePaused],
handleFadeOnHoverInteractionChange,
{ immediate: true },
)
@@ -816,7 +812,6 @@ const cursorPosition = computed(() => ({
'transition-opacity duration-250 ease-in-out',
]"
>
<StatusIsland v-if="IS_DEV" ref="statusIslandRef" />
<ResourceStatusIsland />
<WidgetStage
ref="widgetStageRef"