Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions spx-gui/src/apis/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ export type ListAssetParams = PaginationParams & {
sortOrder?: 'asc' | 'desc'
}

export function listAsset(params?: ListAssetParams) {
return client.get('/assets/list', params) as Promise<ByPage<AssetData>>
export function listAsset(params?: ListAssetParams, signal?: AbortSignal) {
return client.get('/assets/list', params, { signal }) as Promise<ByPage<AssetData>>
}

export function getAsset(id: string) {
Expand Down
129 changes: 98 additions & 31 deletions spx-gui/src/components/asset/gen/backdrop/BackdropGen.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { UIButton } from '@/components/ui'
import { AssetType } from '@/apis/asset'
import type { Backdrop } from '@/models/spx/backdrop'
import { asset2Backdrop } from '@/models/spx/common/asset'
import type { BackdropGen } from '@/models/spx/gen/backdrop-gen'
import { capture, useMessageHandle } from '@/utils/exception'
import { humanizeTimeLeft } from '../common/time-left'
import BackdropSettingInput from './BackdropSettingsInput.vue'
import LayoutWithPreview from '../common/LayoutWithPreview.vue'
import ImagePreview from '../common/ImagePreview.vue'
import ImageSelector from '../common/ImageSelector.vue'
import AssetSuggestions from '../common/AssetSuggestions.vue'
import { useAssetSuggestions } from '../common/use-asset-suggestions'
import BackdropImageItem from './BackdropImageItem.vue'
import BackdropItem from '@/components/asset/library/BackdropItem.vue'

const props = defineProps<{
gen: BackdropGen
descriptionPlaceholder?: string
}>()
const props = withDefaults(
defineProps<{
gen: BackdropGen
descriptionPlaceholder?: string
librarySearchEnabled?: boolean
}>(),
{
descriptionPlaceholder: undefined,
librarySearchEnabled: false
}
)

const emit = defineEmits<{
finished: [Backdrop]
resolved: [Backdrop]
}>()

const canSubmit = computed(() => props.gen.image != null)
Expand All @@ -27,7 +39,7 @@ const handleSubmit = useMessageHandle(
props.gen.recordAdoption().catch((err) => {
capture(err, 'failed to record backdrop asset adoption')
})
emit('finished', backdrop)
emit('resolved', backdrop)
},
{
en: 'Failed to create backdrop',
Expand All @@ -43,6 +55,30 @@ function handleImageSelect(index: number) {
props.gen.setImageIndex(index)
hasPreview.value = true
}

const isLibrarySearchEnabled = computed(
() => props.librarySearchEnabled && props.gen.imagesGenState.status === 'initial'
)

const {
keyword,
suggestions,
isLoading: isSuggestionsLoading,
selected: selectedAsset,
toggle: toggleSelectedAsset
} = useAssetSuggestions(AssetType.Backdrop, () => props.gen.settings.description, isLibrarySearchEnabled)

const handleUseAsset = useMessageHandle(
async () => {
if (selectedAsset.value == null) throw new Error('no asset selected')
const backdrop = await asset2Backdrop(selectedAsset.value)
emit('resolved', backdrop)
},
{
en: 'Failed to use asset',
zh: '使用素材失败'
}
)
</script>

<template>
Expand All @@ -58,40 +94,68 @@ function handleImageSelect(index: number) {
:disabled="handleSubmit.isLoading.value"
:description-placeholder="descriptionPlaceholder"
/>
<ImageSelector
:state="gen.imagesGenState"
:selected="gen.imageIndex"
:disabled="handleSubmit.isLoading.value"
@select="handleImageSelect"
>
<template #loading-item>
<BackdropImageItem loading />
</template>
<template #item="{ file, active, onClick }">
<BackdropImageItem :file="file" :active="active" @click="onClick" />
</template>
<template #tip>
<template v-if="gen.imagesGenState.status === 'running'">
{{ $t({ en: `Generating backdrops... `, zh: `正在生成背景...` }) }}
{{ gen.imagesGenState.timeLeft != null ? $t(humanizeTimeLeft(gen.imagesGenState.timeLeft)) : '' }}
<div class="select-area">
<AssetSuggestions
v-if="isLibrarySearchEnabled"
:type="AssetType.Backdrop"
:loading="isSuggestionsLoading"
:keyword="keyword"
:suggestions="suggestions"
:selected="selectedAsset"
@toggle="toggleSelectedAsset"
>
<template #item="{ asset, selected, onClick }">
<BackdropItem :asset="asset" :selected="selected" @click="onClick" />
</template>
</AssetSuggestions>
<ImageSelector
:state="gen.imagesGenState"
:selected="gen.imageIndex"
:disabled="handleSubmit.isLoading.value"
@select="handleImageSelect"
>
<template #loading-item>
<BackdropImageItem loading />
</template>
<template v-else-if="gen.imagesGenState.status === 'finished'">
{{
$t({
en: 'Select the backdrop you like the most, or generate new ones.',
zh: '选择你最喜欢的一个背景,或者重新生成。'
})
}}
<template #item="{ file, active, onClick }">
<BackdropImageItem :file="file" :active="active" @click="onClick" />
</template>
</template>
</ImageSelector>
<template #tip>
<template v-if="gen.imagesGenState.status === 'running'">
{{ $t({ en: `Generating backdrops... `, zh: `正在生成背景...` }) }}
{{ gen.imagesGenState.timeLeft != null ? $t(humanizeTimeLeft(gen.imagesGenState.timeLeft)) : '' }}
</template>
<template v-else-if="gen.imagesGenState.status === 'finished'">
{{
$t({
en: 'Select the backdrop you like the most, or generate new ones.',
zh: '选择你最喜欢的一个背景,或者重新生成。'
})
}}
</template>
</template>
</ImageSelector>
</div>

<template #preview>
<ImagePreview :file="gen.image" />
</template>
</LayoutWithPreview>
<footer class="footer">
<UIButton
v-if="selectedAsset != null"
v-radar="{
name: 'Use',
desc: 'Click to use the selected library asset'
}"
color="primary"
size="large"
@click="handleUseAsset.fn"
>
{{ $t({ en: 'Use', zh: '采用' }) }}
</UIButton>
<UIButton
v-else
v-radar="{
name: 'Use',
desc: 'Finish and use the generated backdrop in the project'
Expand Down Expand Up @@ -129,6 +193,9 @@ function handleImageSelect(index: number) {
height: 300px;
}
}
.select-area {
height: 170px; // Fixed height to prevent layout shift when suggestions appear/disappear
}
}

.footer {
Expand Down
98 changes: 98 additions & 0 deletions spx-gui/src/components/asset/gen/backdrop/BackdropGenModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script setup lang="ts">
import { computed } from 'vue'
import { UIModal, UIModalClose, useConfirmDialog } from '@/components/ui'
import { useI18n } from '@/utils/i18n'
import { useMessageHandle } from '@/utils/exception'
import { AssetType } from '@/apis/asset'
import type { Backdrop } from '@/models/spx/backdrop'
import type { BackdropGen as BackdropGenModel } from '@/models/spx/gen/backdrop-gen'
import type { SpxProject } from '@/models/spx/project'
import { useAssetGen } from '../use-asset-gen'
import BackdropGenComp from './BackdropGen.vue'

const props = withDefaults(
defineProps<{
visible: boolean
project: SpxProject
// Currently no use case for providing an external gen, but we keep this prop
// for consistency with SpriteGenModal and potential future use cases.
gen?: BackdropGenModel
}>(),
{
gen: undefined
}
)

const emit = defineEmits<{
resolved: [Backdrop]
cancelled: []
}>()

const i18n = useI18n()
const confirm = useConfirmDialog()

const typeRef = computed(() => (props.gen != null ? null : AssetType.Backdrop))
const { assetGen: internalGen } = useAssetGen(props.project, typeRef)
const activeGen = computed(() => props.gen ?? internalGen.value)

const handleModalClose = useMessageHandle(
async () => {
if (props.gen == null) {
await confirm({
title: i18n.t({ zh: '退出背景生成?', en: 'Exit backdrop generation?' }),
content: i18n.t({
zh: '当前内容不会被保存,确定要退出吗?',
en: 'Current progress will not be saved. Are you sure to exit?'
}),
confirmText: i18n.t({ en: 'Exit', zh: '退出' })
})
}
emit('cancelled')
},
{ en: 'Failed to exit modal', zh: '退出失败' }
).fn
</script>

<template>
<UIModal
:radar="{ name: 'Backdrop generation modal', desc: 'Modal for backdrop generation' }"
style="width: 1076px; height: 800px"
:visible="visible"
mask-closable
@update:visible="handleModalClose"
>
<header class="header">
<h2 class="title">{{ $t({ zh: '生成背景', en: 'Backdrop Generator' }) }}</h2>
<UIModalClose class="close" @click="handleModalClose" />
</header>

<BackdropGenComp
v-if="activeGen != null"
class="backdrop-gen"
:gen="activeGen"
library-search-enabled
@resolved="emit('resolved', $event)"
/>
</UIModal>
</template>

<style lang="scss" scoped>
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
height: 56px;
border-bottom: 1px solid var(--ui-color-grey-400);

.title {
font-size: 16px;
color: var(--ui-color-title);
}
}

.backdrop-gen {
flex: 1 1 0;
min-height: 0;
}
</style>
83 changes: 83 additions & 0 deletions spx-gui/src/components/asset/gen/common/AssetSuggestions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { computed } from 'vue'
import { AssetType, type AssetData } from '@/apis/asset'
import { UILoading } from '@/components/ui'

const props = defineProps<{
type: AssetType
loading: boolean
keyword: string
suggestions: AssetData[]
selected: AssetData | null
}>()

const emit = defineEmits<{
toggle: [AssetData]
}>()

function isSelected(asset: AssetData) {
return props.selected?.id === asset.id
}

const entityMessages = {
[AssetType.Backdrop]: { en: 'backdrop', zh: '背景' },
[AssetType.Sprite]: { en: 'sprite', zh: '精灵' },
[AssetType.Sound]: { en: 'sound', zh: '声音' }
}
const entityMessage = computed(() => entityMessages[props.type])
</script>

<template>
<UILoading v-if="loading" />
<div v-else class="asset-suggestions">
<template v-if="suggestions.length > 0">
Comment thread
nighca marked this conversation as resolved.
<ul class="list">
<template v-for="asset in suggestions" :key="asset.id">
<slot name="item" :asset="asset" :selected="isSelected(asset)" :on-click="() => emit('toggle', asset)"></slot>
</template>
</ul>
<p class="tip">
{{
$t({
en: `There are related ${entityMessage.en}s in the asset library. You can choose the one you like or continue generating.`,
zh: `素材库中已有相关的${entityMessage.zh},可以选择你喜欢的${entityMessage.zh}直接使用,或者继续生成。`
})
}}
</p>
</template>
<p v-else-if="keyword.length > 0" class="tip" style="margin-top: 56px">
{{
$t({
en: `No matching ${entityMessage.en}s found in the asset library. You can continue generating.`,
zh: `素材库中未找到匹配的${entityMessage.zh},你可以继续生成。`
})
}}
</p>
</div>
</template>

<style lang="scss" scoped>
.asset-suggestions {
Comment thread
cn0809 marked this conversation as resolved.
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}

.list {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
justify-content: center;
list-style: none;
padding: 0;
margin: 0;
}

.tip {
text-align: center;
font-size: 12px;
color: var(--ui-color-hint-2);
}
</style>
Loading