Skip to content

Commit

Permalink
spx-gui: upload files when adding resource
Browse files Browse the repository at this point in the history
- Added loading message type for global loading-feedback.
- Integrated file upload process into `PreprocessModal`'s confirmation
  for files requiring preprocessing.
- Implemented loading message display for file uploads without
  preprocessing.
- Ensured resource addition only completes after successful file upload.
- Fixed filename change issue in `PreprocessModal` caused by
  `RemoveBackground` application.

Fixes goplus#638
  • Loading branch information
aofei committed Aug 12, 2024
1 parent 6c472ee commit 32ddb87
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 32 deletions.
25 changes: 22 additions & 3 deletions spx-gui/src/components/asset/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useModal } from '@/components/ui'
import { useMessage, useModal } from '@/components/ui'
import { AssetType } from '@/apis/asset'
import { Backdrop } from '@/models/backdrop'
import { Sound } from '@/models/sound'
Expand All @@ -13,6 +13,9 @@ import AssetLibraryModal from './library/AssetLibraryModal.vue'
import AssetAddModal from './library/AssetAddModal.vue'
import LoadFromScratchModal from './scratch/LoadFromScratchModal.vue'
import PreprocessModal from './preprocessing/PreprocessModal.vue'
import { saveFiles } from '@/models/common/cloud'
import { useI18n } from '@/utils/i18n'
import { useNetwork } from '@/utils/network'

