Skip to content

Commit

Permalink
component AnimationSettings (goplus#621)
Browse files Browse the repository at this point in the history
* component AnimationSettings

* use UIIcon instead of inline svg

* fix svg icon
  • Loading branch information
nighca committed Jul 1, 2024
1 parent 79140cd commit 5a8d9b6
Show file tree
Hide file tree
Showing 24 changed files with 649 additions and 21 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
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
<template>
<PanelSummaryItem>
<svg class="icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10 22.0002C9.71667 22.0002 9.47933 21.9042 9.288 21.7122C9.096 21.5209 9 21.2836 9 21.0002V3.00024C9 2.71691 9.096 2.47924 9.288 2.28724C9.47933 2.09591 9.71667 2.00024 10 2.00024C10.2833 2.00024 10.521 2.09591 10.713 2.28724C10.9043 2.47924 11 2.71691 11 3.00024V21.0002C11 21.2836 10.9043 21.5209 10.713 21.7122C10.521 21.9042 10.2833 22.0002 10 22.0002ZM6 16.0002C5.71667 16.0002 5.479 15.9042 5.287 15.7122C5.09567 15.5209 5 15.2836 5 15.0002V9.00024C5 8.71691 5.09567 8.47924 5.287 8.28724C5.479 8.09591 5.71667 8.00024 6 8.00024C6.28333 8.00024 6.521 8.09591 6.713 8.28724C6.90433 8.47924 7 8.71691 7 9.00024V15.0002C7 15.2836 6.90433 15.5209 6.713 15.7122C6.521 15.9042 6.28333 16.0002 6 16.0002ZM14 18.0002C13.7167 18.0002 13.4793 17.9042 13.288 17.7122C13.096 17.5209 13 17.2836 13 17.0002V7.00024C13 6.71691 13.096 6.47924 13.288 6.28724C13.4793 6.09591 13.7167 6.00024 14 6.00024C14.2833 6.00024 14.521 6.09591 14.713 6.28724C14.9043 6.47924 15 6.71691 15 7.00024V17.0002C15 17.2836 14.9043 17.5209 14.713 17.7122C14.521 17.9042 14.2833 18.0002 14 18.0002ZM18 15.0002C17.7167 15.0002 17.4793 14.9042 17.288 14.7122C17.096 14.5209 17 14.2836 17 14.0002V10.0002C17 9.71691 17.096 9.47924 17.288 9.28724C17.4793 9.09591 17.7167 9.00024 18 9.00024C18.2833 9.00024 18.5207 9.09591 18.712 9.28724C18.904 9.47924 19 9.71691 19 10.0002V14.0002C19 14.2836 18.904 14.5209 18.712 14.7122C18.5207 14.9042 18.2833 15.0002 18 15.0002Z"
fill="currentColor"
/>
</svg>
<UIIcon class="icon" type="sound" />
</PanelSummaryItem>
</template>

<script setup lang="ts">
import { Sound } from '@/models/sound'
import { UIIcon } from '@/components/ui'
import PanelSummaryItem from '../common/PanelSummaryItem.vue'
defineProps<{
Expand Down
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>
Loading

0 comments on commit 5a8d9b6

Please sign in to comment.