diff --git a/xmcl-keystone-ui/src/composables/instanceJava.ts b/xmcl-keystone-ui/src/composables/instanceJava.ts index 7c60f320a..0717010fa 100644 --- a/xmcl-keystone-ui/src/composables/instanceJava.ts +++ b/xmcl-keystone-ui/src/composables/instanceJava.ts @@ -29,7 +29,7 @@ export const kInstanceJava: InjectionKey> = S export function useInstanceJava(instance: Ref, version: Ref, all: Ref) { const { resolveJava } = useService(JavaServiceKey) - const { data, mutate, isValidating, error } = useSWRV(() => instance.value.path && `/instance/${instance.value.path}/java-version?version=${version.value && 'id' in version.value ? version.value.id : undefined}`, async () => { + const { data, mutate, isValidating, error } = useSWRV(computed(() => instance.value.path && `/instance/${instance.value.path}/java-version?version=${version.value && 'id' in version.value ? version.value.id : undefined}`), async () => { return await computeJava(all.value, resolveJava, instance.value, version.value) }, { revalidateOnFocus: false }) diff --git a/xmcl-keystone-ui/src/composables/instanceLaunch.ts b/xmcl-keystone-ui/src/composables/instanceLaunch.ts index bd4dcf5f7..619a85479 100644 --- a/xmcl-keystone-ui/src/composables/instanceLaunch.ts +++ b/xmcl-keystone-ui/src/composables/instanceLaunch.ts @@ -126,7 +126,7 @@ export function useInstanceLaunch(instance: Ref, resolvedVersion: Ref< const options: LaunchOptions = { operationId: id, - version: instance.value.version || ver.id, + version: ver.id, gameDirectory: instance.value.path, user: userProfile.value, java: javaRec.path, diff --git a/xmcl-keystone-ui/src/composables/instanceOptions.ts b/xmcl-keystone-ui/src/composables/instanceOptions.ts index bc3afdd54..5108a8dc6 100644 --- a/xmcl-keystone-ui/src/composables/instanceOptions.ts +++ b/xmcl-keystone-ui/src/composables/instanceOptions.ts @@ -5,16 +5,16 @@ import { useState } from './syncableState' export const kInstanceOptions: InjectionKey> = Symbol('InstanceOptions') -export function useInstanceOptions(instance: Ref) { +export function useInstanceOptions(instancePath: Ref) { const { editGameSetting, watch: watchOptions } = useService(InstanceOptionsServiceKey) - const { state, isValidating, error } = useState(() => instance.value.path ? watchOptions(instance.value.path) : undefined, GameOptionsState) + const { state, isValidating, error } = useState(() => instancePath.value ? watchOptions(instancePath.value) : undefined, GameOptionsState) const { locale } = useI18n() watch(state, (newOps) => { if (newOps) { if (newOps.lang === '') { editGameSetting({ - instancePath: instance.value.path, + instancePath: instancePath.value, lang: locale.value.toLowerCase().replace('-', '_'), resourcePacks: newOps.resourcePacks, }) diff --git a/xmcl-keystone-ui/src/composables/instanceVersion.ts b/xmcl-keystone-ui/src/composables/instanceVersion.ts index 1cf87f3f3..154033266 100644 --- a/xmcl-keystone-ui/src/composables/instanceVersion.ts +++ b/xmcl-keystone-ui/src/composables/instanceVersion.ts @@ -30,19 +30,31 @@ export function isResolvedVersion(v?: InstanceResolveVersion): v is ResolvedVers export function useInstanceVersion(instance: Ref, local: Ref) { const { resolveLocalVersion } = useService(VersionServiceKey) - const versionHeader = computed(() => getResolvedVersion(local.value, - instance.value.version, - instance.value.runtime.minecraft, - instance.value.runtime.forge, - instance.value.runtime.neoForged, - instance.value.runtime.fabricLoader, - instance.value.runtime.optifine, - instance.value.runtime.quiltLoader, - instance.value.runtime.labyMod) || { ...EMPTY_VERSION, id: getExpectVersion(instance.value.runtime) }) - const folder = computed(() => versionHeader.value?.id || 'unknown') + const versionHeader = computed(() => { + let result: LocalVersionHeader | undefined + if (instance.value.path) { + result = getResolvedVersion(local.value, + instance.value.version, + instance.value.runtime.minecraft, + instance.value.runtime.forge, + instance.value.runtime.neoForged, + instance.value.runtime.fabricLoader, + instance.value.runtime.optifine, + instance.value.runtime.quiltLoader, + instance.value.runtime.labyMod) + } + if (!result) { + result = { ...EMPTY_VERSION, id: getExpectVersion(instance.value.runtime) } + } + return result + }) + const folder = computed(() => versionHeader.value.id) const { isValidating, mutate, data: resolvedVersion, error } = useSWRV(() => instance.value.path && `/instance/${instance.value.path}/version`, async () => { console.log('update instance version') + if (!instance.value.path) { + return undefined + } if (!versionHeader.value.path) { return { requirements: { ...instance.value.runtime } } } diff --git a/xmcl-keystone-ui/src/composables/instances.ts b/xmcl-keystone-ui/src/composables/instances.ts index 21d466cfc..bb163ee9d 100644 --- a/xmcl-keystone-ui/src/composables/instances.ts +++ b/xmcl-keystone-ui/src/composables/instances.ts @@ -12,8 +12,7 @@ export const kInstances: InjectionKey> = Symbol( * Hook of a view of all instances & some deletion/selection functions */ export function useInstances() { - const path = useLocalStorageCacheStringValue('selectedInstancePath', '' as string) - const { createInstance, getSharedInstancesState, editInstance, deleteInstance } = useService(InstanceServiceKey) + const { createInstance, getSharedInstancesState, editInstance, deleteInstance, validateInstancePath } = useService(InstanceServiceKey) const { state, isValidating, error } = useState(getSharedInstancesState, class extends InstanceState { override instanceEdit(settings: DeepPartial & { path: string }) { const inst = this.instances.find(i => i.path === (settings.path))! @@ -46,6 +45,8 @@ export function useInstances() { }) const _instances = computed(() => state.value?.instances ?? []) const { instances, setToPrevious } = useSortedInstance(_instances) + const _path = useLocalStorageCacheStringValue('selectedInstancePath', '' as string) + const path = ref('') async function edit(options: EditInstanceOptions & { instancePath: string }) { await editInstance(options) @@ -65,21 +66,47 @@ export function useInstances() { } } } - watch(state, async () => { - let firstInstancePath = instances.value[0]?.path ?? '' - if (!firstInstancePath) { - firstInstancePath = await createInstance({ - name: 'Minecraft', - }) - path.value = firstInstancePath - } - const existed = instances.value.find(i => i.path === path.value) - if (!path.value || !existed) { - // Select the first instance - path.value = firstInstancePath + watch(state, async (newVal, oldVal) => { + if (!newVal) return + debugger + if (!oldVal) { + // initialize + const instances = [...newVal.instances] + const lastSelectedPath = _path.value + + const selectDefault = async () => { + // Select the first instance + let defaultPath = instances[0]?.path as string | undefined + if (!defaultPath) { + // Create a default instance + defaultPath = await createInstance({ + name: 'Minecraft', + }) + } + _path.value = defaultPath + } + + if (lastSelectedPath) { + // Validate the last selected path + if (!instances.some(i => i.path === lastSelectedPath)) { + const badInstance = await validateInstancePath(lastSelectedPath) + if (badInstance) { + await selectDefault() + } + } + } else { + // No selected, try to select the first instance + await selectDefault() + } + + path.value = _path.value } }) watch(path, (newPath) => { + if (newPath !== _path.value) { + // save to local storage + _path.value = newPath + } editInstance({ instancePath: newPath, lastAccessDate: Date.now(), diff --git a/xmcl-keystone-ui/src/composables/launchTask.ts b/xmcl-keystone-ui/src/composables/launchTask.ts index cf74c7a43..1a197a066 100644 --- a/xmcl-keystone-ui/src/composables/launchTask.ts +++ b/xmcl-keystone-ui/src/composables/launchTask.ts @@ -6,6 +6,7 @@ export const kLaunchTask: InjectionKey> = Symbo export function useLaunchTask(path: Ref, version: Ref, localVersion: Ref) { return useTask((i) => { + if (!path.value) return false const p = i.param as any if (i.state === TaskState.Cancelled || i.state === TaskState.Succeed || i.state === TaskState.Failed) { return false diff --git a/xmcl-keystone-ui/src/composables/save.ts b/xmcl-keystone-ui/src/composables/save.ts index 7c90d1da9..fb6d31945 100644 --- a/xmcl-keystone-ui/src/composables/save.ts +++ b/xmcl-keystone-ui/src/composables/save.ts @@ -5,9 +5,9 @@ import { useState } from './syncableState' export const kInstanceSave: InjectionKey> = Symbol('InstanceSave') -export function useInstanceSaves(instance: Ref) { +export function useInstanceSaves(instancePath: Ref) { const { watch } = useService(InstanceSavesServiceKey) - const { state, isValidating, error } = useState(() => instance.value.path ? watch(instance.value.path) : undefined, Saves) + const { state, isValidating, error } = useState(() => instancePath.value ? watch(instancePath.value) : undefined, Saves) const saves = computed(() => state.value?.saves || []) diff --git a/xmcl-keystone-ui/src/windows/main/Context.ts b/xmcl-keystone-ui/src/windows/main/Context.ts index f4bbf9241..fff3fa3c1 100644 --- a/xmcl-keystone-ui/src/windows/main/Context.ts +++ b/xmcl-keystone-ui/src/windows/main/Context.ts @@ -66,8 +66,8 @@ export default defineComponent({ const instanceVersion = useInstanceVersion(instance.instance, localVersions.versions) const instanceJava = useInstanceJava(instance.instance, instanceVersion.resolvedVersion, java.all) const instanceDefaultSource = useInstanceDefaultSource(instance.path) - const options = useInstanceOptions(instance.instance) - const saves = useInstanceSaves(instance.instance) + const options = useInstanceOptions(instance.path) + const saves = useInstanceSaves(instance.path) const resourcePacks = useInstanceResourcePacks(instance.path, options.gameOptions) const instanceMods = useInstanceMods(instance.path, instance.runtime, instanceJava.java) const shaderPacks = useInstanceShaderPacks(instance.path, instance.runtime, instanceMods.mods, options.gameOptions) diff --git a/xmcl-runtime-api/src/services/InstanceService.ts b/xmcl-runtime-api/src/services/InstanceService.ts index f42d789b5..e4627f573 100644 --- a/xmcl-runtime-api/src/services/InstanceService.ts +++ b/xmcl-runtime-api/src/services/InstanceService.ts @@ -188,6 +188,8 @@ export interface InstanceService { * @returns The instance path */ acquireInstanceById(id: string): Promise + + validateInstancePath(path: string): Promise<'bad' | 'nondictionary' | 'noperm' | 'exists' | undefined> } export const InstanceServiceKey: ServiceKey = 'InstanceService' diff --git a/xmcl-runtime/instance/InstanceService.ts b/xmcl-runtime/instance/InstanceService.ts index 650ce48d7..b4577d18e 100644 --- a/xmcl-runtime/instance/InstanceService.ts +++ b/xmcl-runtime/instance/InstanceService.ts @@ -661,4 +661,15 @@ export class InstanceService extends StatefulService implements I this.log(`Create new instance ${id} -> ${instancePath}`) return path } + + async validateInstancePath(path: string) { + const err = await validateDirectory(this.app.platform, path) + if (err && err !== 'exists') { + return err + } + if (this.state.all[path]) { + return undefined + } + return await this.loadInstance(path).catch(() => 'bad') ? undefined : 'bad' + } } diff --git a/xmcl-runtime/util/validate.ts b/xmcl-runtime/util/validate.ts index 9bcd078f3..d48bd857e 100644 --- a/xmcl-runtime/util/validate.ts +++ b/xmcl-runtime/util/validate.ts @@ -1,8 +1,8 @@ +import { Platform } from '@xmcl/runtime-api' import { randomBytes } from 'crypto' -import { mkdir, readdir, stat, unlink, writeFile } from 'fs/promises' +import { mkdir, readdir, rmdir, stat, unlink, writeFile } from 'fs/promises' import { join } from 'path' import { isSystemError } from '../util/error' -import { Platform } from '@xmcl/runtime-api' export async function validateDirectory(platform: Platform, path: string) { // Check if the path is the root of the drive @@ -38,7 +38,7 @@ export async function validateDirectory(platform: Platform, path: string) { // Check if we have permission to create the directory try { await mkdir(path, { recursive: true }) - await unlink(path) + await rmdir(path) } catch (e) { if (isSystemError(e)) { if (e.code === 'EACCES') {