fix(stage-ui): extract colors (#87)
This commit is contained in:
@@ -58,6 +58,8 @@ settings:
|
|||||||
from-url-confirm: Load
|
from-url-confirm: Load
|
||||||
from-url-placeholder: Enter Live2D model URL
|
from-url-placeholder: Enter Live2D model URL
|
||||||
title: Change Model
|
title: Change Model
|
||||||
|
edit-motion-map:
|
||||||
|
title: Edit motion map
|
||||||
map-motions:
|
map-motions:
|
||||||
play: Play Motion
|
play: Play Motion
|
||||||
title: Map Motions
|
title: Map Motions
|
||||||
@@ -273,10 +275,10 @@ settings:
|
|||||||
description: Traditional Japanese color palette
|
description: Traditional Japanese color palette
|
||||||
title: Japanese Colors
|
title: Japanese Colors
|
||||||
monet:
|
monet:
|
||||||
description: Impressionist palette inspired by Claude Monet\'s works
|
description: Impressionist palette inspired by Claude Monet's works
|
||||||
title: Monet Colors
|
title: Monet Colors
|
||||||
morandi:
|
morandi:
|
||||||
description: Soft, muted tones inspired by Giorgio Morandi\'s paintings
|
description: Soft, muted tones inspired by Giorgio Morandi's paintings
|
||||||
title: Morandi Colors
|
title: Morandi Colors
|
||||||
nordic:
|
nordic:
|
||||||
description: Scandinavian minimalist color scheme
|
description: Scandinavian minimalist color scheme
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DEFAULT_THEME_COLORS_HUE, useSettings } from '@proj-airi/stage-ui/stores'
|
||||||
|
import { TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger } from 'radix-vue'
|
||||||
|
|
||||||
|
interface Color {
|
||||||
|
hex?: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
colors: Color[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const settings = useSettings()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="colors.length" flex gap-2>
|
||||||
|
<TooltipProvider v-for="{ name, hex } in colors" :key="hex || 'default'">
|
||||||
|
<TooltipRoot>
|
||||||
|
<TooltipTrigger
|
||||||
|
transition="all ease-in-out duration-250"
|
||||||
|
bg-primary-500 size-6 cursor-pointer rounded-full
|
||||||
|
:style="hex ? { background: hex } : { '--theme-colors-hue': DEFAULT_THEME_COLORS_HUE }"
|
||||||
|
:class="settings.isColorSelectedForPrimary(hex) ? 'scale-150 mx-1' : 'hover:scale-110'"
|
||||||
|
@click="settings.applyPrimaryColorFrom(hex)"
|
||||||
|
/>
|
||||||
|
<TooltipPortal>
|
||||||
|
<TooltipContent bg="white dark:neutral-800" rounded-lg px-3 py-1.5 text-sm shadow-md>
|
||||||
|
{{ name }}
|
||||||
|
<TooltipArrow class="fill-white dark:fill-neutral-800" />
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</TooltipRoot>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'click'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const settings = useSettings()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:disabled="settings.loadingLive2dModel"
|
||||||
|
bg="zinc-100 dark:zinc-800"
|
||||||
|
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||||
|
transition="all ease-in-out duration-250"
|
||||||
|
rounded
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
icon: string
|
||||||
|
innerClass?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Collapsable default>
|
||||||
|
<template #trigger="slotProps">
|
||||||
|
<button
|
||||||
|
bg="zinc-100 dark:zinc-800"
|
||||||
|
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||||
|
transition="all ease-in-out duration-250"
|
||||||
|
w-full flex items-center justify-between rounded-lg px-4 py-3 outline-none
|
||||||
|
@click="slotProps.setVisible(!slotProps.visible)"
|
||||||
|
>
|
||||||
|
<div flex gap-1.5>
|
||||||
|
<div :class="icon" size-6 />
|
||||||
|
{{ t(title) }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
i-solar:alt-arrow-down-bold-duotone
|
||||||
|
transition="transform duration-250"
|
||||||
|
:class="{ 'rotate-180': slotProps.visible }"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<div grid gap-4 p-4 :class="innerClass">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</Collapsable>
|
||||||
|
</template>
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
|
||||||
import { Emotion, EmotionNeutralMotionName } from '@proj-airi/stage-ui/constants'
|
import { Emotion, EmotionNeutralMotionName } from '@proj-airi/stage-ui/constants'
|
||||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||||
import { useFileDialog, useObjectUrl } from '@vueuse/core'
|
import { useFileDialog, useObjectUrl } from '@vueuse/core'
|
||||||
import { converter } from 'culori'
|
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import ColorPalette from '../Settings/ColorPalette.vue'
|
||||||
|
import Button from '../Settings/Live2DModelControlButton.vue'
|
||||||
|
import Section from '../Settings/Section.vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
palette: string[]
|
palette: string[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
defineEmits<{
|
||||||
(e: 'extractColorsFromModel'): void
|
(e: 'extractColorsFromModel'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ const modelFile = useFileDialog({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const { live2dModelFile, live2dMotionMap, live2dLoadSource, loadingLive2dModel, availableLive2dMotions, live2dModelUrl, themeColorsHue } = storeToRefs(settings)
|
const { live2dModelFile, live2dMotionMap, live2dLoadSource, loadingLive2dModel, availableLive2dMotions, live2dModelUrl } = storeToRefs(settings)
|
||||||
const localModelUrl = ref(live2dModelUrl.value)
|
const localModelUrl = ref(live2dModelUrl.value)
|
||||||
|
|
||||||
modelFile.onChange((files) => {
|
modelFile.onChange((files) => {
|
||||||
@@ -103,132 +105,39 @@ async function saveMotionMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const exportObjectUrl = useObjectUrl(live2dModelFile)
|
const exportObjectUrl = useObjectUrl(live2dModelFile)
|
||||||
|
|
||||||
function handleHueClick(color: string) {
|
|
||||||
themeColorsHue.value = converter('oklch')(color)?.h ?? 0
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div flex="~ col gap-4">
|
||||||
<Collapsable w-full :default="true">
|
<Section title="settings.live2d.change-model.title" icon="i-solar:magic-stick-3-bold-duotone" inner-class="text-sm">
|
||||||
<template #trigger="slotProps">
|
<div flex items-center gap-2>
|
||||||
<button
|
|
||||||
bg="zinc-100 dark:zinc-800"
|
|
||||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
|
||||||
transition="all ease-in-out duration-250"
|
|
||||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
|
||||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
|
||||||
@click="slotProps.setVisible(!slotProps.visible)"
|
|
||||||
>
|
|
||||||
<div flex="~ row 1" items-center gap-1.5>
|
|
||||||
<div
|
|
||||||
i-solar:magic-stick-3-bold-duotone class="provider-icon size-6"
|
|
||||||
transition="filter duration-250 ease-in-out"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{{ t('settings.live2d.change-model.title') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
|
||||||
<div i-solar:alt-arrow-down-bold-duotone />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<div p-4>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-1 text-sm font-medium">
|
|
||||||
{{ t('settings.live2d.change-model.from-url') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
<input
|
||||||
v-model="localModelUrl"
|
v-model="localModelUrl"
|
||||||
:disabled="settings.loadingLive2dModel"
|
:disabled="settings.loadingLive2dModel"
|
||||||
type="text"
|
class="form-control flex-1"
|
||||||
rounded
|
|
||||||
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
|
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
|
||||||
transition="border duration-250 ease-in-out"
|
transition="border duration-250 ease-in-out"
|
||||||
px-2 py-1 text-sm outline-none
|
|
||||||
:placeholder="t('settings.live2d.change-model.from-url-placeholder')"
|
:placeholder="t('settings.live2d.change-model.from-url-placeholder')"
|
||||||
>
|
>
|
||||||
<button
|
<Button class="form-control" @click="live2dModelUrl = localModelUrl">
|
||||||
:disabled="settings.loadingLive2dModel"
|
{{ t('settings.live2d.change-model.from-url') }}
|
||||||
bg="zinc-100 dark:zinc-800"
|
</Button>
|
||||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
|
||||||
transition="all ease-in-out duration-250"
|
|
||||||
ml-2 rounded px-2 py-1 text-sm outline-none
|
|
||||||
@click="live2dModelUrl = localModelUrl"
|
|
||||||
>
|
|
||||||
{{ t('settings.live2d.change-model.from-url-confirm') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Button class="form-control place-self-end" @click="modelFile.open()">
|
||||||
<div class="flex items-center justify-between">
|
{{ t('settings.live2d.change-model.from-file') }}...
|
||||||
<div>
|
</Button>
|
||||||
<div class="flex items-center gap-1 text-sm font-medium">
|
<Button class="form-control" @click="$emit('extractColorsFromModel')">
|
||||||
{{ t('settings.live2d.change-model.from-file') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
:disabled="settings.loadingLive2dModel"
|
|
||||||
rounded
|
|
||||||
bg="zinc-100 dark:zinc-800"
|
|
||||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
|
||||||
transition="all ease-in-out duration-250"
|
|
||||||
px-2 py-1 text-sm outline-none
|
|
||||||
@click="modelFile.open()"
|
|
||||||
>
|
|
||||||
{{ t('settings.live2d.change-model.from-file-select') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
bg="zinc-100 dark:zinc-800" hover="bg-zinc-200 dark:bg-zinc-700" w-full
|
|
||||||
transition="all ease-in-out duration-250"
|
|
||||||
rounded px-2 py-1 text-sm outline-none @click="emit('extractColorsFromModel')"
|
|
||||||
>
|
|
||||||
Extract colors from model
|
Extract colors from model
|
||||||
</button>
|
</Button>
|
||||||
<div v-if="palette" flex gap-2>
|
<ColorPalette :colors="palette.map(hex => ({ hex, name: hex }))" />
|
||||||
<div v-for="color in palette" :key="color" class="flex items-center gap-1" @click="handleHueClick(color)">
|
</Section>
|
||||||
<div size-6 rounded-full :style="{ backgroundColor: color }" />
|
<Section
|
||||||
</div>
|
v-if="settings.live2dLoadSource === 'file'"
|
||||||
</div>
|
title="settings.live2d.edit-motion-map.title"
|
||||||
</div>
|
icon="i-solar:face-scan-circle-bold-duotone"
|
||||||
</div>
|
|
||||||
</Collapsable>
|
|
||||||
<Collapsable mt-4 w-full :default="true">
|
|
||||||
<template #trigger="slotProps">
|
|
||||||
<button
|
|
||||||
bg="zinc-100 dark:zinc-800"
|
|
||||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
|
||||||
transition="all ease-in-out duration-250"
|
|
||||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
|
||||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
|
||||||
@click="slotProps.setVisible(!slotProps.visible)"
|
|
||||||
>
|
>
|
||||||
<div flex="~ row 1" items-center gap-1.5>
|
<div v-for="motion in settings.availableLive2dMotions" :key="motion.fileName" flex items-center justify-between text-sm>
|
||||||
<div
|
<span font-medium>{{ motion.fileName }}</span>
|
||||||
i-solar:face-scan-circle-bold-duotone class="provider-icon size-6"
|
|
||||||
transition="filter duration-250 ease-in-out"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
Edit motion map
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
|
||||||
<div i-solar:alt-arrow-down-bold-duotone />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<div p-4>
|
|
||||||
<div v-if="settings.live2dLoadSource === 'file'" class="space-y-4">
|
|
||||||
<div v-for="motion in settings.availableLive2dMotions" :key="motion.fileName" class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-1 text-sm font-medium">
|
|
||||||
{{ motion.fileName }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div flex gap-2>
|
<div flex gap-2>
|
||||||
<select v-model="settings.live2dMotionMap[motion.fileName]">
|
<select v-model="settings.live2dMotionMap[motion.fileName]">
|
||||||
@@ -237,48 +146,29 @@ function handleHueClick(color: string) {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
:disabled="settings.loadingLive2dModel"
|
class="form-control"
|
||||||
rounded
|
|
||||||
bg="zinc-100 dark:zinc-800"
|
|
||||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
|
||||||
transition="all ease-in-out duration-250"
|
|
||||||
px-2 py-1 text-sm outline-none
|
|
||||||
@click="settings.live2dCurrentMotion = { group: motion.motionName, index: motion.motionIndex }"
|
@click="settings.live2dCurrentMotion = { group: motion.motionName, index: motion.motionIndex }"
|
||||||
>
|
>
|
||||||
Play
|
Play
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button @click="saveMotionMap">
|
||||||
:disabled="settings.loadingLive2dModel"
|
|
||||||
w-full rounded
|
|
||||||
bg="zinc-100 dark:zinc-800"
|
|
||||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
|
||||||
transition="all ease-in-out duration-250"
|
|
||||||
@click="saveMotionMap"
|
|
||||||
>
|
|
||||||
Save and patch
|
Save and patch
|
||||||
</button>
|
</Button>
|
||||||
<a
|
<a
|
||||||
mt-2 block :href="exportObjectUrl"
|
mt-2 block :href="exportObjectUrl"
|
||||||
:download="`${settings.live2dModelFile?.name}-motion-edited.zip`"
|
:download="`${settings.live2dModelFile?.name || 'live2d'}-motion-edited.zip`"
|
||||||
>
|
>
|
||||||
<button
|
<Button w-full>Export</button>
|
||||||
:disabled="settings.loadingLive2dModel"
|
|
||||||
w-full rounded
|
|
||||||
bg="zinc-100 dark:zinc-800"
|
|
||||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
|
||||||
transition="all ease-in-out duration-250"
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</button>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</Section>
|
||||||
<div v-else>
|
|
||||||
Not available for URL model
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Collapsable>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-control {
|
||||||
|
--at-apply: rounded px-2 py-1 outline-none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function handleSettingsOpen(open: boolean) {
|
|||||||
? 'oklch(35% calc(var(--theme-colors-chroma) * 0.6) var(--theme-colors-hue))'
|
? 'oklch(35% calc(var(--theme-colors-chroma) * 0.6) var(--theme-colors-hue))'
|
||||||
: 'color-mix(in srgb, oklch(95% calc(var(--theme-colors-chroma-50) * 0.5) var(--theme-colors-hue)) 80%, oklch(100% 0 360))'"
|
: 'color-mix(in srgb, oklch(95% calc(var(--theme-colors-chroma-50) * 0.5) var(--theme-colors-hue)) 80%, oklch(100% 0 360))'"
|
||||||
>
|
>
|
||||||
<div relative flex="~ col" z-2 h-100vh w-100vw of-hidden>
|
<div relative flex="~ col" z-2 h-100dvh w-100vw of-hidden>
|
||||||
<!-- header -->
|
<!-- header -->
|
||||||
<div>
|
<div>
|
||||||
<Header class="flex" p2 />
|
<Header class="flex" p2 />
|
||||||
|
|||||||
@@ -28,13 +28,16 @@ async function extractColorsFromModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const frameUrl = URL.createObjectURL(frame)
|
const frameUrl = URL.createObjectURL(frame)
|
||||||
|
try {
|
||||||
const vibrant = new Vibrant(frameUrl)
|
const vibrant = new Vibrant(frameUrl)
|
||||||
|
|
||||||
const paletteFromVibrant = await vibrant.getPalette()
|
const paletteFromVibrant = await vibrant.getPalette()
|
||||||
palette.value = Object.values(paletteFromVibrant).map(color => color?.hex).filter(it => typeof it === 'string')
|
palette.value = Object.values(paletteFromVibrant).map(color => color?.hex).filter(it => typeof it === 'string')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
URL.revokeObjectURL(frameUrl)
|
URL.revokeObjectURL(frameUrl)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -46,14 +49,12 @@ async function extractColorsFromModel() {
|
|||||||
:leave="{ opacity: 0, x: -10 }"
|
:leave="{ opacity: 0, x: -10 }"
|
||||||
:duration="250"
|
:duration="250"
|
||||||
>
|
>
|
||||||
<button @click="router.back()">
|
<button i-solar:alt-arrow-left-line-duotone text-2xl @click="router.back()" />
|
||||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
<h1 relative text-nowrap>
|
||||||
</button>
|
<div absolute left-0 top-0 translate-y="[-80%]" text="neutral-300 dark:neutral-500">
|
||||||
<h1 relative>
|
{{ t('settings.title') }}
|
||||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
|
||||||
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div text-nowrap text-3xl font-semibold>
|
<div text-3xl font-semibold>
|
||||||
{{ t('settings.pages.models.title') }}
|
{{ t('settings.pages.models.title') }}
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { useDevicesList, useLocalStorage } from '@vueuse/core'
|
import { useDevicesList, useLocalStorage } from '@vueuse/core'
|
||||||
|
import { converter } from 'culori'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
export const DEFAULT_THEME_COLORS_HUE = 178.17
|
export const DEFAULT_THEME_COLORS_HUE = 178.17
|
||||||
|
|
||||||
|
const convert = converter('oklch')
|
||||||
|
const getHueFrom = (color?: string) => color ? convert(color)?.h : DEFAULT_THEME_COLORS_HUE
|
||||||
|
|
||||||
export const useSettings = defineStore('settings', () => {
|
export const useSettings = defineStore('settings', () => {
|
||||||
const selectedAudioDevice = ref<MediaDeviceInfo>()
|
const selectedAudioDevice = ref<MediaDeviceInfo>()
|
||||||
|
|
||||||
@@ -29,6 +33,35 @@ export const useSettings = defineStore('settings', () => {
|
|||||||
const themeColorsHue = useLocalStorage('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE)
|
const themeColorsHue = useLocalStorage('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE)
|
||||||
const themeColorsHueDynamic = useLocalStorage('settings/theme/colors/hue-dynamic', false)
|
const themeColorsHueDynamic = useLocalStorage('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
|
||||||
|
}
|
||||||
|
|
||||||
watch(isAudioInputOn, (value) => {
|
watch(isAudioInputOn, (value) => {
|
||||||
if (value === 'false') {
|
if (value === 'false') {
|
||||||
selectedAudioDevice.value = undefined
|
selectedAudioDevice.value = undefined
|
||||||
@@ -38,17 +71,11 @@ export const useSettings = defineStore('settings', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (isAudioInputOn.value === 'true' && !selectedAudioDevice.value) {
|
|
||||||
selectedAudioDevice.value = audioInputs.value[0]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(audioInputs, () => {
|
watch(audioInputs, () => {
|
||||||
if (isAudioInputOn.value === 'true' && !selectedAudioDevice.value) {
|
if (isAudioInputOn.value === 'true' && !selectedAudioDevice.value) {
|
||||||
selectedAudioDevice.value = audioInputs.value[0]
|
selectedAudioDevice.value = audioInputs.value[0]
|
||||||
}
|
}
|
||||||
})
|
}, { immediate: true })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
live2dModelFile,
|
live2dModelFile,
|
||||||
@@ -67,5 +94,9 @@ export const useSettings = defineStore('settings', () => {
|
|||||||
isAudioInputOn,
|
isAudioInputOn,
|
||||||
selectedAudioDevice,
|
selectedAudioDevice,
|
||||||
selectedAudioDeviceId,
|
selectedAudioDeviceId,
|
||||||
|
|
||||||
|
setThemeColorsHue,
|
||||||
|
applyPrimaryColorFrom,
|
||||||
|
isColorSelectedForPrimary,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user