diff --git a/packages/stage-ui-live2d/src/components/scenes/live2d/Model.vue b/packages/stage-ui-live2d/src/components/scenes/live2d/Model.vue index 7f8f63e92..600836a5b 100644 --- a/packages/stage-ui-live2d/src/components/scenes/live2d/Model.vue +++ b/packages/stage-ui-live2d/src/components/scenes/live2d/Model.vue @@ -165,7 +165,7 @@ const beatSync = createBeatSyncController({ }) // Listen for model reload requests (e.g., when runtime motion is uploaded) -live2dStore.onShouldUpdateView(() => { +const disposeShouldUpdateView = live2dStore.onShouldUpdateView(() => { loadModel() }) @@ -620,6 +620,7 @@ onMounted(async () => { onUnmounted(() => { isUnmounted = true + disposeShouldUpdateView?.() }) function listMotionGroups() { diff --git a/packages/stage-ui-live2d/src/stores/live2d.ts b/packages/stage-ui-live2d/src/stores/live2d.ts index 20e283fe1..ccd75de04 100644 --- a/packages/stage-ui-live2d/src/stores/live2d.ts +++ b/packages/stage-ui-live2d/src/stores/live2d.ts @@ -36,10 +36,13 @@ export const defaultModelParameters = { export const useLive2d = defineStore('live2d', () => { const { post, data } = useBroadcastChannel({ name: 'airi-stores-live2d' }) - const shouldUpdateViewHooks = ref void>>([]) + const shouldUpdateViewHooks = ref(new Set<() => void>()) const onShouldUpdateView = (hook: () => void) => { - shouldUpdateViewHooks.value.push(hook) + shouldUpdateViewHooks.value.add(hook) + return () => { + shouldUpdateViewHooks.value.delete(hook) + } } function shouldUpdateView() { diff --git a/packages/stage-ui-three/src/stores/model-store.ts b/packages/stage-ui-three/src/stores/model-store.ts index 1ffcccfd1..7e9a643f7 100644 --- a/packages/stage-ui-three/src/stores/model-store.ts +++ b/packages/stage-ui-three/src/stores/model-store.ts @@ -61,10 +61,13 @@ interface BroadcastChannelEventShouldUpdateView { export const useModelStore = defineStore('modelStore', () => { const { post, data } = useBroadcastChannel({ name: 'airi-stores-live2d' }) - const shouldUpdateViewHooks = ref void>>([]) + const shouldUpdateViewHooks = ref(new Set<() => void>()) const onShouldUpdateView = (hook: () => void) => { - shouldUpdateViewHooks.value.push(hook) + shouldUpdateViewHooks.value.add(hook) + return () => { + shouldUpdateViewHooks.value.delete(hook) + } } function shouldUpdateView() { diff --git a/packages/stage-ui/src/components/scenes/Stage.vue b/packages/stage-ui/src/components/scenes/Stage.vue index 3114c9a96..3721ee297 100644 --- a/packages/stage-ui/src/components/scenes/Stage.vue +++ b/packages/stage-ui/src/components/scenes/Stage.vue @@ -101,6 +101,7 @@ const live2dStore = useLive2d() const vrmStore = useModelStore() const showStage = ref(true) +const viewUpdateCleanups: Array<() => void> = [] // Caption + Presentation broadcast channels type CaptionChannelEvent @@ -114,23 +115,21 @@ type PresentEvent | { type: 'assistant-append', text: string } const { post: postPresent } = useBroadcastChannel({ name: 'airi-chat-present' }) -// TODO: duplicate calls may happen if this component mounted multiple times -live2dStore.onShouldUpdateView(async () => { +viewUpdateCleanups.push(live2dStore.onShouldUpdateView(async () => { showStage.value = false await settingsStore.updateStageModel() setTimeout(() => { showStage.value = true }, 100) -}) +})) -// TODO: duplicate calls may happen if this component mounted multiple times -vrmStore.onShouldUpdateView(async () => { +viewUpdateCleanups.push(vrmStore.onShouldUpdateView(async () => { showStage.value = false await settingsStore.updateStageModel() setTimeout(() => { showStage.value = true }, 100) -}) +})) const audioAnalyser = ref() const nowSpeaking = ref(false) @@ -353,6 +352,7 @@ onUnmounted(() => { } chatHookCleanups.forEach(dispose => dispose?.()) + viewUpdateCleanups.forEach(dispose => dispose?.()) }) defineExpose({