Skip to content

Commit 21ba621

Browse files
authored
Merge pull request #1614 from ebkr/simplify-download-monitor
Mod downloading refactor
2 parents 828944c + e62b2e6 commit 21ba621

File tree

8 files changed

+283
-219
lines changed

8 files changed

+283
-219
lines changed

Diff for: src/components/mixins/DownloadMixin.vue

+35-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
import Vue from 'vue';
33
import Component from 'vue-class-component';
44
5-
import ThunderstoreMod from "../../model/ThunderstoreMod";
5+
import R2Error from "../../model/errors/R2Error";
66
import Game from "../../model/game/Game";
77
import Profile from "../../model/Profile";
8+
import ThunderstoreCombo from "../../model/ThunderstoreCombo";
9+
import ThunderstoreMod from "../../model/ThunderstoreMod";
10+
import { installModsAndResolveConflicts } from "../../utils/ProfileUtils";
811
912
1013
@Component
@@ -22,12 +25,43 @@ export default class DownloadMixin extends Vue {
2225
return this.$store.state.modals.isDownloadModModalOpen;
2326
}
2427
28+
get ignoreCache(): boolean {
29+
const settings = this.$store.getters['settings'];
30+
return settings.getContext().global.ignoreCache;
31+
}
32+
2533
get thunderstoreMod(): ThunderstoreMod | null {
2634
return this.$store.state.modals.downloadModModalMod;
2735
}
2836
2937
get profile(): Profile {
3038
return this.$store.getters['profile/activeProfile'];
3139
}
40+
41+
async downloadCompletedCallback(downloadedMods: ThunderstoreCombo[]): Promise<void> {
42+
try {
43+
await installModsAndResolveConflicts(downloadedMods, this.profile.asImmutableProfile(), this.$store);
44+
} catch (e) {
45+
this.$store.commit('error/handleError', R2Error.fromThrownValue(e));
46+
}
47+
}
48+
49+
static addSolutionsToError(err: R2Error): void {
50+
// Sanity check typing.
51+
if (!(err instanceof R2Error)) {
52+
return;
53+
}
54+
55+
if (
56+
err.name.includes("Failed to download mod") ||
57+
err.name.includes("System.Net.WebException")
58+
) {
59+
err.solution = "Try toggling the preferred Thunderstore CDN in the settings";
60+
}
61+
62+
if (err.message.includes("System.IO.PathTooLongException")) {
63+
err.solution = 'Using "Change data folder" option in the settings to select a shorter path might solve the issue';
64+
}
65+
}
3266
}
3367
</script>

Diff for: src/components/views/DownloadModModal.vue

+64-199
Large diffs are not rendered by default.

Diff for: src/components/views/UpdateAllInstalledModsModal.vue

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<template>
2+
<ModalCard :is-active="isOpen" :can-close="true" v-if="thunderstoreMod === null" @close-modal="closeModal()">
3+
<template v-slot:header>
4+
<h2 class='modal-title'>Update all installed mods</h2>
5+
</template>
6+
<template v-slot:body>
7+
<p>All installed mods will be updated to their latest versions.</p>
8+
<p>Any missing dependencies will be installed.</p>
9+
<p>The following mods will be downloaded and installed:</p>
10+
<br/>
11+
<ul class="list">
12+
<li class="list-item" v-for='(mod, index) in $store.getters["profile/modsWithUpdates"]'
13+
:key='`to-update-${index}-${mod.getFullName()}`'>
14+
{{mod.getName()}} will be updated to: {{mod.getLatestVersion()}}
15+
</li>
16+
</ul>
17+
</template>
18+
<template v-slot:footer>
19+
<button class="button is-info" @click="updateAllToLatestVersion()">Update all</button>
20+
</template>
21+
</ModalCard>
22+
</template>
23+
24+
<script lang="ts">
25+
import { mixins } from "vue-class-component";
26+
import { Component } from "vue-property-decorator";
27+
28+
import ModalCard from "../ModalCard.vue";
29+
import DownloadMixin from "../mixins/DownloadMixin.vue";
30+
import DownloadModVersionSelectModal from "../views/DownloadModVersionSelectModal.vue";
31+
import ThunderstoreCombo from "../../model/ThunderstoreCombo";
32+
import StatusEnum from "../../model/enums/StatusEnum";
33+
import R2Error from "../../model/errors/R2Error";
34+
import ThunderstoreDownloaderProvider from "../../providers/ror2/downloading/ThunderstoreDownloaderProvider";
35+
36+
@Component({
37+
components: {DownloadModVersionSelectModal, ModalCard}
38+
})
39+
export default class UpdateAllInstalledModsModal extends mixins(DownloadMixin) {
40+
41+
42+
async updateAllToLatestVersion() {
43+
this.closeModal();
44+
const modsWithUpdates: ThunderstoreCombo[] = await this.$store.dispatch('profile/getCombosWithUpdates');
45+
46+
const assignId = await this.$store.dispatch(
47+
'download/addDownload',
48+
modsWithUpdates.map(value => `${value.getMod().getName()} (${value.getVersion().toString()})`)
49+
);
50+
51+
this.$store.commit('download/setIsModProgressModalOpen', true);
52+
ThunderstoreDownloaderProvider.instance.downloadLatestOfAll(modsWithUpdates, this.ignoreCache, (progress: number, modName: string, status: number, err: R2Error | null) => {
53+
try {
54+
if (status === StatusEnum.FAILURE) {
55+
this.$store.commit('download/setIsModProgressModalOpen', false);
56+
this.$store.commit('download/updateDownload', {assignId, failed: true});
57+
if (err !== null) {
58+
DownloadMixin.addSolutionsToError(err);
59+
throw err;
60+
}
61+
} else if (status === StatusEnum.PENDING) {
62+
this.$store.commit('download/updateDownload', {assignId, progress, modName});
63+
}
64+
} catch (e) {
65+
this.$store.commit('error/handleError', R2Error.fromThrownValue(e));
66+
}
67+
}, async (downloadedMods) => {
68+
await this.downloadCompletedCallback(downloadedMods);
69+
this.$store.commit('download/setIsModProgressModalOpen', false);
70+
});
71+
}
72+
}
73+
</script>

