Skip to content

Commit

Permalink
component AnimationSettings
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Jul 1, 2024
1 parent 79140cd commit 2eac2f6
Show file tree
Hide file tree
Showing 23 changed files with 647 additions and 15 deletions.
8 changes: 4 additions & 4 deletions spx-gui/src/components/asset/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ function selectAsset(project: Project, asset: AssetModel | undefined) {
}
}

export function useAddAssetFromLibrary() {
export function useAddAssetFromLibrary(autoSelect = true) {
const invokeAssetLibraryModal = useModal(AssetLibraryModal)
return async function addAssetFromLibrary<T extends AssetType>(project: Project, type: T) {
const added = (await invokeAssetLibraryModal({ project, type })) as Array<AssetModel<T>>
selectAsset(project, added[0])
if (autoSelect) selectAsset(project, added[0])
return added
}
}
Expand Down Expand Up @@ -110,13 +110,13 @@ export function useAddCostumeFromLocalFile() {
}
}

export function useAddSoundFromLocalFile() {
export function useAddSoundFromLocalFile(autoSelect = true) {
return async function addSoundFromLocalFile(project: Project) {
const audio = await selectAudio()
const sound = await Sound.create(stripExt(audio.name), fromNativeFile(audio))
const action = { name: { en: 'Add sound', zh: '添加声音' } }
await project.history.doAction(action, () => project.addSound(sound))
selectAsset(project, sound)
if (autoSelect) selectAsset(project, sound)
return sound
}
}
148 changes: 148 additions & 0 deletions spx-gui/src/components/editor/sprite/animation/AnimationSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<template>
<section class="wrapper">
<UIDropdown
trigger="manual"
:visible="activeSetting != null"
placement="top"
@click-outside="handleClickOutside"
>
<template #trigger>
<ul class="settings">
<li
class="setting"
:class="{ active: activeSetting === 'duration' }"
@click="handleSummaryClick('duration')"
>
<UIIcon type="timer" />
{{ $t({ en: 'Duration', zh: '时长' }) }}
<span class="value">{{ formatDuration(animation.duration, 2) }}</span>
</li>
<li
class="setting"
:class="{ active: activeSetting === 'bound-state' }"
@click="handleSummaryClick('bound-state')"
>
<UIIcon type="status" />
{{ $t({ en: 'Binding', zh: '绑定' }) }}
<span v-if="boundStateNum > 0" class="value">{{ boundStateNum }}</span>
</li>
<li
class="setting"
:class="{ active: activeSetting === 'sound' }"
@click="handleSummaryClick('sound')"
>
<UIIcon type="sound" />
{{ $t({ en: 'Sound', zh: '声音' }) }}
<span class="value">{{ animation.sound }}</span>
</li>
</ul>
</template>
<DurationEditor
v-show="activeSetting === 'duration'"
:animation="animation"
@close="handleEditorClose"
/>
<BoundStateEditor
v-show="activeSetting === 'bound-state'"
:animation="animation"
:sprite="sprite"
@close="handleEditorClose"
/>
<SoundEditor
v-show="activeSetting === 'sound'"
:animation="animation"
@close="handleEditorClose"
/>
</UIDropdown>
</section>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { formatDuration } from '@/utils/audio'
import type { Sprite } from '@/models/sprite'
import type { Animation } from '@/models/animation'
import { UIDropdown, UIIcon, isInPopup } from '@/components/ui'
import DurationEditor from './DurationEditor.vue'
import BoundStateEditor from './state/BoundStateEditor.vue'
import SoundEditor from './sound/SoundEditor.vue'
const props = defineProps<{
sprite: Sprite
animation: Animation
}>()
type Setting = 'duration' | 'bound-state' | 'sound'
const activeSetting = ref<Setting | null>(null)
function handleSummaryClick(setting: Setting) {
activeSetting.value = activeSetting.value === setting ? null : setting
}
function handleEditorClose() {
activeSetting.value = null
}
const boundStateNum = computed(
() => props.sprite.getAnimationBoundStates(props.animation.name).length
)
function handleClickOutside(e: MouseEvent) {
// There are popups (dropdown, modal, ...) in setting editor (e.g. "Record" in `SoundEditor`), we should not close the editor when user clicks in the popup content.
// TODO: There should be a systematical solution for this, something like event propagation along the component tree instead of DOM tree.
if (isInPopup(e.target as HTMLElement | null)) return
activeSetting.value = null
}
</script>

<style lang="scss" scoped>
.wrapper {
display: flex;
justify-content: center;
}
.settings {
display: flex;
align-items: center;
padding: 4px;
gap: 4px;
border-radius: var(--ui-border-radius-1);
box-shadow: var(--ui-box-shadow-small);
}
.setting {
display: flex;
height: 32px;
padding: 4px 12px;
align-items: center;
gap: 4px;
border-radius: var(--ui-border-radius-1);
font-size: 12px;
line-height: 1.5;
color: var(--ui-color-text-main);
cursor: pointer;
transition: 0.2s;
&.active {
color: var(--ui-color-primary-main);
background: var(--ui-color-primary-200);
}
}
.value {
padding: 0px 5px;
border-radius: 8px;
max-width: 5em;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 10px;
line-height: 1.6;
color: var(--ui-color-grey-800);
background-color: var(--ui-color-grey-400);
}
</style>
34 changes: 34 additions & 0 deletions spx-gui/src/components/editor/sprite/animation/DurationEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<UIDropdownModal
:title="$t({ en: 'Adjust duration', zh: '调整时长' })"
style="width: 280px"
@cancel="emit('close')"
@confirm="handleConfirm"
>
<UINumberInput v-model:value="duration" :min="0.01">
<template #prefix>{{ $t({ en: 'Duration', zh: '时长' }) }}:</template>
<template #suffix>{{ $t({ en: 's', zh: '' }) }}</template>
</UINumberInput>
</UIDropdownModal>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { Animation } from '@/models/animation'
import { UIDropdownModal, UINumberInput } from '@/components/ui'
const props = defineProps<{
animation: Animation
}>()
const emit = defineEmits<{
close: []
}>()
const duration = ref(props.animation.duration)
function handleConfirm() {
props.animation.setDuration(duration.value)
emit('close')
}
</script>
124 changes: 124 additions & 0 deletions spx-gui/src/components/editor/sprite/animation/sound/SoundEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<template>
<UIDropdownModal
:title="$t({ en: 'Select sound', zh: '选择声音' })"
style="width: 320px; max-height: 400px"
@cancel="emit('close')"
@confirm="handleConfirm"
>
<ul class="sound-items">
<SoundItem
v-for="sound in editorCtx.project.sounds"
:key="sound.name"
:sound="sound"
:active="sound.name === selected"
@click="handleSoundClick(sound.name)"
/>
<UIDropdown trigger="click" placement="top">
<template #trigger>
<UIBlockItem class="add-sound">
<UIIcon class="icon" type="plus" />
</UIBlockItem>
</template>
<UIMenu>
<UIMenuItem @click="handleAddFromLocalFile">{{
$t({ en: 'Select local file', zh: '选择本地文件' })
}}</UIMenuItem>
<UIMenuItem @click="handleAddFromAssetLibrary">{{
$t({ en: 'Choose from asset library', zh: '从素材库选择' })
}}</UIMenuItem>
<UIMenuItem @click="handleRecord">{{ $t({ en: 'Record', zh: '录音' }) }}</UIMenuItem>
</UIMenu>
</UIDropdown>
</ul>
<SoundRecorderModal v-model:visible="recorderVisible" @saved="handleRecorded" />
</UIDropdownModal>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { Animation } from '@/models/animation'
import {
UIDropdownModal,
UIDropdown,
UIMenu,
UIMenuItem,
UIBlockItem,
UIIcon
} from '@/components/ui'
import { useEditorCtx } from '@/components/editor/EditorContextProvider.vue'
import SoundItem from './SoundItem.vue'
import { useAddAssetFromLibrary, useAddSoundFromLocalFile } from '@/components/asset'
import SoundRecorderModal from '@/components/editor/sound/SoundRecorderModal.vue'
import { useMessageHandle } from '@/utils/exception'
import { AssetType } from '@/apis/asset'
import type { Sound } from '@/models/sound'
const props = defineProps<{
animation: Animation
}>()
const emit = defineEmits<{
close: []
}>()
const editorCtx = useEditorCtx()
const selected = ref(props.animation.sound)
function handleSoundClick(sound: string) {
selected.value = selected.value === sound ? null : sound
}
const addFromLocalFile = useAddSoundFromLocalFile(false)
const handleAddFromLocalFile = useMessageHandle(
async () => {
const sound = await addFromLocalFile(editorCtx.project)
selected.value = sound.name
},
{
en: 'Failed to add sound from local file',
zh: '从本地文件添加失败'
}
).fn
const addAssetFromLibrary = useAddAssetFromLibrary(false)
const handleAddFromAssetLibrary = useMessageHandle(
async () => {
const sounds = await addAssetFromLibrary(editorCtx.project, AssetType.Sound)
selected.value = sounds[0].name
},
{ en: 'Failed to add sound from asset library', zh: '从素材库添加失败' }
).fn
const recorderVisible = ref(false)
function handleRecord() {
recorderVisible.value = true
}
function handleRecorded(sound: Sound) {
selected.value = sound.name
}
function handleConfirm() {
props.animation.setSound(selected.value)
emit('close')
}
</script>

<style lang="scss" scoped>
.sound-items {
flex: 1 1 0;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 12px;
}
.add-sound {
justify-content: center;
color: var(--ui-color-primary-main);
.icon {
width: 24px;
height: 24px;
}
}
</style>
45 changes: 45 additions & 0 deletions spx-gui/src/components/editor/sprite/animation/sound/SoundItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<UIBlockItem :active="active">
<div class="content">
<SoundPlayer :src="audioSrc" color="primary" />
</div>
<p class="name">{{ sound.name }}</p>
</UIBlockItem>
</template>

<script setup lang="ts">
import { useFileUrl } from '@/utils/file'
import { Sound } from '@/models/sound'
import { UIBlockItem } from '@/components/ui'
import SoundPlayer from '@/components/editor/sound/SoundPlayer.vue'
const props = defineProps<{
sound: Sound
active: boolean
}>()
const [audioSrc] = useFileUrl(() => props.sound.file)
</script>

<style lang="scss" scoped>
.content {
margin-top: 4px;
width: 56px;
height: 56px;
padding: 10px;
}
.name {
margin-top: 2px;
font-size: 10px;
line-height: 1.6;
padding: 3px 8px 3px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-align: center;
text-overflow: ellipsis;
color: var(--ui-color-title);
}
</style>
Loading

0 comments on commit 2eac2f6

Please sign in to comment.