Skip to content

Commit 61e0216

Browse files
Calculate and display the download progress based on the file size
* Calculate the total file size when initializing the download * Calculate amount of downloaded bytes * Calculate the progress percentage based on the aforementioned values * Save the values to the local store in the callback * (Downloading via importing a profile uses the callback values without saving the progress to the store) * Display the progress (percentage), current progress (file size) and total file size to be downloaded in DownloadMonitor, DownloadProgressModal and ImportProfileModal
1 parent 73d76d1 commit 61e0216

File tree

8 files changed

+152
-62
lines changed

8 files changed

+152
-62
lines changed

src/components/profiles-modals/ImportProfileModal.vue

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { useProfilesComposable } from '../composables/ProfilesComposable';
1515
import { computed, nextTick, ref, watch, watchEffect } from 'vue';
1616
import { getStore } from '../../providers/generic/store/StoreProvider';
1717
import { State } from '../../store';
18+
import FileUtils from "../../utils/FileUtils";
19+
import * as DownloadUtils from "../../utils/DownloadUtils";
1820
1921
const VALID_PROFILE_CODE_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2022
@@ -52,6 +54,7 @@ const profileList = computed(() => store.state.profiles.profileList);
5254
const knownProfileMods = computed(() => profileMods.value.known.map((combo) => combo.getMod()));
5355
const unknownProfileModNames = computed(() => profileMods.value.unknown.join(', '));
5456
const isProfileCodeValid = computed(() => VALID_PROFILE_CODE_REGEX.test(profileImportCode.value));
57+
const profileTotalDownloadSize = ref<number>(0);
5558
5659
const profileCodeInput = ref<HTMLInputElement>();
5760
const profileNameInput = ref<HTMLInputElement>();
@@ -250,15 +253,20 @@ async function onImportTargetSelected() {
250253
async function importProfile(targetProfileName: string, mods: ExportMod[], zipPath: string) {
251254
activeStep.value = 'PROFILE_IS_BEING_IMPORTED';
252255
importPhaseDescription.value = 'Downloading mods: 0%';
253-
const progressCallback = (progress: number|string) => typeof progress === "number"
254-
? importPhaseDescription.value = `Downloading mods: ${Math.floor(progress)}%`
255-
: importPhaseDescription.value = progress;
256+
const progressCallback = (downloadedSize: number) => {
257+
importPhaseDescription.value = `Downloading mods: ${DownloadUtils.generateProgressPercentage(downloadedSize, profileTotalDownloadSize.value)}%` +
258+
` (${FileUtils.humanReadableSize(downloadedSize)} / ${FileUtils.humanReadableSize(profileTotalDownloadSize.value)})`;
259+
};
256260
const isUpdate = importUpdateSelection.value === 'UPDATE';
257261
258262
try {
259263
const combos = profileMods.value.known as ThunderstoreCombo[];
264+
profileTotalDownloadSize.value = await DownloadUtils.getTotalDownloadSizeInBytes(combos, store.state.download.ignoreCache);
265+
260266
await store.dispatch('download/downloadToCache', {combos, progressCallback});
261-
await ProfileUtils.populateImportedProfile(combos, mods, targetProfileName, isUpdate, zipPath, progressCallback);
267+
await ProfileUtils.populateImportedProfile(combos, mods, targetProfileName, isUpdate, zipPath, (progress) => {
268+
importPhaseDescription.value = progress;
269+
});
262270
} catch (e) {
263271
await store.dispatch('profiles/ensureProfileExists');
264272
closeModal();

src/components/views/DownloadProgressModal.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Progress } from '../all';
33
import { getStore } from '../../providers/generic/store/StoreProvider';
44
import { State } from '../../store';
5+
import FileUtils from '../../utils/FileUtils';
56
67
const store = getStore<State>();
78
@@ -29,7 +30,10 @@ function closeModal() {
2930
Installing {{$store.getters['download/currentDownload'].modName}}
3031
</h3>
3132

32-
<p>Downloading: {{$store.getters['download/currentDownload'].downloadProgress}}% complete</p>
33+
<p>
34+
Downloading: {{$store.getters['download/currentDownload'].downloadProgress}}% complete
35+
({{FileUtils.humanReadableSize($store.getters['download/currentDownload'].downloadedSize)}} / {{FileUtils.humanReadableSize($store.getters['download/currentDownload'].totalDownloadSize)}})
36+
</p>
3337

3438
<Progress
3539
:max='100'

src/model/ThunderstoreVersion.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default class ThunderstoreVersion {
1212
private enabled: boolean = true;
1313
private downloads: number = 0;
1414
private downloadUrl: string = '';
15+
private fileSize: number = 0;
1516

1617
public static parseFromThunderstoreData(data: any): ThunderstoreVersion {
1718
const version = new ThunderstoreVersion();
@@ -28,6 +29,7 @@ export default class ThunderstoreVersion {
2829
this.setIcon(version.icon);
2930
this.setDownloadCount(version.downloads);
3031
this.setDownloadUrl(version.download_url);
32+
this.setFileSize(version.file_size);
3133
return this;
3234
}
3335

@@ -106,4 +108,12 @@ export default class ThunderstoreVersion {
106108
public setDownloadUrl(url: string) {
107109
this.downloadUrl = url;
108110
}
111+
112+
public setFileSize(size: number) {
113+
this.fileSize = size;
114+
}
115+
116+
public getFileSize(): number {
117+
return this.fileSize;
118+
}
109119
}

src/pages/DownloadMonitor.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@
5454
<div class="col">
5555
<p v-if="downloadObject.status === DownloadStatusEnum.DOWNLOADING">Downloading: {{ downloadObject.modName }}</p>
5656
<p v-else>Downloading:</p>
57-
<p>{{Math.min(Math.floor(downloadObject.downloadProgress), 100)}}% complete</p>
57+
<p>{{`${Math.min(Math.floor(downloadObject.downloadProgress), 100)}% complete ` +
58+
`(${FileUtils.humanReadableSize(downloadObject.downloadedSize)} / ` +
59+
`${FileUtils.humanReadableSize(downloadObject.totalDownloadSize)})`}}</p>
5860
<Progress
5961
:max='100'
6062
:value='downloadObject.downloadProgress'
@@ -110,6 +112,7 @@
110112
111113
import { Hero } from '../components/all';
112114
import Progress from '../components/Progress.vue';
115+
import FileUtils from "../utils/FileUtils";
113116
import { DownloadStatusEnum } from '../model/enums/DownloadStatusEnum';
114117
</script>
115118

src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default abstract class ThunderstoreDownloaderProvider {
1919
public abstract download(
2020
combos: ThunderstoreCombo[],
2121
ignoreCache: boolean,
22-
totalProgressCallback: (progress: number, modName: string, status: number, err: R2Error | null) => void
22+
totalProgressCallback: (downloadedSize: number, modName: string, status: number, err: R2Error | null) => void
2323
): Promise<void>;
2424

2525
/**
@@ -31,13 +31,4 @@ export default abstract class ThunderstoreDownloaderProvider {
3131
*/
3232
public abstract saveToFile(response: Buffer, combo: ThunderstoreCombo, callback: (success: boolean, error?: R2Error) => void): void;
3333

34-
/**
35-
* Check the cache to see if the mod has already been downloaded.
36-
* This will save bandwidth and disk writes if the cache is enabled.
37-
* To be used inside {@method calculateInitialDownloadSize}
38-
*
39-
* @param combo The mod being downloaded.
40-
*/
41-
public abstract isVersionAlreadyDownloaded(combo: ThunderstoreCombo): Promise<boolean>;
42-
4334
}

src/r2mm/downloading/BetterThunderstoreDownloader.ts

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,52 @@ import FsProvider from '../../providers/generic/file/FsProvider';
99
import FileWriteError from '../../model/errors/FileWriteError';
1010
import ThunderstoreDownloaderProvider from '../../providers/ror2/downloading/ThunderstoreDownloaderProvider';
1111
import ManagerInformation from '../../_managerinf/ManagerInformation';
12-
import { generateProgressPercentage } from '../../utils/DownloadUtils';
12+
import * as DownloadUtils from '../../utils/DownloadUtils';
1313

1414
export default class BetterThunderstoreDownloader extends ThunderstoreDownloaderProvider {
1515

1616
public async download(
1717
combos: ThunderstoreCombo[],
1818
ignoreCache: boolean,
19-
totalProgressCallback: (progress: number, modName: string, status: number, err: R2Error | null) => void
19+
totalProgressCallback: (downloadedSize: number, modName: string, status: number, err: R2Error | null) => void
2020
): Promise<void> {
21+
if (combos.length === 0) {
22+
throw new R2Error('No mods to download', 'An empty list of mods was passed to the downloader');
23+
}
24+
2125
let modInProgressName = combos[0].getMod().getName();
22-
let downloadCount = 0;
26+
let finishedModsDownloadedSize = 0;
27+
let modInProgressSizeProgress = 0;
2328

24-
// Mark the mod 80% processed when the download completes, save the remaining 20% for extracting.
25-
const singleModProgressCallback = (downloadProgress: number, status: number, err: R2Error | null) => {
29+
const singleModProgressCallback = (downloadProgress: number, modInProgressSize: number, status: number, err: R2Error | null) => {
30+
const clampedProgress = Math.max(0, Math.min(100, downloadProgress));
2631
if (status === StatusEnum.FAILURE) {
2732
throw err;
2833
}
2934

30-
let totalDownloadProgress: number;
3135
if (status === StatusEnum.PENDING) {
32-
totalDownloadProgress = generateProgressPercentage(downloadProgress * 0.8, downloadCount, combos.length);
36+
modInProgressSizeProgress = (clampedProgress / 100) * modInProgressSize;
3337
} else if (status === StatusEnum.SUCCESS) {
34-
totalDownloadProgress = generateProgressPercentage(100, downloadCount, combos.length);
35-
downloadCount += 1;
38+
finishedModsDownloadedSize += modInProgressSize;
39+
modInProgressSizeProgress = 0;
3640
} else {
3741
console.error(`Ignore unknown status code "${status}"`);
3842
return;
3943
}
40-
totalProgressCallback(Math.round(totalDownloadProgress), modInProgressName, status, err);
44+
45+
totalProgressCallback(
46+
finishedModsDownloadedSize + modInProgressSizeProgress,
47+
modInProgressName,
48+
status,
49+
err
50+
);
4151
}
4252

4353
for (const comboInProgress of combos) {
4454
modInProgressName = comboInProgress.getMod().getName();
4555

46-
if (!ignoreCache && await this.isVersionAlreadyDownloaded(comboInProgress)) {
47-
singleModProgressCallback(100, StatusEnum.SUCCESS, null);
56+
if (!ignoreCache && await DownloadUtils.isVersionAlreadyDownloaded(comboInProgress)) {
57+
singleModProgressCallback(100, 0, StatusEnum.SUCCESS, null);
4858
continue;
4959
}
5060

@@ -57,10 +67,15 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader
5767
}
5868
}
5969

60-
private async _downloadCombo(combo: ThunderstoreCombo, callback: (progress: number, status: number, err: R2Error | null) => void): Promise<AxiosResponse> {
70+
private async _downloadCombo(combo: ThunderstoreCombo, callback: (progress: number, comboSize: number, status: number, err: R2Error | null) => void): Promise<AxiosResponse> {
6171
return axios.get(combo.getVersion().getDownloadUrl(), {
6272
onDownloadProgress: progress => {
63-
callback((progress.loaded / progress.total) * 100, StatusEnum.PENDING, null);
73+
callback(
74+
(progress.loaded / progress.total) * 100,
75+
combo.getVersion().getFileSize(),
76+
StatusEnum.PENDING,
77+
null
78+
);
6479
},
6580
responseType: 'arraybuffer',
6681
headers: {
@@ -70,14 +85,14 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader
7085
});
7186
}
7287

73-
private async _saveDownloadResponse(response: AxiosResponse, combo: ThunderstoreCombo, callback: (progress: number, status: number, err: R2Error | null) => void): Promise<void> {
88+
private async _saveDownloadResponse(response: AxiosResponse, combo: ThunderstoreCombo, callback: (progress: number, downloadedSize: number, status: number, err: R2Error | null) => void): Promise<void> {
7489
const buf: Buffer = Buffer.from(response.data)
75-
callback(100, StatusEnum.PENDING, null);
90+
callback(100, combo.getVersion().getFileSize(), StatusEnum.PENDING, null);
7691
await this.saveToFile(buf, combo, (success: boolean, error?: R2Error) => {
7792
if (success) {
78-
callback(100, StatusEnum.SUCCESS, error || null);
93+
callback(100, combo.getVersion().getFileSize(), StatusEnum.SUCCESS, null);
7994
} else {
80-
callback(100, StatusEnum.FAILURE, error || null);
95+
callback(100, combo.getVersion().getFileSize(), StatusEnum.FAILURE, error || null);
8196
}
8297
});
8398
}
@@ -109,15 +124,4 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader
109124
}
110125
}
111126

112-
public async isVersionAlreadyDownloaded(combo: ThunderstoreCombo): Promise<boolean> {
113-
const fs = FsProvider.instance;
114-
const cacheDirectory = path.join(PathResolver.MOD_ROOT, 'cache');
115-
try {
116-
await fs.readdir(path.join(cacheDirectory, combo.getMod().getFullName(), combo.getVersion().getVersionNumber().toString()));
117-
return true;
118-
} catch(e) {
119-
return false;
120-
}
121-
}
122-
123127
}

src/store/modules/DownloadModule.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@ interface DownloadProgress {
2424
profile: ImmutableProfile;
2525
modName: string;
2626
downloadProgress: number;
27+
downloadedSize: number;
28+
totalDownloadSize: number;
2729
installProgress: number;
2830
status: DownloadStatusEnum;
2931
}
3032

3133
interface UpdateObject {
3234
downloadId: UUID;
3335
downloadProgress?: number;
36+
downloadedSize?: number;
37+
totalDownloadSize?: number;
3438
installProgress?: number;
3539
modName?: string;
3640
status?: DownloadStatusEnum;
@@ -93,6 +97,8 @@ export const DownloadModule = {
9397
profile,
9498
modName: '',
9599
downloadProgress: 0,
100+
downloadedSize: 0,
101+
totalDownloadSize: 0,
96102
installProgress: 0,
97103
status: DownloadStatusEnum.DOWNLOADING
98104
};
@@ -106,7 +112,7 @@ export const DownloadModule = {
106112
commit('setIgnoreCacheVuexOnly', settings.getContext().global.ignoreCache);
107113
},
108114

109-
async downloadAndInstallCombos({commit, dispatch, rootGetters}, params: {
115+
async downloadAndInstallCombos({commit, dispatch, rootGetters, state}, params: {
110116
combos: ThunderstoreCombo[],
111117
game: Game,
112118
installMode: InstallMode,
@@ -123,9 +129,15 @@ export const DownloadModule = {
123129
downloadId = await dispatch('_addDownload', { combos, installMode, game, profile });
124130
const installedMods = throwForR2Error(await ProfileModList.getModList(profile));
125131
const modsWithDependencies = await getFullDependencyList(combos, game, installedMods, installMode);
132+
133+
const totalDownloadSize = await DownloadUtils.getTotalDownloadSizeInBytes(modsWithDependencies, state.ignoreCache);
134+
commit('updateDownload', { downloadId, totalDownloadSize });
135+
126136
await dispatch('_download', { combos: modsWithDependencies, downloadId });
137+
127138
commit('setInstalling', downloadId);
128139
await dispatch('_installModsAndResolveConflicts', { combos: modsWithDependencies, profile, downloadId });
140+
129141
commit('setDone', downloadId);
130142
} catch (e) {
131143
const r2Error = R2Error.fromThrownValue(e);
@@ -148,7 +160,7 @@ export const DownloadModule = {
148160

149161
async downloadToCache({state}, params: {
150162
combos: ThunderstoreCombo[],
151-
progressCallback: (progress: number, modName: string, status: number, err: R2Error | null) => void
163+
progressCallback: (downloadedSize: number) => void
152164
}) {
153165
const { combos, progressCallback } = params;
154166
await ThunderstoreDownloaderProvider.instance.download(combos, state.ignoreCache, progressCallback);
@@ -162,8 +174,8 @@ export const DownloadModule = {
162174
await ThunderstoreDownloaderProvider.instance.download(
163175
params.combos,
164176
state.ignoreCache,
165-
(downloadProgress, modName, status, err) => {
166-
dispatch('_downloadProgressCallback', { downloadId: params.downloadId, downloadProgress, modName, status, err });
177+
(downloadedSize, modName, status, err) => {
178+
dispatch('_downloadProgressCallback', { downloadId: params.downloadId, downloadedSize, modName, status, err });
167179
}
168180
);
169181
} catch (e) {
@@ -198,22 +210,22 @@ export const DownloadModule = {
198210

199211
async _downloadProgressCallback({commit}, params: {
200212
downloadId: UUID,
201-
downloadProgress: number,
213+
downloadedSize: number,
202214
modName: string,
203215
status: number,
204216
err: R2Error | null
205217
}) {
206-
const { downloadId, downloadProgress, modName, status, err} = params;
218+
const { downloadId, downloadedSize, modName, status, err} = params;
207219

208220
if (status === StatusEnum.FAILURE) {
209221
commit('closeDownloadProgressModal', null, { root: true });
210-
commit('setFailed', params.downloadId);
211-
if (params.err !== null) {
212-
DownloadUtils.addSolutionsToError(params.err);
213-
throw params.err;
222+
commit('setFailed', downloadId);
223+
if (err !== null) {
224+
DownloadUtils.addSolutionsToError(err);
225+
throw err;
214226
}
215227
} else if (status === StatusEnum.PENDING || status === StatusEnum.SUCCESS) {
216-
commit('updateDownload', { downloadId, modName, downloadProgress });
228+
commit('updateDownload', { downloadId, modName, downloadedSize });
217229
}
218230
},
219231
},
@@ -269,6 +281,9 @@ export const DownloadModule = {
269281
const index: number = getIndexOfDownloadProgress(state.allDownloads, update.downloadId);
270282
if (index > -1) {
271283
const newDownloads = [...state.allDownloads];
284+
if (update.downloadedSize !== undefined) {
285+
update.downloadProgress = DownloadUtils.generateProgressPercentage(update.downloadedSize, newDownloads[index].totalDownloadSize);
286+
}
272287
newDownloads[index] = {...newDownloads[index], ...update};
273288
state.allDownloads = newDownloads;
274289
}

0 commit comments

Comments
 (0)