+ {{
+ $t({
+ en: 'A sharing link to the project has been created. Feel free to copy it below to share the project with others.',
+ zh: '项目的分享链接已生成。请复制下方链接,与他人分享该项目。'
+ })
+ }}
+
+
+
+
+ {{ $t({ en: 'Copy', zh: '复制' }) }}
+
+
+
+
+
+
+
+
diff --git a/spx-gui/src/components/project/index.ts b/spx-gui/src/components/project/index.ts
index 6498ce6fe..0cc81a5b3 100644
--- a/spx-gui/src/components/project/index.ts
+++ b/spx-gui/src/components/project/index.ts
@@ -1,11 +1,12 @@
import { useRouter } from 'vue-router'
-import { useModal, useConfirmDialog, useMessage } from '@/components/ui'
+import { useModal, useConfirmDialog } from '@/components/ui'
import { IsPublic, deleteProject } from '@/apis/project'
import ProjectCreateModal from './ProjectCreateModal.vue'
import ProjectOpenModal from './ProjectOpenModal.vue'
-import { useI18n, type LocaleMessage } from '@/utils/i18n'
+import ProjectSharingLinkModal from './ProjectSharingLinkModal.vue'
+import { useI18n } from '@/utils/i18n'
import type { Project } from '@/models/project'
-import { getProjectEditorRoute, getProjectShareRoute } from '@/router'
+import { getProjectEditorRoute } from '@/router'
export function useCreateProject() {
const modal = useModal(ProjectCreateModal)
@@ -26,8 +27,8 @@ export function useOpenProject() {
}
export function useRemoveProject() {
- const withConfirm = useConfirmDialog()
const { t } = useI18n()
+ const withConfirm = useConfirmDialog()
return async function removeProject(owner: string, name: string) {
return withConfirm({
@@ -41,94 +42,61 @@ export function useRemoveProject() {
}
}
-/** Copy sharing link for given project */
-export function useShareProject() {
- const m = useMessage()
- const { t } = useI18n()
- return async function shareProject(project: Project) {
- const { owner, name } = project
- // TODO: the check should be unnecessary
- if (owner == null || name == null) throw new Error(`owner (${owner}), name (${name}) required`)
- await navigator.clipboard.writeText(`${location.origin}${getProjectShareRoute(owner, name)}`)
- m.success(t({ en: 'Link copied to clipboard', zh: '分享链接已复制到剪贴板' }))
+export function useCreateProjectSharingLink() {
+ const modal = useModal(ProjectSharingLinkModal)
+
+ return async function createProjectSharingLink(project: Project) {
+ await modal({ project })
}
}
/**
- * Save and share given project
- * - save current project state
+ * Share given project
* - make project public
* - copy sharing link
*/
-export function useSaveAndShareProject() {
+export function useShareProject() {
const { t } = useI18n()
const withConfirm = useConfirmDialog()
- const shareProject = useShareProject()
+ const createProjectSharingLink = useCreateProjectSharingLink()
- async function saveAndShare(project: Project) {
- if (project.isPublic !== IsPublic.public || project.hasUnsyncedChanges) {
- project.setPublic(IsPublic.public)
- await project.saveToCloud()
- }
- await shareProject(project)
- }
-
- async function saveAndShareWithConfirm(project: Project, confirmMessage: LocaleMessage) {
- await withConfirm({
- title: t({ en: 'Share project', zh: '分享项目' }),
- content: t(confirmMessage),
- confirmHandler: () => saveAndShare(project)
- })
+ async function makePublic(project: Project) {
+ project.setPublic(IsPublic.public)
+ await project.saveToCloud()
}
- return async function saveAndShareProject(project: Project) {
- const { isPublic, hasUnsyncedChanges } = project
- if (isPublic == null) throw new Error('isPublic required')
-
- if (isPublic === IsPublic.public) {
- if (!hasUnsyncedChanges) return saveAndShare(project)
- else
- return saveAndShareWithConfirm(project, {
- en: "To share the project, we will save the project's current state to cloud",
- zh: '分享操作会将当前项目状态保存到云端'
- })
- } else {
- if (!hasUnsyncedChanges)
- return saveAndShareWithConfirm(project, {
- en: 'To share the project, we will make the project public',
- zh: '分享操作会将当前项目设置为公开'
- })
- else
- return saveAndShareWithConfirm(project, {
- en: "To share the project, we will save the project's current state to cloud & make it public",
- zh: '分享操作会将当前项目状态保存到云端,并将项目设置为公开'
- })
+ return async function shareProject(project: Project) {
+ if (project.isPublic !== IsPublic.public) {
+ await withConfirm({
+ title: t({ en: 'Share project', zh: '分享项目' }),
+ content: t({
+ en: 'To share the current project, it will be made public. Would you like to proceed?',
+ zh: '为了分享当前项目,它将被设置为公开。确认继续吗?'
+ }),
+ confirmHandler: () => makePublic(project)
+ })
}
+ return createProjectSharingLink(project)
}
}
export function useStopSharingProject() {
- const withConfirm = useConfirmDialog()
const { t } = useI18n()
+ const withConfirm = useConfirmDialog()
+
+ async function makePersonal(project: Project) {
+ project.setPublic(IsPublic.personal)
+ await project.saveToCloud()
+ }
return async function stopSharingProject(project: Project) {
- let confirmMessage = {
- en: 'If sharing stopped, others will no longer have permission to access the project, and all project-sharing links will expire',
- zh: '停止分享后,其他人不再可以访问项目,所有的项目分享链接也将失效'
- }
- if (project.hasUnsyncedChanges) {
- confirmMessage = {
- en: `The project's current state will be saved to cloud. ${confirmMessage.en}`,
- zh: `当前项目状态将被保存到云端;${confirmMessage.zh}`
- }
- }
return withConfirm({
title: t({ en: 'Stop sharing project', zh: '停止分享项目' }),
- content: t(confirmMessage),
- confirmHandler: async () => {
- project.setPublic(IsPublic.personal)
- await project.saveToCloud()
- }
+ content: t({
+ en: 'If sharing is stopped, others will no longer have access to the current project, and its sharing links will expire. Would you like to proceed?',
+ zh: '如果停止分享,其他人将无法访问当前项目,且分享链接将会失效。确认继续吗?'
+ }),
+ confirmHandler: () => makePersonal(project)
})
}
}
diff --git a/spx-gui/src/components/project/runner/IframeDisplay.vue b/spx-gui/src/components/project/runner/IframeDisplay.vue
index d054e1fd3..5f2e1583b 100644
--- a/spx-gui/src/components/project/runner/IframeDisplay.vue
+++ b/spx-gui/src/components/project/runner/IframeDisplay.vue
@@ -1,12 +1,10 @@
-
@@ -22,14 +27,21 @@ import { UIDivider } from '@/components/ui'
import UIModal from './UIModal.vue'
import UIModalClose from './UIModalClose.vue'
-defineProps<{
- title: string
- visible?: boolean
- centerTitle?: boolean
- // maybe it is better to let caller specify the body class instead of body style,
- // but it is now not possible with scoped style & naive-ui `Modal`, which is similar to the issue we encountered in `UIDropdown.vue`
- bodyStyle?: CSSProperties
-}>()
+withDefaults(
+ defineProps<{
+ title: string
+ visible?: boolean
+ autoFocus?: boolean
+ centerTitle?: boolean
+ // maybe it is better to let caller specify the body class instead of body style,
+ // but it is now not possible with scoped style & naive-ui `Modal`, which is similar to the issue we encountered in `UIDropdown.vue`
+ bodyStyle?: CSSProperties
+ }>(),
+ {
+ autoFocus: true,
+ bodyStyle: () => ({})
+ }
+)
const emit = defineEmits<{
'update:visible': [visible: boolean]
@@ -70,7 +82,7 @@ const handleCloseButton = () => {
}
.body {
- padding: 20px 24px;
+ padding: 20px 24px 24px;
}
.close {
diff --git a/spx-gui/src/components/ui/modal/UIModal.vue b/spx-gui/src/components/ui/modal/UIModal.vue
index 7463bf8f2..553b61b33 100644
--- a/spx-gui/src/components/ui/modal/UIModal.vue
+++ b/spx-gui/src/components/ui/modal/UIModal.vue
@@ -11,6 +11,7 @@
+
+
diff --git a/spx-gui/src/models/project/index.test.ts b/spx-gui/src/models/project/index.test.ts
index a2a08ed99..1c7dd30e3 100644
--- a/spx-gui/src/models/project/index.test.ts
+++ b/spx-gui/src/models/project/index.test.ts
@@ -181,7 +181,7 @@ describe('Project', () => {
await vi.advanceTimersByTimeAsync(500)
const secondSavePromise = project.saveToCloud()
- vi.runAllTimersAsync()
+ vi.runOnlyPendingTimersAsync()
await expect(firstSavePromise).rejects.toThrow(Cancelled)
await expect(secondSavePromise).resolves.not.toThrow()
@@ -207,40 +207,37 @@ describe('Project', () => {
})
// https://github.com/goplus/builder/pull/794#discussion_r1728120369
- it('should handle failed auto-save correctly', async () => {
- const project = makeProject()
-
+ it('should handle failed auto-save-to-cloud correctly', async () => {
const cloudSaveMock = vi.spyOn(cloudHelper, 'save').mockRejectedValue(new Error('save failed'))
const localSaveMock = vi.spyOn(localHelper, 'save').mockResolvedValue(undefined)
const localClearMock = vi.spyOn(localHelper, 'clear').mockResolvedValue(undefined)
- await project.startEditing('localCacheKey')
+ const project = makeProject()
+
project.setAutoSaveMode(AutoSaveMode.Cloud)
+ project['startAutoSaveToCloud']('localCacheKey')
+ await flushPromises() // flush immediate watchers
+ const autoSaveToCloud = project['autoSaveToCloud']
+ expect(autoSaveToCloud).toBeTruthy()
+
+ project['filesHash'] = 'newHash'
+ project['lastSyncedFilesHash'] = 'hash'
+ expect(project.hasUnsyncedChanges).toBe(true)
- const newSprite = new Sprite('newSprite')
- project.addSprite(newSprite)
- await flushPromises()
- await vi.advanceTimersByTimeAsync(1000) // wait for changes to be picked up
- await flushPromises()
+ autoSaveToCloud?.()
expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Pending)
expect(project.hasUnsyncedChanges).toBe(true)
- await vi.advanceTimersByTimeAsync(1500) // wait for auto-save to trigger
- await flushPromises()
+ await vi.runOnlyPendingTimersAsync()
expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Failed)
expect(project.hasUnsyncedChanges).toBe(true)
expect(cloudSaveMock).toHaveBeenCalledTimes(1)
expect(localSaveMock).toHaveBeenCalledTimes(1)
- project.removeSprite(newSprite.id)
- await flushPromises()
- await vi.advanceTimersByTimeAsync(1000) // wait for changes to be picked up
- await flushPromises()
- expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Failed)
+ project['lastSyncedFilesHash'] = project['filesHash']
expect(project.hasUnsyncedChanges).toBe(false)
- await vi.advanceTimersByTimeAsync(5000) // wait for auto-retry to trigger
- await flushPromises()
+ await vi.runOnlyPendingTimersAsync()
expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Saved)
expect(project.hasUnsyncedChanges).toBe(false)
expect(cloudSaveMock).toHaveBeenCalledTimes(1)
@@ -249,50 +246,46 @@ describe('Project', () => {
})
it('should cancel pending auto-save-to-cloud when project is disposed', async () => {
- const project = makeProject()
-
const cloudSaveMock = vi.spyOn(cloudHelper, 'save').mockRejectedValue(undefined)
- await project.startEditing('localCacheKey')
+ const project = makeProject()
+
project.setAutoSaveMode(AutoSaveMode.Cloud)
+ project['startAutoSaveToCloud']('localCacheKey')
+ await flushPromises() // flush immediate watchers
+ const autoSaveToCloud = project['autoSaveToCloud']
+ expect(autoSaveToCloud).toBeTruthy()
- const newSprite = new Sprite('newSprite')
- project.addSprite(newSprite)
- await flushPromises()
- await vi.advanceTimersByTimeAsync(1000) // wait for changes to be picked up
- await flushPromises()
+ project['filesHash'] = 'newHash'
+ project['lastSyncedFilesHash'] = 'hash'
+ expect(project.hasUnsyncedChanges).toBe(true)
+
+ autoSaveToCloud?.()
expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Pending)
expect(project.hasUnsyncedChanges).toBe(true)
project.dispose()
-
- await vi.advanceTimersByTimeAsync(1500 * 2) // wait longer to ensure auto-save does not trigger
- await flushPromises()
+ await vi.runOnlyPendingTimersAsync()
expect(project.autoSaveToCloudState).toBe(AutoSaveToCloudState.Pending)
expect(project.hasUnsyncedChanges).toBe(true)
expect(cloudSaveMock).toHaveBeenCalledTimes(0)
})
it('should cancel pending auto-save-to-local-cache when project is disposed', async () => {
- const project = makeProject()
-
const localSaveMock = vi.spyOn(localHelper, 'save').mockResolvedValue(undefined)
- await project.startEditing('localCacheKey')
+ const project = makeProject()
+
project.setAutoSaveMode(AutoSaveMode.LocalCache)
+ project['startAutoSaveToLocalCache']('localCacheKey')
+ await flushPromises() // flush immediate watchers
+ const autoSaveToLocalCache = project['autoSaveToLocalCache']
+ expect(autoSaveToLocalCache).toBeTruthy()
- const newSprite = new Sprite('newSprite')
- project.addSprite(newSprite)
- await flushPromises()
- await vi.advanceTimersByTimeAsync(1000) // wait for changes to be picked up
- await flushPromises()
- expect(project.hasUnsyncedChanges).toBe(true)
+ autoSaveToLocalCache?.()
project.dispose()
-
- await vi.advanceTimersByTimeAsync(1000 * 2) // wait longer to ensure auto-save does not trigger
- await flushPromises()
- expect(project.hasUnsyncedChanges).toBe(true)
+ await vi.runOnlyPendingTimersAsync()
expect(localSaveMock).toHaveBeenCalledTimes(0)
})
})
diff --git a/spx-gui/src/models/project/index.ts b/spx-gui/src/models/project/index.ts
index b09f64c93..2bed7b4ed 100644
--- a/spx-gui/src/models/project/index.ts
+++ b/spx-gui/src/models/project/index.ts
@@ -418,84 +418,70 @@ export class Project extends Disposable {
autoSaveMode = AutoSaveMode.Off
setAutoSaveMode(autoSaveMode: AutoSaveMode) {
this.autoSaveMode = autoSaveMode
- }
- autoSaveToCloudState = AutoSaveToCloudState.Saved
-
- /** Initialize editing features */
- async startEditing(localCacheKey: string) {
- if (this.lastSyncedFilesHash == null) {
- this.lastSyncedFilesHash = await hashFiles(this.exportGameFiles())
+ switch (autoSaveMode) {
+ case AutoSaveMode.Cloud:
+ if (this.hasUnsyncedChanges) this.autoSaveToCloud?.()
+ break
+ case AutoSaveMode.LocalCache:
+ this.autoSaveToLocalCache?.()
+ break
}
- if (this.filesHash == null) {
- this.filesHash = this.lastSyncedFilesHash
- }
-
- // watch for changes of game files, update filesHash, and auto save to cloud if hasUnsyncedChanges
- const autoSaveToCloud = (() => {
- const save = debounce(async () => {
- if (this.autoSaveToCloudState !== AutoSaveToCloudState.Pending) return
- this.autoSaveToCloudState = AutoSaveToCloudState.Saving
-
- try {
- if (this.isSavingToCloud) {
- await untilConditionMet(
- () => this.isSavingToCloud,
- () => !this.isSavingToCloud
- )
- }
+ }
- if (this.hasUnsyncedChanges) await this.saveToCloud()
- this.autoSaveToCloudState = AutoSaveToCloudState.Saved
- } catch (e) {
- this.autoSaveToCloudState = AutoSaveToCloudState.Failed
- if (e instanceof Cancelled) {
- autoSaveToCloud()
- save.flush()
- } else {
- retryAutoSave()
- await this.saveToLocalCache(localCacheKey) // prevent data loss
- console.error('failed to auto save to cloud', e)
- }
- return
+ /** watch for changes of game files, update filesHash, and auto save to cloud if hasUnsyncedChanges */
+ autoSaveToCloudState = AutoSaveToCloudState.Saved
+ private autoSaveToCloud: (() => void) | null = null
+ private startAutoSaveToCloud(localCacheKey: string) {
+ const retryAutoSaveToCloud = debounce(async () => {
+ if (this.autoSaveToCloudState !== AutoSaveToCloudState.Failed) return
+ if (this.hasUnsyncedChanges) {
+ this.autoSaveToCloud?.()
+ } else {
+ this.autoSaveToCloudState = AutoSaveToCloudState.Saved
+ await localHelper.clear(localCacheKey)
+ }
+ }, 5000)
+ this.addDisposer(retryAutoSaveToCloud.cancel)
+
+ const saveToCloud = debounce(async () => {
+ if (this.autoSaveToCloudState !== AutoSaveToCloudState.Pending) return
+ this.autoSaveToCloudState = AutoSaveToCloudState.Saving
+
+ try {
+ if (this.isSavingToCloud) {
+ await untilConditionMet(
+ () => this.isSavingToCloud,
+ () => !this.isSavingToCloud
+ )
}
- if (this.hasUnsyncedChanges) autoSaveToCloud()
- else await localHelper.clear(localCacheKey)
- }, 1500)
- this.addDisposer(save.cancel)
-
- const retryAutoSave = debounce(async () => {
- if (this.autoSaveToCloudState !== AutoSaveToCloudState.Failed) return
- if (this.hasUnsyncedChanges) {
- autoSaveToCloud()
+ if (this.hasUnsyncedChanges) await this.saveToCloud()
+ this.autoSaveToCloudState = AutoSaveToCloudState.Saved
+ } catch (e) {
+ this.autoSaveToCloudState = AutoSaveToCloudState.Failed
+ if (e instanceof Cancelled) {
+ this.autoSaveToCloud?.()
+ saveToCloud.flush()
} else {
- this.autoSaveToCloudState = AutoSaveToCloudState.Saved
- await localHelper.clear(localCacheKey)
+ retryAutoSaveToCloud()
+ await this.saveToLocalCache(localCacheKey) // prevent data loss
+ console.error('failed to auto save to cloud', e)
}
- }, 5000)
- this.addDisposer(retryAutoSave.cancel)
-
- // fire pending or retryable auto saves immediately when a new save occurs, making autoSaveToCloudState more responsive
- this.addDisposer(
- watch(
- () => this.isSavingToCloud,
- async () => {
- if (this.isSavingToCloud) {
- await retryAutoSave.flush()
- save.flush()
- }
- },
- { immediate: true }
- )
- )
-
- return () => {
- retryAutoSave.cancel()
- if (this.autoSaveToCloudState !== AutoSaveToCloudState.Saving)
- this.autoSaveToCloudState = AutoSaveToCloudState.Pending
- if (this.autoSaveMode === AutoSaveMode.Cloud) save()
+ return
}
- })()
+
+ if (this.hasUnsyncedChanges) this.autoSaveToCloud?.()
+ else await localHelper.clear(localCacheKey)
+ }, 1500)
+ this.addDisposer(saveToCloud.cancel)
+
+ this.autoSaveToCloud = () => {
+ retryAutoSaveToCloud.cancel()
+ if (this.autoSaveToCloudState !== AutoSaveToCloudState.Saving)
+ this.autoSaveToCloudState = AutoSaveToCloudState.Pending
+ if (this.autoSaveMode === AutoSaveMode.Cloud) saveToCloud()
+ }
+
this.addDisposer(
watch(
() => this.exportGameFiles(),
@@ -505,53 +491,64 @@ export class Project extends Disposable {
const filesHash = await hashFiles(files)
if (cancelled) return // avoid race condition and ensure filesHash accuracy
this.filesHash = filesHash
- if (this.hasUnsyncedChanges) autoSaveToCloud()
+ if (this.hasUnsyncedChanges) this.autoSaveToCloud?.()
},
{ immediate: true }
)
)
- // watch for all changes, auto save to local cache, or touch all game files to trigger lazy loading to ensure they are in memory
- const autoSaveToLocalCache = (() => {
- const save = debounce(() => this.saveToLocalCache(localCacheKey), 1000)
- this.addDisposer(save.cancel)
-
- const delazyLoadGameFiles = debounce(() => {
- const files = this.exportGameFiles()
- const fileList = Object.keys(files)
- fileList.map((path) => files[path]!.arrayBuffer())
- }, 1000)
- this.addDisposer(delazyLoadGameFiles.cancel)
-
- return () => {
- if (this.autoSaveMode === AutoSaveMode.LocalCache) save()
- else delazyLoadGameFiles()
- }
- })()
- this.addDisposer(
- watch(() => [this.exportMetadata(), this.exportGameFiles()], autoSaveToLocalCache, {
- immediate: true
- })
- )
-
- // watch for autoSaveMode switch, and trigger auto save accordingly
+ // fire pending or retryable auto saves immediately when a new save occurs, making autoSaveToCloudState more responsive
this.addDisposer(
watch(
- () => this.autoSaveMode,
- () => {
- switch (this.autoSaveMode) {
- case AutoSaveMode.Cloud:
- if (this.hasUnsyncedChanges) autoSaveToCloud()
- break
- case AutoSaveMode.LocalCache:
- autoSaveToLocalCache()
- break
+ () => this.isSavingToCloud,
+ async () => {
+ if (this.isSavingToCloud) {
+ await retryAutoSaveToCloud.flush()
+ saveToCloud.flush()
}
},
{ immediate: true }
)
)
}
+
+ /** watch for all changes, auto save to local cache, or touch all game files to trigger lazy loading to ensure they are in memory */
+ private autoSaveToLocalCache: (() => void) | null = null
+ private startAutoSaveToLocalCache(localCacheKey: string) {
+ const saveToLocalCache = debounce(() => this.saveToLocalCache(localCacheKey), 1000)
+ this.addDisposer(saveToLocalCache.cancel)
+
+ const touchGameFiles = debounce(() => {
+ const files = this.exportGameFiles()
+ Object.keys(files).map((path) => files[path]!.arrayBuffer())
+ }, 1000)
+ this.addDisposer(touchGameFiles.cancel)
+
+ this.autoSaveToLocalCache = () => {
+ if (this.autoSaveMode === AutoSaveMode.LocalCache) saveToLocalCache()
+ else touchGameFiles()
+ }
+
+ this.addDisposer(
+ watch(
+ () => [this.exportMetadata(), this.exportGameFiles()],
+ () => this.autoSaveToLocalCache?.(),
+ {
+ immediate: true
+ }
+ )
+ )
+ }
+
+ /** Initialize editing features */
+ async startEditing(localCacheKey: string) {
+ this.filesHash = await hashFiles(this.exportGameFiles())
+ if (this.lastSyncedFilesHash == null) {
+ this.lastSyncedFilesHash = this.filesHash
+ }
+ this.startAutoSaveToCloud(localCacheKey)
+ this.startAutoSaveToLocalCache(localCacheKey)
+ }
}
/** Get full name for project, which stands for a globally unique identifier for the project */
diff --git a/spx-gui/src/stores/user.ts b/spx-gui/src/stores/user.ts
index 5b86240e7..394e8dc17 100644
--- a/spx-gui/src/stores/user.ts
+++ b/spx-gui/src/stores/user.ts
@@ -45,23 +45,24 @@ export const useUserStore = defineStore('spx-user', {
}),
actions: {
async getFreshAccessToken(): Promise {
- if (!this.accessTokenValid()) {
- if (!this.refreshTokenValid()) {
- this.signOut()
- return null
- }
+ if (this.isAccessTokenValid()) return this.accessToken
+ if (this.isRefreshTokenValid()) {
try {
const tokenResp = await casdoorSdk.pkce.refreshAccessToken(this.refreshToken!)
this.setToken(tokenResp)
- } catch (error) {
- // TODO: not to clear storage for network error
- console.error('Failed to refresh access token', error)
- this.signOut()
- throw error
+ } catch (e) {
+ console.error('Failed to refresh access token', e)
+ throw e
}
+
+ // Due to js-pkce's lack of error handling, we must check if the access token is valid after calling `PKCE.refreshAccessToken`.
+ // The token might still be invalid if, e.g., the server has already revoked the refresh token.
+ if (this.isAccessTokenValid()) return this.accessToken
}
- return this.accessToken
+
+ this.signOut()
+ return null
},
setToken(tokenResp: ITokenResponse) {
const accessTokenExpiresAt = tokenResp.expires_in
@@ -82,16 +83,16 @@ export const useUserStore = defineStore('spx-user', {
this.refreshTokenExpiresAt = null
},
hasSignedIn() {
- return this.accessTokenValid() || this.refreshTokenValid()
+ return this.isAccessTokenValid() || this.isRefreshTokenValid()
},
- accessTokenValid() {
+ isAccessTokenValid() {
const delta = 60 * 1000 // 1 minute
return !!(
this.accessToken &&
(this.accessTokenExpiresAt === null || this.accessTokenExpiresAt - delta > Date.now())
)
},
- refreshTokenValid() {
+ isRefreshTokenValid() {
const delta = 60 * 1000 // 1 minute
return !!(
this.refreshToken &&
diff --git a/tools/ispx/go.mod b/tools/ispx/go.mod
index 4a6e88c02..0a9c0e204 100644
--- a/tools/ispx/go.mod
+++ b/tools/ispx/go.mod
@@ -5,7 +5,7 @@ go 1.21
require (
github.com/goplus/igop v0.27.1
github.com/goplus/reflectx v1.2.2
- github.com/goplus/spx v1.0.1-0.20240909022156-1b3a5bfdee29
+ github.com/goplus/spx v1.0.1-0.20240914015341-5e8dc870bb56
github.com/hajimehoshi/ebiten/v2 v2.8.0-alpha.3
)
diff --git a/tools/ispx/go.sum b/tools/ispx/go.sum
index 593af6c06..82acd8935 100644
--- a/tools/ispx/go.sum
+++ b/tools/ispx/go.sum
@@ -37,8 +37,8 @@ github.com/goplus/mod v0.13.10 h1:5Om6KOvo31daN7N30kWU1vC5zhsJPM+uPbcEN/FnlzE=
github.com/goplus/mod v0.13.10/go.mod h1:HDuPZgpWiaTp3PUolFgsiX+Q77cbUWB/mikVHfYND3c=
github.com/goplus/reflectx v1.2.2 h1:T1p20OIH/HcnAvQQNnDLwl6AZOjU34icsfc6migD6L8=
github.com/goplus/reflectx v1.2.2/go.mod h1:wHOS9ilbB4zrecI0W1dMmkW9JMcpXV7VjALVbNU9xfM=
-github.com/goplus/spx v1.0.1-0.20240909022156-1b3a5bfdee29 h1:wHe/XPmOzeliV2RwF+k3fj2NHsBqUMZCcuQFJw257fI=
-github.com/goplus/spx v1.0.1-0.20240909022156-1b3a5bfdee29/go.mod h1:UNZub8iRpxDvLpjJYYpiM7r36GqweTn2SNSKN49kG90=
+github.com/goplus/spx v1.0.1-0.20240914015341-5e8dc870bb56 h1:xWfgcMaB3CKMtlQVr7AH2kgMm6xDDfQ/T4LGoZA70sQ=
+github.com/goplus/spx v1.0.1-0.20240914015341-5e8dc870bb56/go.mod h1:1hkX9vPdDHrsA6LG8itiLMp83KIDJ0KlJI9CmpCoiCA=
github.com/hajimehoshi/ebiten/v2 v2.8.0-alpha.3 h1:cKpQdzW3I+iLID68l25GaxzPZHSZVRdE9/Pz4SOPBR8=
github.com/hajimehoshi/ebiten/v2 v2.8.0-alpha.3/go.mod h1:iKp1U/H0J0rv9X4ztGSTXCyN6z/DKKrUL+D9sjg7SbI=
github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=