refactor(stage-ui): settings store simplified (#965)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user