fix(stage-ui,stage-pages): yes re-implemented useLocalStorageManualReset as refManualReset will not propagate

This commit is contained in:
Neko Ayaka
2026-01-17 15:59:00 +08:00
parent e38366d37b
commit c0d69ca910
17 changed files with 112 additions and 74 deletions
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { FieldInput } from '@proj-airi/ui'
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const defaultWebSocketUrl = import.meta.env.VITE_AIRI_WS_URL || 'ws://localhost:6121/ws'
const websocketUrl = refManualReset<string>(useLocalStorage('settings/connection/websocket-url', defaultWebSocketUrl))
const websocketUrl = useLocalStorageManualReset('settings/connection/websocket-url', defaultWebSocketUrl)
</script>
<template>
@@ -0,0 +1 @@
export * from './use-local-storage-manual-reset'
@@ -0,0 +1,20 @@
import type { ManualResetRefReturn, UseStorageOptions } from '@vueuse/core'
import type { MaybeRefOrGetter, WatchOptions } from 'vue'
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { unref, watch } from 'vue'
export function useLocalStorageManualReset<T>(key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<T>, options?: UseStorageOptions<T> & WatchOptions): ManualResetRefReturn<T> {
const value = unref(initialValue)
const localStorageState = useLocalStorage<T>(key, value, options)
const state = refManualReset<T>(localStorageState)
const { resume, pause } = watch(state, newValue => localStorageState.value = newValue, options)
watch(localStorageState, (newValue) => {
pause()
state.value = newValue
resume()
}, options)
return state
}
@@ -40,17 +40,13 @@ withDefaults(defineProps<{
})
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
const componentStateCanvas = defineModel<'pending' | 'loading' | 'mounted'>('canvasState', { default: 'pending' })
const componentStateModel = defineModel<'pending' | 'loading' | 'mounted'>('modelState', { default: 'pending' })
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
const live2d = useLive2d()
const {
scale,
position,
} = storeToRefs(live2d)
const { scale, position } = storeToRefs(live2d)
watch([componentStateModel, componentStateCanvas], () => {
componentState.value = (componentStateModel.value === 'mounted' && componentStateCanvas.value === 'mounted')
@@ -1,4 +1,5 @@
import { refManualReset, useBroadcastChannel, useLocalStorage } from '@vueuse/core'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { useBroadcastChannel } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
@@ -56,18 +57,18 @@ export const useLive2d = defineStore('live2d', () => {
}
})
const position = refManualReset<{ x: number, y: number }>(useLocalStorage('settings/live2d/position', { x: 0, y: 0 })) // position is relative to the center of the screen, units are %
const position = useLocalStorageManualReset<{ x: number, y: number }>('settings/live2d/position', { x: 0, y: 0 }) // position is relative to the center of the screen, units are %
const positionInPercentageString = computed(() => ({
x: `${position.value.x}%`,
y: `${position.value.y}%`,
}))
const currentMotion = refManualReset<{ group: string, index?: number }>(() => ({ group: 'Idle', index: 0 }))
const availableMotions = refManualReset<{ motionName: string, motionIndex: number, fileName: string }[]>(() => [])
const motionMap = refManualReset<Record<string, string>>(useLocalStorage('settings/live2d/motion-map', {}))
const scale = refManualReset<number>(useLocalStorage('settings/live2d/scale', 1))
const currentMotion = useLocalStorageManualReset<{ group: string, index?: number }>('settings/live2d/current-motion', () => ({ group: 'Idle', index: 0 }))
const availableMotions = useLocalStorageManualReset<{ motionName: string, motionIndex: number, fileName: string }[]>('settings/live2d/available-motions', () => [])
const motionMap = useLocalStorageManualReset<Record<string, string>>('settings/live2d/motion-map', {})
const scale = useLocalStorageManualReset('settings/live2d/scale', 1)
// Live2D model parameters
const modelParameters = refManualReset<Record<string, number>>(useLocalStorage('settings/live2d/parameters', defaultModelParameters))
const modelParameters = useLocalStorageManualReset<Record<string, number>>('settings/live2d/parameters', defaultModelParameters)
function resetState() {
position.reset()
@@ -85,8 +85,16 @@ watch(selectedModel, async () => {
Select Model
</Button>
</ModelSelectorDialog>
<Live2D v-if="stageModelRenderer === 'live2d'" :palette="palette" @extract-colors-from-model="$emit('extractColorsFromModel')" />
<VRM v-if="stageModelRenderer === 'vrm'" :palette="palette" @extract-colors-from-model="$emit('extractColorsFromModel')" />
<Live2D
v-if="stageModelRenderer === 'live2d'"
:palette="palette"
@extract-colors-from-model="$emit('extractColorsFromModel')"
/>
<VRM
v-if="stageModelRenderer === 'vrm'"
:palette="palette"
@extract-colors-from-model="$emit('extractColorsFromModel')"
/>
</div>
<!-- Live2D component for 2D stage view -->
<template v-if="stageModelRenderer === 'live2d'">
@@ -19,7 +19,13 @@ defineEmits<{
const { t } = useI18n()
const settings = useSettings()
const { live2dDisableFocus, live2dIdleAnimationEnabled, live2dAutoBlinkEnabled, live2dForceAutoBlinkEnabled, live2dShadowEnabled } = storeToRefs(settings)
const {
live2dDisableFocus,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dShadowEnabled,
} = storeToRefs(settings)
const live2d = useLive2d()
const {
@@ -527,8 +527,9 @@ defineExpose({
<Live2DScene
v-if="stageModelRenderer === 'live2d' && showStage"
ref="live2dSceneRef"
v-model:state="componentState" min-w="50% <lg:full" min-h="100 sm:100" h-full w-full
flex-1
v-model:state="componentState"
min-w="50% <lg:full" min-h="100 sm:100"
h-full w-full flex-1
:model-src="stageModelSelectedUrl"
:model-id="stageModelSelected"
:focus-at="focusAt"
+5 -4
View File
@@ -1,10 +1,11 @@
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { useLocalStorageManualReset } from '../../../stage-shared/src/composables/use-local-storage-manual-reset'
export const useMcpStore = defineStore('mcp', () => {
const serverCmd = refManualReset<string>(useLocalStorage<string>('settings/mcp/server-cmd', ''))
const serverArgs = refManualReset<string>(useLocalStorage<string>('settings/mcp/server-args', ''))
const connected = refManualReset<boolean>(useLocalStorage<boolean>('mcp/connected', false)) // use local storage to sync between windows
const serverCmd = useLocalStorageManualReset<string>('settings/mcp/server-cmd', '')
const serverArgs = useLocalStorageManualReset<string>('settings/mcp/server-args', '')
const connected = useLocalStorageManualReset<boolean>('mcp/connected', false) // use local storage to sync between windows
function resetState() {
serverCmd.reset()
@@ -1,6 +1,6 @@
import type { Card, ccv3 } from '@proj-airi/ccc'
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { nanoid } from 'nanoid'
import { defineStore, storeToRefs } from 'pinia'
import { computed, watch } from 'vue'
@@ -57,8 +57,8 @@ export interface AiriCard extends Card {
export const useAiriCardStore = defineStore('airi-card', () => {
const { t } = useI18n()
const cards = refManualReset<Map<string, AiriCard>>(useLocalStorage('airi-cards', new Map()))
const activeCardId = refManualReset<string>(useLocalStorage('airi-card-active-id', 'default'))
const cards = useLocalStorageManualReset<Map<string, AiriCard>>('airi-cards', new Map())
const activeCardId = useLocalStorageManualReset<string>('airi-card-active-id', 'default')
const activeCard = computed(() => cards.value.get(activeCardId.value))
@@ -1,4 +1,5 @@
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { refManualReset } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed } from 'vue'
@@ -8,9 +9,9 @@ export const useConsciousnessStore = defineStore('consciousness', () => {
const providersStore = useProvidersStore()
// State
const activeProvider = refManualReset<string>(useLocalStorage<string>('settings/consciousness/active-provider', ''))
const activeModel = refManualReset<string>(useLocalStorage<string>('settings/consciousness/active-model', ''))
const activeCustomModelName = refManualReset<string>(useLocalStorage<string>('settings/consciousness/active-custom-model', ''))
const activeProvider = useLocalStorageManualReset<string>('settings/consciousness/active-provider', '')
const activeModel = useLocalStorageManualReset<string>('settings/consciousness/active-model', '')
const activeCustomModelName = useLocalStorageManualReset<string>('settings/consciousness/active-custom-model', '')
const expandedDescriptions = refManualReset<Record<string, boolean>>(() => ({}))
const modelSearchQuery = refManualReset<string>('')
@@ -1,4 +1,4 @@
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
import { computed } from 'vue'
@@ -6,8 +6,8 @@ import { useConfiguratorByModsChannelServer } from '../configurator'
export const useDiscordStore = defineStore('discord', () => {
const configurator = useConfiguratorByModsChannelServer()
const enabled = refManualReset<boolean>(useLocalStorage<boolean>('settings/discord/enabled', false))
const token = refManualReset<string>(useLocalStorage<string>('settings/discord/token', ''))
const enabled = useLocalStorageManualReset<boolean>('settings/discord/enabled', false)
const token = useLocalStorageManualReset<string>('settings/discord/token', '')
function saveSettings() {
// Data is automatically saved to localStorage via useLocalStorage
@@ -1,4 +1,4 @@
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
import { computed } from 'vue'
@@ -8,10 +8,10 @@ export function createGamingModuleStore(moduleName: string, defaultPort: number)
return defineStore(moduleName, () => {
const configurator = useConfiguratorByModsChannelServer()
const enabled = refManualReset<boolean>(useLocalStorage<boolean>(`settings/${moduleName}/enabled`, false))
const serverAddress = refManualReset<string>(useLocalStorage<string>(`settings/${moduleName}/server-address`, ''))
const serverPort = refManualReset<number | null>(useLocalStorage<number | null>(`settings/${moduleName}/server-port`, defaultPort))
const username = refManualReset<string>(useLocalStorage<string>(`settings/${moduleName}/username`, ''))
const enabled = useLocalStorageManualReset<boolean>(`settings/${moduleName}/enabled`, false)
const serverAddress = useLocalStorageManualReset<string>(`settings/${moduleName}/server-address`, '')
const serverPort = useLocalStorageManualReset<number | null>(`settings/${moduleName}/server-port`, defaultPort)
const username = useLocalStorageManualReset<string>(`settings/${moduleName}/username`, '')
function saveSettings() {
configurator.updateFor(moduleName, {
@@ -3,7 +3,8 @@ import type { WithUnknown } from '@xsai/shared'
import type { StreamTranscriptionResult, StreamTranscriptionOptions as XSAIStreamTranscriptionOptions } from '@xsai/stream-transcription'
import { tryCatch } from '@moeru/std'
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { refManualReset } from '@vueuse/core'
import { generateTranscription } from '@xsai/generate-transcription'
import { defineStore, storeToRefs } from 'pinia'
import { computed, ref, shallowRef } from 'vue'
@@ -49,12 +50,12 @@ export const useHearingStore = defineStore('hearing-store', () => {
const { allAudioTranscriptionProvidersMetadata } = storeToRefs(providersStore)
// State
const activeTranscriptionProvider = refManualReset<string>(useLocalStorage('settings/hearing/active-provider', ''))
const activeTranscriptionModel = refManualReset<string>(useLocalStorage('settings/hearing/active-model', ''))
const activeCustomModelName = refManualReset<string>(useLocalStorage('settings/hearing/active-custom-model', ''))
const activeTranscriptionProvider = useLocalStorageManualReset('settings/hearing/active-provider', '')
const activeTranscriptionModel = useLocalStorageManualReset('settings/hearing/active-model', '')
const activeCustomModelName = useLocalStorageManualReset('settings/hearing/active-custom-model', '')
const transcriptionModelSearchQuery = refManualReset<string>('')
const autoSendEnabled = refManualReset<boolean>(useLocalStorage('settings/hearing/auto-send-enabled', false))
const autoSendDelay = refManualReset<number>(useLocalStorage('settings/hearing/auto-send-delay', 2000)) // Default 2 seconds
const autoSendEnabled = useLocalStorageManualReset<boolean>('settings/hearing/auto-send-enabled', false)
const autoSendDelay = useLocalStorageManualReset<number>('settings/hearing/auto-send-delay', 2000) // Default 2 seconds
// Computed properties
const availableProvidersMetadata = computed(() => allAudioTranscriptionProvidersMetadata.value)
@@ -2,7 +2,8 @@ import type { SpeechProviderWithExtraOptions } from '@xsai-ext/providers/utils'
import type { VoiceInfo } from '../providers'
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { refManualReset } from '@vueuse/core'
import { generateSpeech } from '@xsai/generate-speech'
import { defineStore, storeToRefs } from 'pinia'
import { computed, onMounted, watch } from 'vue'
@@ -16,18 +17,18 @@ export const useSpeechStore = defineStore('speech', () => {
const { allAudioSpeechProvidersMetadata } = storeToRefs(providersStore)
// State
const activeSpeechProvider = refManualReset<string>(useLocalStorage<string>('settings/speech/active-provider', ''))
const activeSpeechModel = refManualReset<string>(useLocalStorage<string>('settings/speech/active-model', 'eleven_multilingual_v2'))
const activeSpeechVoiceId = refManualReset<string>(useLocalStorage<string>('settings/speech/voice', ''))
const activeSpeechProvider = useLocalStorageManualReset<string>('settings/speech/active-provider', '')
const activeSpeechModel = useLocalStorageManualReset<string>('settings/speech/active-model', 'eleven_multilingual_v2')
const activeSpeechVoiceId = useLocalStorageManualReset<string>('settings/speech/voice', '')
const activeSpeechVoice = refManualReset<VoiceInfo | undefined>(undefined)
const pitch = refManualReset<number>(useLocalStorage<number>('settings/speech/pitch', 0))
const rate = refManualReset<number>(useLocalStorage<number>('settings/speech/rate', 1))
const ssmlEnabled = refManualReset<boolean>(useLocalStorage<boolean>('settings/speech/ssml-enabled', false))
const pitch = useLocalStorageManualReset<number>('settings/speech/pitch', 0)
const rate = useLocalStorageManualReset<number>('settings/speech/rate', 1)
const ssmlEnabled = useLocalStorageManualReset<boolean>('settings/speech/ssml-enabled', false)
const isLoadingSpeechProviderVoices = refManualReset<boolean>(false)
const speechProviderError = refManualReset<string | null>(null)
const availableVoices = refManualReset<Record<string, VoiceInfo[]>>(() => ({}))
const selectedLanguage = refManualReset<string>(useLocalStorage<string>('settings/speech/language', 'en-US'))
const selectedLanguage = useLocalStorageManualReset<string>('settings/speech/language', 'en-US')
const modelSearchQuery = refManualReset<string>('')
// Computed properties
@@ -1,4 +1,4 @@
import { refManualReset, useLocalStorage } from '@vueuse/core'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
import { computed } from 'vue'
@@ -7,11 +7,11 @@ import { useConfiguratorByModsChannelServer } from '../configurator'
export const useTwitterStore = defineStore('twitter', () => {
const configurator = useConfiguratorByModsChannelServer()
const enabled = refManualReset<boolean>(useLocalStorage<boolean>('settings/twitter/enabled', false))
const apiKey = refManualReset<string>(useLocalStorage<string>('settings/twitter/api-key', ''))
const apiSecret = refManualReset<string>(useLocalStorage<string>('settings/twitter/api-secret', ''))
const accessToken = refManualReset<string>(useLocalStorage<string>('settings/twitter/access-token', ''))
const accessTokenSecret = refManualReset<string>(useLocalStorage<string>('settings/twitter/access-token-secret', ''))
const enabled = useLocalStorageManualReset<boolean>('settings/twitter/enabled', false)
const apiKey = useLocalStorageManualReset<string>('settings/twitter/api-key', '')
const apiSecret = useLocalStorageManualReset<string>('settings/twitter/api-secret', '')
const accessToken = useLocalStorageManualReset<string>('settings/twitter/access-token', '')
const accessTokenSecret = useLocalStorageManualReset<string>('settings/twitter/access-token-secret', '')
function saveSettings() {
// Data is automatically saved to localStorage via useLocalStorage
+17 -16
View File
@@ -2,7 +2,8 @@ import type { DisplayModel } from './display-models'
import messages from '@proj-airi/i18n/locales'
import { refManualReset, useEventListener, useLocalStorage } from '@vueuse/core'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { refManualReset, useEventListener } from '@vueuse/core'
import { converter } from 'culori'
import { defineStore } from 'pinia'
import { onMounted, watch } from 'vue'
@@ -39,9 +40,9 @@ const getHueFrom = (color?: string) => color ? convert(color)?.h : DEFAULT_THEME
export const useSettings = defineStore('settings', () => {
const displayModelsStore = useDisplayModelsStore()
const language = refManualReset<string>(useLocalStorage('settings/language', ''))
const language = useLocalStorageManualReset<string>('settings/language', '')
const stageModelSelected = refManualReset<string | undefined>(useLocalStorage('settings/stage/model', 'preset-live2d-1'))
const stageModelSelected = useLocalStorageManualReset('settings/stage/model', 'preset-live2d-1')
const stageModelSelectedDisplayModel = refManualReset<DisplayModel | undefined>(undefined)
const stageModelSelectedUrl = refManualReset<string | undefined>(undefined)
const stageModelRenderer = refManualReset<'live2d' | 'vrm' | 'disabled' | undefined>(undefined)
@@ -100,21 +101,21 @@ export const useSettings = defineStore('settings', () => {
const stageViewControlsEnabled = refManualReset<boolean>(false)
const live2dDisableFocus = refManualReset<boolean>(useLocalStorage<boolean>('settings/live2d/disable-focus', false))
const live2dIdleAnimationEnabled = refManualReset<boolean>(useLocalStorage<boolean>('settings/live2d/idle-animation-enabled', true))
const live2dAutoBlinkEnabled = refManualReset<boolean>(useLocalStorage<boolean>('settings/live2d/auto-blink-enabled', true))
const live2dForceAutoBlinkEnabled = refManualReset<boolean>(useLocalStorage<boolean>('settings/live2d/force-auto-blink-enabled', false))
const live2dShadowEnabled = refManualReset<boolean>(useLocalStorage<boolean>('settings/live2d/shadow-enabled', true))
const live2dDisableFocus = useLocalStorageManualReset<boolean>('settings/live2d/disable-focus', false)
const live2dIdleAnimationEnabled = useLocalStorageManualReset<boolean>('settings/live2d/idle-animation-enabled', true)
const live2dAutoBlinkEnabled = useLocalStorageManualReset<boolean>('settings/live2d/auto-blink-enabled', true)
const live2dForceAutoBlinkEnabled = useLocalStorageManualReset<boolean>('settings/live2d/force-auto-blink-enabled', false)
const live2dShadowEnabled = useLocalStorageManualReset<boolean>('settings/live2d/shadow-enabled', true)
const disableTransitions = refManualReset<boolean>(useLocalStorage<boolean>('settings/disable-transitions', true))
const usePageSpecificTransitions = refManualReset<boolean>(useLocalStorage<boolean>('settings/use-page-specific-transitions', true))
const disableTransitions = useLocalStorageManualReset<boolean>('settings/disable-transitions', true)
const usePageSpecificTransitions = useLocalStorageManualReset<boolean>('settings/use-page-specific-transitions', true)
const themeColorsHue = refManualReset<number>(useLocalStorage<number>('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE))
const themeColorsHueDynamic = refManualReset<boolean>(useLocalStorage<boolean>('settings/theme/colors/hue-dynamic', false))
const themeColorsHue = useLocalStorageManualReset<number>('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE)
const themeColorsHueDynamic = useLocalStorageManualReset<boolean>('settings/theme/colors/hue-dynamic', false)
const allowVisibleOnAllWorkspaces = refManualReset<boolean>(useLocalStorage<boolean>('settings/allow-visible-on-all-workspaces', true))
const allowVisibleOnAllWorkspaces = useLocalStorageManualReset<boolean>('settings/allow-visible-on-all-workspaces', true)
const controlsIslandIconSize = refManualReset<'auto' | 'large' | 'small'>(useLocalStorage<'auto' | 'large' | 'small'>('settings/controls-island/icon-size', 'auto'))
const controlsIslandIconSize = useLocalStorageManualReset<'auto' | 'large' | 'small'>('settings/controls-island/icon-size', 'auto')
function getLanguage() {
let language = localStorage.getItem('settings/language')
@@ -228,8 +229,8 @@ export const useSettings = defineStore('settings', () => {
export const useSettingsAudioDevice = defineStore('settings-audio-devices', () => {
const { audioInputs, deviceConstraints, selectedAudioInput: selectedAudioInputNonPersist, startStream, stopStream, stream, askPermission } = useAudioDevice()
const selectedAudioInputPersist = refManualReset<string>(useLocalStorage('settings/audio/input', selectedAudioInputNonPersist.value))
const selectedAudioInputEnabledPersist = refManualReset<boolean>(useLocalStorage('settings/audio/input/enabled', false))
const selectedAudioInputPersist = useLocalStorageManualReset('settings/audio/input', selectedAudioInputNonPersist.value)
const selectedAudioInputEnabledPersist = useLocalStorageManualReset('settings/audio/input/enabled', false)
watch(selectedAudioInputPersist, (newValue) => {
selectedAudioInputNonPersist.value = newValue