diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index cb7b81054..c2ed8eb0e 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -22,6 +22,7 @@ import { setupCaptionWindowManager } from './windows/caption' import { setupChatWindowReusableFunc } from './windows/chat' import { setupInlayWindow } from './windows/inlay' import { setupMainWindow } from './windows/main' +import { setupNoticeWindowManager } from './windows/notice' import { setupSettingsWindowReusableFunc } from './windows/settings' import { toggleWindowShow } from './windows/shared/window' import { setupWidgetsWindowManager } from './windows/widgets' @@ -103,13 +104,14 @@ app.whenReady().then(async () => { const channelServerModule = injeca.provide('modules:channel-server', async () => setupChannelServer()) const chatWindow = injeca.provide('windows:chat', { build: () => setupChatWindowReusableFunc() }) const widgetsManager = injeca.provide('windows:widgets', { build: () => setupWidgetsWindowManager() }) + const noticeWindow = injeca.provide('windows:notice', { build: () => setupNoticeWindowManager() }) const settingsWindow = injeca.provide('windows:settings', { dependsOn: { widgetsManager }, build: ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn), }) const mainWindow = injeca.provide('windows:main', { - dependsOn: { settingsWindow, chatWindow, widgetsManager }, + dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow }, build: async ({ dependsOn }) => setupMainWindow(dependsOn), }) const captionWindow = injeca.provide('windows:caption', { diff --git a/apps/stage-tamagotchi/src/main/windows/main/index.ts b/apps/stage-tamagotchi/src/main/windows/main/index.ts index a8903c381..38a7dc49f 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/index.ts @@ -1,6 +1,7 @@ import type { BrowserWindowConstructorOptions, Rectangle } from 'electron' import type { WidgetsWindowManager } from '../widgets' +import type { NoticeWindowManager } from '../notice' import { dirname, join, resolve } from 'node:path' import { env } from 'node:process' @@ -31,6 +32,7 @@ export async function setupMainWindow(params: { settingsWindow: () => Promise chatWindow: () => Promise widgetsManager: WidgetsWindowManager + noticeWindow: NoticeWindowManager }) { const { setup: setupConfig, @@ -131,6 +133,7 @@ export async function setupMainWindow(params: { settingsWindow: params.settingsWindow, chatWindow: params.chatWindow, widgetsManager: params.widgetsManager, + noticeWindow: params.noticeWindow, }) /** diff --git a/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts index 5d23d5b97..7da3d3dba 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts @@ -1,12 +1,13 @@ import type { BrowserWindow } from 'electron' +import type { NoticeWindowManager } from '../../notice' import type { WidgetsWindowManager } from '../../widgets' import { defineInvokeHandler } from '@moeru/eventa' import { createContext } from '@moeru/eventa/adapters/electron/main' import { ipcMain } from 'electron' -import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings } from '../../../../shared/eventa' +import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings, noticeWindowEventa } from '../../../../shared/eventa' import { createWidgetsService } from '../../../services/airi/widgets' import { createScreenService, createWindowService } from '../../../services/electron' import { toggleWindowShow } from '../../shared' @@ -16,6 +17,7 @@ export function setupMainWindowElectronInvokes(params: { settingsWindow: () => Promise chatWindow: () => Promise widgetsManager: WidgetsWindowManager + noticeWindow: NoticeWindowManager }) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and @@ -31,4 +33,5 @@ export function setupMainWindowElectronInvokes(params: { defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' })) defineInvokeHandler(context, electronOpenSettings, async () => toggleWindowShow(await params.settingsWindow())) defineInvokeHandler(context, electronOpenChat, async () => toggleWindowShow(await params.chatWindow())) + defineInvokeHandler(context, noticeWindowEventa.openWindow, (payload) => params.noticeWindow.open(payload)) } diff --git a/apps/stage-tamagotchi/src/renderer/assets/videos/tutorial/tutorial-fade-on-hover.dark.mp4 b/apps/stage-tamagotchi/src/renderer/assets/videos/tutorial/tutorial-fade-on-hover.dark.mp4 new file mode 100644 index 000000000..4132204d8 Binary files /dev/null and b/apps/stage-tamagotchi/src/renderer/assets/videos/tutorial/tutorial-fade-on-hover.dark.mp4 differ diff --git a/apps/stage-tamagotchi/src/renderer/assets/videos/tutorial/tutorial-fade-on-hover.light.mp4 b/apps/stage-tamagotchi/src/renderer/assets/videos/tutorial/tutorial-fade-on-hover.light.mp4 new file mode 100644 index 000000000..2b010473c Binary files /dev/null and b/apps/stage-tamagotchi/src/renderer/assets/videos/tutorial/tutorial-fade-on-hover.light.mp4 differ diff --git a/apps/stage-tamagotchi/src/renderer/components/StageIslands/ControlsIsland/ControlsIslandFadeOnHover.vue b/apps/stage-tamagotchi/src/renderer/components/StageIslands/ControlsIsland/ControlsIslandFadeOnHover.vue new file mode 100644 index 000000000..a6ad5b6dd --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/components/StageIslands/ControlsIsland/ControlsIslandFadeOnHover.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/stage-tamagotchi/src/renderer/components/StageIslands/ControlsIsland/index.vue b/apps/stage-tamagotchi/src/renderer/components/StageIslands/ControlsIsland/index.vue index cde4bf399..3741de793 100644 --- a/apps/stage-tamagotchi/src/renderer/components/StageIslands/ControlsIsland/index.vue +++ b/apps/stage-tamagotchi/src/renderer/components/StageIslands/ControlsIsland/index.vue @@ -7,6 +7,7 @@ import { ref } from 'vue' import ControlButton from './ControlButton.vue' import ControlButtonTooltip from './ControlButtonTooltip.vue' +import ControlsIslandFadeOnHover from './ControlsIslandFadeOnHover.vue' import ControlsIslandHearingConfig from './ControlsIslandHearingConfig.vue' import IndicatorMicVolume from './IndicatorMicVolume.vue' @@ -75,6 +76,8 @@ defineExpose({ hearingDialogOpen }) + +
diff --git a/apps/stage-tamagotchi/src/renderer/pages/index.vue b/apps/stage-tamagotchi/src/renderer/pages/index.vue index 720098c88..efe1f8791 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/index.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/index.vue @@ -13,7 +13,7 @@ import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consci import { useHearingSpeechInputPipeline } from '@proj-airi/stage-ui/stores/modules/hearing' import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings' -import { refDebounced, useBroadcastChannel } from '@vueuse/core' +import { refDebounced, useBroadcastChannel, watchPausable } from '@vueuse/core' import { storeToRefs } from 'pinia' import { computed, onUnmounted, ref, toRef, watch } from 'vue' @@ -28,6 +28,7 @@ import { useElectronMouseInWindow, useElectronRelativeMouse, } from '../composables/electron-vueuse' +import { useControlsIslandStore } from '../stores/controls-island' import { useWindowStore } from '../stores/window' const resourceStatusIslandRef = ref>() @@ -53,16 +54,17 @@ const setIgnoreMouseEvents = useElectronEventaInvoke(electron.window.setIgnoreMo const { scale, positionInPercentageString } = storeToRefs(useLive2d()) const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore()) +const { fadeOnHoverEnabled } = storeToRefs(useControlsIslandStore()) watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true }) -const { pause, resume } = watch(isTransparent, (transparent) => { - shouldFadeOnCursorWithin.value = !transparent +const { pause, resume } = watchPausable(isTransparent, (transparent) => { + shouldFadeOnCursorWithin.value = fadeOnHoverEnabled.value && !transparent }, { immediate: true }) const hearingDialogOpen = computed(() => controlsIslandRef.value?.hearingDialogOpen ?? false) -watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, hearingDialogOpen], () => { +watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, hearingDialogOpen, fadeOnHoverEnabled], () => { if (hearingDialogOpen.value) { // Hearing dialog/drawer is open; keep window interactive isIgnoringMouseEvents.value = false @@ -83,13 +85,14 @@ watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTrans pause() } else { - // Otherwise allow click-through while we fade UI based on transparency + // Otherwise allow click-through while we fade UI based on transparency (when enabled) isIgnoringMouseEvents.value = true - if (!isOutsideWindow.value && !isTransparent.value) { - shouldFadeOnCursorWithin.value = true - } + shouldFadeOnCursorWithin.value = fadeOnHoverEnabled.value && !isOutsideWindow.value && !isTransparent.value setIgnoreMouseEvents([true, { forward: true }]) - resume() + if (fadeOnHoverEnabled.value) + resume() + else + pause() } }) @@ -226,7 +229,9 @@ watch([stream, () => vadLoaded.value], async ([s, loaded]) => { :y-offset="positionInPercentageString.y" mb=" - +
diff --git a/apps/stage-tamagotchi/src/renderer/pages/notice/fade-on-hover.vue b/apps/stage-tamagotchi/src/renderer/pages/notice/fade-on-hover.vue new file mode 100644 index 000000000..6e32eb81f --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/pages/notice/fade-on-hover.vue @@ -0,0 +1,207 @@ + + + + + +meta: + layout: plain + diff --git a/apps/stage-tamagotchi/src/renderer/stores/controls-island.ts b/apps/stage-tamagotchi/src/renderer/stores/controls-island.ts new file mode 100644 index 000000000..aa8a7f215 --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/stores/controls-island.ts @@ -0,0 +1,21 @@ +import { useLocalStorage } from '@vueuse/core' +import { defineStore } from 'pinia' + +export const useControlsIslandStore = defineStore('controls-island', () => { + // Persist fade-on-hover preference per user + const fadeOnHoverEnabled = useLocalStorage('controls-island/fade-on-hover-enabled', false) + + function enableFadeOnHover() { + fadeOnHoverEnabled.value = true + } + + function disableFadeOnHover() { + fadeOnHoverEnabled.value = false + } + + return { + fadeOnHoverEnabled, + enableFadeOnHover, + disableFadeOnHover, + } +}) diff --git a/packages/i18n/src/locales/en/tamagotchi/stage.yaml b/packages/i18n/src/locales/en/tamagotchi/stage.yaml index c231b509e..72fa53d11 100644 --- a/packages/i18n/src/locales/en/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/en/tamagotchi/stage.yaml @@ -8,3 +8,21 @@ docs: navbar: appearance: title: Appearance +'controls-island': + 'fade-on-hover': + enable: Auto hide + disable: Always show +notice: + 'fade-on-hover': + title: Fade on Hover + intro: Automatically fades the character when your cursor roams nearby. It helps reduce distractions while keeping your character visible. + opacity: After turning on, the opacity drops to {value} for both {targets} when your cursor is nearby. + toggle: You can toggle it off by hovering at {controls}, then clicking the {icon} again. + controls-label: Controls Island + icon-label: eye-slash icon + value: '0' + targets: Character and UI + confirm: Got it + preparing: Preparing… + read-more: Read more + preview-title: What is it? diff --git a/packages/i18n/src/locales/es/tamagotchi/stage.yaml b/packages/i18n/src/locales/es/tamagotchi/stage.yaml index 6ab10c8a0..377f5bf2f 100644 --- a/packages/i18n/src/locales/es/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/es/tamagotchi/stage.yaml @@ -8,3 +8,21 @@ docs: navbar: appearance: title: Apariencia +'controls-island': + 'fade-on-hover': + enable: Ocultar al pasar el cursor + disable: Mostrar siempre +notice: + 'fade-on-hover': + title: Desvanecer al pasar el cursor + intro: Difumina el personaje automáticamente cuando el cursor está cerca. Reduce distracciones sin perder visibilidad. + opacity: Al activarlo, la opacidad baja a {value} para {targets} cuando el cursor está cerca. + toggle: Puedes desactivarlo pasando el cursor por {controls} y tocando de nuevo el {icon}. + controls-label: Isla de controles + icon-label: ícono de ojo tachado + value: '0' + targets: Personaje y UI + confirm: Entendido + preparing: Preparando… + read-more: Leer más + preview-title: ¿Qué es esto? diff --git a/packages/i18n/src/locales/fr/tamagotchi/stage.yaml b/packages/i18n/src/locales/fr/tamagotchi/stage.yaml index 555f0631c..7ce0bfa37 100644 --- a/packages/i18n/src/locales/fr/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/fr/tamagotchi/stage.yaml @@ -8,3 +8,21 @@ docs: navbar: appearance: title: Apparence +'controls-island': + 'fade-on-hover': + enable: Masquer au survol + disable: Toujours afficher +notice: + 'fade-on-hover': + title: Disparaître au survol + intro: Fait disparaître légèrement le personnage quand le curseur s’approche. Moins de distractions tout en restant visible. + opacity: Une fois activé, l’opacité descend à {value} pour {targets} quand le curseur est à proximité. + toggle: Vous pouvez le désactiver en survolant l’{controls}, puis en cliquant à nouveau sur l’{icon}. + controls-label: Îlot de commandes + icon-label: icône œil barré + value: '0' + targets: Personnage et UI + confirm: Compris + preparing: Préparation… + read-more: En savoir plus + preview-title: C’est quoi ? diff --git a/packages/i18n/src/locales/ru/tamagotchi/stage.yaml b/packages/i18n/src/locales/ru/tamagotchi/stage.yaml index 718801f6e..2bcf02d5e 100644 --- a/packages/i18n/src/locales/ru/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/ru/tamagotchi/stage.yaml @@ -8,3 +8,21 @@ docs: navbar: appearance: title: Внешний вид +'controls-island': + 'fade-on-hover': + enable: Скрывать при наведении + disable: Всегда показывать +notice: + 'fade-on-hover': + title: Исчезать при наведении + intro: Автоматически делает персонажа прозрачнее, когда курсор рядом. Меньше отвлекает, но остаётся видимым. + opacity: После включения непрозрачность падает до {value} для {targets}, когда курсор рядом. + toggle: Можно выключить, наведя на {controls} и снова нажав на {icon}. + controls-label: Остров управления + icon-label: значок перечёркнутого глаза + value: '0' + targets: Персонажа и интерфейса + confirm: Понятно + preparing: Подготавливаем… + read-more: Подробнее + preview-title: Что это? diff --git a/packages/i18n/src/locales/vi/tamagotchi/stage.yaml b/packages/i18n/src/locales/vi/tamagotchi/stage.yaml index 7b0025f2f..3dfd55636 100644 --- a/packages/i18n/src/locales/vi/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/vi/tamagotchi/stage.yaml @@ -8,3 +8,21 @@ docs: navbar: appearance: title: Giao diện +'controls-island': + 'fade-on-hover': + enable: Ẩn khi rê chuột + disable: Luôn hiển thị +notice: + 'fade-on-hover': + title: Mờ dần khi rê chuột + intro: Tự làm mờ nhân vật khi con trỏ ở gần. Giảm xao nhãng nhưng vẫn nhìn thấy nhân vật. + opacity: Bật lên thì độ mờ giảm xuống {value} cho {targets} khi con trỏ ở gần. + toggle: Tắt đi bằng cách rê chuột qua {controls} rồi bấm lại {icon}. + controls-label: Đảo Điều Khiển + icon-label: biểu tượng mắt gạch + value: '0' + targets: Nhân vật và giao diện + confirm: Đã hiểu + preparing: Đang chuẩn bị… + read-more: Xem thêm + preview-title: Đây là gì? diff --git a/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml b/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml index 45131f5fb..6fafe3e5c 100644 --- a/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml @@ -8,3 +8,21 @@ docs: navbar: appearance: title: 外观 +'controls-island': + 'fade-on-hover': + enable: 悬停时隐藏 + disable: 总是显示 +notice: + 'fade-on-hover': + title: 悬停淡出 + intro: 当光标靠近时自动让角色变淡,减少干扰又保持可见。 + opacity: 开启后,不透明度会降到 {value} ,作用于 {targets} ,当光标靠近时。 + toggle: 想关闭,移动到 {controls} 区域,再点击一次 {icon}。 + controls-label: 控制岛 + icon-label: 隐藏图标 + value: '0' + targets: 角色和界面 + confirm: 知道了 + preparing: 准备中… + read-more: 阅读更多 + preview-title: 这是什么? diff --git a/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml b/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml index 1b99d5a89..e313d9ae3 100644 --- a/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml @@ -8,3 +8,21 @@ docs: navbar: appearance: title: 外貌 +'controls-island': + 'fade-on-hover': + enable: 懸停時隱藏 + disable: 總是顯示 +notice: + 'fade-on-hover': + title: 懸停淡出 + intro: 當游標靠近時自動讓角色變淡,減少干擾同時保持可見。 + opacity: 開啟後,不透明度會降到 {value} ,作用於 {targets} ,當游標靠近時。 + toggle: 要關閉時,將游標移到 {controls} 區域,再點擊一次 {icon}。 + controls-label: 控制島 + icon-label: 隱藏圖示 + value: '0' + targets: 角色與介面 + confirm: 知道了 + preparing: 準備中… + read-more: 閱讀更多 + preview-title: 這是什麼?