function selectAsset(project: Project, asset: AssetModel | undefined) {
if (asset instanceof Sprite) project.select({ type: 'sprite', name: asset.name })
Expand Down Expand Up @@ -58,7 +61,11 @@ export function useAddSpriteFromLocalFile() {
const files = nativeFiles.map((f) => fromNativeFile(f))
const spriteName = files.length > 1 ? '' : stripExt(files[0].name)
const sprite = Sprite.create(spriteName)
const costumes = await preprocess({ files, actionMessage })
const costumes = await preprocess({
files,
title: actionMessage,
confirmText: { en: 'Add', zh: '添加' }
})
for (const costume of costumes) {
sprite.addCostume(costume)
}
Expand All @@ -77,7 +84,11 @@ export function useAddCostumeFromLocalFile() {
const actionMessage = { en: 'Add costume', zh: '添加造型' }
const nativeFiles = await selectImgs()
const files = nativeFiles.map((f) => fromNativeFile(f))
const costumes = await preprocess({ files, actionMessage })
const costumes = await preprocess({
files,
title: actionMessage,
confirmText: { en: 'Add', zh: '添加' }
})
await project.history.doAction({ name: actionMessage }, () => {
for (const costume of costumes) sprite.addCostume(costume)
sprite.setDefaultCostume(costumes[0].name)
Expand All @@ -86,9 +97,17 @@ export function useAddCostumeFromLocalFile() {
}

export function useAddSoundFromLocalFile(autoSelect = true) {
const m = useMessage()
const { t } = useI18n()
const { isOnline } = useNetwork()
return async function addSoundFromLocalFile(project: Project) {
const audio = await selectAudio()
const sound = await Sound.create(stripExt(audio.name), fromNativeFile(audio))

if (isOnline.value) {
await m.withLoading(saveFiles(sound.export()), t({ en: 'Uploading files', zh: '上传文件中' }))
}

const action = { name: { en: 'Add sound', zh: '添加声音' } }
await project.history.doAction(action, () => project.addSound(sound))
if (autoSelect) selectAsset(project, sound)
Expand Down
46 changes: 35 additions & 11 deletions spx-gui/src/components/asset/preprocessing/PreprocessModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<UIFormModal
style="width: 780px"
:visible="props.visible && ready"
:title="$t(actionMessage)"
:title="$t(title)"
:body-style="{ padding: '0' }"
@update:visible="emit('cancelled')"
>
Expand Down Expand Up @@ -68,9 +68,10 @@
class="submit-btn"
size="large"
:disabled="selectedCostumes.length === 0"
@click="handleConfirm"
:loading="handleConfirm.isLoading.value"
@click="handleConfirm.fn"
>
{{ $t(actionMessage) }}
{{ $t(confirmText) }}
</UIButton>
</footer>
</UIFormModal>
Expand All @@ -94,12 +95,23 @@ import SplitSpriteSheet from './split-sprite-sheet/SplitSpriteSheet.vue'
import splitSpriteSheetThumbnail from './split-sprite-sheet/thumbnail.svg'
import RemoveBackground from './remove-background/RemoveBackground.vue'
import removeBackgroundThumbnail from './remove-background/thumbnail.svg'
import { saveFiles } from '@/models/common/cloud'
import { useMessageHandle } from '@/utils/exception'
import { useNetwork } from '@/utils/network'
const props = defineProps<{
visible: boolean
files: File[]
actionMessage: LocaleMessage
}>()
const { isOnline } = useNetwork()
const props = withDefaults(
defineProps<{
visible: boolean
files: File[]
title: LocaleMessage
confirmText?: LocaleMessage
}>(),
{
confirmText: () => ({ en: 'Confirm', zh: '确认' })
}
)
const emit = defineEmits<{
cancelled: []
Expand Down Expand Up @@ -217,9 +229,21 @@ async function handleCostumeClick(costume: Costume) {
else selectedCostumes.splice(index, 1)
}
function handleConfirm() {
emit('resolved', selectedCostumes)
}
const handleConfirm = useMessageHandle(
async () => {
if (isOnline.value) {
const files = selectedCostumes
.map((costume) => costume.export(''))
.reduce((acc, [, files]) => ({ ...acc, ...files }), {})
await saveFiles(files)
}
emit('resolved', selectedCostumes)
},
{
en: 'Failed to upload files',
zh: '上传文件失败'
}
)
// Avoid UI flickering when there's no supported methods
const ready = ref(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { loadImg } from '@/utils/dom'
import { stripExt } from '@/utils/path'
import { extname, stripExt } from '@/utils/path'
import { memoizeAsync } from '@/utils/utils'
import { getMimeFromExt } from '@/utils/file'
import { toJpeg } from '@/utils/img'
Expand Down Expand Up @@ -51,7 +51,10 @@ async function apply() {
const inputUrls = await getUrls(props.input)
const outputUrls = await batchRemoveBackground(inputUrls)
await drawTransitions(outputUrls)
const outputFiles = outputUrls.map((url) => createFileWithWebUrl(url))
const outputFiles = outputUrls.map((url, index) => {
const name = stripExt(props.input[index].name) + extname(url)
return createFileWithWebUrl(url, name)
})
emit('applied', outputFiles)
}
Expand Down
15 changes: 14 additions & 1 deletion spx-gui/src/components/editor/stage/BackdropsEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { UIMenu, UIMenuItem } from '@/components/ui'
import { UIMenu, UIMenuItem, useMessage } from '@/components/ui'
import { useMessageHandle } from '@/utils/exception'
import { selectImg } from '@/utils/file'
import { fromNativeFile } from '@/models/common/file'
Expand All @@ -38,6 +38,13 @@ import { useEditorCtx } from '../EditorContextProvider.vue'
import EditorList from '../common/EditorList.vue'
import BackdropItem from './BackdropItem.vue'
import BackdropDetail from './BackdropDetail.vue'
import { saveFiles } from '@/models/common/cloud'
import { useI18n } from '@/utils/i18n'
import { useNetwork } from '@/utils/network'
const m = useMessage()
const { t } = useI18n()
const { isOnline } = useNetwork()
const editorCtx = useEditorCtx()
const stage = computed(() => editorCtx.project.stage)
Expand All @@ -54,6 +61,12 @@ const handleAddFromLocalFile = useMessageHandle(
const file = fromNativeFile(img)
const backdrop = await Backdrop.create(stripExt(img.name), file)
const action = { name: { en: 'Add backdrop', zh: '添加背景' } }
if (isOnline.value) {
const [, backdropFiles] = backdrop.export()
await m.withLoading(saveFiles(backdropFiles), t({ en: 'Uploading files', zh: '上传文件中' }))
}
editorCtx.project.history.doAction(action, () => {
stage.value.addBackdrop(backdrop)
stage.value.setDefaultBackdrop(backdrop.name)
Expand Down
8 changes: 4 additions & 4 deletions spx-gui/src/components/ui/message/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ export function useMessage() {
},
error(content: string) {
nMessage.error(content, options)
},
async withLoading<T>(promise: Promise<T>, content: string): Promise<T> {
const reactive = nMessage.loading(content, { ...options, duration: 0 })
return promise.finally(() => reactive.destroy())
}
// We do not provide loading message (which do loading-feedback globally),
// instead, we do loading-feedback in-place, with
// * component `UILoading`
// * prop `loading` for components like `UIButton`
}
}
27 changes: 16 additions & 11 deletions spx-gui/src/models/common/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IsPublic, addProject, getProject, updateProject } from '@/apis/project'
import { getUpInfo as getRawUpInfo, makeObjectUrls, type UpInfo as RawUpInfo } from '@/apis/util'
import { DefaultException } from '@/utils/exception'
import type { Metadata } from '../project'
import { File, toNativeFile, toText, type Files } from './file'
import { File, toNativeFile, toText, type Files, isText } from './file'
import { hashFileCollection } from './hash'

// Supported universal Url schemes for files
Expand All @@ -19,9 +19,6 @@ const fileUniversalUrlSchemes = {
kodo: 'kodo:' // for objects stored in Qiniu Kodo, e.g. kodo://bucket/key
} as const

// File types that can be inlined in the Data Urls
const inlineableFileTypes = ['text/plain', 'application/json']

export async function load(owner: string, name: string) {
const projectData = await getProject(owner, name)
return await parseProjectData(projectData)
Expand Down Expand Up @@ -109,19 +106,27 @@ async function saveFile(file: File) {
const savedUrl = getUniversalUrl(file)
if (savedUrl != null) return savedUrl

const url = inlineableFileTypes.includes(file.type)
? await inlineFile(file)
: await uploadToKodo(file)
const url = await (isInlineable(file) ? inlineFile(file) : uploadToKodo(file))
setUniversalUrl(file, url)
return url
}

const isInlineable = isText

async function inlineFile(file: File): Promise<UniversalUrl> {
// Little trick from [https://fetch.spec.whatwg.org/#data-urls]: `12. If mimeType starts with ';', then prepend 'text/plain' to mimeType.`
// Saves some bytes.
const mimeType = file.type === 'text/plain' ? ';' : file.type
let mimeType, content
if (isText(file)) {
// Little trick from [https://fetch.spec.whatwg.org/#data-urls]: `12. If mimeType starts with ';', then prepend 'text/plain' to mimeType.`
// Saves some bytes.
mimeType = file.type === 'text/plain' ? ';' : file.type

// TODO: Implement file compression (see https://github.com/goplus/builder/issues/492)
content = await toText(file)
} else {
throw new Error('unsupported file type for inlining')
}

const urlEncodedContent = encodeURIComponent(await toText(file))
const urlEncodedContent = encodeURIComponent(content)
return `${fileUniversalUrlSchemes.data}${mimeType},${urlEncodedContent}`
}

Expand Down
5 changes: 5 additions & 0 deletions spx-gui/src/models/common/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ export async function toText(file: File) {
return decoder.decode(ab)
}

export const isText = (() => {
const types = ['application/json']
return (file: File) => file.type.startsWith('text/') || types.includes(file.type)
})()

export function fromConfig(name: string, config: unknown, options?: Options) {
return fromText(name, JSON.stringify(config), options)
}
Expand Down

0 comments on commit 32ddb87

Please sign in to comment.