refactor(stage-ui): settings store simplified (#965)

This commit is contained in:
leafyy
2026-01-19 16:50:48 +08:00
committed by GitHub
parent 372f356964
commit 8661714b8a
9 changed files with 406 additions and 280 deletions
+1
View File
@@ -29,6 +29,7 @@
"./stores/providers/aliyun": "./src/stores/providers/aliyun/index.ts",
"./stores/analytics": "./src/stores/analytics/index.ts",
"./stores/character": "./src/stores/character/index.ts",
"./stores/settings": "./src/stores/settings/index.ts",
"./stores/*": "./src/stores/*.ts",
"./stores": "./src/stores/index.ts",
"./workers/vad": "./src/workers/vad/index.ts",
-280
View File
@@ -1,280 +0,0 @@
import type { DisplayModel } from './display-models'
import messages from '@proj-airi/i18n/locales'
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'
import { useAudioDevice } from './audio'
import { DisplayModelFormat, useDisplayModelsStore } from './display-models'
const languageRemap: Record<string, string> = {
'zh-CN': 'zh-Hans',
'zh-TW': 'zh-Hans', // TODO: remove this when zh-Hant is supported
'zh-HK': 'zh-Hans', // TODO: remove this when zh-Hant is supported
'zh-Hant': 'zh-Hans', // TODO: remove this when zh-Hant is supported
'en-US': 'en',
'en-GB': 'en',
'en-AU': 'en',
'en': 'en',
'es-ES': 'es',
'es-MX': 'es',
'es-AR': 'es',
'es': 'es',
'ru': 'ru',
'ru-RU': 'ru',
'fr': 'fr',
'fr-FR': 'fr',
'ja': 'ja',
'ja-JP': 'ja',
}
export const DEFAULT_THEME_COLORS_HUE = 220.44
const convert = converter('oklch')
const getHueFrom = (color?: string) => color ? convert(color)?.h : DEFAULT_THEME_COLORS_HUE
export const useSettings = defineStore('settings', () => {
const displayModelsStore = useDisplayModelsStore()
const language = useLocalStorageManualReset<string>('settings/language', '')
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)
async function updateStageModel() {
if (!stageModelSelected.value) {
stageModelSelectedUrl.value = undefined
stageModelSelectedDisplayModel.value = undefined
stageModelRenderer.value = 'disabled'
return
}
const model = await displayModelsStore.getDisplayModel(stageModelSelected.value)
if (!model) {
stageModelSelectedUrl.value = undefined
stageModelSelectedDisplayModel.value = undefined
stageModelRenderer.value = 'disabled'
return
}
switch (model.format) {
case DisplayModelFormat.Live2dZip:
stageModelRenderer.value = 'live2d'
break
case DisplayModelFormat.VRM:
stageModelRenderer.value = 'vrm'
break
default:
stageModelRenderer.value = 'disabled'
break
}
if (model.type === 'file') {
if (stageModelSelectedUrl.value) {
URL.revokeObjectURL(stageModelSelectedUrl.value)
}
stageModelSelectedUrl.value = URL.createObjectURL(model.file)
}
else {
stageModelSelectedUrl.value = model.url
}
stageModelSelectedDisplayModel.value = model
}
async function initializeStageModel() {
await updateStageModel()
}
useEventListener('unload', () => {
if (stageModelSelectedUrl.value) {
URL.revokeObjectURL(stageModelSelectedUrl.value)
}
})
const stageViewControlsEnabled = refManualReset<boolean>(false)
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 = useLocalStorageManualReset<boolean>('settings/disable-transitions', true)
const usePageSpecificTransitions = useLocalStorageManualReset<boolean>('settings/use-page-specific-transitions', true)
const themeColorsHue = useLocalStorageManualReset<number>('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE)
const themeColorsHueDynamic = useLocalStorageManualReset<boolean>('settings/theme/colors/hue-dynamic', false)
const allowVisibleOnAllWorkspaces = useLocalStorageManualReset<boolean>('settings/allow-visible-on-all-workspaces', true)
const controlsIslandIconSize = useLocalStorageManualReset<'auto' | 'large' | 'small'>('settings/controls-island/icon-size', 'auto')
function getLanguage() {
let language = localStorage.getItem('settings/language')
if (!language) {
// Fallback to browser language
language = navigator.language || 'en'
}
const languages = Object.keys(messages!)
if (languageRemap[language || 'en'] != null) {
language = languageRemap[language || 'en']
}
if (language && languages.includes(language))
return language
return 'en'
}
function setThemeColorsHue(hue = DEFAULT_THEME_COLORS_HUE) {
themeColorsHue.value = hue
themeColorsHueDynamic.value = false
}
function applyPrimaryColorFrom(color?: string) {
setThemeColorsHue(getHueFrom(color))
}
/**
* Check if a color is currently selected based on its hue value
* @param hexColor Hex color code to check
* @returns True if the color's hue matches the current theme hue
*/
function isColorSelectedForPrimary(hexColor?: string) {
// If dynamic coloring is enabled, no preset color is manually selected
if (themeColorsHueDynamic.value)
return false
// Convert hex color to OKLCH
const h = getHueFrom(hexColor)
if (!h)
return false
// Compare hue values with a small tolerance for floating point comparison
const hueDifference = Math.abs(h - themeColorsHue.value)
return hueDifference < 0.01 || hueDifference > 359.99
}
async function resetState() {
if (stageModelSelectedUrl.value)
URL.revokeObjectURL(stageModelSelectedUrl.value)
language.reset()
stageModelSelected.reset()
stageModelSelectedDisplayModel.reset()
stageModelSelectedUrl.reset()
stageModelRenderer.reset()
stageViewControlsEnabled.reset()
live2dDisableFocus.reset()
live2dIdleAnimationEnabled.reset()
live2dAutoBlinkEnabled.reset()
live2dForceAutoBlinkEnabled.reset()
live2dShadowEnabled.reset()
disableTransitions.reset()
usePageSpecificTransitions.reset()
themeColorsHue.reset()
themeColorsHueDynamic.reset()
allowVisibleOnAllWorkspaces.reset()
controlsIslandIconSize.reset()
await updateStageModel()
}
onMounted(() => language.value = getLanguage())
return {
disableTransitions,
usePageSpecificTransitions,
language,
stageModelRenderer,
stageModelSelected,
stageModelSelectedUrl,
stageModelSelectedDisplayModel,
stageViewControlsEnabled,
live2dDisableFocus,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dShadowEnabled,
themeColorsHue,
themeColorsHueDynamic,
allowVisibleOnAllWorkspaces,
controlsIslandIconSize,
setThemeColorsHue,
applyPrimaryColorFrom,
isColorSelectedForPrimary,
initializeStageModel,
updateStageModel,
resetState,
}
})
export const useSettingsAudioDevice = defineStore('settings-audio-devices', () => {
const { audioInputs, deviceConstraints, selectedAudioInput: selectedAudioInputNonPersist, startStream, stopStream, stream, askPermission } = useAudioDevice()
const selectedAudioInputPersist = useLocalStorageManualReset('settings/audio/input', selectedAudioInputNonPersist.value)
const selectedAudioInputEnabledPersist = useLocalStorageManualReset('settings/audio/input/enabled', false)
watch(selectedAudioInputPersist, (newValue) => {
selectedAudioInputNonPersist.value = newValue
})
watch(selectedAudioInputEnabledPersist, (val) => {
if (val) {
startStream()
}
else {
stopStream()
}
})
onMounted(() => {
const hasSelectedInput = selectedAudioInputPersist.value
&& audioInputs.value.some(device => device.deviceId === selectedAudioInputPersist.value)
if (selectedAudioInputEnabledPersist.value && hasSelectedInput) {
startStream()
}
if (selectedAudioInputNonPersist.value && !selectedAudioInputEnabledPersist.value) {
selectedAudioInputPersist.value = selectedAudioInputNonPersist.value
}
})
function resetState() {
selectedAudioInputPersist.reset()
selectedAudioInputNonPersist.value = ''
selectedAudioInputEnabledPersist.reset()
stopStream()
}
return {
audioInputs,
deviceConstraints,
selectedAudioInput: selectedAudioInputPersist,
enabled: selectedAudioInputEnabledPersist,
stream,
askPermission,
startStream,
stopStream,
resetState,
}
})
@@ -0,0 +1,58 @@
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
import { onMounted, watch } from 'vue'
import { useAudioDevice } from '../audio'
export const useSettingsAudioDevice = defineStore('settings-audio-devices', () => {
const { audioInputs, deviceConstraints, selectedAudioInput: selectedAudioInputNonPersist, startStream, stopStream, stream, askPermission } = useAudioDevice()
const selectedAudioInputPersist = useLocalStorageManualReset<string>('settings/audio/input', selectedAudioInputNonPersist.value)
const selectedAudioInputEnabledPersist = useLocalStorageManualReset<boolean>('settings/audio/input/enabled', false)
watch(selectedAudioInputPersist, (newValue) => {
selectedAudioInputNonPersist.value = newValue
})
watch(selectedAudioInputEnabledPersist, (val) => {
if (val) {
startStream()
}
else {
stopStream()
}
})
onMounted(() => {
const hasSelectedInput = selectedAudioInputPersist.value
&& audioInputs.value.some(device => device.deviceId === selectedAudioInputPersist.value)
if (selectedAudioInputEnabledPersist.value && hasSelectedInput) {
startStream()
}
if (selectedAudioInputNonPersist.value && !selectedAudioInputEnabledPersist.value) {
selectedAudioInputPersist.value = selectedAudioInputNonPersist.value
}
})
function resetState() {
selectedAudioInputPersist.reset()
selectedAudioInputNonPersist.value = ''
selectedAudioInputEnabledPersist.reset()
stopStream()
}
return {
audioInputs,
deviceConstraints,
selectedAudioInput: selectedAudioInputPersist,
enabled: selectedAudioInputEnabledPersist,
stream,
askPermission,
startStream,
stopStream,
resetState,
}
})
@@ -0,0 +1,18 @@
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
export const useSettingsControlsIsland = defineStore('settings-controls-island', () => {
const allowVisibleOnAllWorkspaces = useLocalStorageManualReset<boolean>('settings/allow-visible-on-all-workspaces', true)
const controlsIslandIconSize = useLocalStorageManualReset<'auto' | 'large' | 'small'>('settings/controls-island/icon-size', 'auto')
function resetState() {
allowVisibleOnAllWorkspaces.reset()
controlsIslandIconSize.reset()
}
return {
allowVisibleOnAllWorkspaces,
controlsIslandIconSize,
resetState,
}
})
@@ -0,0 +1,67 @@
import messages from '@proj-airi/i18n/locales'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
import { onMounted } from 'vue'
const languageRemap: Record<string, string> = {
'zh-CN': 'zh-Hans',
'zh-TW': 'zh-Hans', // TODO: remove this when zh-Hant is supported
'zh-HK': 'zh-Hans', // TODO: remove this when zh-Hant is supported
'zh-Hant': 'zh-Hans', // TODO: remove this when zh-Hant is supported
'en-US': 'en',
'en-GB': 'en',
'en-AU': 'en',
'en': 'en',
'es-ES': 'es',
'es-MX': 'es',
'es-AR': 'es',
'es': 'es',
'ru': 'ru',
'ru-RU': 'ru',
'fr': 'fr',
'fr-FR': 'fr',
'ja': 'ja',
'ja-JP': 'ja',
}
export const useSettingsGeneral = defineStore('settings-general', () => {
const language = useLocalStorageManualReset<string>('settings/language', '')
const disableTransitions = useLocalStorageManualReset<boolean>('settings/disable-transitions', true)
const usePageSpecificTransitions = useLocalStorageManualReset<boolean>('settings/use-page-specific-transitions', true)
function getLanguage() {
let language = localStorage.getItem('settings/language')
if (!language) {
// Fallback to browser language
language = navigator.language || 'en'
}
const languages = Object.keys(messages!)
if (languageRemap[language || 'en'] != null) {
language = languageRemap[language || 'en']
}
if (language && languages.includes(language))
return language
return 'en'
}
function resetState() {
language.reset()
disableTransitions.reset()
usePageSpecificTransitions.reset()
}
onMounted(() => language.value = getLanguage())
return {
language,
disableTransitions,
usePageSpecificTransitions,
getLanguage,
resetState,
}
})
@@ -0,0 +1,84 @@
import { defineStore, storeToRefs } from 'pinia'
import { useSettingsControlsIsland } from './controls-island'
import { useSettingsGeneral } from './general'
import { useSettingsLive2d } from './live2d'
import { useSettingsStageModel } from './stage-model'
import { useSettingsTheme } from './theme'
// Export sub-stores
export * from './audio-device'
export * from './controls-island'
export * from './general'
export * from './live2d'
export * from './stage-model'
export * from './theme'
// Export constants
export { DEFAULT_THEME_COLORS_HUE } from './theme'
/**
* Unified settings store for backward compatibility.
* This aggregates all sub-stores into one interface.
*
* @deprecated Use individual setting stores (useSettingsCore, useSettingsTheme, etc.) instead.
* This store exists only for backward compatibility and will be removed in a future version.
*/
export const useSettings = defineStore('settings', () => {
const general = useSettingsGeneral()
const stageModel = useSettingsStageModel()
const live2d = useSettingsLive2d()
const theme = useSettingsTheme()
const controlsIsland = useSettingsControlsIsland()
async function resetState() {
await stageModel.resetState()
general.resetState()
live2d.resetState()
theme.resetState()
controlsIsland.resetState()
}
// Extract refs from sub-stores to maintain proper reactivity
const generalRefs = storeToRefs(general)
const stageModelRefs = storeToRefs(stageModel)
const live2dRefs = storeToRefs(live2d)
const themeRefs = storeToRefs(theme)
const controlsIslandRefs = storeToRefs(controlsIsland)
return {
// Core settings
disableTransitions: generalRefs.disableTransitions,
usePageSpecificTransitions: generalRefs.usePageSpecificTransitions,
language: generalRefs.language,
// Stage model settings
stageModelRenderer: stageModelRefs.stageModelRenderer,
stageModelSelected: stageModelRefs.stageModelSelected,
stageModelSelectedUrl: stageModelRefs.stageModelSelectedUrl,
stageModelSelectedDisplayModel: stageModelRefs.stageModelSelectedDisplayModel,
stageViewControlsEnabled: stageModelRefs.stageViewControlsEnabled,
// Live2D settings
live2dDisableFocus: live2dRefs.live2dDisableFocus,
live2dIdleAnimationEnabled: live2dRefs.live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled: live2dRefs.live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled: live2dRefs.live2dForceAutoBlinkEnabled,
live2dShadowEnabled: live2dRefs.live2dShadowEnabled,
// Theme settings
themeColorsHue: themeRefs.themeColorsHue,
themeColorsHueDynamic: themeRefs.themeColorsHueDynamic,
// UI settings
allowVisibleOnAllWorkspaces: controlsIslandRefs.allowVisibleOnAllWorkspaces,
controlsIslandIconSize: controlsIslandRefs.controlsIslandIconSize,
// Methods
setThemeColorsHue: theme.setThemeColorsHue,
applyPrimaryColorFrom: theme.applyPrimaryColorFrom,
isColorSelectedForPrimary: theme.isColorSelectedForPrimary,
initializeStageModel: stageModel.initializeStageModel,
updateStageModel: stageModel.updateStageModel,
resetState,
}
})
@@ -0,0 +1,27 @@
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
export const useSettingsLive2d = defineStore('settings-live2d', () => {
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)
function resetState() {
live2dDisableFocus.reset()
live2dIdleAnimationEnabled.reset()
live2dAutoBlinkEnabled.reset()
live2dForceAutoBlinkEnabled.reset()
live2dShadowEnabled.reset()
}
return {
live2dDisableFocus,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dShadowEnabled,
resetState,
}
})
@@ -0,0 +1,95 @@
import type { DisplayModel } from '../display-models'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { refManualReset, useEventListener } from '@vueuse/core'
import { defineStore } from 'pinia'
import { DisplayModelFormat, useDisplayModelsStore } from '../display-models'
export const useSettingsStageModel = defineStore('settings-stage-model', () => {
const displayModelsStore = useDisplayModelsStore()
const stageModelSelected = useLocalStorageManualReset<string>('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)
const stageViewControlsEnabled = refManualReset<boolean>(false)
async function updateStageModel() {
if (!stageModelSelected.value) {
stageModelSelectedUrl.value = undefined
stageModelSelectedDisplayModel.value = undefined
stageModelRenderer.value = 'disabled'
return
}
const model = await displayModelsStore.getDisplayModel(stageModelSelected.value)
if (!model) {
stageModelSelectedUrl.value = undefined
stageModelSelectedDisplayModel.value = undefined
stageModelRenderer.value = 'disabled'
return
}
switch (model.format) {
case DisplayModelFormat.Live2dZip:
stageModelRenderer.value = 'live2d'
break
case DisplayModelFormat.VRM:
stageModelRenderer.value = 'vrm'
break
default:
stageModelRenderer.value = 'disabled'
break
}
if (model.type === 'file') {
if (stageModelSelectedUrl.value) {
URL.revokeObjectURL(stageModelSelectedUrl.value)
}
stageModelSelectedUrl.value = URL.createObjectURL(model.file)
}
else {
stageModelSelectedUrl.value = model.url
}
stageModelSelectedDisplayModel.value = model
}
async function initializeStageModel() {
await updateStageModel()
}
useEventListener('unload', () => {
if (stageModelSelectedUrl.value) {
URL.revokeObjectURL(stageModelSelectedUrl.value)
}
})
async function resetState() {
if (stageModelSelectedUrl.value)
URL.revokeObjectURL(stageModelSelectedUrl.value)
stageModelSelected.reset()
stageModelSelectedDisplayModel.reset()
stageModelSelectedUrl.reset()
stageModelRenderer.reset()
stageViewControlsEnabled.reset()
await updateStageModel()
}
return {
stageModelRenderer,
stageModelSelected,
stageModelSelectedUrl,
stageModelSelectedDisplayModel,
stageViewControlsEnabled,
initializeStageModel,
updateStageModel,
resetState,
}
})
@@ -0,0 +1,56 @@
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { converter } from 'culori'
import { defineStore } from 'pinia'
export const DEFAULT_THEME_COLORS_HUE = 220.44
const convert = converter('oklch')
const getHueFrom = (color?: string) => color ? convert(color)?.h : DEFAULT_THEME_COLORS_HUE
export const useSettingsTheme = defineStore('settings-theme', () => {
const themeColorsHue = useLocalStorageManualReset<number>('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE)
const themeColorsHueDynamic = useLocalStorageManualReset<boolean>('settings/theme/colors/hue-dynamic', false)
function setThemeColorsHue(hue = DEFAULT_THEME_COLORS_HUE) {
themeColorsHue.value = hue
themeColorsHueDynamic.value = false
}
function applyPrimaryColorFrom(color?: string) {
setThemeColorsHue(getHueFrom(color))
}
/**
* Check if a color is currently selected based on its hue value
* @param hexColor Hex color code to check
* @returns True if the color's hue matches the current theme hue
*/
function isColorSelectedForPrimary(hexColor?: string) {
// If dynamic coloring is enabled, no preset color is manually selected
if (themeColorsHueDynamic.value)
return false
// Convert hex color to OKLCH
const h = getHueFrom(hexColor)
if (!h)
return false
// Compare hue values with a small tolerance for floating point comparison
const hueDifference = Math.abs(h - themeColorsHue.value)
return hueDifference < 0.01 || hueDifference > 359.99
}
function resetState() {
themeColorsHue.reset()
themeColorsHueDynamic.reset()
}
return {
themeColorsHue,
themeColorsHueDynamic,
setThemeColorsHue,
applyPrimaryColorFrom,
isColorSelectedForPrimary,
resetState,
}
})