diff --git a/spx-gui/env.d.ts b/spx-gui/env.d.ts
deleted file mode 100644
index c09f5f39c..000000000
--- a/spx-gui/env.d.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-///
-
-interface ImportMetaEnv {
- readonly VITE_API_BASE_URL: string
- readonly VITE_PUBLISH_BASE_URL:string
- readonly VITE_CASDOOR_ENDPOINT: string
- readonly VITE_CASDOOR_CLIENT_ID: string
- readonly VITE_CASDOOR_ORGANIZATION_NAME: string
- readonly VITE_CASDOOR_APP_NAME: string
-}
-
-interface ImportMeta {
- readonly env: ImportMetaEnv
-}
diff --git a/spx-gui/src/api/asset.ts b/spx-gui/src/api/asset.ts
index c7d7364d5..2ce125f5e 100644
--- a/spx-gui/src/api/asset.ts
+++ b/spx-gui/src/api/asset.ts
@@ -1,244 +1,74 @@
-/*
- * @Author: Yao xinyue
- * @Date: 2024-01-22 11:17:08
- * @LastEditors: xuning 453594138@qq.com
- * @LastEditTime: 2024-03-14 11:22:33
- * @FilePath: \spx-gui\src\api\asset.ts
- * @Description:
- */
-import { service } from '@/axios'
-import type { Asset, PageAssetResponse } from '@/interface/library.ts' // Adjust the import paths as needed
-import type { ResponseData } from '@/axios'
-import type { AxiosResponse } from 'axios'
-import { PublicStatus } from '@/class/project'
+import type { ByPage, PaginationParams, FileCollection } from './common'
+import { client, IsPublic } from './common'
-export enum PublishState {
- NotPublished = -1,
- PrivateLibrary = 0,
- PublicAndPrivateLibrary = 1
-}
-
-/**
- * Fetches a list of assets
- *
- * @param assetLibraryType 'public' / 'private';
- * @param pageIndex The index of the page to retrieve in a paginated list.
- * @param pageSize The number of assets to retrieve per page.
- * @param assetType The type of the asset. See src/constant/constant.ts for details.
- * @param category (Optional) The category of the assets to filter by.
- * @param isOrderByTime (Optional) Whether to order assets by time.
- * @param isOrderByHot (Optional) Whether to order assets by popularity.
- * @returns PageAssetResponse
- */
-export function getAssetList({
- isPublic,
- pageIndex,
- pageSize,
- assetType,
- category,
- isOrderByTime,
- isOrderByHot,
- author
-}: {
- pageIndex: number
- pageSize: number
- assetType: number
- category?: string
- isOrderByTime?: boolean
- isOrderByHot?: boolean
- isPublic?: PublicStatus
- author?: string
-}): Promise {
- const baseAssetUrl = '/assets/list'
- const params = new URLSearchParams()
- params.append('pageIndex', pageIndex.toString())
- params.append('pageSize', pageSize.toString())
- params.append('assetType', assetType.toString())
-
- if (isPublic != null) {
- params.append('isPublic', isPublic.toString())
- }
- if (category) {
- params.append('category', category)
- }
- if (isOrderByTime) {
- params.append('isOrderByTime', '1')
- }
- if (isOrderByHot) {
- params.append('isOrderByHot', '1')
- }
- if (author) {
- params.append('author', author)
- }
-
- const url = `${baseAssetUrl}?${params.toString()}`
+export { IsPublic }
- return service({
- url: url,
- method: 'get'
- })
+export enum AssetType {
+ Sprite = 0,
+ Backdrop = 1,
+ Sound = 2
}
-/**
- * Fetches a single asset
- *
- * @param id
- * @returns Asset
- */
-export function getAsset(id: number): Promise {
- const url = `/asset/${id}`
- return service({
- url: url,
- method: 'get'
- })
+export type AssetData = {
+ // Globally unique ID
+ id: string
+ // Name to display
+ displayName: string
+ // Name of asset owner
+ owner: string
+ // Asset Category
+ category: string
+ // Public status
+ isPublic: IsPublic
+ // Files the asset contains
+ files: FileCollection
+ // Preview URL for the asset, e.g., a gif for a sprite
+ preview: string
+ // Asset Type
+ assetType: AssetType
+ // Click count of the asset
+ clickCount: number
}
-/**
- * @description: Search Asset by name.
- * @param {string} search
- * @param {number} assetType
- * @return { SearchAssetResponse }
- */
-export function searchAssetByName(
- pageIndex: number,
- pageSize: number,
- search: string,
- assetType: number
-): Promise {
- const baseAssetUrl = `/assets/search`
+export type AddAssetParams = Pick
- const params = new URLSearchParams()
- params.append('pageIndex', pageIndex.toString())
- params.append('pageSize', pageSize.toString())
- params.append('search', search)
- params.append('assetType', assetType.toString())
+export function addAsset(params: AddAssetParams) {
+ return client.post('/asset', params) as Promise
+}
- const url = `${baseAssetUrl}?${params.toString()}`
+export type UpdateAssetParams = AddAssetParams
- return service({
- url: url,
- method: 'get'
- })
+export function updateAsset(id: string, params: UpdateAssetParams) {
+ return client.put(`/asset/${encodeURIComponent(id)}`, params) as Promise
}
-/**
- * Save asset
- *
- * @param id
- * @param name
- * @param uid
- * @param category
- * @param isPublic
- * @param assetType The type of the asset. See src/constant/constant.ts for details.
- * @param file
- */
-export async function saveAsset(
- id: number,
- name: string,
- uid: number,
- category: string,
- isPublic: number,
- assetType: number,
- file: File
-): Promise {
- const url = '/asset/save'
- const formData = new FormData()
- formData.append('id', id.toString())
- formData.append('name', name)
- formData.append('uid', uid.toString())
- formData.append('category', category)
- formData.append('isPublic', isPublic ? '1' : '0')
- formData.append('assetType', assetType.toString())
- formData.append('file', file)
-
- return service({
- url: url,
- method: 'post',
- data: formData,
- headers: {
- 'Content-Type': 'multipart/form-data'
- }
- })
+export function deleteAsset(id: string) {
+ return client.delete(`/asset/${encodeURIComponent(id)}`) as Promise
}
-/**
- * @description: Add asset click count.
- * @param id
- * @param assetType The type of the asset. See src/constant/constant.ts for details.
- * @return {Promise>>}
- */
-export function addAssetClickCount(id: number): Promise>> {
- const url = `/asset/${id}/click-count`
- return service({
- url: url,
- method: 'post'
- })
+export enum ListAssetParamOrderBy {
+ Default = 'default',
+ TimeDesc = 'time',
+ ClickCountDesc = 'clickCount'
}
-/**
- * @description: Publish asset to library.
- * @param { string } name - sprite name named by user.
- * @param { File[] } files - sprite costumes files, saved to show in lib.
- * @param { string } assetType - sprite assetType, 0: sprite, 1: backdrop, 2: sound
- * @param { PublishState } publishState - The publishing state of the asset. -1: not publish, 0: private lib, 1: public lib.
- * @param { string|undefined } [gif] - Optional. The address of the sprite's GIF.
- * Only provide this parameter if there is more than one file.
- * It is used to display in the library when the sprite is hovering.
- * @param { string|undefined } category - the category of the sprite(used to classify in library).
- * @return { Promise } - The result of the publishing operation.
- */
-export function publishAsset(
- name: string,
- files: File[],
- assetType: number,
- publishState: PublishState,
- previewAddress?: string,
+export type ListAssetParams = PaginationParams & {
+ keyword?: string
+ assetType?: AssetType
category?: string
-): Promise {
- const url = `/asset`
- const formData = new FormData()
- formData.append('name', name)
- formData.append('assetType', assetType.toString())
- files.forEach((file) => {
- formData.append('files', file)
- })
- if (previewAddress) {
- formData.append('previewAddress', previewAddress)
- }
- if (category) {
- formData.append('category', category)
- }
- formData.append('publishState', publishState.toString())
+ isPublic?: IsPublic
+ owner?: string
+ orderBy?: ListAssetParamOrderBy
+}
- // Assume `service` is a predefined function for handling HTTP requests.
- return service({
- url: url,
- method: 'post',
- data: formData,
- headers: {
- 'Content-Type': 'multipart/form-data'
- }
- })
+export function listAsset(params?: ListAssetParams) {
+ return client.get('/assets/list', params) as Promise>
}
-/**
- * @description: generate gif by costumes files
- * @param {File} files - sprite costumes files
- * @return {string} get sprites gif address.
- */
-export function generateGifByCostumes(files: File[]): Promise>> {
- const url = `/util/to-gif`
- const formData = new FormData()
- files.forEach((file) => {
- formData.append('files', file)
- })
+export function getAsset(id: string) {
+ return client.get(`/asset/${encodeURIComponent(id)}`) as Promise
+}
- return service({
- url: url,
- method: 'post',
- data: formData,
- headers: {
- 'Content-Type': 'multipart/form-data'
- }
- })
+export function increaseAssetClickCount(id: string) {
+ return client.post(`/asset/${encodeURIComponent(id)}/click`) as Promise
}
diff --git a/spx-gui/src/api/common/client.ts b/spx-gui/src/api/common/client.ts
new file mode 100644
index 000000000..1ab9481a8
--- /dev/null
+++ b/spx-gui/src/api/common/client.ts
@@ -0,0 +1,111 @@
+/**
+ * @desc Client (& error) definition for spx-backend APIs
+ */
+
+import { apiBaseUrl } from '@/util/env'
+
+export type RequestOptions = {
+ method: string
+ headers?: Headers
+}
+
+/** Response body when error encountered for API calling */
+export type ApiErrorPayload = {
+ /** Code for program comsuming */
+ code: number
+ /** Message for developer reading */
+ msg: string
+}
+
+function isApiErrorPayload(body: any): body is ApiErrorPayload {
+ return body && (typeof body.code === 'number') && (typeof body.msg === 'string')
+}
+
+export class ApiError extends Error {
+ name = 'ApiError'
+ constructor(
+ public code: number,
+ msg: string
+ ) {
+ super(`[${code}] ${msg}`)
+ }
+}
+
+/** AuthProvider provide value for header Authorization */
+export type AuthProvider = () => string | null | Promise
+
+export class Client {
+
+ private getAuth: AuthProvider = () => null
+
+ setAuthProvider(provider: AuthProvider) {
+ this.getAuth = provider
+ }
+
+ private async prepareRequest(url: string, payload: unknown, options?: RequestOptions) {
+ url = apiBaseUrl + url
+ const method = options?.method ?? 'GET'
+ const body = payload != null ? JSON.stringify(payload) : null
+ const authorization = await this.getAuth()
+ const headers = options?.headers ?? new Headers()
+ headers.set('Content-Type', 'application/json')
+ if (authorization != null) {
+ headers.set('Authorization', authorization)
+ }
+ return new Request(url, { method, headers, body })
+ }
+
+ private async handleResponse(resp: Response): Promise {
+ if (!resp.ok) {
+ const body = await resp.json()
+ if (!isApiErrorPayload(body)) {
+ throw new Error('api call failed')
+ }
+ throw new ApiError(body.code, body.msg)
+ }
+ return resp.json()
+ }
+
+ private async request(url: string, payload: unknown, options?: RequestOptions) {
+ const req = await this.prepareRequest(url, payload, options)
+ const resp = await fetch(req)
+ return this.handleResponse(resp)
+ }
+
+ get(url: string, params?: QueryParams) {
+ url = params == null ? url : withQueryParams(url, params)
+ return this.request(url, null, { method: 'GET' })
+ }
+
+ post(url: string, payload?: unknown) {
+ return this.request(url, payload, { method: 'POST' })
+ }
+
+ put(url: string, payload?: unknown) {
+ return this.request(url, payload, { method: 'PUT' })
+ }
+
+ delete(url: string, params?: QueryParams) {
+ url = params == null ? url : withQueryParams(url, params)
+ return this.request(url, null, { method: 'DELETE' })
+ }
+
+}
+
+type QueryParams = {
+ [k: string]: unknown
+}
+
+function withQueryParams(url: string, params: QueryParams) {
+ const usp = new URLSearchParams()
+ Object.keys(params).forEach(k => {
+ const v = params[k]
+ if (v != null) usp.append(k, v + '')
+ })
+ const querystring = usp.toString()
+ if (querystring !== '') {
+ const sep = url.includes('?') ? '&' : '?'
+ url = url + sep + querystring
+ }
+ return url
+}
diff --git a/spx-gui/src/api/common/index.ts b/spx-gui/src/api/common/index.ts
new file mode 100644
index 000000000..672493bf8
--- /dev/null
+++ b/spx-gui/src/api/common/index.ts
@@ -0,0 +1,25 @@
+import { Client } from './client'
+
+export type PaginationParams = {
+ pageSize?: number
+ pageIndex?: number
+}
+
+export type ByPage = {
+ total: number
+ data: T[]
+}
+
+export const OwnerAll = '*'
+
+export enum IsPublic {
+ Personal = 0,
+ Public = 1
+}
+
+/** Map from relative path to URL */
+export type FileCollection = {
+ [path: string]: string
+}
+
+export const client = new Client()
diff --git a/spx-gui/src/api/project.ts b/spx-gui/src/api/project.ts
index 7122dfd57..5daad5a6f 100644
--- a/spx-gui/src/api/project.ts
+++ b/spx-gui/src/api/project.ts
@@ -1,110 +1,64 @@
-import type { PageData, Project } from '@/interface/library'
-import { service } from '@/axios'
-import type { ResponseData } from '@/axios'
-import type { FormatResponse } from '@/components/code-editor'
-import type { AxiosResponse } from 'axios'
-import { PublicStatus } from '@/class/project'
+import type { ByPage, PaginationParams, FileCollection } from './common'
+import { client, IsPublic } from './common'
-/**
- * Saves a project.
- *
- * @param name The name of the project.
- * @param file The code file(zip) to be uploaded.
- * @param id
- * @returns Project
- */
-export async function saveProject(name: string, file: File, id?: string): Promise {
- const url = '/project'
- const formData = new FormData()
- formData.append('name', name)
- formData.append('file', file)
- id && formData.append('id', id)
+export { IsPublic }
- const res: AxiosResponse> = await service({
- url: url,
- method: 'post',
- data: formData,
- headers: {
- 'Content-Type': 'multipart/form-data'
- }
- })
- if (res.data.code >= 200 && res.data.code < 300) {
- return res.data.data
- } else {
- throw new Error(res.data.msg)
- }
+export type { FileCollection }
+
+export enum ProjectDataType {
+ Sprite = 0,
+ Backdrop = 1,
+ Sound = 2
+}
+
+export type ProjectData = {
+ // Globally Unique ID
+ id: string
+ // Project name, unique for projects of same owner
+ name: string
+ // Name of project owner
+ owner: string
+ // Public status
+ isPublic: IsPublic
+ // Files the project contains
+ files: FileCollection
+ // Project version
+ version: number
+ // Create time
+ cTime: string
+ // Update time
+ uTime: string
+}
+
+export type AddProjectParams = Pick
+
+export function addProject(params: AddProjectParams) {
+ return client.post('/project', params) as Promise
}
-/**
- * Fetches a list of projects.
- * @param pageIndex The index of the page to retrieve in a paginated list.
- * @param pageSize The number of projects to retrieve per page.
- * @param isPublic Public projects or user projects.
- * @returns Project[]
- */
-export async function getProjects(
- pageIndex: number,
- pageSize: number,
- isPublic?: PublicStatus,
- author?: string
-): Promise> {
- const baseUrl = `/projects/list`
- const params = new URLSearchParams()
- params.append('pageIndex', String(pageIndex))
- params.append('pageSize', String(pageSize))
- isPublic !== undefined && params.append('isPublic', String(isPublic))
- author && params.append('author', author)
- const url = `${baseUrl}?${params.toString()}`
- return service({ url: url, method: 'get' }).then((res) => res.data.data)
+export type UpdateProjectParams = Pick
+
+function encode(owner: string, name: string) {
+ return `${encodeURIComponent(owner)}/${encodeURIComponent(name)}`
}
-/**
- * Fetches a single project.
- * @param id The id of the project
- * @returns Project
- */
-export async function getProject(id: string): Promise {
- const url = `/project/${id}`
- return service({ url: url, method: 'get' }).then((res) => res.data.data)
+export function updateProject(owner: string, name: string, params: UpdateProjectParams) {
+ return client.put(`/project/${encode(owner, name)}`, params) as Promise
}
-/**
- * Removes a project.
- * @param id The id of the project
- * @returns string
- */
-export async function removeProject(id: string): Promise {
- const url = `/project/${id}`
- return service({ url: url, method: 'delete' }).then((res) => res.data.data)
+export function deleteProject(owner: string, name: string) {
+ return client.delete(`/project/${encode(owner, name)}`) as Promise
}
-/**
- * Update project isPublic status.
- * @param id project id that will be public
- * @returns
- */
-export async function updateProjectIsPublic(id: string, status: PublicStatus): Promise {
- const url = `/project/${id}/is-public?isPublic=${status}`
- return service({ url: url, method: 'put' }).then((res) => res.data.data)
+export type ListProjectParams = PaginationParams & {
+ isPublic?: IsPublic
+ owner?: string
}
-/**
- * Format spx code
- *
- * @param body The string content to be formatted.
- * @returns string
- */
-export function formatSpxCode(body: string): Promise>> {
- const url = '/util/fmt'
- const formData = new FormData()
- formData.append('body', body)
+export function listProject(params?: ListProjectParams) {
+ return client.get('/projects/list', params) as Promise>
+}
- return service({
- url: url,
- method: 'post',
- data: formData,
- headers: {
- 'Content-Type': 'multipart/form-data'
- }
- })
+export function getProject(owner: string, name: string) {
+ return client.get(`/project/${encode(owner, name)}`) as Promise
}
diff --git a/spx-gui/src/api/test.ts b/spx-gui/src/api/test.ts
new file mode 100644
index 000000000..0ff261a5c
--- /dev/null
+++ b/spx-gui/src/api/test.ts
@@ -0,0 +1,254 @@
+import { IsPublic } from './common';
+import * as assetApis from './asset-2'
+import * as projectApis from './project-2'
+
+;(window as any).assetApis = assetApis
+;(window as any).projectApis = projectApis
+
+function is(src: T, target: T) {
+ if (src !== target) throw new Error('not equal')
+}
+
+;(window as any).testAsset = async function testAsset() {
+ {
+ const { data: assets } = await assetApis.listAsset({ pageSize: 100 })
+ for (const p of assets) {
+ await assetApis.deleteAsset(p.id)
+ }
+ }
+ {
+ const res = await assetApis.listAsset()
+ is(res.total, 0)
+ is(res.data.length, 0)
+ }
+ {
+ const res = await assetApis.listAsset({ owner: "*" })
+ is(res.total, 0)
+ is(res.data.length, 0)
+ }
+ {
+ const res = await assetApis.listAsset({ owner: "xxx" })
+ is(res.total, 0)
+ is(res.data.length, 0)
+ }
+ {
+ const p = await assetApis.addAsset({
+ displayName: 't1',
+ isPublic: IsPublic.Public,
+ files: { a: 'aaa' },
+ assetType: assetApis.AssetType.Sprite,
+ category: 'test',
+ preview: 'a'
+ })
+ is(p.displayName, 't1')
+ is(p.isPublic, IsPublic.Public)
+ is(p.files.a, 'aaa')
+ is(p.assetType, assetApis.AssetType.Sprite)
+ is(p.category, 'test')
+ is(p.preview, 'a')
+ is(p.clickCount, 0)
+ }
+ let t2: assetApis.AssetData
+ {
+ const p = t2 = await assetApis.addAsset({
+ displayName: 't2',
+ isPublic: IsPublic.Personal,
+ files: { a: 'aaa', b: 'bbb' },
+ assetType: assetApis.AssetType.Backdrop,
+ category: 'test',
+ preview: 'b'
+ })
+ is(p.displayName, 't2')
+ is(p.isPublic, IsPublic.Personal)
+ is(p.files.a, 'aaa')
+ is(p.files.b, 'bbb')
+ is(p.assetType, assetApis.AssetType.Backdrop)
+ is(p.category, 'test')
+ is(p.preview, 'b')
+ is(p.clickCount, 0)
+ }
+ {
+ const p = await assetApis.addAsset({
+ displayName: 't3',
+ isPublic: IsPublic.Public,
+ files: {},
+ assetType: assetApis.AssetType.Sound,
+ category: 'test',
+ preview: 'c'
+ })
+ is(p.displayName, 't3')
+ is(p.isPublic, IsPublic.Public)
+ is(JSON.stringify(p.files), '{}')
+ is(p.assetType, assetApis.AssetType.Sound)
+ is(p.category, 'test')
+ is(p.preview, 'c')
+ is(p.clickCount, 0)
+ }
+ {
+ const p = await assetApis.addAsset({
+ displayName: 't4',
+ isPublic: IsPublic.Personal,
+ files: {},
+ assetType: assetApis.AssetType.Sound,
+ category: 'test-2',
+ preview: 'd'
+ })
+ is(p.displayName, 't4')
+ is(p.isPublic, IsPublic.Personal)
+ is(JSON.stringify(p.files), '{}')
+ is(p.assetType, assetApis.AssetType.Sound)
+ is(p.category, 'test-2')
+ is(p.preview, 'd')
+ is(p.clickCount, 0)
+ }
+ {
+ const { total, data } = await assetApis.listAsset()
+ is(total, 4)
+ is(data.length, 4)
+ is(data.map(p => p.displayName).join(','), 't1,t2,t3,t4')
+ }
+ {
+ const { total, data } = await assetApis.listAsset({ pageSize: 2 })
+ is(total, 4)
+ is(data.length, 2)
+ is(data.map(p => p.displayName).join(','), 't1,t2')
+ }
+ {
+ const { total, data } = await assetApis.listAsset({ pageIndex: 2, pageSize: 10 })
+ is(total, 4)
+ is(data.length, 0)
+ }
+ {
+ const { total, data } = await assetApis.listAsset({ pageIndex: 2, pageSize: 2 })
+ is(total, 4)
+ is(data.length, 2)
+ is(data.map(p => p.displayName).join(','), 't3,t4')
+ }
+ {
+ const p = await assetApis.updateAsset(t2.id, {
+ ...t2,
+ isPublic: IsPublic.Public,
+ files: {}
+ })
+ is(p.displayName, 't2')
+ is(p.isPublic, IsPublic.Public)
+ is(JSON.stringify(p.files), '{}')
+ }
+ {
+ const { total, data } = await assetApis.listAsset({ isPublic: IsPublic.Public })
+ is(total, 3)
+ is(data.length, 3)
+ is(data.map(p => p.displayName).join(','), 't1,t2,t3')
+ }
+ {
+ const { total, data } = await assetApis.listAsset({ owner: '*' })
+ is(total, 3)
+ is(data.length, 3)
+ is(data.map(p => p.displayName).join(','), 't1,t2,t3')
+ }
+ {
+ await assetApis.increaseAssetClickCount(t2.id)
+ t2 = await assetApis.getAsset(t2.id)
+ is(t2.clickCount, 1)
+ }
+}
+
+;(window as any).testProject = async function testProject() {
+ {
+ const { data: projects } = await projectApis.listProject({ pageSize: 100 })
+ for (const p of projects) {
+ await projectApis.deleteProject(p.owner, p.name)
+ }
+ }
+ {
+ const res = await projectApis.listProject()
+ is(res.total, 0)
+ is(res.data.length, 0)
+ }
+ {
+ const res = await projectApis.listProject({ owner: "*" })
+ is(res.total, 0)
+ is(res.data.length, 0)
+ }
+ {
+ const res = await projectApis.listProject({ owner: "xxx" })
+ is(res.total, 0)
+ is(res.data.length, 0)
+ }
+ {
+ const p = await projectApis.addProject({ name: 't1', isPublic: IsPublic.Public, files: { a: 'aaa' } })
+ is(p.name, 't1')
+ is(p.isPublic, IsPublic.Public)
+ is(p.files.a, 'aaa')
+ is(p.version, 1)
+ }
+ {
+ const p = await projectApis.addProject({ name: 't2', isPublic: IsPublic.Personal, files: { a: 'aaa', b: 'bbb' } })
+ is(p.name, 't2')
+ is(p.isPublic, IsPublic.Personal)
+ is(p.files.a, 'aaa')
+ is(p.files.b, 'bbb')
+ is(p.version, 1)
+ }
+ {
+ const p = await projectApis.addProject({ name: 't3', isPublic: IsPublic.Public, files: { a: 'aaa', b: 'bbb', c: 'ccc' } })
+ is(p.name, 't3')
+ is(p.isPublic, IsPublic.Public)
+ is(p.files.a, 'aaa')
+ is(p.files.b, 'bbb')
+ is(p.files.c, 'ccc')
+ is(p.version, 1)
+ }
+ {
+ const p = await projectApis.addProject({ name: 't4', isPublic: IsPublic.Personal, files: {} })
+ is(p.name, 't4')
+ is(p.isPublic, IsPublic.Personal)
+ is(JSON.stringify(p.files), '{}')
+ is(p.version, 1)
+ }
+ {
+ const { total, data } = await projectApis.listProject()
+ is(total, 4)
+ is(data.length, 4)
+ is(data.map(p => p.name).join(','), 't1,t2,t3,t4')
+ }
+ {
+ const { total, data } = await projectApis.listProject({ pageSize: 2 })
+ is(total, 4)
+ is(data.length, 2)
+ is(data.map(p => p.name).join(','), 't1,t2')
+ }
+ {
+ const { total, data } = await projectApis.listProject({ pageIndex: 2, pageSize: 10 })
+ is(total, 4)
+ is(data.length, 0)
+ }
+ {
+ const { total, data } = await projectApis.listProject({ pageIndex: 2, pageSize: 2 })
+ is(total, 4)
+ is(data.length, 2)
+ is(data.map(p => p.name).join(','), 't3,t4')
+ }
+ {
+ const p = await projectApis.updateProject('nighca', 't2', {
+ isPublic: IsPublic.Public,
+ files: {}
+ })
+ is(p.name, 't2')
+ is(p.version, 2)
+ is(p.isPublic, IsPublic.Public)
+ is(JSON.stringify(p.files), '{}')
+ }
+ {
+ const { total, data } = await projectApis.listProject({ isPublic: IsPublic.Public })
+ is(total, 3)
+ is(data.length, 3)
+ is(data.map(p => p.name).join(','), 't1,t2,t3')
+ }
+ {
+ const { total, data } = await projectApis.listProject({ owner: '*' })
+ is(total, 3)
+ is(data.length, 3)
+ is(data.map(p => p.name).join(','), 't1,t2,t3')
+ }
+}
\ No newline at end of file
diff --git a/spx-gui/src/api/util.ts b/spx-gui/src/api/util.ts
new file mode 100644
index 000000000..0c0a82ba1
--- /dev/null
+++ b/spx-gui/src/api/util.ts
@@ -0,0 +1,20 @@
+/**
+ * @desc util-related APIs of spx-backend
+ */
+
+import { client } from './common'
+
+export interface FormatError {
+ Column: number
+ Line: number
+ Msg: string
+}
+
+export interface FormatResponse {
+ Body: string
+ Error: FormatError
+}
+
+export function formatSpxCode(body: string) {
+ return client.post('/util/fmt', { body }) as Promise
+}
diff --git a/spx-gui/src/main.ts b/spx-gui/src/main.ts
index 3c7283e5c..bc38cc737 100644
--- a/spx-gui/src/main.ts
+++ b/spx-gui/src/main.ts
@@ -14,16 +14,11 @@ import { initI18n } from '@/language'
import { addFileUrl } from './util/file'
import VueKonva from 'vue-konva'
import { initStore, useUserStore } from './store'
-import { serviceManager } from '@/axios'
-import { createDiscreteApi } from 'naive-ui'
+import { client } from './api/common'
-const { message } = createDiscreteApi(['message'])
const initServive = async () => {
const userStore = useUserStore()
- serviceManager.setAccessTokenFn(userStore.getFreshAccessToken)
- serviceManager.setNotifyErrorFn((msg: string) => {
- message.error(msg)
- })
+ client.setAuthProvider(userStore.getFreshAccessToken)
}
async function initApp() {
diff --git a/spx-gui/src/model/Costume.ts b/spx-gui/src/model/Costume.ts
new file mode 100644
index 000000000..7f595af23
--- /dev/null
+++ b/spx-gui/src/model/Costume.ts
@@ -0,0 +1,39 @@
+import { reactive } from 'vue'
+import { File, type ReadFile } from './common/file'
+import { Model } from './common/model'
+
+export type CostumeConfig = {
+ name: string
+ path: string
+ x: number
+ y: number
+ faceRight: number
+ bitmapResolution: number
+}
+
+export class Costume extends Model {
+
+ name!: string
+ path!: string
+ file!: File
+ x!: number
+ y!: number
+ faceRight!: number
+ bitmapResolution!: number
+
+ constructor(config: CostumeConfig, public _read: ReadFile) {
+ super(config)
+ return reactive(this)
+ }
+
+ async init() {
+ this.file = await this._read(this.path)
+ }
+
+ getConfig(): CostumeConfig {
+ return {
+ name: this.name, path: this.path, x: this.x, y: this.y,
+ faceRight: this.faceRight, bitmapResolution: this.bitmapResolution
+ }
+ }
+}
diff --git a/spx-gui/src/model/backdrop.ts b/spx-gui/src/model/backdrop.ts
new file mode 100644
index 000000000..b48b4c581
--- /dev/null
+++ b/spx-gui/src/model/backdrop.ts
@@ -0,0 +1,5 @@
+import { type CostumeConfig, Costume } from './Costume'
+
+export type BackdropConfig = CostumeConfig
+
+export class Backdrop extends Costume {}
diff --git a/spx-gui/src/model/common/file.ts b/spx-gui/src/model/common/file.ts
new file mode 100644
index 000000000..6d8eba841
--- /dev/null
+++ b/spx-gui/src/model/common/file.ts
@@ -0,0 +1,97 @@
+/**
+ * @file class File
+ * @desc File-like class, while load lazily from network
+ */
+
+export class File {
+
+ /** File content */
+ content: ArrayBuffer | null = null
+
+ constructor(
+ /** File name */
+ public name: string,
+ /** Public URL */
+ public url: string | null = null,
+ contentInit: ArrayBuffer | string | null = null
+ ) {
+ if (contentInit != null) {
+ if (typeof contentInit === 'string') {
+ this.content = str2Ab(contentInit)
+ } else {
+ this.content = contentInit
+ }
+ }
+ }
+
+ _promisedContent: Promise | null = null
+
+ async _load() {
+ if (this.url == null) throw new Error('file url expected')
+ const resp = await fetch(this.url)
+ const blob = await resp.blob()
+ return blob.arrayBuffer()
+ }
+
+ async arrayBuffer() {
+ if (this.content != null) return this.content
+ if (this._promisedContent != null) return this._promisedContent
+ return this._promisedContent = this._load().then(ab => {
+ return this.content = ab
+ })
+ }
+
+ async text() {
+ const ab = await this.arrayBuffer()
+ const decoder = new TextDecoder()
+ return decoder.decode(ab)
+ }
+
+ // async url() {
+ // const ab = await this.arrayBuffer()
+ // return URL.createObjectURL(new Blob([ab]))
+ // }
+
+}
+
+export type Files = {
+ [path: string]: File
+}
+
+// export class MemFile extends File {
+
+// constructor(name: string, private ab: ArrayBuffer) {
+// super(name)
+// }
+
+// protected async load() {
+// return this.ab
+// }
+
+// }
+
+// export class CloudFile extends File {
+
+// constructor(private urlStr: string, name?: string) {
+// super(name ?? getName(urlStr))
+// }
+
+// protected async load() {
+// const resp = await fetch(this.urlStr) // TODO: request details
+// const blob = await resp.blob()
+// return blob.arrayBuffer()
+// }
+
+// override async url() {
+// return this.urlStr
+// }
+
+// }
+
+export type ReadFile = (path: string) => Promise
+
+function str2Ab(str: string) {
+ const encoder = new TextEncoder()
+ const view = encoder.encode(str)
+ return view.buffer
+}
\ No newline at end of file
diff --git a/spx-gui/src/model/common/model.ts b/spx-gui/src/model/common/model.ts
new file mode 100644
index 000000000..e10fd43bb
--- /dev/null
+++ b/spx-gui/src/model/common/model.ts
@@ -0,0 +1,29 @@
+/**
+ * @file class Model
+ * @desc Base class for object-models
+ */
+
+type Disposer = () => void
+
+export abstract class Model {
+
+ constructor(initialValues?: unknown) {
+ Object.assign(this, initialValues)
+ }
+
+ abstract init(): Promise
+
+ _disposers: Disposer[] = []
+
+ addDisposer(disposer: Disposer) {
+ this._disposers.push(disposer)
+ }
+
+ dispose() {
+ const disposers = this._disposers.splice(0)
+ for (const disposer of disposers) {
+ disposer()
+ }
+ }
+
+}
diff --git a/spx-gui/src/model/project/drivers/base.ts b/spx-gui/src/model/project/drivers/base.ts
new file mode 100644
index 000000000..3e5c8b0f0
--- /dev/null
+++ b/spx-gui/src/model/project/drivers/base.ts
@@ -0,0 +1,22 @@
+import type { IsPublic } from '@/api/common'
+import { File, type Files } from '../../common/file'
+
+export type Metadata = {
+ owner: string | null
+ name: string | null
+ isPublic: IsPublic | null
+}
+
+export interface ReadableDriver {
+ /** Get metadata of project */
+ getMetaData(): Promise
+ /** List all files' relative path in project. Relative path starts with slash, e.g. `index.json` */
+ listFiles(): Promise
+ /** Read file content for given relative path. Relative path starts with slash, e.g. `index.json` */
+ readFile(path: string): Promise
+}
+
+export interface WritableDriver {
+ /** TODO */
+ write(metadata: Metadata, files: Files): Promise
+}
diff --git a/spx-gui/src/model/project/drivers/cloud.ts b/spx-gui/src/model/project/drivers/cloud.ts
new file mode 100644
index 000000000..9bf145bb4
--- /dev/null
+++ b/spx-gui/src/model/project/drivers/cloud.ts
@@ -0,0 +1,53 @@
+import { filename } from '@/util/path'
+import { File, type Files } from '../../common/file'
+import { type ReadableDriver, type Metadata, type WritableDriver } from './base'
+import type { ProjectData, FileCollection } from '@/api/project'
+import { getProject, updateProject } from '@/api/project'
+
+export class CloudDriver implements ReadableDriver, WritableDriver {
+
+ constructor(private owner: string, private name: string) {}
+
+ private projectDataPromise: Promise | undefined
+
+ private fetchProjectData(): Promise {
+ if (this.projectDataPromise != null) return this.projectDataPromise
+ this.projectDataPromise = getProject(this.owner, this.name)
+ return this.projectDataPromise
+ }
+
+ async getMetaData(): Promise {
+ const { owner, name, isPublic } = await this.fetchProjectData()
+ return { owner, name, isPublic }
+ }
+
+ async listFiles(): Promise {
+ const { files } = await this.fetchProjectData()
+ return Object.keys(files)
+ }
+
+ async readFile(path: string): Promise {
+ const { files } = await this.fetchProjectData()
+ const url = files[path]
+ return new File(filename(url), url)
+ }
+
+ private async uploadFile(file: File) {
+ if (file.url != null) return file.url
+ return file.url = await upload(file)
+ }
+
+ async write({ isPublic }: Metadata, files: Files) {
+ if (isPublic == null) throw new Error('isPublic required')
+ const fileUrls: FileCollection = {}
+ await Promise.all(Object.keys(files).map(async path => {
+ fileUrls[path] = await this.uploadFile(files[path])
+ }))
+ await updateProject(this.owner, this.name, { isPublic, files: fileUrls })
+ }
+
+}
+
+async function upload(file: File) {
+ return `https://TODO/${file.name}`
+}
diff --git a/spx-gui/src/model/project/drivers/local.ts b/spx-gui/src/model/project/drivers/local.ts
new file mode 100644
index 000000000..3a6d715fd
--- /dev/null
+++ b/spx-gui/src/model/project/drivers/local.ts
@@ -0,0 +1,89 @@
+import localforage from 'localforage'
+import { File, type Files } from '../../common/file'
+import { type ReadableDriver, type Metadata, type WritableDriver } from './base'
+
+const storage = localforage.createInstance({
+ name: 'spx-gui',
+ storeName: 'project'
+})
+
+type MetadataEx = Metadata & {
+ files: string[]
+}
+
+type RawFile = {
+ name: string
+ url: string | null
+ content: ArrayBuffer | null
+}
+
+export class LocalDriver implements ReadableDriver, WritableDriver {
+
+ constructor(private key: string) {}
+
+ private async getMetadataEx() {
+ const metadataEx = await storage.getItem(this.key)
+ if (metadataEx == null) throw new Error('metadata not found in storage')
+ return metadataEx as MetadataEx
+ }
+
+ private async setMetadataEx(metadataEx: MetadataEx) {
+ await storage.setItem(this.key, metadataEx)
+ }
+
+ private async removeMetadataEx() {
+ await storage.removeItem(this.key)
+ }
+
+ async getMetaData(): Promise {
+ return this.getMetadataEx()
+ }
+
+ async listFiles(): Promise {
+ const { files } = await this.getMetadataEx()
+ return files
+ }
+
+ async readFile(path: string): Promise {
+ const rawFile = await storage.getItem(`${this.key}/${path}`)
+ if (rawFile == null) throw new Error('file not found in storage')
+ const { name, url, content } = rawFile as RawFile
+ return new File(name, url, content)
+ }
+
+ private async writeFile(path: string, file: File) {
+ const rawFile: RawFile = {
+ name: file.name,
+ url: file.url,
+ content: file.content
+ }
+ await storage.setItem(`${this.key}/${path}`, rawFile)
+ }
+
+ private async removeFile(path: string) {
+ await storage.removeItem(`${this.key}/${path}`)
+ }
+
+ private async clear() {
+ try {
+ const { files } = await this.getMetadataEx()
+ await Promise.all([
+ this.removeMetadataEx(),
+ ...files.map(path => this.removeFile(path))
+ ])
+ } catch (e) {
+ // TODO: check error
+ }
+ }
+
+ async write(metadata: Metadata, files: Files): Promise {
+ await this.clear()
+ const fileList = Object.keys(files)
+ const metadataEx = { ...metadata, files: fileList }
+ await Promise.all([
+ this.setMetadataEx(metadataEx),
+ ...fileList.map(path => this.writeFile(path, files[path]))
+ ])
+ }
+
+}
diff --git a/spx-gui/src/model/project/drivers/zip.ts b/spx-gui/src/model/project/drivers/zip.ts
new file mode 100644
index 000000000..4d355305e
--- /dev/null
+++ b/spx-gui/src/model/project/drivers/zip.ts
@@ -0,0 +1,52 @@
+import JSZip from 'jszip'
+import { File as LazyFile, type Files as LazyFiles } from '../../common/file'
+import { type ReadableDriver, type Metadata, type WritableDriver } from './base'
+import { filename, stripExt } from '@/util/path'
+
+export class ZipReader implements ReadableDriver {
+
+ constructor(private zipFile: File) {}
+
+ private jsZip: Promise | null = null
+
+ private loadJSZip(): Promise {
+ if (this.jsZip != null) return this.jsZip
+ return JSZip.loadAsync(this.zipFile)
+ }
+
+ async getMetaData(): Promise {
+ return {
+ owner: null,
+ name: stripExt(this.zipFile.name),
+ isPublic: null
+ }
+ }
+
+ async listFiles(): Promise {
+ const jszip = await this.loadJSZip()
+ return Object.keys(jszip.files)
+ }
+
+ async readFile(path: string): Promise {
+ const jszip = await this.loadJSZip()
+ const zipEntry = jszip.files[path]
+ const content = await zipEntry.async('arraybuffer')
+ return new LazyFile(filename(path), null, content)
+ }
+
+}
+
+export class ZipWriter implements WritableDriver {
+
+ async write({ name }: Metadata, files: LazyFiles): Promise {
+ const zip = new JSZip()
+ await Promise.all(Object.keys(files).map(async path => {
+ const content = await files[path].arrayBuffer()
+ zip.file(path, content)
+ }))
+
+ const blob = await zip.generateAsync({ type: 'blob' })
+ return new File([blob], name + '.zip')
+ }
+
+}
\ No newline at end of file
diff --git a/spx-gui/src/model/project/index.ts b/spx-gui/src/model/project/index.ts
new file mode 100644
index 000000000..b50a271b2
--- /dev/null
+++ b/spx-gui/src/model/project/index.ts
@@ -0,0 +1,108 @@
+/**
+ * @file class Project
+ * @desc Object-model definition for Project
+ */
+
+import { reactive } from 'vue'
+import { type ProjectData } from '../../api/project'
+import { IsPublic } from '../../api/project'
+import { Model } from '../common/model'
+
+import type { Metadata, ReadableDriver, WritableDriver } from './drivers/base'
+import { CloudDriver } from './drivers/cloud'
+import { LocalDriver } from './drivers/local'
+import { ZipReader, ZipWriter } from './drivers/zip'
+import type { Files } from '../common/file'
+
+const localSavedKey = 'TODO'
+
+export class Project extends Model {
+
+ static
+
+ id: string | null = null
+ name: string | null = null
+ owner: string | null = null
+ isPublic: IsPublic | null = null
+ version: number | null = null
+ cTime: string | null = null
+ uTime: string | null = null
+
+ // get identifier() {
+ // return `${this.owner}/${this.name}`
+ // }
+
+ constructor() {
+ super()
+ return reactive(this) as Project
+ }
+
+ private applyMetadata({ owner, name, isPublic }: Metadata) {
+ this.owner = owner
+ this.name = name
+ this.isPublic = isPublic
+ }
+
+ private async initFrom(readable: ReadableDriver) {
+
+ const [metadata, filePaths] = await Promise.all([
+ readable.getMetaData(),
+ readable.listFiles()
+ ])
+
+ this.applyMetadata(metadata)
+
+ await Promise.all(filePaths.map(async filePath => {
+ const { content, url } = await readable.readFile(filePath)
+ // TODO
+ }))
+ }
+
+ initBlank(owner: string, name = '') {
+ this.owner = owner
+ this.name = name
+ }
+
+ initFromZip(zipFile: File) {
+ const zipStorage = new ZipReader(zipFile)
+ return this.initFrom(zipStorage)
+ }
+
+ initFromLocal() {
+ const localStorage = new LocalDriver(localSavedKey)
+ return this.initFrom(localStorage)
+ }
+
+ async initFromCloud(owner: string, name: string) {
+ const storage = new CloudDriver(owner, name)
+ return this.initFrom(storage)
+ }
+
+ private getFiles(): Files {
+ // TODO
+ }
+
+ private async writeTo(writable: WritableDriver): Promise {
+ return writable.write({
+ owner: this.owner,
+ name: this.name,
+ isPublic: this.isPublic
+ }, this.getFiles())
+ }
+
+ saveToZip() {
+ return this.writeTo(new ZipWriter())
+ }
+
+ writeToLocal() {
+ const localWriter = new LocalDriver(localSavedKey)
+ return this.writeTo(localWriter)
+ }
+
+ writeToCloud() {
+ if (this.owner == null || this.name == null) throw new Error('owner & name requried')
+ const cloudWriter = new CloudDriver(this.owner, this.name)
+ return this.writeTo(cloudWriter)
+ }
+
+}
diff --git a/spx-gui/src/model/sound.ts b/spx-gui/src/model/sound.ts
new file mode 100644
index 000000000..2e9eeecc6
--- /dev/null
+++ b/spx-gui/src/model/sound.ts
@@ -0,0 +1,26 @@
+import { reactive } from 'vue'
+import { File, type ReadFile } from './common/file'
+import { Model } from './common/model'
+
+export type CostumeConfig = {
+ path: string
+ rate: number
+ sampleCount: number
+}
+
+export class Costume extends Model {
+
+ path!: string
+ file!: File
+ rate!: number
+ sampleCount!: number
+
+ constructor(public name: string, config: CostumeConfig, public _read: ReadFile) {
+ super(config)
+ return reactive(this)
+ }
+
+ async init() {
+ this.file = await this._read(this.path)
+ }
+}
diff --git a/spx-gui/src/model/sprite.ts b/spx-gui/src/model/sprite.ts
new file mode 100644
index 000000000..980eb8127
--- /dev/null
+++ b/spx-gui/src/model/sprite.ts
@@ -0,0 +1,89 @@
+/**
+ * @file class Sprite
+ * @desc Object-model definition for Sprite & Costume
+ */
+
+import { reactive } from 'vue';
+import { File, type Files, type ReadFile } from './common/file'
+import { Model } from './common/model'
+import { join } from '@/util/path'
+import { resolve } from 'path'
+import { type CostumeConfig, Costume } from './Costume'
+
+export type SpriteConfig = {
+ heading: number
+ x: number
+ y: number
+ size: number
+ rotationStyle: string
+ costumes: CostumeConfig[]
+ costumeIndex: number
+ visible: boolean
+ isDraggable: boolean
+ // TODO:
+ // costumeSet: costumeSet
+ // costumeMPSet: costumeMPSet
+ // currentCostumeIndex: int
+ // fAnimations: map
+ // mAnimations: map
+ // tAnimations: map
+}
+
+const spriteAssetPath = 'assets/sprites'
+
+export class Sprite extends Model {
+
+ heading!: number
+ x!: number
+ y!: number
+ size!: number
+ rotationStyle!: string
+ costumes!: Costume[]
+ costumeIndex!: number
+ visible!: boolean
+ isDraggable!: boolean
+
+ constructor(
+ public name: string,
+ { costumes: costumeConfigs, ...config }: SpriteConfig,
+ public code: string,
+ public _read: ReadFile,
+ ) {
+ super({
+ ...config,
+ costumes: costumeConfigs.map(
+ c => new Costume(c, this._read)
+ )
+ })
+ return reactive(this)
+ }
+
+ async init() {
+ await Promise.all(this.costumes.map(c => c.init()))
+ }
+
+ getConfig(): SpriteConfig {
+ return {
+ heading: this.heading,
+ x: this.x,
+ y: this.y,
+ size: this.size,
+ rotationStyle: this.rotationStyle,
+ costumes: this.costumes.map(c => c.getConfig()),
+ costumeIndex: this.costumeIndex,
+ visible: this.visible,
+ isDraggable: this.isDraggable
+ }
+ }
+
+ getFiles() {
+ const files: Files = {}
+ const assetPath = join(spriteAssetPath, this.name)
+ files[`${this.name}.spx`] = new File(`${this.name}.spx`, null, this.code)
+ files[`${assetPath}/index.json`] = new File('index.json', null, JSON.stringify(this.getConfig()))
+ for (const costume of this.costumes) {
+ files[resolve(assetPath, costume.path)] = costume.file
+ }
+ return files
+ }
+}
diff --git a/spx-gui/src/model/stage.ts b/spx-gui/src/model/stage.ts
new file mode 100644
index 000000000..f5b77a155
--- /dev/null
+++ b/spx-gui/src/model/stage.ts
@@ -0,0 +1,64 @@
+/**
+ * @file class Stage
+ * @desc Object-model definition for Stage & Costume
+ */
+
+import { reactive } from 'vue';
+import { File, type Files, type ReadFile } from './common/file'
+import { Model } from './common/model'
+import { resolve } from 'path'
+import { Backdrop, type BackdropConfig } from './backdrop'
+
+export type StageConfig = {
+ scenes: BackdropConfig[]
+ sceneIndex: number
+ // TODO:
+ // costumes: CostumeConfig[]
+ // currentCostumeIndex: number
+}
+
+const stageAssetPath = 'assets'
+const stageCodeFileName = 'main.spx'
+
+export class Stage extends Model {
+
+ backdrops!: Backdrop[]
+ backdropIndex!: number
+
+ constructor(
+ public name: string,
+ { scenes: backdropConfigs, sceneIndex: backdropIndex, ...config }: StageConfig,
+ public code: string,
+ public _read: ReadFile,
+ ) {
+ super({
+ ...config,
+ backdrops: backdropConfigs.map(
+ c => new Backdrop(c, this._read)
+ ),
+ backdropIndex
+ })
+ return reactive(this)
+ }
+
+ async init() {
+ await Promise.all(this.backdrops.map(c => c.init()))
+ }
+
+ getConfig(): StageConfig {
+ return {
+ scenes: this.backdrops.map(b => b.getConfig()),
+ sceneIndex: this.backdropIndex
+ }
+ }
+
+ getFiles() {
+ const files: Files = {}
+ files[stageCodeFileName] = new File(stageCodeFileName, null, this.code)
+ for (const backdrop of this.backdrops) {
+ files[resolve(stageAssetPath, backdrop.path)] = backdrop.file
+ }
+ // config file is handled by project
+ return files
+ }
+}
diff --git a/spx-gui/src/store/user.ts b/spx-gui/src/store/user.ts
index 51240d148..ddd44fb0a 100644
--- a/spx-gui/src/store/user.ts
+++ b/spx-gui/src/store/user.ts
@@ -1,4 +1,5 @@
-import { casdoorSdk } from '@/util/casdoor'
+import CasdoorSdk from '@/util/casdoor'
+import { casdoorConfig } from '@/util/env'
import type ITokenResponse from 'js-pkce/dist/ITokenResponse'
import { defineStore } from 'pinia'
@@ -28,6 +29,11 @@ export interface JwtPayload {
phone: string
}
+const casdoorSdk = new CasdoorSdk({
+ ...casdoorConfig,
+ redirectPath: '/callback'
+})
+
export const useUserStore = defineStore('spx-user', {
state: () => ({
accessToken: null as string | null,
diff --git a/spx-gui/src/util/casdoor.ts b/spx-gui/src/util/casdoor.ts
index af4a3cd3a..8309c4609 100644
--- a/spx-gui/src/util/casdoor.ts
+++ b/spx-gui/src/util/casdoor.ts
@@ -1,11 +1,3 @@
-const config: SdkConfig = {
- serverUrl: import.meta.env.VITE_CASDOOR_ENDPOINT,
- clientId: import.meta.env.VITE_CASDOOR_CLIENT_ID,
- organizationName: import.meta.env.VITE_CASDOOR_ORGANIZATION_NAME,
- appName: import.meta.env.VITE_CASDOOR_APP_NAME,
- redirectPath: '/callback'
-}
-
// The following content is copied from the casdoor-sdk package, as
// we need to modify some of the code.
// Original source:
@@ -155,4 +147,4 @@ class Sdk {
}
}
-export const casdoorSdk = new Sdk(config)
+export default Sdk
diff --git a/spx-gui/src/util/env.ts b/spx-gui/src/util/env.ts
new file mode 100644
index 000000000..eae460a9d
--- /dev/null
+++ b/spx-gui/src/util/env.ts
@@ -0,0 +1,21 @@
+/**
+ * @file env file
+ * @desc Exports enviroment variables from `.env.*` file
+ */
+
+///
+
+// Base URL for spx-backend APIs, e.g. `/api`
+export const apiBaseUrl = import.meta.env.VITE_API_BASE_URL as string
+
+// Base URL for the application, e.g. `https://builder.goplus.org`
+// TODO: what about import.meta.BASE_URL
+export const publishBaseUrl = import.meta.env.VITE_PUBLISH_BASE_UR as string
+
+// Casdoor configurations
+export const casdoorConfig = {
+ serverUrl: import.meta.env.VITE_CASDOOR_ENDPOINT as string,
+ clientId: import.meta.env.VITE_CASDOOR_CLIENT_ID as string,
+ organizationName: import.meta.env.VITE_CASDOOR_ORGANIZATION_NAME as string,
+ appName: import.meta.env.VITE_CASDOOR_APP_NAME as string,
+}
diff --git a/spx-gui/src/util/path.ts b/spx-gui/src/util/path.ts
new file mode 100644
index 000000000..e172cdc89
--- /dev/null
+++ b/spx-gui/src/util/path.ts
@@ -0,0 +1,29 @@
+
+export function join(base: string, ...paths: string[]) {
+ // TODO
+ return [base, ...paths].join('/')
+}
+
+export function resolve(base: string, ...paths: string[]) {
+ // TODO
+ return [base, ...paths].join('/')
+}
+
+export function filename(urlOrPath: string) {
+ // TODO
+ const slashPos = urlOrPath.lastIndexOf('/')
+ if (slashPos >= 0) urlOrPath = urlOrPath.slice(slashPos + 1)
+ return urlOrPath
+}
+
+export function dirname(urlOrPath: string) {
+ // TODO
+ const slashPos = urlOrPath.lastIndexOf('/')
+ if (slashPos >= 0) urlOrPath = urlOrPath.slice(0, slashPos)
+ return urlOrPath
+}
+
+export function stripExt(fileName: string) {
+ // TODO
+ return fileName
+}
diff --git a/spx-gui/vite.config.ts b/spx-gui/vite.config.ts
index bd986e2dc..9e9853eaa 100644
--- a/spx-gui/vite.config.ts
+++ b/spx-gui/vite.config.ts
@@ -40,6 +40,15 @@ export default defineConfig(({ mode }) => {
},
optimizeDeps: {
include: [`monaco-editor/esm/vs/editor/editor.worker`]
+ },
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ },
+ }
}
}
})