Diff for: src/pages/DownloadMonitor.vue

+3-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div>
33
<Hero title="Downloads" subtitle="Monitor progress of downloads" hero-type="primary"/>
4-
<template v-if="activeDownloads.length === 0">
4+
<template v-if="$store.state.download.allDownloads.length === 0">
55
<div class='text-center top'>
66
<div class="margin-right">
77
<br/>
@@ -13,7 +13,7 @@
1313
</div>
1414
</template>
1515
<template v-else>
16-
<div v-for="([assignId, downloadObject], index) of activeDownloads" :key="`download-progress-${index}`">
16+
<div v-for="(downloadObject, index) of $store.getters['download/newestFirst']" :key="`download-progress-${index}`">
1717
<div>
1818
<div class="container margin-right">
1919
<div class="border-at-bottom pad pad--sides">
@@ -43,10 +43,9 @@
4343
<script lang="ts">
4444
4545
import { Component, Vue } from 'vue-property-decorator';
46+
4647
import { Hero } from '../components/all';
47-
import DownloadModModal from '../components/views/DownloadModModal.vue';
4848
import Progress from '../components/Progress.vue';
49-
import Timeout = NodeJS.Timeout;
5049
5150
@Component({
5251
components: {
@@ -55,20 +54,6 @@ import Timeout = NodeJS.Timeout;
5554
}
5655
})
5756
export default class DownloadMonitor extends Vue {
58-
private refreshInterval!: Timeout;
59-
private activeDownloads: [number, any][] = [];
60-
61-
created() {
62-
this.activeDownloads = [...DownloadModModal.allVersions].reverse();
63-
this.refreshInterval = setInterval(() => {
64-
this.activeDownloads = [...DownloadModModal.allVersions].reverse();
65-
}, 100);
66-
}
67-
68-
destroyed() {
69-
clearInterval(this.refreshInterval);
70-
}
71-
7257
}
7358
7459
</script>

