feat(stage-tamagotchi): show connection status (#1311)
This commit is contained in:
@@ -5,6 +5,7 @@ import type { I18n } from '../libs/i18n'
|
||||
import type { ServerChannel } from '../services/airi/channel-server'
|
||||
import type { setupBeatSync } from '../windows/beat-sync'
|
||||
import type { setupCaptionWindowManager } from '../windows/caption'
|
||||
import type { SettingsWindowManager } from '../windows/settings'
|
||||
import type { WidgetsWindowManager } from '../windows/widgets'
|
||||
|
||||
import { env } from 'node:process'
|
||||
@@ -79,7 +80,7 @@ function isPositionMatch(window: BrowserWindow, targetX: number, targetY: number
|
||||
|
||||
export function setupTray(params: {
|
||||
mainWindow: BrowserWindow
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
settingsWindow: SettingsWindowManager
|
||||
captionWindow: ReturnType<typeof setupCaptionWindowManager>
|
||||
widgetsWindow: WidgetsWindowManager
|
||||
beatSyncBgWindow: Awaited<ReturnType<typeof setupBeatSync>>
|
||||
@@ -172,7 +173,7 @@ export function setupTray(params: {
|
||||
],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: params.i18n.t('tamagotchi.electron.tray.menu.labels.label.settings'), click: () => params.settingsWindow().then(window => toggleWindowShow(window)) },
|
||||
{ label: params.i18n.t('tamagotchi.electron.tray.menu.labels.label.settings'), click: () => void params.settingsWindow.openWindow('/settings') },
|
||||
{ label: params.i18n.t('tamagotchi.electron.tray.menu.labels.label.about'), click: () => params.aboutWindow().then(window => toggleWindowShow(window)) },
|
||||
{ type: 'separator' },
|
||||
{ label: params.i18n.t('tamagotchi.electron.tray.menu.labels.label.open_inlay'), click: () => setupInlayWindow({ i18n: params.i18n, serverChannel: params.serverChannel }) },
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { InferOutput } from 'valibot'
|
||||
import type { I18n } from '../../libs/i18n'
|
||||
import type { ServerChannel } from '../../services/airi/channel-server'
|
||||
import type { NoticeWindowManager } from '../notice'
|
||||
import type { SettingsWindowManager } from '../settings'
|
||||
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { env } from 'node:process'
|
||||
@@ -41,7 +42,7 @@ const appConfigSchema = object({
|
||||
type AppConfig = InferOutput<typeof appConfigSchema>
|
||||
|
||||
export async function setupDashboardWindow(params: {
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
settingsWindow: SettingsWindowManager
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
noticeWindow: NoticeWindowManager
|
||||
onWindowCreated?: (window: BrowserWindow) => void
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { BrowserWindow } from 'electron'
|
||||
import type { I18n } from '../../../libs/i18n'
|
||||
import type { ServerChannel } from '../../../services/airi/channel-server'
|
||||
import type { NoticeWindowManager } from '../../notice'
|
||||
import type { SettingsWindowManager } from '../../settings'
|
||||
|
||||
import { defineInvokeHandler } from '@moeru/eventa'
|
||||
import { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
@@ -14,7 +15,7 @@ import { setupBaseWindowElectronInvokes } from '../../shared/window'
|
||||
|
||||
export async function setupDashboardWindowElectronInvokes(params: {
|
||||
window: BrowserWindow
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
settingsWindow: SettingsWindowManager
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
noticeWindow: NoticeWindowManager
|
||||
i18n: I18n
|
||||
@@ -30,7 +31,7 @@ export async function setupDashboardWindowElectronInvokes(params: {
|
||||
await setupBaseWindowElectronInvokes({ context, window: params.window, serverChannel: params.serverChannel, i18n: params.i18n })
|
||||
|
||||
defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' }))
|
||||
defineInvokeHandler(context, electronOpenSettings, async () => toggleWindowShow(await params.settingsWindow()))
|
||||
defineInvokeHandler(context, electronOpenSettings, payload => params.settingsWindow.openWindow(payload?.route))
|
||||
defineInvokeHandler(context, electronOpenChat, async () => toggleWindowShow(await params.chatWindow()))
|
||||
defineInvokeHandler(context, noticeWindowEventa.openWindow, payload => params.noticeWindow.open(payload))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { McpStdioManager } from '../../services/airi/mcp-servers'
|
||||
import type { AutoUpdater } from '../../services/electron/auto-updater'
|
||||
import type { NoticeWindowManager } from '../notice'
|
||||
import type { OnboardingWindowManager } from '../onboarding'
|
||||
import type { SettingsWindowManager } from '../settings'
|
||||
import type { WidgetsWindowManager } from '../widgets'
|
||||
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
@@ -46,7 +47,7 @@ const appConfigSchema = object({
|
||||
type AppConfig = InferOutput<typeof appConfigSchema>
|
||||
|
||||
export async function setupMainWindow(params: {
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
settingsWindow: SettingsWindowManager
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
widgetsManager: WidgetsWindowManager
|
||||
noticeWindow: NoticeWindowManager
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { McpStdioManager } from '../../../services/airi/mcp-servers'
|
||||
import type { AutoUpdater } from '../../../services/electron/auto-updater'
|
||||
import type { NoticeWindowManager } from '../../notice'
|
||||
import type { OnboardingWindowManager } from '../../onboarding'
|
||||
import type { SettingsWindowManager } from '../../settings'
|
||||
import type { WidgetsWindowManager } from '../../widgets'
|
||||
|
||||
import { defineInvokeHandler } from '@moeru/eventa'
|
||||
@@ -22,7 +23,7 @@ import { setupBaseWindowElectronInvokes } from '../../shared/window'
|
||||
|
||||
export async function setupMainWindowElectronInvokes(params: {
|
||||
window: BrowserWindow
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
settingsWindow: SettingsWindowManager
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
widgetsManager: WidgetsWindowManager
|
||||
noticeWindow: NoticeWindowManager
|
||||
@@ -46,7 +47,7 @@ export async function setupMainWindowElectronInvokes(params: {
|
||||
createOnboardingService({ context, onboardingWindowManager: params.onboardingWindowManager })
|
||||
|
||||
defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' }))
|
||||
defineInvokeHandler(context, electronOpenSettings, async () => toggleWindowShow(await params.settingsWindow()))
|
||||
defineInvokeHandler(context, electronOpenSettings, payload => params.settingsWindow.openWindow(payload?.route))
|
||||
defineInvokeHandler(context, electronOpenChat, async () => toggleWindowShow(await params.chatWindow()))
|
||||
defineInvokeHandler(context, noticeWindowEventa.openWindow, payload => params.noticeWindow.open(payload))
|
||||
}
|
||||
|
||||
@@ -12,10 +12,17 @@ import { BrowserWindow, shell } from 'electron'
|
||||
|
||||
import icon from '../../../../resources/icon.png?asset'
|
||||
|
||||
import { electronSettingsNavigate } from '../../../shared/eventa'
|
||||
import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location'
|
||||
import { createReusableWindow } from '../../libs/electron/window-manager'
|
||||
import { toggleWindowShow } from '../shared'
|
||||
import { setupSettingsWindowInvokes } from './rpc/index.electron'
|
||||
|
||||
export interface SettingsWindowManager {
|
||||
getWindow: () => Promise<BrowserWindow>
|
||||
openWindow: (route?: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function setupSettingsWindowReusableFunc(params: {
|
||||
widgetsManager: WidgetsWindowManager
|
||||
autoUpdater: AutoUpdater
|
||||
@@ -24,8 +31,13 @@ export function setupSettingsWindowReusableFunc(params: {
|
||||
serverChannel: ServerChannel
|
||||
mcpStdioManager: McpStdioManager
|
||||
i18n: I18n
|
||||
}) {
|
||||
return createReusableWindow(async () => {
|
||||
}): SettingsWindowManager {
|
||||
const rendererBase = baseUrl(resolve(getElectronMainDirname(), '..', 'renderer'))
|
||||
const defaultRoute = '/settings'
|
||||
let currentRoute = defaultRoute
|
||||
let settingsContext: Awaited<ReturnType<typeof setupSettingsWindowInvokes>> | undefined
|
||||
|
||||
const reusable = createReusableWindow(async () => {
|
||||
const window = new BrowserWindow({
|
||||
title: 'Settings',
|
||||
width: 600.0,
|
||||
@@ -48,8 +60,8 @@ export function setupSettingsWindowReusableFunc(params: {
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
await load(window, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/settings'))
|
||||
await setupSettingsWindowInvokes({
|
||||
await load(window, withHashRoute(rendererBase, currentRoute))
|
||||
settingsContext = await setupSettingsWindowInvokes({
|
||||
settingsWindow: window,
|
||||
widgetsManager: params.widgetsManager,
|
||||
autoUpdater: params.autoUpdater,
|
||||
@@ -59,8 +71,32 @@ export function setupSettingsWindowReusableFunc(params: {
|
||||
i18n: params.i18n,
|
||||
})
|
||||
|
||||
window.on('closed', () => {
|
||||
if (settingsContext)
|
||||
settingsContext = undefined
|
||||
})
|
||||
|
||||
initScreenCaptureForWindow(window)
|
||||
|
||||
return window
|
||||
}).getWindow
|
||||
})
|
||||
|
||||
async function openWindow(route?: string) {
|
||||
if (route) {
|
||||
currentRoute = route
|
||||
}
|
||||
|
||||
const window = await reusable.getWindow()
|
||||
|
||||
if (route && settingsContext) {
|
||||
settingsContext.emit(electronSettingsNavigate, { route })
|
||||
}
|
||||
|
||||
toggleWindowShow(window)
|
||||
}
|
||||
|
||||
return {
|
||||
getWindow: reusable.getWindow,
|
||||
openWindow,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,4 +43,6 @@ export async function setupSettingsWindowInvokes(params: {
|
||||
defineInvokeHandler(context, electronOpenDevtoolsWindow, async (payload) => {
|
||||
await params.devtoolsMarkdownStressWindow.openWindow(payload?.route)
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
electronGetServerChannelConfig,
|
||||
electronMcpCallTool,
|
||||
electronMcpListTools,
|
||||
electronOpenSettings,
|
||||
electronPluginInspect,
|
||||
electronPluginList,
|
||||
electronPluginLoad,
|
||||
@@ -36,6 +35,7 @@ import {
|
||||
electronPluginSetEnabled,
|
||||
electronPluginUnload,
|
||||
electronPluginUpdateCapability,
|
||||
electronSettingsNavigate,
|
||||
electronStartTrackMousePosition,
|
||||
i18nSetLocale,
|
||||
pluginProtocolListProviders,
|
||||
@@ -105,6 +105,17 @@ watch(dark, () => updateThemeColor(), { immediate: true })
|
||||
watch(route, () => updateThemeColor(), { immediate: true })
|
||||
onMounted(() => updateThemeColor())
|
||||
|
||||
context.value.on(electronSettingsNavigate, (event) => {
|
||||
const targetRoute = event?.body?.route
|
||||
if (!targetRoute || route.fullPath === targetRoute) {
|
||||
return
|
||||
}
|
||||
|
||||
void router.push(targetRoute).catch((error) => {
|
||||
console.warn('Failed to navigate settings window:', error)
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
analyticsStore.initialize()
|
||||
cardStore.initialize()
|
||||
@@ -133,9 +144,6 @@ onMounted(async () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Listen for open-settings IPC message from main process
|
||||
defineInvokeHandler(context.value, electronOpenSettings, () => router.push('/settings'))
|
||||
})
|
||||
|
||||
watch(themeColorsHue, () => {
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ function refreshWindow() {
|
||||
<div v-if="expanded" border="1 neutral-200 dark:neutral-800" mb-2 flex flex-col gap-1 rounded-2xl p-2 backdrop-blur-xl class="bg-neutral-100/80 shadow-2xl shadow-black/20 dark:bg-neutral-900/80">
|
||||
<div grid grid-cols-3 gap-2>
|
||||
<ControlButtonTooltip disable-hoverable-content>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @click="openSettings">
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @click="openSettings({ route: '/settings' })">
|
||||
<div i-solar:settings-minimalistic-outline :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ControlButtonTooltip from '../controls-island/control-button-tooltip.vue'
|
||||
import ControlButton from '../controls-island/control-button.vue'
|
||||
|
||||
import { electronOpenSettings } from '../../../../shared/eventa'
|
||||
|
||||
const { t, te } = useI18n()
|
||||
const { connected } = storeToRefs(useModsServerChannelStore())
|
||||
const openSettings = useElectronEventaInvoke(electronOpenSettings)
|
||||
const flickerDuration = ref('6.4s')
|
||||
const flickerDelay = ref('0s')
|
||||
|
||||
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 status-island-lamp-flicker',
|
||||
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',
|
||||
]
|
||||
})
|
||||
|
||||
const iconStyle = computed(() => {
|
||||
if (connected.value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
'--status-island-flicker-delay': flickerDelay.value,
|
||||
'--status-island-flicker-duration': flickerDuration.value,
|
||||
}
|
||||
})
|
||||
|
||||
const buttonLabel = computed(() => {
|
||||
if (connected.value) {
|
||||
return te('tamagotchi.stage.status-island.connected')
|
||||
? t('tamagotchi.stage.status-island.connected')
|
||||
: 'WebSocket connected'
|
||||
}
|
||||
|
||||
return te('tamagotchi.stage.status-island.disconnected')
|
||||
? t('tamagotchi.stage.status-island.disconnected')
|
||||
: 'WebSocket disconnected'
|
||||
})
|
||||
|
||||
const tooltipLabel = computed(() => {
|
||||
const openSettingsLabel = te('tamagotchi.stage.status-island.open-settings')
|
||||
? t('tamagotchi.stage.status-island.open-settings')
|
||||
: 'Open WebSocket settings'
|
||||
|
||||
return `${buttonLabel.value}. ${openSettingsLabel}`
|
||||
})
|
||||
|
||||
function randomizeFlicker(resetPhase = false) {
|
||||
flickerDuration.value = `${(5.8 + Math.random() * 1.8).toFixed(2)}s`
|
||||
|
||||
if (resetPhase) {
|
||||
flickerDelay.value = `${(-Math.random() * 5.4).toFixed(2)}s`
|
||||
return
|
||||
}
|
||||
|
||||
flickerDelay.value = '0s'
|
||||
}
|
||||
|
||||
function handleFlickerIteration() {
|
||||
if (!connected.value) {
|
||||
randomizeFlicker()
|
||||
}
|
||||
}
|
||||
|
||||
watch(connected, (isConnected) => {
|
||||
if (isConnected) {
|
||||
flickerDelay.value = '0s'
|
||||
return
|
||||
}
|
||||
|
||||
randomizeFlicker(true)
|
||||
}, { immediate: true })
|
||||
</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="iconStyle" @animationiteration="handleFlickerIteration" />
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
{{ tooltipLabel }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes status-island-lamp-flicker {
|
||||
0%,
|
||||
6%,
|
||||
22%,
|
||||
33%,
|
||||
52%,
|
||||
68%,
|
||||
86%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
3% {
|
||||
opacity: 0.74;
|
||||
}
|
||||
|
||||
9% {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
13% {
|
||||
opacity: 0.38;
|
||||
}
|
||||
|
||||
18% {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
27% {
|
||||
opacity: 0.44;
|
||||
}
|
||||
|
||||
29% {
|
||||
opacity: 0.84;
|
||||
}
|
||||
|
||||
41% {
|
||||
opacity: 0.42;
|
||||
}
|
||||
|
||||
45% {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
57% {
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
61% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
73% {
|
||||
opacity: 0.36;
|
||||
}
|
||||
|
||||
74.4% {
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
75.2% {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
78% {
|
||||
opacity: 0.94;
|
||||
}
|
||||
|
||||
91% {
|
||||
opacity: 0.52;
|
||||
}
|
||||
}
|
||||
|
||||
.status-island-lamp-flicker {
|
||||
animation-delay: var(--status-island-flicker-delay, 0s);
|
||||
animation-duration: var(--status-island-flicker-duration, 6.4s);
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: status-island-lamp-flicker;
|
||||
animation-timing-function: ease-in-out;
|
||||
filter: drop-shadow(0 0 0.14rem rgb(251 191 36 / 0.18));
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
@@ -29,6 +29,7 @@ import { computed, onMounted, onUnmounted, ref, toRef, watch } from 'vue'
|
||||
|
||||
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 { useControlsIslandStore } from '../stores/controls-island'
|
||||
@@ -37,6 +38,7 @@ import { useWindowStore } from '../stores/window'
|
||||
import { shouldSampleStageTransparency } from '../utils/stage-three-transparency'
|
||||
|
||||
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')
|
||||
@@ -51,7 +53,9 @@ 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.
|
||||
@@ -108,7 +112,7 @@ const { pause, resume } = watch(isTransparent, (transparent) => {
|
||||
|
||||
const hearingDialogOpen = computed(() => controlsIslandRef.value?.hearingDialogOpen ?? false)
|
||||
|
||||
watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, hearingDialogOpen, fadeOnHoverEnabled, stagePaused], () => {
|
||||
watch([isOutsideFor250Ms, isOutsideStatusIslandFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, hearingDialogOpen, fadeOnHoverEnabled, stagePaused], () => {
|
||||
if (stagePaused.value) {
|
||||
isIgnoringMouseEvents.value = false
|
||||
shouldFadeOnCursorWithin.value = false
|
||||
@@ -126,7 +130,7 @@ watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTrans
|
||||
return
|
||||
}
|
||||
|
||||
const insideControls = !isOutsideFor250Ms.value
|
||||
const insideControls = !isOutsideFor250Ms.value || !isOutsideStatusIslandFor250Ms.value
|
||||
const nearBorder = isAroundWindowBorderFor250Ms.value
|
||||
|
||||
if (insideControls || nearBorder) {
|
||||
@@ -371,6 +375,7 @@ watch([stream, () => vadLoaded.value], async ([s, loaded]) => {
|
||||
'transition-opacity duration-250 ease-in-out',
|
||||
]"
|
||||
>
|
||||
<StatusIsland ref="statusIslandRef" />
|
||||
<ResourceStatusIsland />
|
||||
<WidgetStage
|
||||
ref="widgetStageRef"
|
||||
|
||||
@@ -17,7 +17,8 @@ export const electronStartTrackMousePosition = defineInvokeEventa('eventa:invoke
|
||||
export const electronStartDraggingWindow = defineInvokeEventa('eventa:invoke:electron:start-dragging-window')
|
||||
|
||||
export const electronOpenMainDevtools = defineInvokeEventa('eventa:invoke:electron:windows:main:devtools:open')
|
||||
export const electronOpenSettings = defineInvokeEventa('eventa:invoke:electron:windows:settings:open')
|
||||
export const electronOpenSettings = defineInvokeEventa<void, { route?: string }>('eventa:invoke:electron:windows:settings:open')
|
||||
export const electronSettingsNavigate = defineEventa<{ route: string }>('eventa:event:electron:windows:settings:navigate')
|
||||
export const electronOpenChat = defineInvokeEventa('eventa:invoke:electron:windows:chat:open')
|
||||
export const electronOpenSettingsDevtools = defineInvokeEventa('eventa:invoke:electron:windows:settings:devtools:open')
|
||||
export const electronOpenDevtoolsWindow = defineInvokeEventa<void, { route?: string }>('eventa:invoke:electron:windows:devtools:open')
|
||||
|
||||
@@ -24,6 +24,10 @@ docs:
|
||||
'close': Close
|
||||
'expand': Expand
|
||||
'collapse': Collapse
|
||||
'status-island':
|
||||
connected: WebSocket connected
|
||||
disconnected: WebSocket disconnected
|
||||
'open-settings': Open WebSocket settings
|
||||
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
|
||||
@@ -24,6 +24,10 @@ docs:
|
||||
'close': 关闭
|
||||
'expand': 展开
|
||||
'collapse': 折叠
|
||||
'status-island':
|
||||
connected: WebSocket 已连接
|
||||
disconnected: WebSocket 已断开
|
||||
'open-settings': 打开 WebSocket 设置
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: 悬停淡出
|
||||
|
||||
@@ -174,10 +174,22 @@ export class Client<C = undefined> {
|
||||
|
||||
const ws = new WebSocket(this.opts.url)
|
||||
this.websocket = ws
|
||||
const isCurrentSocket = () => this.websocket === ws
|
||||
|
||||
ws.onmessage = this.handleMessageBound
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
if (!isCurrentSocket()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.handleMessageBound(event)
|
||||
}
|
||||
ws.onerror = (event: any) => {
|
||||
if (!isCurrentSocket()) {
|
||||
return
|
||||
}
|
||||
|
||||
settle(() => {
|
||||
this.websocket = undefined
|
||||
this.connected = false
|
||||
|
||||
this.opts.onError?.(event)
|
||||
@@ -185,6 +197,12 @@ export class Client<C = undefined> {
|
||||
})
|
||||
}
|
||||
ws.onclose = () => {
|
||||
if (!isCurrentSocket()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.websocket = undefined
|
||||
|
||||
if (!settled && !this.connected) {
|
||||
settle(() => {
|
||||
reject(new Error('WebSocket closed before open'))
|
||||
@@ -202,6 +220,10 @@ export class Client<C = undefined> {
|
||||
}
|
||||
}
|
||||
ws.onopen = () => {
|
||||
if (!isCurrentSocket()) {
|
||||
return
|
||||
}
|
||||
|
||||
settle(() => {
|
||||
this.connected = true
|
||||
|
||||
@@ -355,8 +377,10 @@ export class Client<C = undefined> {
|
||||
close(): void {
|
||||
this.shouldClose = true
|
||||
this.stopHeartbeat()
|
||||
if (this.websocket) {
|
||||
this.websocket.close()
|
||||
const websocket = this.websocket
|
||||
this.websocket = undefined
|
||||
if (websocket) {
|
||||
websocket.close()
|
||||
this.connected = false
|
||||
}
|
||||
}
|
||||
@@ -425,6 +449,7 @@ export class Client<C = undefined> {
|
||||
|
||||
const ws = this.websocket
|
||||
this.connected = false
|
||||
this.websocket = undefined
|
||||
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
|
||||
ws.close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user