Skip to content

Commit 521c38a

Browse files
committed
feat(stage-tamagotchi): notice before toggling Fade on Hover
1 parent 2284580 commit 521c38a

File tree

17 files changed

+438
-12
lines changed

17 files changed

+438
-12
lines changed

apps/stage-tamagotchi/src/main/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { setupCaptionWindowManager } from './windows/caption'
2222
import { setupChatWindowReusableFunc } from './windows/chat'
2323
import { setupInlayWindow } from './windows/inlay'
2424
import { setupMainWindow } from './windows/main'
25+
import { setupNoticeWindowManager } from './windows/notice'
2526
import { setupSettingsWindowReusableFunc } from './windows/settings'
2627
import { toggleWindowShow } from './windows/shared/window'
2728
import { setupWidgetsWindowManager } from './windows/widgets'
@@ -103,13 +104,14 @@ app.whenReady().then(async () => {
103104
const channelServerModule = injeca.provide('modules:channel-server', async () => setupChannelServer())
104105
const chatWindow = injeca.provide('windows:chat', { build: () => setupChatWindowReusableFunc() })
105106
const widgetsManager = injeca.provide('windows:widgets', { build: () => setupWidgetsWindowManager() })
107+
const noticeWindow = injeca.provide('windows:notice', { build: () => setupNoticeWindowManager() })
106108

107109
const settingsWindow = injeca.provide('windows:settings', {
108110
dependsOn: { widgetsManager },
109111
build: ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn),
110112
})
111113
const mainWindow = injeca.provide('windows:main', {
112-
dependsOn: { settingsWindow, chatWindow, widgetsManager },
114+
dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow },
113115
build: async ({ dependsOn }) => setupMainWindow(dependsOn),
114116
})
115117
const captionWindow = injeca.provide('windows:caption', {

apps/stage-tamagotchi/src/main/windows/main/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { BrowserWindowConstructorOptions, Rectangle } from 'electron'
22

33
import type { WidgetsWindowManager } from '../widgets'
4+
import type { NoticeWindowManager } from '../notice'
45

56
import { dirname, join, resolve } from 'node:path'
67
import { env } from 'node:process'
@@ -31,6 +32,7 @@ export async function setupMainWindow(params: {
3132
settingsWindow: () => Promise<BrowserWindow>
3233
chatWindow: () => Promise<BrowserWindow>
3334
widgetsManager: WidgetsWindowManager
35+
noticeWindow: NoticeWindowManager
3436
}) {
3537
const {
3638
setup: setupConfig,
@@ -131,6 +133,7 @@ export async function setupMainWindow(params: {
131133
settingsWindow: params.settingsWindow,
132134
chatWindow: params.chatWindow,
133135
widgetsManager: params.widgetsManager,
136+
noticeWindow: params.noticeWindow,
134137
})
135138

136139
/**

apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import type { BrowserWindow } from 'electron'
22

3+
import type { NoticeWindowManager } from '../../notice'
34
import type { WidgetsWindowManager } from '../../widgets'
45

56
import { defineInvokeHandler } from '@moeru/eventa'
67
import { createContext } from '@moeru/eventa/adapters/electron/main'
78
import { ipcMain } from 'electron'
89

9-
import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings } from '../../../../shared/eventa'
10+
import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings, noticeWindowEventa } from '../../../../shared/eventa'
1011
import { createWidgetsService } from '../../../services/airi/widgets'
1112
import { createScreenService, createWindowService } from '../../../services/electron'
1213
import { toggleWindowShow } from '../../shared'
@@ -16,6 +17,7 @@ export function setupMainWindowElectronInvokes(params: {
1617
settingsWindow: () => Promise<BrowserWindow>
1718
chatWindow: () => Promise<BrowserWindow>
1819
widgetsManager: WidgetsWindowManager
20+
noticeWindow: NoticeWindowManager
1921
}) {
2022
// TODO: once we refactored eventa to support window-namespaced contexts,
2123
// we can remove the setMaxListeners call below since eventa will be able to dispatch and
@@ -31,4 +33,5 @@ export function setupMainWindowElectronInvokes(params: {
3133
defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' }))
3234
defineInvokeHandler(context, electronOpenSettings, async () => toggleWindowShow(await params.settingsWindow()))
3335
defineInvokeHandler(context, electronOpenChat, async () => toggleWindowShow(await params.chatWindow()))
36+
defineInvokeHandler(context, noticeWindowEventa.openWindow, (payload) => params.noticeWindow.open(payload))
3437
}
Binary file not shown.
Binary file not shown.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { useI18n } from 'vue-i18n'
4+
5+
import ControlButton from './ControlButton.vue'
6+
import ControlButtonTooltip from './ControlButtonTooltip.vue'
7+
8+
import { noticeWindowEventa } from '../../../../shared/eventa'
9+
import { useElectronEventaInvoke } from '../../../composables/electron-vueuse/use-electron-eventa-context'
10+
import { useControlsIslandStore } from '../../../stores/controls-island'
11+
12+
const uiStore = useControlsIslandStore()
13+
const enabled = computed(() => uiStore.fadeOnHoverEnabled)
14+
const { t } = useI18n()
15+
16+
const requestNotice = useElectronEventaInvoke(noticeWindowEventa.openWindow)
17+
const NOTICE_WINDOW_ID = 'fade-on-hover'
18+
19+
async function handleToggle() {
20+
if (enabled.value) {
21+
uiStore.disableFadeOnHover()
22+
return
23+
}
24+
25+
try {
26+
const acknowledged = await requestNotice({
27+
id: NOTICE_WINDOW_ID,
28+
route: '/notice/fade-on-hover',
29+
type: 'fade-on-hover',
30+
})
31+
if (acknowledged)
32+
uiStore.enableFadeOnHover()
33+
}
34+
catch (error) {
35+
console.error('Failed to open fade-on-hover notice:', error)
36+
}
37+
}
38+
</script>
39+
40+
<template>
41+
<ControlButtonTooltip>
42+
<ControlButton
43+
:class="{ 'border-primary-300/70 shadow-[0_10px_24px_rgba(0,0,0,0.22)]': enabled }"
44+
@click="handleToggle"
45+
>
46+
<Transition name="fade" mode="out-in">
47+
<div v-if="enabled" i-ph:eye size-5 text="primary-700 dark:primary-300" />
48+
<div v-else i-ph:eye-slash size-5 text="neutral-800 dark:neutral-300" />
49+
</Transition>
50+
</ControlButton>
51+
52+
<template #tooltip>
53+
{{ enabled ? t('tamagotchi.stage.controls-island.fade-on-hover.disable') : t('tamagotchi.stage.controls-island.fade-on-hover.enable') }}
54+
</template>
55+
</ControlButtonTooltip>
56+
</template>

apps/stage-tamagotchi/src/renderer/components/StageIslands/ControlsIsland/index.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ref } from 'vue'
77
88
import ControlButton from './ControlButton.vue'
99
import ControlButtonTooltip from './ControlButtonTooltip.vue'
10+
import ControlsIslandFadeOnHover from './ControlsIslandFadeOnHover.vue'
1011
import ControlsIslandHearingConfig from './ControlsIslandHearingConfig.vue'
1112
import IndicatorMicVolume from './IndicatorMicVolume.vue'
1213
@@ -75,6 +76,8 @@ defineExpose({ hearingDialogOpen })
7576
</template>
7677
</ControlButtonTooltip>
7778

79+
<ControlsIslandFadeOnHover />
80+
7881
<ControlButtonTooltip>
7982
<ControlButton cursor-move :class="{ 'drag-region': isLinux }" @mousedown="startDraggingWindow?.()">
8083
<div i-ph:arrows-out-cardinal size-5 text="neutral-800 dark:neutral-300" />

apps/stage-tamagotchi/src/renderer/pages/index.vue

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consci
1313
import { useHearingSpeechInputPipeline } from '@proj-airi/stage-ui/stores/modules/hearing'
1414
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
1515
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
16-
import { refDebounced, useBroadcastChannel } from '@vueuse/core'
16+
import { refDebounced, useBroadcastChannel, watchPausable } from '@vueuse/core'
1717
import { storeToRefs } from 'pinia'
1818
import { computed, onUnmounted, ref, toRef, watch } from 'vue'
1919
@@ -28,6 +28,7 @@ import {
2828
useElectronMouseInWindow,
2929
useElectronRelativeMouse,
3030
} from '../composables/electron-vueuse'
31+
import { useControlsIslandStore } from '../stores/controls-island'
3132
import { useWindowStore } from '../stores/window'
3233
3334
const resourceStatusIslandRef = ref<InstanceType<typeof ResourceStatusIsland>>()
@@ -53,16 +54,17 @@ const setIgnoreMouseEvents = useElectronEventaInvoke(electron.window.setIgnoreMo
5354
5455
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
5556
const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
57+
const { fadeOnHoverEnabled } = storeToRefs(useControlsIslandStore())
5658
5759
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
5860
59-
const { pause, resume } = watch(isTransparent, (transparent) => {
60-
shouldFadeOnCursorWithin.value = !transparent
61+
const { pause, resume } = watchPausable(isTransparent, (transparent) => {
62+
shouldFadeOnCursorWithin.value = fadeOnHoverEnabled.value && !transparent
6163
}, { immediate: true })
6264
6365
const hearingDialogOpen = computed(() => controlsIslandRef.value?.hearingDialogOpen ?? false)
6466
65-
watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, hearingDialogOpen], () => {
67+
watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, hearingDialogOpen, fadeOnHoverEnabled], () => {
6668
if (hearingDialogOpen.value) {
6769
// Hearing dialog/drawer is open; keep window interactive
6870
isIgnoringMouseEvents.value = false
@@ -83,13 +85,14 @@ watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTrans
8385
pause()
8486
}
8587
else {
86-
// Otherwise allow click-through while we fade UI based on transparency
88+
// Otherwise allow click-through while we fade UI based on transparency (when enabled)
8789
isIgnoringMouseEvents.value = true
88-
if (!isOutsideWindow.value && !isTransparent.value) {
89-
shouldFadeOnCursorWithin.value = true
90-
}
90+
shouldFadeOnCursorWithin.value = fadeOnHoverEnabled.value && !isOutsideWindow.value && !isTransparent.value
9191
setIgnoreMouseEvents([true, { forward: true }])
92-
resume()
92+
if (fadeOnHoverEnabled.value)
93+
resume()
94+
else
95+
pause()
9396
}
9497
})
9598
@@ -226,7 +229,9 @@ watch([stream, () => vadLoaded.value], async ([s, loaded]) => {
226229
:y-offset="positionInPercentageString.y"
227230
mb="<md:18"
228231
/>
229-
<ControlsIsland ref="controlsIslandRef" />
232+
<ControlsIsland
233+
ref="controlsIslandRef"
234+
/>
230235
</div>
231236
</div>
232237
<div v-show="isLoading" h-full w-full>

0 commit comments

Comments
 (0)