Skip to content

Commit

Permalink
Merge pull request #1625 from ebkr/mod-installation-progress-tracking
Browse files Browse the repository at this point in the history
Mod installation progress tracking
  • Loading branch information
anttimaki authored Feb 24, 2025
2 parents ad85719 + a362322 commit dfaca8c
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 100 deletions.
53 changes: 48 additions & 5 deletions src/components/mixins/DownloadMixin.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
<script lang='ts'>
import Vue from 'vue';
import Component from 'vue-class-component';
import { Store } from "vuex";
import R2Error from "../../model/errors/R2Error";
import StatusEnum from "../../model/enums/StatusEnum";
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 ConflictManagementProvider from "../../providers/generic/installing/ConflictManagementProvider";
import ProfileModList from "../../r2mm/mods/ProfileModList";
import { installModsToProfile } from "../../utils/ProfileUtils";
@Component
Expand Down Expand Up @@ -42,14 +47,52 @@ export default class DownloadMixin extends Vue {
return this.$store.getters['profile/activeProfile'];
}
async downloadCompletedCallback(downloadedMods: ThunderstoreCombo[]): Promise<void> {
async downloadCompletedCallback(downloadedMods: ThunderstoreCombo[], assignId: number): Promise<void> {
try {
await installModsAndResolveConflicts(downloadedMods, this.profile.asImmutableProfile(), this.$store);
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));
}
}
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 || status === StatusEnum.SUCCESS) {
store.commit('download/updateDownload', {assignId, modName, downloadProgress});
}
}
async installModsAndResolveConflicts(
downloadedMods: ThunderstoreCombo[],
profile: ImmutableProfile,
assignId: number
): Promise<void> {
await ProfileModList.requestLock(async () => {
const modList: ManifestV2[] = await installModsToProfile(downloadedMods, profile, undefined,(status, modName, installProgress) => {
this.$store.commit('download/updateDownload', {assignId, modName, installProgress});
});
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
92 changes: 48 additions & 44 deletions src/components/views/DownloadModModal.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
<template>
<div>
<div id='downloadProgressModal' :class="['modal', {'is-active':$store.state.download.isModProgressModalOpen}]" v-if="$store.getters['download/currentDownload'] !== null">
<div
id='downloadProgressModal'
:class="['modal', {'is-active':$store.state.download.isModProgressModalOpen}]"
v-if="$store.getters['download/currentDownload'] !== null"
>
<div class="modal-background" @click="setIsModProgressModalOpen(false);"></div>
<div class='modal-content'>
<div class='notification is-info'>
<h3 class='title'>Downloading {{$store.getters['download/currentDownload'].modName}}</h3>
<p>{{Math.floor($store.getters['download/currentDownload'].progress)}}% complete</p>

<h3 v-if="$store.getters['download/currentDownload'].downloadProgress < 100" class='title'>
Downloading {{$store.getters['download/currentDownload'].modName}}
</h3>
<h3 v-else class='title'>
Installing {{$store.getters['download/currentDownload'].modName}}
</h3>

<p>Downloading: {{Math.floor($store.getters['download/currentDownload'].downloadProgress)}}% complete</p>

<Progress
:max='100'
:value="$store.getters['download/currentDownload'].downloadProgress"
:className="['is-dark']"
/>

<p v-if="$store.getters['download/currentDownload'].installProgress">
Installing: {{Math.floor($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'].progress"
:value="$store.getters['download/currentDownload'].installProgress"
:className="['is-dark']"
/>
</div>
Expand All @@ -31,7 +54,6 @@ import ModalCard from '../ModalCard.vue';
import DownloadModVersionSelectModal from "../../components/views/DownloadModVersionSelectModal.vue";
import UpdateAllInstalledModsModal from "../../components/views/UpdateAllInstalledModsModal.vue";
import DownloadMixin from "../mixins/DownloadMixin.vue";
import StatusEnum from '../../model/enums/StatusEnum';
import R2Error from '../../model/errors/R2Error';
import ManifestV2 from '../../model/ManifestV2';
import Profile from '../../model/Profile';
Expand Down Expand Up @@ -73,9 +95,9 @@ import ProfileModList from '../../r2mm/mods/ProfileModList';
profile.asImmutableProfile(),
combo,
ignoreCache,
(progress: number, modName: string, status: number, err: R2Error | null) => {
(downloadProgress: number, modName: string, status: number, err: R2Error | null) => {
try {
DownloadModModal.downloadProgressCallback(store, assignId, progress, modName, status, err);
DownloadMixin.downloadProgressCallback(store, assignId, downloadProgress, modName, status, err);
} catch (e) {
reject(e);
}
Expand All @@ -95,30 +117,19 @@ import ProfileModList from '../../r2mm/mods/ProfileModList';
}
}
const modList = await ProfileModList.getModList(profile.asImmutableProfile());
if (!(modList instanceof R2Error)) {
const err = await ConflictManagementProvider.instance.resolveConflicts(modList, profile.asImmutableProfile());
if (err instanceof R2Error) {
return reject(err);
}
if (modList instanceof R2Error) {
return reject(modList);
}
const err = await ConflictManagementProvider.instance.resolveConflicts(modList, profile.asImmutableProfile());
if (err instanceof R2Error) {
return reject(err);
}
return resolve();
});
}, 1);
});
}
static downloadProgressCallback(store: Store<any>, assignId: number, progress: number, modName: string, status: number, err: R2Error | null) {
if (status === StatusEnum.FAILURE) {
store.commit('download/updateDownload', {assignId, failed: true});
if (err !== null) {
DownloadMixin.addSolutionsToError(err);
throw err;
}
} else if (status === StatusEnum.PENDING) {
store.commit('download/updateDownload', {assignId, progress, modName});
}
}
async downloadHandler(tsMod: ThunderstoreMod, tsVersion: ThunderstoreVersion) {
this.closeModal();
Expand All @@ -140,36 +151,29 @@ import ProfileModList from '../../r2mm/mods/ProfileModList';
this.profile.asImmutableProfile(),
tsCombo,
this.ignoreCache,
(progress, modName, status, err) => { this.downloadProgressCallback(assignId, progress, modName, status, err); }
(downloadProgress, modName, status, err) => {
DownloadMixin.downloadProgressCallback(
this.$store,
assignId,
downloadProgress,
modName,
status,
err,
() => this.setIsModProgressModalOpen(false)
);
}
);
} catch (e) {
this.setIsModProgressModalOpen(false);
this.$store.commit('download/updateDownload', { assignId, failed: true });
this.$store.commit('error/handleError', R2Error.fromThrownValue(e));
return;
}
await this.downloadCompletedCallback(downloadedMods);
await this.downloadCompletedCallback(downloadedMods, assignId);
this.setIsModProgressModalOpen(false);
}, 1);
}
downloadProgressCallback(assignId: number, progress: 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, progress, modName});
}
} catch (e) {
this.$store.commit('error/handleError', R2Error.fromThrownValue(e));
}
};
static async installModAfterDownload(profile: Profile, mod: ThunderstoreMod, version: ThunderstoreVersion): Promise<R2Error | void> {
return new Promise(async (resolve, reject) => {
const manifestMod: ManifestV2 = new ManifestV2().fromThunderstoreMod(mod, version);
Expand Down
6 changes: 3 additions & 3 deletions src/components/views/UpdateAllInstalledModsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default class UpdateAllInstalledModsModal extends mixins(DownloadMixin)
);
this.setIsModProgressModalOpen(true);
ThunderstoreDownloaderProvider.instance.downloadLatestOfAll(modsWithUpdates, this.ignoreCache, (progress: number, modName: string, status: number, err: R2Error | null) => {
ThunderstoreDownloaderProvider.instance.downloadLatestOfAll(modsWithUpdates, this.ignoreCache, (downloadProgress: number, modName: string, status: number, err: R2Error | null) => {
try {
if (status === StatusEnum.FAILURE) {
this.setIsModProgressModalOpen(false);
Expand All @@ -59,13 +59,13 @@ 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, modName, downloadProgress});
}
} catch (e) {
this.$store.commit('error/handleError', R2Error.fromThrownValue(e));
}
}, async (downloadedMods) => {
await this.downloadCompletedCallback(downloadedMods);
await this.downloadCompletedCallback(downloadedMods, assignId);
this.setIsModProgressModalOpen(false);
});
}
Expand Down
64 changes: 52 additions & 12 deletions src/pages/DownloadMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,58 @@
<div class="border-at-bottom pad pad--sides">
<div class="card is-shadowless">
<p><strong>{{ downloadObject.initialMods.join(", ") }}</strong></p>
<p v-if="downloadObject.progress < 100">Downloading: {{ downloadObject.modName }}</p>
<p>{{Math.min(Math.floor(downloadObject.progress), 100)}}% complete</p>
<Progress v-if="!downloadObject.failed"
:max='100'
:value='downloadObject.progress'
:className="['is-info']"
/>
<Progress v-else-if="downloadObject.failed"
:max='100'
:value='100'
:className="['is-danger']"
/>

<div v-if="downloadObject.failed">
<p>Download failed</p>
<Progress
:max='100'
:value='100'
:className="['is-danger']"
/>
</div>

<div v-else-if="downloadObject.downloadProgress === 100 && downloadObject.installProgress === 100">
<p>Download complete</p>
<Progress
:max='100'
:value='100'
:className="['is-success']"
/>
</div>

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

</div>
</div>
</div>
</div>
Expand Down
28 changes: 16 additions & 12 deletions src/r2mm/downloading/BetterThunderstoreDownloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,38 +96,42 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader
}

