From cf950e75473a40315d502e534e539a39e528096e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Thu, 11 Jan 2024 12:35:31 +0200 Subject: [PATCH 1/5] Add LocalModList.updateModListAfterChange to reduce code duplication --- src/components/views/LocalModList.vue | 46 ++++++++++----------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/src/components/views/LocalModList.vue b/src/components/views/LocalModList.vue index b97a3dd38..f07ce6f90 100644 --- a/src/components/views/LocalModList.vue +++ b/src/components/views/LocalModList.vue @@ -330,6 +330,17 @@ import SearchUtils from '../../utils/SearchUtils'; return ModBridge.getCachedThunderstoreModFromMod(mod); } + async updateModListAfterChange(updatedList: ManifestV2[]) { + await this.$store.dispatch("updateModList", updatedList); + + const err = await ConflictManagementProvider.instance.resolveConflicts(updatedList, this.contextProfile!); + if (err instanceof R2Error) { + this.$emit('error', err); + } + + this.filterModList(); + } + async moveUp(vueMod: any) { const mod: ManifestV2 = new ManifestV2().fromReactive(vueMod); const updatedList = await ProfileModList.shiftModEntryUp(mod, this.contextProfile!); @@ -337,12 +348,7 @@ import SearchUtils from '../../utils/SearchUtils'; this.$emit('error', updatedList); return; } - await this.$store.dispatch("updateModList",updatedList); - const err = await ConflictManagementProvider.instance.resolveConflicts(updatedList, this.contextProfile!); - if (err instanceof R2Error) { - this.$emit('error', err); - } - this.filterModList(); + await this.updateModListAfterChange(updatedList); } async moveDown(vueMod: any) { @@ -352,12 +358,7 @@ import SearchUtils from '../../utils/SearchUtils'; this.$emit('error', updatedList); return; } - await this.$store.dispatch("updateModList", updatedList); - const err = await ConflictManagementProvider.instance.resolveConflicts(updatedList, this.contextProfile!); - if (err instanceof R2Error) { - this.$emit('error', err); - } - this.filterModList(); + await this.updateModListAfterChange(updatedList); } isLatest(mod: ManifestV2): boolean { @@ -438,12 +439,7 @@ import SearchUtils from '../../utils/SearchUtils'; this.$emit('error', updatedList); return updatedList; } - await this.$store.dispatch("updateModList", updatedList); - const err = await ConflictManagementProvider.instance.resolveConflicts(updatedList, this.contextProfile!); - if (err instanceof R2Error) { - this.$emit('error', err); - } - this.filterModList(); + await this.updateModListAfterChange(updatedList); } async uninstallMod(vueMod: any) { @@ -473,12 +469,7 @@ import SearchUtils from '../../utils/SearchUtils'; this.$emit('error', result); return; } - await this.$store.dispatch("updateModList", result); - const err = await ConflictManagementProvider.instance.resolveConflicts(result, this.contextProfile!); - if (err instanceof R2Error) { - this.$emit('error', err); - } - this.filterModList(); + await this.updateModListAfterChange(result); } showDependencyList(vueMod: any, displayType: string) { @@ -552,12 +543,7 @@ import SearchUtils from '../../utils/SearchUtils'; this.$emit('error', updatedList); return updatedList; } - await this.$store.dispatch("updateModList",updatedList); - const err = await ConflictManagementProvider.instance.resolveConflicts(updatedList, this.contextProfile!); - if (err instanceof R2Error) { - this.$emit('error', err); - } - this.filterModList(); + await this.updateModListAfterChange(updatedList); } updateMod(vueMod: any) { From f716d11e0e8013cf51ae65bc7c06fb063440904f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Thu, 11 Jan 2024 13:10:08 +0200 Subject: [PATCH 2/5] Postpone updating local mod list to improve uninstall performance There's three methods that control how mods are uninstalled: uninstallModRequireConfirmation, which calls performUninstallMod directly if the mod has no dependants, and uninstallMod if the mod has dependencies. uninstallMod in turn calls performUninstallMod for the mod and all the dependants separately. This commit does the following changes: - By default, performUninstallMod now calls updateModListAfterChange helper, which differs from the original implementation in that it also calls filterModList method - Since uninstallModRequireConfirmation calls performUninstallMod, it no longer needs to call filterModList directly - performUninstallMod also accepts a new boolean parameter which can be used to prevent it from calling updateModListAfterChange - uninstallMod uses the said parameter to prevent the updateModListAfterChange getting called after each individual mod is uninstalled. It calls updateModListAfterChange itself directly in the end As far as I can tell this has no ill effects to the end result, i.e. the profile files that remain on the disk appear to be identical. The only downside I'm aware of is that the modal that shows the list of mods no longer gets updated during the process, so it might appear to the user that nothing happens. The benefit is that the process takes significantly less time when uninstalling a mod with lots of dependants. Prior to changes uninstalling BepInEx from a profile that contained 109 mods took 147 seconds, while after the changes it "only" takes 24 seconds. --- src/components/views/LocalModList.vue | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/views/LocalModList.vue b/src/components/views/LocalModList.vue index f07ce6f90..41fccde4c 100644 --- a/src/components/views/LocalModList.vue +++ b/src/components/views/LocalModList.vue @@ -381,7 +381,7 @@ import SearchUtils from '../../utils/SearchUtils'; return Dependants.getDependencyList(mod, this.modifiableModList); } - async performUninstallMod(mod: ManifestV2): Promise { + async performUninstallMod(mod: ManifestV2, updateModList=true): Promise { const uninstallError: R2Error | null = await ProfileInstallerProvider.instance.uninstallMod(mod, this.contextProfile!); if (uninstallError instanceof R2Error) { // Uninstall failed @@ -396,10 +396,8 @@ import SearchUtils from '../../utils/SearchUtils'; this.$emit('error', modList); return modList; } - await this.$store.dispatch("updateModList",modList); - const err = await ConflictManagementProvider.instance.resolveConflicts(modList, this.contextProfile!); - if (err instanceof R2Error) { - this.$emit('error', err); + if (updateModList) { + await this.updateModListAfterChange(modList); } } @@ -446,13 +444,13 @@ import SearchUtils from '../../utils/SearchUtils'; let mod: ManifestV2 = new ManifestV2().fromReactive(vueMod); try { for (const dependant of Dependants.getDependantList(mod, this.modifiableModList)) { - const result = await this.performUninstallMod(dependant); + const result = await this.performUninstallMod(dependant, false); if (result instanceof R2Error) { this.$emit('error', result); return; } } - const result = await this.performUninstallMod(mod); + const result = await this.performUninstallMod(mod, false); if (result instanceof R2Error) { this.$emit('error', result); return; @@ -483,7 +481,6 @@ import SearchUtils from '../../utils/SearchUtils'; const mod: ManifestV2 = new ManifestV2().fromReactive(vueMod); if (this.getDependantList(mod).size === 0) { this.performUninstallMod(mod); - this.filterModList(); } else { this.showDependencyList(mod, DependencyListDisplayType.UNINSTALL); } From 40206020c4e527caa8848c046b8a575ead7cab5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Thu, 11 Jan 2024 16:59:47 +0200 Subject: [PATCH 3/5] Show the mod being currently uninstalled in the LocalModList modal This works as an indicator to the user that something is happening in case the uninstallation of multiple mods takes a bit longer. --- src/components/views/LocalModList.vue | 10 ++++++++++ src/css/custom.scss | 3 +++ 2 files changed, 13 insertions(+) diff --git a/src/components/views/LocalModList.vue b/src/components/views/LocalModList.vue index 41fccde4c..3d915d97a 100644 --- a/src/components/views/LocalModList.vue +++ b/src/components/views/LocalModList.vue @@ -100,6 +100,9 @@ @click="uninstallMod(selectedManifestMod)"> Uninstall + + Uninstalling {{ modBeingUninstalled }} + - From b64c89cf306488d50b3e846aae32bbd9a5ae32a7 Mon Sep 17 00:00:00 2001 From: Mythic Date: Tue, 23 Jan 2024 00:25:04 +0200 Subject: [PATCH 5/5] Reduce the likelihood of state management bugs Make sure the profile mod list is refreshed with the latest information even if the uninstall logic exits midway. --- src/components/views/LocalModList.vue | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/views/LocalModList.vue b/src/components/views/LocalModList.vue index ab92a06d8..347aff97c 100644 --- a/src/components/views/LocalModList.vue +++ b/src/components/views/LocalModList.vue @@ -387,7 +387,7 @@ import SearchUtils from '../../utils/SearchUtils'; return Dependants.getDependencyList(mod, this.modifiableModList); } - async performUninstallMod(mod: ManifestV2, updateModList=true): Promise { + async performUninstallMod(mod: ManifestV2, updateModList=true): Promise { const uninstallError: R2Error | null = await ProfileInstallerProvider.instance.uninstallMod(mod, this.contextProfile!); if (uninstallError instanceof R2Error) { // Uninstall failed @@ -405,6 +405,7 @@ import SearchUtils from '../../utils/SearchUtils'; if (updateModList) { await this.updateModListAfterChange(modList); } + return modList; } async disableMod(vueMod: any) { @@ -448,6 +449,7 @@ import SearchUtils from '../../utils/SearchUtils'; async uninstallMod(vueMod: any) { let mod: ManifestV2 = new ManifestV2().fromReactive(vueMod); + let lastSuccess: ManifestV2[] | null = null; try { for (const dependant of Dependants.getDependantList(mod, this.modifiableModList)) { this.modBeingUninstalled = dependant.getName(); @@ -456,6 +458,8 @@ import SearchUtils from '../../utils/SearchUtils'; this.$emit('error', result); this.modBeingUninstalled = null; return; + } else { + lastSuccess = result; } } this.modBeingUninstalled = mod.getName(); @@ -464,16 +468,21 @@ import SearchUtils from '../../utils/SearchUtils'; this.$emit('error', result); this.modBeingUninstalled = null; return; + } else { + lastSuccess = result; } } catch (e) { // Failed to uninstall mod. const err: Error = e as Error; this.$emit('error', err); - this.modBeingUninstalled = null; LoggerProvider.instance.Log(LogSeverity.ACTION_STOPPED, `${err.name}\n-> ${err.message}`); + } finally { + this.modBeingUninstalled = null; + if (lastSuccess) { + await this.updateModListAfterChange(lastSuccess); + } } this.selectedManifestMod = null; - this.modBeingUninstalled = null; const result: ManifestV2[] | R2Error = await ProfileModList.getModList(this.contextProfile!); if (result instanceof R2Error) { this.$emit('error', result);