diff --git a/packages/stage-ui/src/components/scenarios/settings/model-settings/live2d.vue b/packages/stage-ui/src/components/scenarios/settings/model-settings/live2d.vue index d258d383e..053009311 100644 --- a/packages/stage-ui/src/components/scenarios/settings/model-settings/live2d.vue +++ b/packages/stage-ui/src/components/scenarios/settings/model-settings/live2d.vue @@ -3,9 +3,9 @@ import type { ModelSettingsRuntimeSnapshot } from './runtime' import { defaultModelParameters, useLive2d } from '@proj-airi/stage-ui-live2d' import { OPFSCache } from '@proj-airi/stage-ui-live2d/utils/opfs-loader' -import { Button, Checkbox, FieldRange, SelectTab } from '@proj-airi/ui' +import { Button, Checkbox, FieldCheckbox, FieldCombobox, FieldRange, SelectTab } from '@proj-airi/ui' import { storeToRefs } from 'pinia' -import { computed, onMounted, onUnmounted, ref, watch } from 'vue' +import { computed, onMounted, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { useSettings } from '../../../../stores/settings' @@ -44,44 +44,36 @@ const { } = storeToRefs(live2d) const selectedRuntimeMotion = ref('') -const selectedRuntimeMotionName = ref('') -const runtimeMotions = ref>([]) -const showMotionSelector = ref(false) +const runtimeMotions = ref>([]) const canExtractColors = computed(() => props.runtimeSnapshot.canCapturePreview) +const runtimeMotionOptions = computed(() => runtimeMotions.value.map(motion => ({ + label: motion.name, + value: motion.displayPath, + description: motion.displayPath, +}))) const fpsOptions = computed(() => [ { value: 0, label: t('settings.live2d.fps.options.unlimited') }, { value: 60, label: '60' }, { value: 30, label: '30' }, ]) -// Get available runtime motions from the model +watch(() => live2d.availableMotions, (motions) => { + runtimeMotions.value = motions.map(m => ({ + name: m.fileName.split('/').pop() || m.fileName, + displayPath: m.fileName, + group: m.motionName, + index: m.motionIndex, + })) + + console.info('Available motions:', runtimeMotions.value) +}, { immediate: true }) + onMounted(() => { - // Listen for available motions updates - watch(() => live2d.availableMotions, (motions) => { - // Show all motions with their full paths - runtimeMotions.value = motions.map(m => ({ - name: m.fileName.split('/').pop() || m.fileName, - fullPath: m.fileName, // Full path like "hiyori_free_zh/runtime/motions/idle.motion3.json" - displayPath: m.fileName, // Show full path for clarity - group: m.motionName, - index: m.motionIndex, - })) - - console.info('Available motions:', runtimeMotions.value) - }, { immediate: true }) - // Restore selected motion const savedPath = localStorage.getItem('selected-runtime-motion') - const savedName = localStorage.getItem('selected-runtime-motion-name') if (savedPath) { selectedRuntimeMotion.value = savedPath } - if (savedName) { - selectedRuntimeMotionName.value = savedName - } - - // Add click outside handler - document.addEventListener('click', handleClickOutside) }) // Function to reset all parameters to default values @@ -101,12 +93,17 @@ async function clearModelCache() { } } -// Runtime motion selection handlers -function handleMotionSelect(motion: any) { - selectedRuntimeMotion.value = motion.displayPath // Store full path - selectedRuntimeMotionName.value = motion.name // Store just the filename for display +function handleMotionSelect(selectedMotionPath: string | number | undefined) { + if (typeof selectedMotionPath !== 'string') { + return + } + + const motion = runtimeMotions.value.find(item => item.displayPath === selectedMotionPath) + if (!motion) { + return + } + localStorage.setItem('selected-runtime-motion', motion.displayPath) - localStorage.setItem('selected-runtime-motion-name', motion.name) localStorage.setItem('selected-runtime-motion-group', motion.group) localStorage.setItem('selected-runtime-motion-index', motion.index.toString()) @@ -116,29 +113,11 @@ function handleMotionSelect(motion: any) { // Set the current motion to the selected runtime motion currentMotion.value = { group: motion.group, index: motion.index } - showMotionSelector.value = false - - console.info('✅ Selected runtime motion:', motion.name) + console.info('Selected runtime motion:', motion.name) console.info('Full path:', motion.displayPath) console.info('Group:', motion.group, 'Index:', motion.index) } -function toggleMotionSelector() { - showMotionSelector.value = !showMotionSelector.value -} - -// Close dropdown when clicking outside -function handleClickOutside(event: MouseEvent) { - const target = event.target as HTMLElement - if (!target.closest('[data-motion-selector]')) { - showMotionSelector.value = false - } -} - -onUnmounted(() => { - document.removeEventListener('click', handleClickOutside) -}) - // async function patchMotionMap(source: File, motionMap: Record): Promise { // if (!Object.keys(motionMap).length) // return source @@ -296,61 +275,25 @@ onUnmounted(() => { size="sm" :expand="false" > -
- - {{ t('settings.live2d.focus.button-disable.title') }} - - -
+ -
- Idle Animation -
- - - -
-
- No motions available -
- -
-
-
+ + +
@@ -379,21 +322,23 @@ onUnmounted(() => {
- + - + Clear Model Cache +
diff --git a/packages/ui/src/components/form/combobox-select/combobox-select.vue b/packages/ui/src/components/form/combobox-select/combobox-select.vue index ade8dfee8..7f71a30ae 100644 --- a/packages/ui/src/components/form/combobox-select/combobox-select.vue +++ b/packages/ui/src/components/form/combobox-select/combobox-select.vue @@ -1,10 +1,14 @@ diff --git a/packages/ui/src/components/form/combobox/combobox.vue b/packages/ui/src/components/form/combobox/combobox.vue index 0f0396e41..ee4db5f0c 100644 --- a/packages/ui/src/components/form/combobox/combobox.vue +++ b/packages/ui/src/components/form/combobox/combobox.vue @@ -16,19 +16,58 @@ import { ComboboxTrigger, ComboboxViewport, } from 'reka-ui' +import { computed } from 'vue' -const props = defineProps<{ - options: { groupLabel: string, children?: { label: string, value: T }[] }[] +interface ComboboxOptionItem { + label: string + value: T + description?: string + disabled?: boolean + icon?: string +} + +interface ComboboxOptionGroupItem { + groupLabel?: string + children?: ComboboxOptionItem[] +} + +const props = withDefaults(defineProps<{ + options: ComboboxOptionItem[] | ComboboxOptionGroupItem[] placeholder?: string + disabled?: boolean contentMinWidth?: string | number contentWidth?: string | number -}>() +}>(), { + disabled: false, +}) const modelValue = defineModel({ required: false }) +const normalizedOptions = computed[]>(() => { + if (!props.options.length) { + return [] + } + + const [firstOption] = props.options + if ('value' in firstOption) { + return [ + { + groupLabel: '', + children: props.options as ComboboxOptionItem[], + }, + ] + } + + return props.options as ComboboxOptionGroupItem[] +}) + +const flattenedOptions = computed[]>(() => + normalizedOptions.value.flatMap(group => group.children ?? []), +) + function toDisplayValue(value: T): string { - const option = props.options.flatMap(group => group.children).find(option => option?.value === value) - return option ? option.label : props.placeholder || '' + const option = flattenedOptions.value.find(option => option.value === value) + return option?.label ?? props.placeholder ?? '' } function toCssSize(value?: string | number): string | undefined { @@ -41,15 +80,20 @@ function toCssSize(value?: string | number): string | undefined {