const dependencies = await this.getDependenciesWithCorrectVersions(combo, modList);
const allModsToDownload = [...dependencies, combo];
const allModsToDownload = [combo, ...dependencies];

let modInProgressName = combo.getMod().getName();
let downloadCount = 0;
const singleModProgressCallback = (progress: number, status: number, err: R2Error | null) => {
// Mark the mod 80% processed when the download completes, save the remaining 20% for extracting.
const singleModProgressCallback = (downloadProgress: number, status: number, err: R2Error | null) => {
if (status === StatusEnum.FAILURE) {
throw err;
}

let totalProgress: number;
let totalDownloadProgress: number;
if (status === StatusEnum.PENDING) {
totalProgress = this.generateProgressPercentage(progress, downloadCount, allModsToDownload.length);
totalDownloadProgress = this.generateProgressPercentage(downloadProgress * 0.8, downloadCount, allModsToDownload.length);
} else if (status === StatusEnum.SUCCESS) {
totalProgress = this.generateProgressPercentage(100, downloadCount, allModsToDownload.length);
totalDownloadProgress = this.generateProgressPercentage(100, downloadCount, allModsToDownload.length);
downloadCount += 1;
} else {
console.error(`Ignore unknown status code "${status}"`);
return;
}
totalProgressCallback(totalProgress, combo.getMod().getName(), status, err);
totalProgressCallback(Math.round(totalDownloadProgress), modInProgressName, status, err);
}

for (const combo of allModsToDownload) {
if (!ignoreCache && await this.isVersionAlreadyDownloaded(combo)) {
totalProgressCallback(100, combo.getMod().getName(), StatusEnum.SUCCESS, null);
for (const comboInProgress of allModsToDownload) {
modInProgressName = comboInProgress.getMod().getName();

if (!ignoreCache && await this.isVersionAlreadyDownloaded(comboInProgress)) {
totalProgressCallback(100, modInProgressName, StatusEnum.SUCCESS, null);
continue;
}

try {
const response = await this._downloadCombo(combo, singleModProgressCallback);
await this._saveDownloadResponse(response, combo, singleModProgressCallback);
const response = await this._downloadCombo(comboInProgress, singleModProgressCallback);
await this._saveDownloadResponse(response, comboInProgress, singleModProgressCallback);
} catch(e) {
throw R2Error.fromThrownValue(e, `Failed to download mod ${combo.getVersion().getFullName()}`);
throw R2Error.fromThrownValue(e, `Failed to download mod ${comboInProgress.getVersion().getFullName()}`);
}
}
return allModsToDownload;
Expand Down
Loading

0 comments on commit dfaca8c

Please sign in to comment.