diff --git a/spx-gui/.env.prod b/spx-gui/.env.prod new file mode 100644 index 000000000..855c321cd --- /dev/null +++ b/spx-gui/.env.prod @@ -0,0 +1,11 @@ +# Config for env production + +# Casdoor configuration +VITE_CASDOOR_ENDPOINT="https://acc.goplus.org" +VITE_CASDOOR_CLIENT_ID="4ff910257e9cdd89b6b8" + +# Used not for authentication, but for fetching user profile +VITE_CASDOOR_ORGANIZATION_NAME="built-in" +VITE_CASDOOR_APP_NAME="application_stem" + +VITE_API_BASE_URL="https://builder.goplus.org/api" diff --git a/spx-gui/src/apis/common/client.ts b/spx-gui/src/apis/common/client.ts index 474b84339..bd75bfc37 100644 --- a/spx-gui/src/apis/common/client.ts +++ b/spx-gui/src/apis/common/client.ts @@ -11,6 +11,7 @@ export type RequestOptions = { headers?: Headers /** Timeout duration in milisecond, from request-sent to server-response-got */ timeout?: number + signal?: AbortSignal } /** Response body when exception encountered for API calling */ @@ -74,6 +75,11 @@ export class Client { const req = await this.prepareRequest(url, payload, options) const timeout = options?.timeout ?? defaultTimeout const ctrl = new AbortController() + if (options?.signal != null) { + // TODO: Reimplement this using `AbortSignal.any()` once it is widely supported. + options.signal.throwIfAborted() + options.signal.addEventListener('abort', () => ctrl.abort(options.signal?.reason)) + } const resp = await Promise.race([ fetch(req, { signal: ctrl.signal }), new Promise((_, reject) => setTimeout(() => reject(new TimeoutException()), timeout)) diff --git a/spx-gui/src/apis/project.ts b/spx-gui/src/apis/project.ts index e573b2a93..f1c5c2712 100644 --- a/spx-gui/src/apis/project.ts +++ b/spx-gui/src/apis/project.ts @@ -30,8 +30,8 @@ export type ProjectData = { export type AddProjectParams = Pick -export function addProject(params: AddProjectParams) { - return client.post('/project', params) as Promise +export function addProject(params: AddProjectParams, signal?: AbortSignal) { + return client.post('/project', params, { signal }) as Promise } export type UpdateProjectParams = Pick @@ -40,8 +40,13 @@ function encode(owner: string, name: string) { return `${encodeURIComponent(owner)}/${encodeURIComponent(name)}` } -export function updateProject(owner: string, name: string, params: UpdateProjectParams) { - return client.put(`/project/${encode(owner, name)}`, params) as Promise +export function updateProject( + owner: string, + name: string, + params: UpdateProjectParams, + signal?: AbortSignal +) { + return client.put(`/project/${encode(owner, name)}`, params, { signal }) as Promise } export function deleteProject(owner: string, name: string) { diff --git a/spx-gui/src/components/asset/animation/GroupCostumesModal.vue b/spx-gui/src/components/asset/animation/GroupCostumesModal.vue index 9fe8f3fda..6fef5d583 100644 --- a/spx-gui/src/components/asset/animation/GroupCostumesModal.vue +++ b/spx-gui/src/components/asset/animation/GroupCostumesModal.vue @@ -15,7 +15,7 @@
    { project.addSprite(sprite) + await sprite.autoFitCostumes() await sprite.autoFit() }) selectAsset(project, sprite) @@ -89,9 +90,10 @@ export function useAddCostumeFromLocalFile() { title: actionMessage, confirmText: { en: 'Add', zh: '添加' } }) - await project.history.doAction({ name: actionMessage }, () => { + await project.history.doAction({ name: actionMessage }, async () => { for (const costume of costumes) sprite.addCostume(costume) - sprite.setDefaultCostume(costumes[0].name) + await sprite.autoFitCostumes(costumes) + sprite.setDefaultCostume(costumes[0].id) }) } } diff --git a/spx-gui/src/components/asset/preprocessing/PreprocessModal.vue b/spx-gui/src/components/asset/preprocessing/PreprocessModal.vue index d1d702ecc..357aeba15 100644 --- a/spx-gui/src/components/asset/preprocessing/PreprocessModal.vue +++ b/spx-gui/src/components/asset/preprocessing/PreprocessModal.vue @@ -56,7 +56,7 @@
      m.value === method) // methods are applied in order, so we need to unapply the following methods, as thier inputs have changed outputs.splice(idx) - outputs.push(output) + outputs[idx] = output updateCostumes(output) } function handleMethodCancel(method: Method) { const idx = supportedMethods.value.findIndex((m) => m.value === method) outputs.splice(idx) - outputs.push(null) + outputs[idx] = null updateCostumes(getMethodInput(method)) } @@ -220,11 +220,11 @@ async function updateCostumes(files: File[]) { } function isCostumeSelected(costume: Costume) { - return selectedCostumes.some((a) => a.name === costume.name) + return selectedCostumes.some((a) => a.id === costume.id) } async function handleCostumeClick(costume: Costume) { - const index = selectedCostumes.findIndex((c) => c.name === costume.name) + const index = selectedCostumes.findIndex((c) => c.id === costume.id) if (index < 0) selectedCostumes.push(costume) else selectedCostumes.splice(index, 1) } @@ -233,7 +233,11 @@ const handleConfirm = useMessageHandle( async () => { if (isOnline.value) { const files = selectedCostumes - .map((costume) => costume.export('')) + .map((costume) => + costume.export({ + basePath: '' + }) + ) .reduce((acc, [, files]) => ({ ...acc, ...files }), {}) await saveFiles(files) } @@ -288,7 +292,7 @@ watch( margin-bottom: -16px; } .footer-title { - color: --ui-color-title; + color: var(--ui-color-title); } .costume-wrapper { width: 100%; diff --git a/spx-gui/src/components/editor/code-editor/CodeEditor.vue b/spx-gui/src/components/editor/code-editor/CodeEditor.vue index 86aeb764f..f64b34d45 100644 --- a/spx-gui/src/components/editor/code-editor/CodeEditor.vue +++ b/spx-gui/src/components/editor/code-editor/CodeEditor.vue @@ -32,6 +32,7 @@
      @@ -87,6 +88,7 @@ import { useFileUrl } from '@/utils/file' withDefaults( defineProps<{ loading?: boolean + file: string value: string }>(), { diff --git a/spx-gui/src/components/editor/code-editor/code-text-editor/CodeTextEditor.vue b/spx-gui/src/components/editor/code-editor/code-text-editor/CodeTextEditor.vue index 2bf619032..9a1647502 100644 --- a/spx-gui/src/components/editor/code-editor/code-text-editor/CodeTextEditor.vue +++ b/spx-gui/src/components/editor/code-editor/code-text-editor/CodeTextEditor.vue @@ -4,6 +4,29 @@ diff --git a/spx-gui/src/components/editor/panels/sprite/SpriteItem.vue b/spx-gui/src/components/editor/panels/sprite/SpriteItem.vue index 42f2ca712..676a7479c 100644 --- a/spx-gui/src/components/editor/panels/sprite/SpriteItem.vue +++ b/spx-gui/src/components/editor/panels/sprite/SpriteItem.vue @@ -34,9 +34,10 @@ const [imgSrc, imgLoading] = useFileUrl(() => props.sprite.defaultCostume?.img) const handleRemove = useMessageHandle( async () => { - const sname = props.sprite.name - const action = { name: { en: `Remove sprite ${sname}`, zh: `删除精灵 ${sname}` } } - await editorCtx.project.history.doAction(action, () => editorCtx.project.removeSprite(sname)) + const spriteId = props.sprite.id + const spriteName = props.sprite.name + const action = { name: { en: `Remove sprite ${spriteName}`, zh: `删除精灵 ${spriteName}` } } + await editorCtx.project.history.doAction(action, () => editorCtx.project.removeSprite(spriteId)) }, { en: 'Failed to remove sprite', diff --git a/spx-gui/src/components/editor/panels/sprite/SpritesPanel.vue b/spx-gui/src/components/editor/panels/sprite/SpritesPanel.vue index 8f01a90ea..74789d23f 100644 --- a/spx-gui/src/components/editor/panels/sprite/SpritesPanel.vue +++ b/spx-gui/src/components/editor/panels/sprite/SpritesPanel.vue @@ -23,7 +23,7 @@ @@ -93,11 +93,11 @@ const summaryList = ref>() const summaryListData = useSummaryList(sprites, () => summaryList.value?.listWrapper ?? null) function isSelected(sprite: Sprite) { - return sprite.name === editorCtx.project.selectedSprite?.name + return sprite.id === editorCtx.project.selectedSprite?.id } function handleSpriteClick(sprite: Sprite) { - editorCtx.project.select({ type: 'sprite', name: sprite.name }) + editorCtx.project.select({ type: 'sprite', id: sprite.id }) } const addFromLocalFile = useAddSpriteFromLocalFile() diff --git a/spx-gui/src/components/editor/preview/stage-viewer/NodeTransformer.vue b/spx-gui/src/components/editor/preview/stage-viewer/NodeTransformer.vue index 1d4a3a073..15df133ba 100644 --- a/spx-gui/src/components/editor/preview/stage-viewer/NodeTransformer.vue +++ b/spx-gui/src/components/editor/preview/stage-viewer/NodeTransformer.vue @@ -7,7 +7,7 @@ import { computed, effect, nextTick, ref } from 'vue' import type { Node } from 'konva/lib/Node' import { useEditorCtx } from '../../EditorContextProvider.vue' import type { CustomTransformer, CustomTransformerConfig } from './custom-transformer' -import { getNodeName } from './node' +import { getNodeId } from './node' const props = defineProps<{ nodeReadyMap: Map @@ -37,12 +37,12 @@ effect(async () => { const project = editorCtx.project const selected = project.selectedSprite ?? project.stage.selectedWidget if (selected == null) return - const nodeName = getNodeName(selected) + const nodeId = getNodeId(selected) // Wait for node ready, so that Konva can get correct node size - if (!props.nodeReadyMap.get(nodeName)) return + if (!props.nodeReadyMap.get(nodeId)) return const stage = transformerNode.getStage() if (stage == null) throw new Error('no stage') - const selectedNode = stage.findOne((node: Node) => node.getAttr('nodeName') === nodeName) + const selectedNode = stage.findOne((node: Node) => node.getAttr('nodeId') === nodeId) if (selectedNode == null || selectedNode === (transformerNode as any).node()) return await nextTick() // Wait to ensure the selected node updated by Konva transformerNode.nodes([selectedNode]) diff --git a/spx-gui/src/components/editor/preview/stage-viewer/SpriteNode.vue b/spx-gui/src/components/editor/preview/stage-viewer/SpriteNode.vue index 3f9dc028e..2f726c788 100644 --- a/spx-gui/src/components/editor/preview/stage-viewer/SpriteNode.vue +++ b/spx-gui/src/components/editor/preview/stage-viewer/SpriteNode.vue @@ -23,7 +23,7 @@ import type { Size } from '@/models/common' import { nomalizeDegree, round } from '@/utils/utils' import { useFileImg } from '@/utils/file' import { useEditorCtx } from '../../EditorContextProvider.vue' -import { getNodeName } from './node' +import { getNodeId } from './node' const props = defineProps<{ sprite: Sprite @@ -37,11 +37,11 @@ const costume = computed(() => props.sprite.defaultCostume) const bitmapResolution = computed(() => costume.value?.bitmapResolution ?? 1) const [image] = useFileImg(() => costume.value?.img) -const nodeName = computed(() => getNodeName(props.sprite)) +const nodeId = computed(() => getNodeId(props.sprite)) watchEffect((onCleanup) => { - props.nodeReadyMap.set(nodeName.value, image.value != null) - onCleanup(() => props.nodeReadyMap.delete(nodeName.value)) + props.nodeReadyMap.set(nodeId.value, image.value != null) + onCleanup(() => props.nodeReadyMap.delete(nodeId.value)) }) onMounted(() => { @@ -51,7 +51,7 @@ onMounted(() => { // Konva warning: Node has no parent. zIndex parameter is ignored. // Konva warning: Unexpected value 2 for zIndex property. zIndex is just index of a node in children of its parent. Expected value is from 0 to 1. // ``` - const zIndex = editorCtx.project.zorder.indexOf(props.sprite.name) + const zIndex = editorCtx.project.zorder.indexOf(props.sprite.id) if (zIndex >= 0) { nodeRef.value!.getNode().zIndex(zIndex) } @@ -75,7 +75,7 @@ const config = computed(() => { const { visible, x, y, rotationStyle, heading, size, pivot } = props.sprite const scale = size / bitmapResolution.value const config = { - nodeName: nodeName.value, + nodeId: nodeId.value, image: image.value ?? undefined, draggable: true, offsetX: 0, @@ -124,6 +124,6 @@ function handleChange(e: KonvaEventObject, action: Action) { } function handleMousedown() { - editorCtx.project.select({ type: 'sprite', name: props.sprite.name }) + editorCtx.project.select({ type: 'sprite', id: props.sprite.id }) } diff --git a/spx-gui/src/components/editor/preview/stage-viewer/StageViewer.vue b/spx-gui/src/components/editor/preview/stage-viewer/StageViewer.vue index 3694ee453..d4b983a38 100644 --- a/spx-gui/src/components/editor/preview/stage-viewer/StageViewer.vue +++ b/spx-gui/src/components/editor/preview/stage-viewer/StageViewer.vue @@ -13,7 +13,7 @@ (null) @@ -160,21 +160,19 @@ const konvaBackdropConfig = computed(() => { const loading = computed(() => { if (backdropSrcLoading.value || !backdropImg.value) return true - if (editorCtx.project.sprites.some((s) => !nodeReadyMap.get(getNodeName(s)))) return true - if (editorCtx.project.stage.widgets.some((w) => !nodeReadyMap.get(getNodeName(w)))) return true + if (editorCtx.project.sprites.some((s) => !nodeReadyMap.get(getNodeId(s)))) return true + if (editorCtx.project.stage.widgets.some((w) => !nodeReadyMap.get(getNodeId(w)))) return true return false }) const visibleSprites = computed(() => { const { zorder, sprites } = editorCtx.project - return zorder.map((name) => sprites.find((s) => s.name === name)).filter(Boolean) as Sprite[] + return zorder.map((id) => sprites.find((s) => s.id === id)).filter(Boolean) as Sprite[] }) const visibleWidgets = computed(() => { const { widgetsZorder, widgets } = editorCtx.project.stage - return widgetsZorder - .map((name) => widgets.find((w) => w.name === name)) - .filter(Boolean) as Widget[] + return widgetsZorder.map((id) => widgets.find((w) => w.id === id)).filter(Boolean) as Widget[] }) const menuVisible = ref(false) @@ -219,23 +217,23 @@ async function moveZorder(direction: 'up' | 'down' | 'top' | 'bottom') { await project.history.doAction({ name: moveActionNames[direction] }, () => { if (selectedSprite != null) { if (direction === 'up') { - project.upSpriteZorder(selectedSprite.name) + project.upSpriteZorder(selectedSprite.id) } else if (direction === 'down') { - project.downSpriteZorder(selectedSprite.name) + project.downSpriteZorder(selectedSprite.id) } else if (direction === 'top') { - project.topSpriteZorder(selectedSprite.name) + project.topSpriteZorder(selectedSprite.id) } else if (direction === 'bottom') { - project.bottomSpriteZorder(selectedSprite.name) + project.bottomSpriteZorder(selectedSprite.id) } } else if (selectedWidget != null) { if (direction === 'up') { - project.stage.upWidgetZorder(selectedWidget.name) + project.stage.upWidgetZorder(selectedWidget.id) } else if (direction === 'down') { - project.stage.downWidgetZorder(selectedWidget.name) + project.stage.downWidgetZorder(selectedWidget.id) } else if (direction === 'top') { - project.stage.topWidgetZorder(selectedWidget.name) + project.stage.topWidgetZorder(selectedWidget.id) } else if (direction === 'bottom') { - project.stage.bottomWidgetZorder(selectedWidget.name) + project.stage.bottomWidgetZorder(selectedWidget.id) } } }) diff --git a/spx-gui/src/components/editor/preview/stage-viewer/node.ts b/spx-gui/src/components/editor/preview/stage-viewer/node.ts index fbe138685..7db506879 100644 --- a/spx-gui/src/components/editor/preview/stage-viewer/node.ts +++ b/spx-gui/src/components/editor/preview/stage-viewer/node.ts @@ -2,7 +2,7 @@ import { Sprite } from '@/models/sprite' import type { Widget } from '@/models/widget' /** Get name which identifies the node, which may be a sprite or a widget */ -export function getNodeName(target: Sprite | Widget) { +export function getNodeId(target: Sprite | Widget) { const type = target instanceof Sprite ? 'sprite' : 'widget' - return `${type}:${target.name}` + return `${type}:${target.id}` } diff --git a/spx-gui/src/components/editor/preview/stage-viewer/widgets/MonitorNode.vue b/spx-gui/src/components/editor/preview/stage-viewer/widgets/MonitorNode.vue index 95e1cb74e..5fd6eddfc 100644 --- a/spx-gui/src/components/editor/preview/stage-viewer/widgets/MonitorNode.vue +++ b/spx-gui/src/components/editor/preview/stage-viewer/widgets/MonitorNode.vue @@ -23,7 +23,7 @@ import { round } from '@/utils/utils' import type { Monitor } from '@/models/widget/monitor' import { useUIVariables } from '@/components/ui' import { useEditorCtx } from '@/components/editor/EditorContextProvider.vue' -import { getNodeName } from '../node' +import { getNodeId } from '../node' const props = defineProps<{ monitor: Monitor @@ -34,7 +34,7 @@ const props = defineProps<{ const uiVariables = useUIVariables() const editorCtx = useEditorCtx() -const nodeName = computed(() => getNodeName(props.monitor)) +const nodeId = computed(() => getNodeId(props.monitor)) const labelTextRef = ref>() const valueTextRef = ref>() const labelTextWidth = ref(0) @@ -47,9 +47,9 @@ async function updateTextWidth() { // text change triggers node-size change, we need to trigger transformer update manually. // It's a Transformer bug that it doesn't update correctly when attached node size changed causing by text content change - props.nodeReadyMap.set(nodeName.value, false) + props.nodeReadyMap.set(nodeId.value, false) await nextTick() - props.nodeReadyMap.set(nodeName.value, true) + props.nodeReadyMap.set(nodeId.value, true) } watch( @@ -67,7 +67,7 @@ onMounted(() => { // Konva warning: Node has no parent. zIndex parameter is ignored. // Konva warning: Unexpected value 2 for zIndex property. zIndex is just index of a node in children of its parent. Expected value is from 0 to 1. // ``` - const zIndex = editorCtx.project.stage.widgetsZorder.indexOf(props.monitor.name) + const zIndex = editorCtx.project.stage.widgetsZorder.indexOf(props.monitor.id) if (zIndex >= 0) { labelTextRef.value!.getNode().zIndex(zIndex) } @@ -100,7 +100,7 @@ const valueWidth = computed(() => valueTextWidth.value + valuePaddingX * 2) const groupConfig = computed(() => { const { visible, x, y, size } = props.monitor return { - nodeName: nodeName.value, + nodeId: nodeId.value, visible, draggable: true, x: props.mapSize.width / 2 + x, @@ -170,6 +170,6 @@ function handleChange(e: KonvaEventObject, action: Action) { function handleMousedown() { editorCtx.project.select({ type: 'stage' }) - editorCtx.project.stage.selectWidget(props.monitor.name) + editorCtx.project.stage.selectWidget(props.monitor.id) } diff --git a/spx-gui/src/components/editor/preview/stage-viewer/widgets/WidgetNode.vue b/spx-gui/src/components/editor/preview/stage-viewer/widgets/WidgetNode.vue index c785a4fbc..ed1c4c73b 100644 --- a/spx-gui/src/components/editor/preview/stage-viewer/widgets/WidgetNode.vue +++ b/spx-gui/src/components/editor/preview/stage-viewer/widgets/WidgetNode.vue @@ -12,7 +12,7 @@ import type { Size } from '@/models/common' import type { Widget } from '@/models/widget' import MonitorNode from './MonitorNode.vue' import { Monitor } from '@/models/widget/monitor' -import { getNodeName } from '../node' +import { getNodeId } from '../node' const props = defineProps<{ widget: Widget @@ -22,8 +22,8 @@ const props = defineProps<{ // TODO: when there are more widget types, we may extract more common logic for reuse watchEffect((onCleanup) => { - const nodeName = getNodeName(props.widget) - props.nodeReadyMap.set(nodeName, true) - onCleanup(() => props.nodeReadyMap.delete(nodeName)) + const nodeId = getNodeId(props.widget) + props.nodeReadyMap.set(nodeId, true) + onCleanup(() => props.nodeReadyMap.delete(nodeId)) }) diff --git a/spx-gui/src/components/editor/sound/SoundPlayer.vue b/spx-gui/src/components/editor/sound/SoundPlayer.vue index baa5f561e..9f63e792a 100644 --- a/spx-gui/src/components/editor/sound/SoundPlayer.vue +++ b/spx-gui/src/components/editor/sound/SoundPlayer.vue @@ -7,12 +7,13 @@ :color="color" :play-handler="handlePlay" :loading="loading" - @stop="handleStop" + @stop="stop" /> diff --git a/spx-gui/src/components/editor/sound/waveform/WaveformPlayer.vue b/spx-gui/src/components/editor/sound/waveform/WaveformPlayer.vue index 26797218f..869762330 100644 --- a/spx-gui/src/components/editor/sound/waveform/WaveformPlayer.vue +++ b/spx-gui/src/components/editor/sound/waveform/WaveformPlayer.vue @@ -18,6 +18,7 @@ import { ref, watch } from 'vue' import WaveformWithControls from './WaveformWithControls.vue' import { getAudioContext, trimAndApplyGainToWavBlob } from '@/utils/audio' import { useAsyncComputed } from '@/utils/utils' +import { registerPlayer } from '@/utils/player-registry' const props = defineProps<{ audioSrc?: string @@ -26,6 +27,8 @@ const props = defineProps<{ gain: number }>() +const registered = registerPlayer(stop) + const play = async () => { if (!audioElement.value) return if (!Number.isFinite(audioElement.value.duration)) { @@ -36,20 +39,24 @@ const play = async () => { return } audioElement.value.currentTime = audioElement.value.duration * props.range.left + registered.onStart() await audioElement.value.play() emit('play') } +function stop() { + if (!audioElement.value) return + audioElement.value.pause() + audioElement.value.currentTime = Number.isFinite(audioElement.value.duration) + ? audioElement.value.duration * props.range.left + : 0 + progress.value = 0 + registered.onStopped() +} + defineExpose({ play, - stop: () => { - if (!audioElement.value) return - audioElement.value.pause() - audioElement.value.currentTime = Number.isFinite(audioElement.value.duration) - ? audioElement.value.duration * props.range.left - : 0 - progress.value = 0 - }, + stop, exportWav: async (): Promise => { if (!props.audioSrc) throw new Error('audioSrc is not provided') const audio = audioElement.value diff --git a/spx-gui/src/components/editor/sprite/AnimationDetail.vue b/spx-gui/src/components/editor/sprite/AnimationDetail.vue index ce4f6942a..c80098892 100644 --- a/spx-gui/src/components/editor/sprite/AnimationDetail.vue +++ b/spx-gui/src/components/editor/sprite/AnimationDetail.vue @@ -6,7 +6,7 @@ :duration="animation.duration" class="animation-player" /> - + @@ -30,7 +30,7 @@ const props = defineProps<{ const editorCtx = useEditorCtx() const renameCostume = useModal(AnimationRenameModal) const sound = computed( - () => editorCtx.project.sounds.find((sound) => sound.name === props.animation.sound) ?? null + () => editorCtx.project.sounds.find((sound) => sound.id === props.animation.sound) ?? null ) const handleRename = useMessageHandle( diff --git a/spx-gui/src/components/editor/sprite/AnimationEditor.vue b/spx-gui/src/components/editor/sprite/AnimationEditor.vue index b79230655..0906d95f0 100644 --- a/spx-gui/src/components/editor/sprite/AnimationEditor.vue +++ b/spx-gui/src/components/editor/sprite/AnimationEditor.vue @@ -13,11 +13,11 @@ diff --git a/spx-gui/src/components/ui/block-items/UIBlockItem.vue b/spx-gui/src/components/ui/block-items/UIBlockItem.vue index f13aefbf9..ba32c190d 100644 --- a/spx-gui/src/components/ui/block-items/UIBlockItem.vue +++ b/spx-gui/src/components/ui/block-items/UIBlockItem.vue @@ -39,17 +39,16 @@ const style = computed(() => ({