Skip to content

Commit

Permalink
Further improve installation progress tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
VilppeRiskidev committed Feb 3, 2025
1 parent a7b06bd commit c359b28
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 84 deletions.
60 changes: 36 additions & 24 deletions src/components/mixins/DownloadMixin.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
<script lang='ts'>
import Vue from 'vue';
import Component from 'vue-class-component';
import { Store } from "vuex";
import StatusEnum from "../../model/enums/StatusEnum";
import R2Error from "../../model/errors/R2Error";
import R2Error, { throwForR2Error } from "../../model/errors/R2Error";
import Game from "../../model/game/Game";
import Profile from "../../model/Profile";
import ManifestV2 from "../../model/ManifestV2";
import Profile, { ImmutableProfile } from "../../model/Profile";
import ThunderstoreCombo from "../../model/ThunderstoreCombo";
import ThunderstoreMod from "../../model/ThunderstoreMod";
import { installModsAndResolveConflicts } from "../../utils/ProfileUtils";
import { Store } from "vuex";
import ConflictManagementProvider from "../../providers/generic/installing/ConflictManagementProvider";
import ProfileModList from "../../r2mm/mods/ProfileModList";
import { installModsToProfile } from "../../utils/ProfileUtils";
@Component
Expand Down Expand Up @@ -46,41 +49,50 @@ export default class DownloadMixin extends Vue {
async downloadCompletedCallback(downloadedMods: ThunderstoreCombo[], assignId: number): Promise<void> {
try {
await installModsAndResolveConflicts(downloadedMods, this.profile.asImmutableProfile(), this.$store, assignId);
await this.installModsAndResolveConflicts(downloadedMods, this.profile.asImmutableProfile(), assignId);
} catch (e) {
this.$store.commit('download/updateDownload', {assignId, failed: true});
this.$store.commit('error/handleError', R2Error.fromThrownValue(e));
}
}
downloadProgressCallback(assignId: number, downloadProgress: number, modName: string, status: number, err: R2Error | null) {
try {
if (status === StatusEnum.FAILURE) {
this.setIsModProgressModalOpen(false);
this.$store.commit('download/updateDownload', {assignId, failed: true});
if (err !== null) {
DownloadMixin.addSolutionsToError(err);
throw err;
}
} else if (status === StatusEnum.PENDING) {
this.$store.commit('download/updateDownload', {assignId, downloadProgress, modName});
}
} catch (e) {
this.$store.commit('error/handleError', R2Error.fromThrownValue(e));
}
};
static downloadProgressCallback(store: Store<any>, assignId: number, downloadProgress: number, modName: string, status: number, err: R2Error | null) {
static downloadProgressCallback(
store: Store<any>,
assignId: number,
downloadProgress: number,
modName: string,
status: number,
err: R2Error | null,
closeModProgressModal?: () => void,
) {
if (status === StatusEnum.FAILURE) {
if (closeModProgressModal !== undefined) {
closeModProgressModal();
}
store.commit('download/updateDownload', {assignId, failed: true});
if (err !== null) {
DownloadMixin.addSolutionsToError(err);
throw err;
}
} else if (status === StatusEnum.PENDING) {
} else if (status === StatusEnum.PENDING || status === StatusEnum.SUCCESS) {
store.commit('download/updateDownload', {assignId, downloadProgress, modName});
}
}
async installModsAndResolveConflicts(
downloadedMods: ThunderstoreCombo[],
profile: ImmutableProfile,
assignId: number
): Promise<void> {
await ProfileModList.requestLock(async () => {
const modList: ManifestV2[] = await installModsToProfile(downloadedMods, profile, undefined,(status, progress) => {
this.$store.commit('download/updateDownload', {assignId, installProgress: progress});
});
await this.$store.dispatch('profile/updateModList', modList);
throwForR2Error(await ConflictManagementProvider.instance.resolveConflicts(modList, profile));
});
}
static addSolutionsToError(err: R2Error): void {
// Sanity check typing.
if (!(err instanceof R2Error)) {
Expand Down
12 changes: 5 additions & 7 deletions src/components/profiles-modals/ImportProfileModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -212,20 +212,18 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
async importProfile(targetProfileName: string, mods: ExportMod[], zipPath: string) {
this.activeStep = 'PROFILE_IS_BEING_IMPORTED';
this.importPhaseDescription = 'Downloading mods: 0%';
const progressCallback = (progressText: string) => this.importPhaseDescription = progressText;
const progressCallback = (progress: number|string) => typeof progress === "number"
? this.importPhaseDescription = `Downloading mods: ${Math.floor(progress)}%`
: this.importPhaseDescription = progress;
const game = this.$store.state.activeGame;
const settings = this.$store.getters['settings'];
const ignoreCache = settings.getContext().global.ignoreCache;
const isUpdate = this.importUpdateSelection === 'UPDATE';
try {
const comboList = await ProfileUtils.exportModsToCombos(mods, game);
await ThunderstoreDownloaderProvider.instance.downloadImportedMods(comboList, ignoreCache, (progress) => {
progressCallback(`Downloading mods: ${Math.floor(progress)}%`)
});
await ProfileUtils.populateImportedProfile(comboList, mods, targetProfileName, isUpdate, zipPath, (progress) => {
progressCallback(`Copying mods to profile: ${progress}%`)
});
await ThunderstoreDownloaderProvider.instance.downloadImportedMods(comboList, ignoreCache, progressCallback);
await ProfileUtils.populateImportedProfile(comboList, mods, targetProfileName, isUpdate, zipPath, progressCallback);
} catch (e) {
await this.$store.dispatch('profiles/ensureProfileExists');
this.closeModal();
Expand Down
8 changes: 4 additions & 4 deletions src/components/views/DownloadModModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
:value="$store.getters['download/currentDownload'].downloadProgress"
:className="['is-dark']"
/>
<p v-if="$store.getters['download/currentDownload'].installationProgress">Installation: {{$store.getters['download/currentDownload'].installationProgress}}% complete</p>
<p v-else>Installation: waiting for download to finish</p>
<p v-if="$store.getters['download/currentDownload'].installProgress">Installing: {{$store.getters['download/currentDownload'].installProgress}}% complete</p>
<p v-else>Installing: waiting for download to finish</p>
<Progress
:max='100'
:value="$store.getters['download/currentDownload'].installationProgress"
:value="$store.getters['download/currentDownload'].installProgress"
:className="['is-dark']"
/>
</div>
Expand Down Expand Up @@ -134,7 +134,7 @@ import ProfileModList from '../../r2mm/mods/ProfileModList';
this.profile.asImmutableProfile(),
tsCombo,
this.ignoreCache,
(downloadProgress, modName, status, err) => { this.downloadProgressCallback(assignId, downloadProgress, modName, status, err); }
(downloadProgress, modName, status, err) => { DownloadMixin.downloadProgressCallback(this.$store, assignId, downloadProgress, modName, status, err, () => this.setIsModProgressModalOpen(false)); }
);
} catch (e) {
this.setIsModProgressModalOpen(false);
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/UpdateAllInstalledModsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default class UpdateAllInstalledModsModal extends mixins(DownloadMixin)
throw err;
}
} else if (status === StatusEnum.PENDING) {
this.$store.commit('download/updateDownload', {assignId, progress, modName});
this.$store.commit('download/updateDownload', {assignId, downloadProgress: progress, modName});
}
} catch (e) {
this.$store.commit('error/handleError', R2Error.fromThrownValue(e));
Expand Down
43 changes: 29 additions & 14 deletions src/pages/DownloadMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,43 @@
<div class="card is-shadowless">
<p><strong>{{ downloadObject.initialMods.join(", ") }}</strong></p>

<div v-if="!downloadObject.failed && downloadObject.downloadProgress < 100">
<p>Downloading: {{ downloadObject.modName }}</p>
<p>{{Math.min(Math.floor(downloadObject.downloadProgress), 100)}}% complete</p>
<div v-if="downloadObject.failed">
<p>Download failed</p>
<Progress
:max='100'
:value='downloadObject.downloadProgress'
:className="['is-info']"
:value='100'
:className="['is-danger']"
/>
</div>

<div v-else-if="!downloadObject.failed && downloadObject.installationProgress < 100">
<p v-if="downloadObject.installationProgress < 100">Installing: {{ downloadObject.modName }}</p>
<p>{{Math.min(Math.floor(downloadObject.installationProgress), 100)}}% complete</p>
<Progress
:max='100'
:value='downloadObject.installationProgress'
:className="['is-info']"
/>
<div v-else class="row">

<div class="col">
<p v-if="downloadObject.downloadProgress < 100">Downloading: {{ downloadObject.modName }}</p>
<p v-else>Downloading:</p>
<p>{{Math.min(Math.floor(downloadObject.downloadProgress), 100)}}% complete</p>
<Progress
:max='100'
:value='downloadObject.downloadProgress'
:className="['is-info']"
/>
</div>

<div v-if="downloadObject.installProgress < 100" class="col">
<p>Installing:</p>
<p v-if="downloadObject.downloadProgress < 100">Installing: waiting for download to finish</p>
<p v-else>{{Math.min(Math.floor(downloadObject.installProgress), 100)}}% complete</p>
<Progress
:max='100'
:value='downloadObject.installProgress'
:className="['is-info']"
/>
</div>

</div>

<div v-else-if="downloadObject.failed">
<p>{{downloadObject.failed ? "Download failed" : `${Math.min(Math.floor(downloadObject.installationProgress), 100)}% complete`}}</p>
<p>{{downloadObject.failed ? "Download failed" : `${Math.min(Math.floor(downloadObject.installProgress), 100)}% complete`}}</p>
<Progress
:max='100'
:value='100'
Expand Down
12 changes: 8 additions & 4 deletions src/r2mm/downloading/BetterThunderstoreDownloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader
const allModsToDownload = [...dependencies, combo];

let downloadCount = 0;
const singleModProgressCallback = (progress: number, status: number, err: R2Error | null) => {
const singleModProgressCallback = (modName: string, progress: number, status: number, err: R2Error | null) => {
if (status === StatusEnum.FAILURE) {
throw err;
}
Expand All @@ -114,7 +114,7 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader
console.error(`Ignore unknown status code "${status}"`);
return;
}
totalProgressCallback(totalProgress, combo.getMod().getName(), status, err);
totalProgressCallback(totalProgress, modName, status, err);
}

for (const combo of allModsToDownload) {
Expand All @@ -124,8 +124,12 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader
}

try {
const response = await this._downloadCombo(combo, singleModProgressCallback);
await this._saveDownloadResponse(response, combo, singleModProgressCallback);
const response = await this._downloadCombo(combo, (progress, status, err) => {
singleModProgressCallback(combo.getMod().getName(), progress, status, err)
});
await this._saveDownloadResponse(response, combo, (progress, status, err) => {
singleModProgressCallback(combo.getMod().getName(), progress, status, err)
});
} catch(e) {
throw R2Error.fromThrownValue(e, `Failed to download mod ${combo.getVersion().getFullName()}`);
}
Expand Down
6 changes: 3 additions & 3 deletions src/store/modules/DownloadModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface DownloadProgress {
initialMods: string[];
modName: string;
downloadProgress: number;
installationProgress: number;
installProgress: number;
failed: boolean;
}

Expand Down Expand Up @@ -43,7 +43,7 @@ export const DownloadModule = {
initialMods,
modName: '',
downloadProgress: 0,
installationProgress: 0,
installProgress: 0,
failed: false,
};
state.allDownloads = [...state.allDownloads, downloadObject];
Expand All @@ -54,7 +54,7 @@ export const DownloadModule = {
getters: <GetterTree<State, RootState>>{
activeDownloadCount(state) {
const active = state.allDownloads.filter(
dl => !dl.failed && dl.downloadProgress < 100 && dl.installationProgress < 100
dl => !dl.failed && dl.downloadProgress < 100 && dl.installProgress < 100
);
return active.length;
},
Expand Down
36 changes: 9 additions & 27 deletions src/utils/ProfileUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import path from "path";

import * as yaml from "yaml";
import { Store } from "vuex";

import FileUtils from "./FileUtils";
import R2Error, { throwForR2Error } from "../model/errors/R2Error";
Expand All @@ -13,7 +12,6 @@ import Profile, { ImmutableProfile } from "../model/Profile";
import ThunderstoreCombo from "../model/ThunderstoreCombo";
import VersionNumber from "../model/VersionNumber";
import FsProvider from "../providers/generic/file/FsProvider";
import ConflictManagementProvider from "../providers/generic/installing/ConflictManagementProvider";
import ZipProvider from "../providers/generic/zip/ZipProvider";
import ProfileInstallerProvider from "../providers/ror2/installing/ProfileInstallerProvider";
import * as PackageDb from '../r2mm/manager/PackageDexieStore';
Expand All @@ -37,7 +35,7 @@ export async function exportModsToCombos(exportMods: ExportMod[], game: Game): P
async function extractConfigsToImportedProfile(
file: string,
profileName: string,
progressCallback: (status: number) => void
progressCallback: (status: string) => void
) {
const zipEntries = await ZipProvider.instance.getEntries(file);
const excludedFiles = ["export.r2x", "mods.yml"];
Expand All @@ -54,25 +52,10 @@ async function extractConfigsToImportedProfile(
}

const progress = Math.floor(((index + 1) / zipEntries.length) * 100);
progressCallback(progress);
progressCallback(`Copying configs to profile: ${progress}%`);
}
}

export async function installModsAndResolveConflicts(
downloadedMods: ThunderstoreCombo[],
profile: ImmutableProfile,
store: Store<any>,
assignId: number
): Promise<void> {
await ProfileModList.requestLock(async () => {
const modList: ManifestV2[] = await installModsToProfile(downloadedMods, profile, undefined, (installationProgress) => {
store.commit('download/updateDownload', {assignId, installationProgress});
});
await store.dispatch('profile/updateModList', modList);
throwForR2Error(await ConflictManagementProvider.instance.resolveConflicts(modList, profile));
});
}

/**
* Install mods to target profile and sync the changes to mods.yml file
* This is more performant than calling ProfileModList.addMod() on a
Expand All @@ -82,7 +65,7 @@ export async function installModsToProfile(
comboList: ThunderstoreCombo[],
profile: ImmutableProfile,
disabledModsOverride?: string[],
progressCallback?: (status: number) => void
progressCallback?: (status: string, progress?: number) => void
): Promise<ManifestV2[]> {
const profileMods = await ProfileModList.getModList(profile);
if (profileMods instanceof R2Error) {
Expand Down Expand Up @@ -123,7 +106,7 @@ export async function installModsToProfile(

if (typeof progressCallback === "function") {
const progress = Math.floor(((index + 1) / comboList.length) * 100);
progressCallback(progress);
progressCallback(`Copying mods to profile: ${progress}%`, progress);
}
}
} catch (e) {
Expand All @@ -137,6 +120,9 @@ export async function installModsToProfile(
}

throwForR2Error(await ProfileModList.saveModList(profile, profileMods));
if (typeof progressCallback === "function") {
progressCallback("Copying mods to profile: 100%", 100);
}
return profileMods;
}

Expand Down Expand Up @@ -203,12 +189,8 @@ export async function populateImportedProfile(

try {
const disabledMods = exportModList.filter((m) => !m.isEnabled()).map((m) => m.getName());
await installModsToProfile(comboList, profile, disabledMods, (progress) => {
progressCallback(`Copying mods to profile: ${progress}%`);
});
await extractConfigsToImportedProfile(zipPath, profile.getProfileName(), (progress) => {
progressCallback(`Copying configs to profile: ${progress}%`);
});
await installModsToProfile(comboList, profile, disabledMods, progressCallback);
await extractConfigsToImportedProfile(zipPath, profile.getProfileName(), progressCallback);
} catch (e) {
await FileUtils.recursiveRemoveDirectoryIfExists(profile.getProfilePath());
throw e;
Expand Down

0 comments on commit c359b28

Please sign in to comment.