Skip to content

Commit

Permalink
Fix some todos (#289)
Browse files Browse the repository at this point in the history
* hooks for file url

* disable eslint rule no-redeclare

* timeout for api client

* remove useless TODOs

* remove userInfo assertion in validateName

* keep consistent order for sprites & sounds

* unit test for filename & sripExt

* typo

* use file type to construct blob
  • Loading branch information
nighca authored Apr 10, 2024
1 parent abf12ed commit d68add0
Show file tree
Hide file tree
Showing 19 changed files with 203 additions and 132 deletions.
3 changes: 2 additions & 1 deletion spx-gui/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = {
shallowOnly: true
}
],
'no-console': ['warn', { allow: ['warn', 'error'] }]
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-redeclare': 'off'
}
}
25 changes: 23 additions & 2 deletions spx-gui/src/apis/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
*/

import { apiBaseUrl } from '@/utils/env'
import { Exception } from '@/utils/exception'
import { ApiException } from './exception'

export type RequestOptions = {
method: string
headers?: Headers
/** Timeout duration in milisecond, from request-sent to server-response-got */
timeout?: number
}

/** Response body when exception encountered for API calling */
Expand All @@ -25,6 +28,17 @@ function isApiExceptionPayload(body: any): body is ApiExceptionPayload {
/** AuthProvider provide value for header Authorization */
export type AuthProvider = () => Promise<string | null>

// milisecond
const defaultTimeout = 10 * 1000

class TimeoutException extends Exception {
name = 'TimeoutException'
userMessage = { en: 'request timeout', zh: '请求超时' }
constructor() {
super('request timeout')
}
}

export class Client {
private getAuth: AuthProvider = async () => null

Expand All @@ -46,7 +60,6 @@ export class Client {
}

private async handleResponse(resp: Response): Promise<unknown> {
// TODO: timeout
if (!resp.ok) {
const body = await resp.json()
if (!isApiExceptionPayload(body)) {
Expand All @@ -59,7 +72,15 @@ export class Client {

private async request(url: string, payload: unknown, options?: RequestOptions) {
const req = await this.prepareRequest(url, payload, options)
const resp = await fetch(req)
const timeout = options?.timeout ?? defaultTimeout
const ctrl = new AbortController()
const resp = await Promise.race([
fetch(req, { signal: ctrl.signal }),
new Promise<never>((_, reject) => setTimeout(() => reject(new TimeoutException()), timeout))
]).catch((e) => {
if (e instanceof TimeoutException) ctrl.abort()
throw e
})
return this.handleResponse(resp)
}

Expand Down
2 changes: 0 additions & 2 deletions spx-gui/src/components/editor/EditorContextProvider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,12 @@ const props = defineProps<{
const selectedRef = ref<Selected | null>(null)
/* eslint-disable no-redeclare */ // TODO: there should be no need to configure this
function select(selected: null): void
function select(type: 'stage'): void
function select(type: 'sprite' | 'sound', name: string): void
function select(type: any, name?: string) {
selectedRef.value = name == null ? { type } : { type, name }
}
/* eslint-enable no-redeclare */
// When sprite name changed, we lose the selected state
// TODO: consider moving selected to model Project, so we can deal with renaming easily
Expand Down
43 changes: 26 additions & 17 deletions spx-gui/src/components/editor/EditorHomepage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@
</header>
<main v-if="userStore.userInfo" class="editor-main">
<template v-if="projectName">
<EditorContextProvider v-if="project" :project="project" :user-info="userStore.userInfo">
<div v-if="isLoading" class="loading-wrapper">
<NSpin size="large" />
</div>
<div v-else-if="error != null">
{{ _t(error.userMessage) }}
</div>
<EditorContextProvider
v-else-if="project != null"
:project="project"
:user-info="userStore.userInfo"
>
<ProjectEditor />
</EditorContextProvider>
<NSpin v-else size="large" />
<div v-else>TODO</div>
</template>
<template v-else>
<ProjectList @selected="handleSelected" />
Expand All @@ -19,7 +29,7 @@
</template>

<script setup lang="ts">
import { watchEffect, ref, watch } from 'vue'
import { watchEffect, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { NButton, NSpin } from 'naive-ui'
import type { ProjectData } from '@/apis/project'
Expand All @@ -28,10 +38,10 @@ import { Project } from '@/models/project'
import TopNav from '@/components/top-nav/TopNav.vue'
import ProjectList from '@/components/project/ProjectList.vue'
import { useCreateProject } from '@/components/project'
import ProjectEditor from './ProjectEditor.vue'
import { getProjectEditorRoute } from '@/router'
import { computed } from 'vue'
import { useQuery } from '@/utils/exception'
import EditorContextProvider from './EditorContextProvider.vue'
import ProjectEditor from './ProjectEditor.vue'
const localCacheKey = 'TODO_GOPLUS_BUILDER_CACHED_PROJECT'
Expand All @@ -46,26 +56,25 @@ watchEffect(() => {
const router = useRouter()
const createProject = useCreateProject()
const project = ref<Project | null>(null)
const projectName = computed(
() => router.currentRoute.value.params.projectName as string | undefined
)
watch(
() => projectName.value,
async (projectName) => {
if (userStore.userInfo == null) return
if (projectName == null) {
project.value = null
return
}
const {
data: project,
isFetching: isLoading,
error
} = useQuery(
async () => {
if (userStore.userInfo == null) return null
if (projectName.value == null) return null
// TODO: UI logic to handle conflicts when there are local cache
const newProject = new Project()
await newProject.loadFromCloud(userStore.userInfo.name, projectName)
await newProject.loadFromCloud(userStore.userInfo.name, projectName.value)
newProject.syncToLocalCache(localCacheKey)
project.value = newProject
return newProject
},
{ immediate: true }
{ en: 'Load project failed', zh: '加载项目失败' }
)
watch(
Expand Down
11 changes: 3 additions & 8 deletions spx-gui/src/components/editor/panels/sprite/SpriteItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
</template>

<script setup lang="ts">
import { ref, effect, computed } from 'vue'
import { computed } from 'vue'
import { useFileUrl } from '@/utils/file'
import { Sprite } from '@/models/sprite'
const props = defineProps<{
Expand All @@ -19,13 +20,7 @@ const emit = defineEmits<{
remove: []
}>()
const imgSrc = ref<string | null>(null)
effect(async () => {
const img = props.sprite.costume?.img
imgSrc.value = img != null ? await img.url() : null // TODO: race condition
})
const imgSrc = useFileUrl(() => props.sprite.costume?.img)
const imgStyle = computed(() => imgSrc.value && { backgroundImage: `url("${imgSrc.value}")` })
</script>

Expand Down
14 changes: 3 additions & 11 deletions spx-gui/src/components/editor/panels/stage/StagePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
</template>

<script setup lang="ts">
import { computed, ref, effect } from 'vue'
import { computed } from 'vue'
import { NDropdown, NButton } from 'naive-ui'
import { useAddAssetFromLibrary, useAddAssetToLibrary } from '@/components/library'
import { useMessageHandle } from '@/utils/exception'
import { useI18n } from '@/utils/i18n'
import { AssetType } from '@/apis/asset'
import { selectImg } from '@/utils/file'
import { selectImg, useFileUrl } from '@/utils/file'
import { fromNativeFile } from '@/models/common/file'
import { Backdrop } from '@/models/backdrop'
import { stripExt } from '@/utils/path'
Expand All @@ -40,15 +40,7 @@ function select() {
}
const backdrop = computed(() => editorCtx.project.stage.backdrop)
// TODO: we may need a special img component for [File](src/models/common/file.ts)
const imgSrc = ref<string | null>(null)
effect(async () => {
const img = backdrop.value?.img
imgSrc.value = img != null ? await img.url() : null // TODO: race condition
})
const imgSrc = useFileUrl(() => backdrop.value?.img)
const imgStyle = computed(() => imgSrc.value && { backgroundImage: `url("${imgSrc.value}")` })
const handleUpload = useMessageHandle(
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/components/editor/panels/todo/AssetAddBtn.vue
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ const beforeUpload = async (
if (uploadFile.file != null) {
const file = await fromNativeFile(uploadFile.file)
let fileName = uploadFile.name
let assetName = stripExt(fileName) // TODO: naming conflict
let assetName = stripExt(fileName)
switch (fileType) {
case 'backdrop': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
</v-layer>
</template>
<script setup lang="ts">
import { defineProps, watch, ref } from 'vue'
import { defineProps } from 'vue'
import { useImgFile } from '@/utils/file'
import type { Stage } from '@/models/stage'
import type { Size } from '@/models/common'
Expand All @@ -35,18 +36,5 @@ const props = defineProps<{
stage: Stage
}>()
const image = ref<HTMLImageElement>()
watch(
() => props.stage.backdrop?.img,
async (backdropImg) => {
image.value?.remove()
if (backdropImg != null) {
const _image = new window.Image()
_image.src = await backdropImg.url()
image.value = _image
}
},
{ immediate: true }
)
const image = useImgFile(() => props.stage.backdrop?.img)
</script>
22 changes: 3 additions & 19 deletions spx-gui/src/components/editor/preview/stage-viewer/Costume.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import type { Sprite } from '@/models/sprite'
import type { SpriteDragMoveEvent, SpriteApperanceChangeEvent } from './common'
import type { Rect } from 'konva/lib/shapes/Rect'
import type { Size } from '@/models/common'
import { useImgFile } from '@/utils/file'
// ----------props & emit------------------------------------
const props = defineProps<{
sprite: Sprite
Expand All @@ -57,7 +59,7 @@ const displayScale = computed(
)
// ----------data related -----------------------------------
const image = ref<HTMLImageElement>()
const image = useImgFile(() => currentCostume.value?.img)
const costume = ref()
// ----------computed properties-----------------------------
// Computed spx's sprite position to konva's relative position by about changing sprite postion
Expand Down Expand Up @@ -88,24 +90,6 @@ watch(
}
)
watch(
() => currentCostume.value,
async (new_costume) => {
if (new_costume != null) {
const _image = new window.Image()
_image.src = await new_costume.img.url()
_image.onload = () => {
image.value = _image
}
} else {
image.value?.remove()
}
},
{
immediate: true
}
)
// ----------methods-----------------------------------------
/**
* @description: map spx's sprite position to konva's relative position
Expand Down
3 changes: 2 additions & 1 deletion spx-gui/src/components/project/ProjectCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ async function validateName(name: string): Promise<ValidationResult> {
}
// check naming conflict
const username = userStore.userInfo!.name // TODO: remove `!` here
if (userStore.userInfo == null) throw new Error('login required')
const username = userStore.userInfo.name
const existedProject = await getProject(username, name).catch((e) => {
if (e instanceof ApiException && e.code === ApiExceptionCode.errorNotFound) return null
throw e
Expand Down
10 changes: 5 additions & 5 deletions spx-gui/src/models/common/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ function parseProjectData({ files: fileUrls, ...metadata }: ProjectData) {

export async function uploadFiles(files: Files): Promise<FileCollection> {
const fileUrls: FileCollection = {}
await Promise.all(
Object.keys(files).map(async (path) => {
// TODO: keep the files' order
fileUrls[path] = await uploadFile(files[path]!)
})
const entries = await Promise.all(
Object.keys(files).map(async (path) => [path, await uploadFile(files[path]!)] as const)
)
for (const [path, fileUrl] of entries) {
fileUrls[path] = fileUrl
}
return fileUrls
}

Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/models/common/disposable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

export type Disposer = () => void

export abstract class Disposble {
export class Disposble {
_disposers: Disposer[] = []

addDisposer(disposer: Disposer) {
Expand Down
14 changes: 11 additions & 3 deletions spx-gui/src/models/common/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import { getMimeFromExt } from '@/utils/file'
import { extname } from '@/utils/path'
import type { Disposer } from './disposable'
import { Cancelled } from '@/utils/exception'

export type Options = {
/** MIME type of file */
Expand Down Expand Up @@ -40,10 +42,16 @@ export class File {
}))
}

// TODO: remember to do URL.revokeObjectURL
async url() {
async url(onCleanup: (disposer: Disposer) => void) {
let cancelled = false
onCleanup(() => {
cancelled = true
})
const ab = await this.arrayBuffer()
return URL.createObjectURL(new Blob([ab]))
if (cancelled) throw new Cancelled()
const url = URL.createObjectURL(new Blob([ab], { type: this.type }))
onCleanup(() => URL.revokeObjectURL(url))
return url
}
}

Expand Down
Loading

0 comments on commit d68add0

Please sign in to comment.