Skip to content

Commit

Permalink
refactor(spx-gui): remove "codeFile" concept and optimize project sav…
Browse files Browse the repository at this point in the history
…ing (#630)

- Removed the "codeFile" concept from sprite and stage models introduced
  in #508. Since #617, lazy-loading for plain text files is no longer
  needed.
- Avoided redundant `parseProjectData` in `@/models/common/cloud.save`.

Fixes #369
  • Loading branch information
aofei authored Jul 3, 2024
1 parent 988e6da commit bccc791
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 64 deletions.
3 changes: 1 addition & 2 deletions spx-gui/src/components/editor/sprite/SpriteEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useAsyncComputed } from '@/utils/utils'
import type { Sprite } from '@/models/sprite'
import { UITabs, UITab } from '@/components/ui'
import CodeEditor from '../code-editor/CodeEditor.vue'
Expand All @@ -39,7 +38,7 @@ const props = defineProps<{
const editorCtx = useEditorCtx()
const selectedTab = ref<'code' | 'costumes'>('code')
const codeEditor = ref<InstanceType<typeof CodeEditor>>()
const code = useAsyncComputed(() => props.sprite.getCode())
const code = computed(() => props.sprite.code)
// use `computed` to keep reference-equal for `mergeable`, see details in project history
const actionUpdateCode = computed(() => ({
Expand Down
5 changes: 2 additions & 3 deletions spx-gui/src/components/editor/stage/StageEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useAsyncComputed } from '@/utils/utils'
import { computed, ref } from 'vue'
import type { Stage } from '@/models/stage'
import { UITabs, UITab } from '@/components/ui'
import CodeEditor from '../code-editor/CodeEditor.vue'
Expand All @@ -41,7 +40,7 @@ const props = defineProps<{
const editorCtx = useEditorCtx()
const selectedTab = ref<'code' | 'backdrops'>('code')
const codeEditor = ref<InstanceType<typeof CodeEditor>>()
const code = useAsyncComputed(() => props.stage.getCode())
const code = computed(() => props.stage.code)
const action = {
name: { en: 'Update stage code', zh: '修改舞台代码' },
Expand Down
45 changes: 24 additions & 21 deletions spx-gui/src/models/common/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function save(metadata: Metadata, files: Files) {
const projectData = await (id != null
? updateProject(owner, name, { isPublic, files: fileCollection })
: addProject({ name, isPublic, files: fileCollection }))
return await parseProjectData(projectData)
return { metadata: projectData, files }
}

export async function parseProjectData({ files: fileCollection, ...metadata }: ProjectData) {
Expand All @@ -47,20 +47,11 @@ export async function parseProjectData({ files: fileCollection, ...metadata }: P
export async function saveFiles(
files: Files
): Promise<{ fileCollection: FileCollection; fileCollectionHash: string }> {
const fileCollection: FileCollection = {}
for (const [path, file] of Object.entries(files)) {
if (!file) continue
if (inlineableFileTypes.includes(file.type)) {
// 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

const urlEncodedContent = encodeURIComponent(await toText(file))
fileCollection[path] = `${fileUniversalUrlSchemes.data}${mimeType},${urlEncodedContent}`
} else {
fileCollection[path] = await uploadFile(file)
}
}
const fileCollection = Object.fromEntries(
await Promise.all(
Object.keys(files).map(async (path) => [path, await saveFile(files[path]!)] as const)
)
)
const fileCollectionHash = await hashFileCollection(fileCollection)
return { fileCollection, fileCollectionHash }
}
Expand Down Expand Up @@ -109,15 +100,27 @@ function createFileWithWebUrl(name: string, webUrl: WebUrl) {
})
}

async function uploadFile(file: File) {
const uploadedUrl = getUniversalUrl(file)
if (uploadedUrl != null) return uploadedUrl
const url = await uploadToKodo(file)
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)
setUniversalUrl(file, url)
return url
}

type QiniuUploadRes = {
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

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

type KodoUploadRes = {
key: string
hash: string
}
Expand All @@ -136,7 +139,7 @@ async function uploadToKodo(file: File): Promise<UniversalUrl> {
},
{ region: region as any }
)
const { key } = await new Promise<QiniuUploadRes>((resolve, reject) => {
const { key } = await new Promise<KodoUploadRes>((resolve, reject) => {
observable.subscribe({
error(e) {
reject(e)
Expand Down
8 changes: 4 additions & 4 deletions spx-gui/src/models/project/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,25 +109,25 @@ describe('History', () => {

expect(history.getRedoAction()).toBeNull()
expect(history.getUndoAction()).toBe(actionUpdateCode)
expect(await project.sprites[0].getCode()).toBe('code')
expect(project.sprites[0].code).toBe('code')

await history.doAction(actionUpdateCode, () => {
project.sprites[0].setCode('code2')
})

expect(history.getRedoAction()).toBeNull()
expect(history.getUndoAction()).toBe(actionUpdateCode)
expect(await project.sprites[0].getCode()).toBe('code2')
expect(project.sprites[0].code).toBe('code2')

await history.undo()
expect(history.getRedoAction()).toBe(actionUpdateCode)
expect(history.getUndoAction()).toBeNull()
expect(await project.sprites[0].getCode()).toBe('')
expect(project.sprites[0].code).toBe('')

await history.redo()
expect(history.getRedoAction()).toBeNull()
expect(history.getUndoAction()).toBe(actionUpdateCode)
expect(await project.sprites[0].getCode()).toBe('code2')
expect(project.sprites[0].code).toBe('code2')
})

it('should work well with concurrent actions', async () => {
Expand Down
33 changes: 16 additions & 17 deletions spx-gui/src/models/sprite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { reactive } from 'vue'
import { join } from '@/utils/path'
import { fromText, type Files, fromConfig, toText, toConfig, listDirs, File } from './common/file'
import { fromText, type Files, fromConfig, toText, toConfig, listDirs } from './common/file'
import { Disposble } from './common/disposable'
import {
ensureValidAnimationName,
Expand Down Expand Up @@ -62,6 +62,10 @@ export function getSpriteAssetPath(name: string) {
return join(spriteAssetPath, name)
}

function getSpriteCodeFileName(name: string) {
return `${name}.spx`
}

export const spriteConfigFileName = 'index.json'

export class Sprite extends Disposble {
Expand All @@ -77,16 +81,9 @@ export class Sprite extends Disposble {
this.name = name
}

private codeFile: File | null
private get codeFileName() {
return `${this.name}.spx`
}
async getCode() {
if (this.codeFile == null) return ''
return toText(this.codeFile)
}
code: string
setCode(code: string) {
this.codeFile = fromText(this.codeFileName, code)
this.code = code
}

costumes: Costume[]
Expand Down Expand Up @@ -193,10 +190,10 @@ export class Sprite extends Disposble {
this.isDraggable = isDraggable
}

constructor(name: string, codeFile: File | null = null, inits?: SpriteInits) {
constructor(name: string, code: string = '', inits?: SpriteInits) {
super()
this.name = name
this.codeFile = codeFile
this.code = code
this.costumes = []
this.animations = []
this.animationBindings = {
Expand Down Expand Up @@ -246,8 +243,8 @@ export class Sprite extends Disposble {
}

/** Create sprite within builder (by user actions) */
static create(nameBase: string, codeFile?: File, inits?: SpriteInits) {
return new Sprite(getSpriteName(null, nameBase), codeFile, {
static create(nameBase: string, code?: string, inits?: SpriteInits) {
return new Sprite(getSpriteName(null, nameBase), code, {
heading: 90,
x: 0,
y: 0,
Expand All @@ -270,8 +267,9 @@ export class Sprite extends Disposble {
tAnimations,
...inits
} = (await toConfig(configFile)) as RawSpriteConfig
const codeFile = files[name + '.spx']
const sprite = new Sprite(name, codeFile, inits)
const codeFile = files[getSpriteCodeFileName(name)]
const code = codeFile != null ? await toText(codeFile) : ''
const sprite = new Sprite(name, code, inits)
if (costumeConfigs != null) {
const costumes = (costumeConfigs ?? []).map((c) => Costume.load(c, files, pathPrefix))
for (const costume of costumes) {
Expand Down Expand Up @@ -342,7 +340,8 @@ export class Sprite extends Disposble {
animBindings: animBindings
}
if (includeCode) {
files[this.codeFileName] = this.codeFile ?? fromText(this.codeFileName, '')
const codeFileName = getSpriteCodeFileName(this.name)
files[codeFileName] = fromText(codeFileName, this.code)
}
files[`${assetPath}/${spriteConfigFileName}`] = fromConfig(spriteConfigFileName, config)
return files
Expand Down
26 changes: 9 additions & 17 deletions spx-gui/src/models/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { reactive } from 'vue'
import { filename } from '@/utils/path'
import { toText, type Files, fromText, File } from './common/file'
import { toText, type Files, fromText } from './common/file'
import { ensureValidBackdropName } from './common/asset-name'
import type { Size } from './common'
import { Backdrop, type RawBackdropConfig } from './backdrop'
Expand Down Expand Up @@ -44,13 +44,9 @@ const stageCodeFilePath = stageCodeFilePaths[0]
const stageCodeFileName = filename(stageCodeFilePath)

export class Stage {
private codeFile: File | null
async getCode() {
if (this.codeFile == null) return ''
return toText(this.codeFile)
}
code: string
setCode(code: string) {
this.codeFile = fromText(stageCodeFileName, code)
this.code = code
}

backdrops: Backdrop[]
Expand Down Expand Up @@ -113,8 +109,8 @@ export class Stage {
return { width: this.mapWidth, height: this.mapHeight }
}

constructor(codeFile: File | null = null, inits?: Partial<StageInits>) {
this.codeFile = codeFile
constructor(code: string = '', inits?: Partial<StageInits>) {
this.code = code
this.backdrops = []
this.backdropIndex = inits?.backdropIndex ?? 0
this.mapWidth = inits?.mapWidth ?? 480
Expand All @@ -136,13 +132,9 @@ export class Stage {
files: Files
) {
// TODO: empty stage
let codeFile: File | undefined
for (const codeFilePath of stageCodeFilePaths) {
if (files[codeFilePath] == null) continue
codeFile = files[codeFilePath]
break
}
const stage = new Stage(codeFile, {
const codeFilePath = stageCodeFilePaths.find((path) => files[path])
const code = codeFilePath != null ? await toText(files[codeFilePath]!) : ''
const stage = new Stage(code, {
backdropIndex: backdropIndex ?? sceneIndex ?? currentCostumeIndex,
mapWidth: map?.width,
mapHeight: map?.height,
Expand All @@ -160,7 +152,7 @@ export class Stage {
export(): [RawStageConfig, Files] {
const files: Files = {}
const backdropConfigs: RawBackdropConfig[] = []
files[stageCodeFilePath] = this.codeFile ?? fromText(stageCodeFileName, '')
files[stageCodeFilePath] = fromText(stageCodeFileName, this.code)
for (const backdrop of this.backdrops) {
const [backdropConfig, backdropFiles] = backdrop.export()
backdropConfigs.push(backdropConfig)
Expand Down

0 comments on commit bccc791

Please sign in to comment.