Diff for: src/pages/Manager.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ import ModalCard from '../components/ModalCard.vue';
635635
});
636636
return;
637637
}
638-
DownloadModModal.downloadSpecific(this.profile, combo, ignoreCache)
638+
DownloadModModal.downloadSpecific(this.profile, combo, ignoreCache, this.$store)
639639
.then(async value => {
640640
const modList = await ProfileModList.getModList(this.profile.asImmutableProfile());
641641
if (!(modList instanceof R2Error)) {

Diff for: src/store/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Vue from 'vue';
22
import Vuex, { ActionContext } from 'vuex';
33

44
import ErrorModule from './modules/ErrorModule';
5+
import { DownloadModule } from './modules/DownloadModule';
56
import ModalsModule from './modules/ModalsModule';
67
import ModFilterModule from './modules/ModFilterModule';
78
import ProfileModule from './modules/ProfileModule';
@@ -123,6 +124,7 @@ export const store = {
123124
},
124125
modules: {
125126
error: ErrorModule,
127+
download: DownloadModule,
126128
modals: ModalsModule,
127129
modFilters: ModFilterModule,
128130
profile: ProfileModule,

Diff for: src/store/modules/DownloadModule.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { ActionTree, GetterTree } from "vuex";
2+
3+
import { State as RootState } from "../../store";
4+
import R2Error from "../../model/errors/R2Error";
5+
6+
interface DownloadProgress {
7+
assignId: number;
8+
initialMods: string[];
9+
modName: string;
10+
progress: number;
11+
failed: boolean;
12+
}
13+
14+
interface UpdateObject {
15+
assignId: number;
16+
progress?: number;
17+
modName?: string;
18+
failed?: boolean;
19+
}
20+
21+
interface State {
22+
allDownloads: DownloadProgress[],
23+
isModProgressModalOpen: boolean,
24+
}
25+
26+
/**
27+
* State for mod downloads.
28+
*/
29+
export const DownloadModule = {
30+
namespaced: true,
31+
32+
state: (): State => ({
33+
allDownloads: [],
34+
isModProgressModalOpen: false,
35+
}),
36+
37+
actions: <ActionTree<State, RootState>>{
38+
addDownload({state}, initialMods: string[]): number {
39+
const assignId = state.allDownloads.length;
40+
const downloadObject: DownloadProgress = {
41+
assignId,
42+
initialMods,
43+
modName: '',
44+
progress: 0,
45+
failed: false,
46+
};
47+
state.allDownloads = [...state.allDownloads, downloadObject];
48+
return assignId;
49+
},
50+
},
51+
52+
getters: <GetterTree<State, RootState>>{
53+
activeDownloadCount(state) {
54+
const active = state.allDownloads.filter(
55+
dl => !dl.failed && dl.progress < 100
56+
);
57+
return active.length;
58+
},
59+
currentDownload(state) {
60+
return state.allDownloads[state.allDownloads.length-1] || null;
61+
},
62+
newestFirst(state) {
63+
return Array.from(state.allDownloads).reverse();
64+
},
65+
},
66+
67+
mutations: {
68+
updateDownload(state: State, update: UpdateObject) {
69+
const newDownloads = [...state.allDownloads];
70+
const index = newDownloads.findIndex((old: DownloadProgress) => {
71+
return old.assignId === update.assignId;
72+
});
73+
74+
if (index === -1) {
75+
// The DownloadProgress by the ID from the update wasn't found at all.
76+
console.warn(`Couldn\'t find DownloadProgress object with assignId ${update.assignId}.`);
77+
return;
78+
}
79+
80+
if (index !== update.assignId) {
81+
console.log(`There was a mismatch between download update\'s assign ID (${update.assignId}) and the index it was found at (${index}).`);
82+
}
83+
84+
newDownloads[index] = {...newDownloads[index], ...update};
85+
state.allDownloads = newDownloads;
86+
},
87+
setIsModProgressModalOpen(state: State, isModProgressModalOpen: boolean) {
88+
state.isModProgressModalOpen = isModProgressModalOpen;
89+
}
90+
},
91+
}

Diff for: src/utils/ProfileUtils.ts

+14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from "path";
22

33
import * as yaml from "yaml";
4+
import { Store } from "vuex";
45

56
import FileUtils from "./FileUtils";
67
import R2Error from "../model/errors/R2Error";
@@ -12,6 +13,7 @@ import Profile, { ImmutableProfile } from "../model/Profile";
1213
import ThunderstoreCombo from "../model/ThunderstoreCombo";
1314
import VersionNumber from "../model/VersionNumber";
1415
import FsProvider from "../providers/generic/file/FsProvider";
16+
import ConflictManagementProvider from "../providers/generic/installing/ConflictManagementProvider";
1517
import ZipProvider from "../providers/generic/zip/ZipProvider";
1618
import ProfileInstallerProvider from "../providers/ror2/installing/ProfileInstallerProvider";
1719
import * as PackageDb from '../r2mm/manager/PackageDexieStore';
@@ -56,6 +58,18 @@ async function extractConfigsToImportedProfile(
5658
}
5759
}
5860

61+
export async function installModsAndResolveConflicts(
62+
downloadedMods: ThunderstoreCombo[],
63+
profile: ImmutableProfile,
64+
store: Store<any>
65+
): Promise<void> {
66+
await ProfileModList.requestLock(async () => {
67+
const modList: ManifestV2[] = await installModsToProfile(downloadedMods, profile);
68+
await store.dispatch('profile/updateModList', modList);
69+
throwForR2Error(await ConflictManagementProvider.instance.resolveConflicts(modList, profile));
70+
});
71+
}
72+
5973
/**
6074
* Install mods to target profile and sync the changes to mods.yml file
6175
* This is more performant than calling ProfileModList.addMod() on a

0 commit comments

Comments
 (0)