From 8661714b8af7446844e13ffddb0ca1d57724092b Mon Sep 17 00:00:00 2001 From: leafyy Date: Mon, 19 Jan 2026 16:50:48 +0800 Subject: [PATCH] refactor(stage-ui): settings store simplified (#965) --- packages/stage-ui/package.json | 1 + packages/stage-ui/src/stores/settings.ts | 280 ------------------ .../src/stores/settings/audio-device.ts | 58 ++++ .../src/stores/settings/controls-island.ts | 18 ++ .../stage-ui/src/stores/settings/general.ts | 67 +++++ .../stage-ui/src/stores/settings/index.ts | 84 ++++++ .../stage-ui/src/stores/settings/live2d.ts | 27 ++ .../src/stores/settings/stage-model.ts | 95 ++++++ .../stage-ui/src/stores/settings/theme.ts | 56 ++++ 9 files changed, 406 insertions(+), 280 deletions(-) delete mode 100644 packages/stage-ui/src/stores/settings.ts create mode 100644 packages/stage-ui/src/stores/settings/audio-device.ts create mode 100644 packages/stage-ui/src/stores/settings/controls-island.ts create mode 100644 packages/stage-ui/src/stores/settings/general.ts create mode 100644 packages/stage-ui/src/stores/settings/index.ts create mode 100644 packages/stage-ui/src/stores/settings/live2d.ts create mode 100644 packages/stage-ui/src/stores/settings/stage-model.ts create mode 100644 packages/stage-ui/src/stores/settings/theme.ts diff --git a/packages/stage-ui/package.json b/packages/stage-ui/package.json index 6861a44d3..85e79922d 100644 --- a/packages/stage-ui/package.json +++ b/packages/stage-ui/package.json @@ -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", diff --git a/packages/stage-ui/src/stores/settings.ts b/packages/stage-ui/src/stores/settings.ts deleted file mode 100644 index baa134e71..000000000 --- a/packages/stage-ui/src/stores/settings.ts +++ /dev/null @@ -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 = { - '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('settings/language', '') - - const stageModelSelected = useLocalStorageManualReset('settings/stage/model', 'preset-live2d-1') - const stageModelSelectedDisplayModel = refManualReset(undefined) - const stageModelSelectedUrl = refManualReset(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(false) - - const live2dDisableFocus = useLocalStorageManualReset('settings/live2d/disable-focus', false) - const live2dIdleAnimationEnabled = useLocalStorageManualReset('settings/live2d/idle-animation-enabled', true) - const live2dAutoBlinkEnabled = useLocalStorageManualReset('settings/live2d/auto-blink-enabled', true) - const live2dForceAutoBlinkEnabled = useLocalStorageManualReset('settings/live2d/force-auto-blink-enabled', false) - const live2dShadowEnabled = useLocalStorageManualReset('settings/live2d/shadow-enabled', true) - - const disableTransitions = useLocalStorageManualReset('settings/disable-transitions', true) - const usePageSpecificTransitions = useLocalStorageManualReset('settings/use-page-specific-transitions', true) - - const themeColorsHue = useLocalStorageManualReset('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE) - const themeColorsHueDynamic = useLocalStorageManualReset('settings/theme/colors/hue-dynamic', false) - - const allowVisibleOnAllWorkspaces = useLocalStorageManualReset('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, - } -}) diff --git a/packages/stage-ui/src/stores/settings/audio-device.ts b/packages/stage-ui/src/stores/settings/audio-device.ts new file mode 100644 index 000000000..19a931c59 --- /dev/null +++ b/packages/stage-ui/src/stores/settings/audio-device.ts @@ -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('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, + } +}) diff --git a/packages/stage-ui/src/stores/settings/controls-island.ts b/packages/stage-ui/src/stores/settings/controls-island.ts new file mode 100644 index 000000000..d009401ff --- /dev/null +++ b/packages/stage-ui/src/stores/settings/controls-island.ts @@ -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('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, + } +}) diff --git a/packages/stage-ui/src/stores/settings/general.ts b/packages/stage-ui/src/stores/settings/general.ts new file mode 100644 index 000000000..51cfee83b --- /dev/null +++ b/packages/stage-ui/src/stores/settings/general.ts @@ -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 = { + '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('settings/language', '') + + const disableTransitions = useLocalStorageManualReset('settings/disable-transitions', true) + const usePageSpecificTransitions = useLocalStorageManualReset('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, + } +}) diff --git a/packages/stage-ui/src/stores/settings/index.ts b/packages/stage-ui/src/stores/settings/index.ts new file mode 100644 index 000000000..783d575b9 --- /dev/null +++ b/packages/stage-ui/src/stores/settings/index.ts @@ -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, + } +}) diff --git a/packages/stage-ui/src/stores/settings/live2d.ts b/packages/stage-ui/src/stores/settings/live2d.ts new file mode 100644 index 000000000..e386c073b --- /dev/null +++ b/packages/stage-ui/src/stores/settings/live2d.ts @@ -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('settings/live2d/disable-focus', false) + const live2dIdleAnimationEnabled = useLocalStorageManualReset('settings/live2d/idle-animation-enabled', true) + const live2dAutoBlinkEnabled = useLocalStorageManualReset('settings/live2d/auto-blink-enabled', true) + const live2dForceAutoBlinkEnabled = useLocalStorageManualReset('settings/live2d/force-auto-blink-enabled', false) + const live2dShadowEnabled = useLocalStorageManualReset('settings/live2d/shadow-enabled', true) + + function resetState() { + live2dDisableFocus.reset() + live2dIdleAnimationEnabled.reset() + live2dAutoBlinkEnabled.reset() + live2dForceAutoBlinkEnabled.reset() + live2dShadowEnabled.reset() + } + + return { + live2dDisableFocus, + live2dIdleAnimationEnabled, + live2dAutoBlinkEnabled, + live2dForceAutoBlinkEnabled, + live2dShadowEnabled, + resetState, + } +}) diff --git a/packages/stage-ui/src/stores/settings/stage-model.ts b/packages/stage-ui/src/stores/settings/stage-model.ts new file mode 100644 index 000000000..6ff1b1fa4 --- /dev/null +++ b/packages/stage-ui/src/stores/settings/stage-model.ts @@ -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('settings/stage/model', 'preset-live2d-1') + const stageModelSelectedDisplayModel = refManualReset(undefined) + const stageModelSelectedUrl = refManualReset(undefined) + const stageModelRenderer = refManualReset<'live2d' | 'vrm' | 'disabled' | undefined>(undefined) + + const stageViewControlsEnabled = refManualReset(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, + } +}) diff --git a/packages/stage-ui/src/stores/settings/theme.ts b/packages/stage-ui/src/stores/settings/theme.ts new file mode 100644 index 000000000..b05ce4202 --- /dev/null +++ b/packages/stage-ui/src/stores/settings/theme.ts @@ -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('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE) + const themeColorsHueDynamic = useLocalStorageManualReset('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, + } +})