refactor: upload progress calculation (#5350)

This commit is contained in:
Ramires Viana
2025-08-06 11:47:48 -03:00
committed by GitHub
parent 6d620c00a1
commit c14cf86f83
10 changed files with 321 additions and 412 deletions

View File

@@ -1,20 +1,25 @@
<template>
<div
v-if="filesInUploadCount > 0"
v-if="uploadStore.activeUploads.size > 0"
class="upload-files"
v-bind:class="{ closed: !open }"
>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
<h2>
{{
$t("prompts.uploadFiles", {
files: uploadStore.pendingUploadCount,
})
}}
</h2>
<div class="upload-info">
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
<div class="upload-speed">{{ speedMbytes }}/s</div>
<div class="upload-eta">{{ formattedETA }} remaining</div>
<div class="upload-percentage">
{{ getProgressDecimal }}% Completed
</div>
<div class="upload-percentage">{{ sentPercent }}% Completed</div>
<div class="upload-fraction">
{{ getTotalProgressBytes }} / {{ getTotalSize }}
{{ sentMbytes }} /
{{ totalMbytes }}
</div>
</div>
<button
@@ -40,17 +45,21 @@
<div class="card-content file-icons">
<div
class="file"
v-for="file in filesInUpload"
:key="file.id"
:data-dir="file.isDir"
:data-type="file.type"
:aria-label="file.name"
v-for="upload in uploadStore.activeUploads"
:key="upload.path"
:data-dir="upload.type === 'dir'"
:data-type="upload.type"
:aria-label="upload.name"
>
<div class="file-name">
<i class="material-icons"></i> {{ file.name }}
<i class="material-icons"></i> {{ upload.name }}
</div>
<div class="file-progress">
<div v-bind:style="{ width: file.progress + '%' }"></div>
<div
v-bind:style="{
width: (upload.sentBytes / upload.totalBytes) * 100 + '%',
}"
></div>
</div>
</div>
</div>
@@ -58,63 +67,126 @@
</div>
</template>
<script>
import { mapState, mapWritableState, mapActions } from "pinia";
import { useUploadStore } from "@/stores/upload";
<script setup lang="ts">
import { useFileStore } from "@/stores/file";
import { abortAllUploads } from "@/api/tus";
import { useUploadStore } from "@/stores/upload";
import { storeToRefs } from "pinia";
import { computed, ref, watch } from "vue";
import buttons from "@/utils/buttons";
import { useI18n } from "vue-i18n";
import { partial } from "filesize";
export default {
name: "uploadFiles",
data: function () {
return {
open: false,
};
},
computed: {
...mapState(useUploadStore, [
"filesInUpload",
"filesInUploadCount",
"uploadSpeed",
"getETA",
"getProgress",
"getProgressDecimal",
"getTotalProgressBytes",
"getTotalSize",
]),
...mapWritableState(useFileStore, ["reload"]),
formattedETA() {
if (!this.getETA || this.getETA === Infinity) {
return "--:--:--";
}
const { t } = useI18n({});
let totalSeconds = this.getETA;
const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.round(totalSeconds % 60);
const open = ref<boolean>(false);
const speed = ref<number>(0);
const eta = ref<number>(Infinity);
return `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
},
},
methods: {
...mapActions(useUploadStore, ["reset"]), // Mapping reset action from upload store
toggle: function () {
this.open = !this.open;
},
abortAll() {
if (confirm(this.$t("upload.abortUpload"))) {
abortAllUploads();
buttons.done("upload");
this.open = false;
this.reset(); // Resetting the upload store state
this.reload = true; // Trigger reload in the file store
}
},
},
const fileStore = useFileStore();
const uploadStore = useUploadStore();
const { sentBytes, totalBytes } = storeToRefs(uploadStore);
const byteToMbyte = partial({ exponent: 2 });
const sentPercent = computed(() =>
((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
);
const sentMbytes = computed(() => byteToMbyte(uploadStore.sentBytes));
const totalMbytes = computed(() => byteToMbyte(uploadStore.totalBytes));
const speedMbytes = computed(() => byteToMbyte(speed.value));
let lastSpeedUpdate: number = 0;
let recentSpeeds: number[] = [];
const calculateSpeed = (sentBytes: number, oldSentBytes: number) => {
// Reset the state when the uploads batch is complete
if (sentBytes === 0) {
lastSpeedUpdate = 0;
recentSpeeds = [];
eta.value = Infinity;
speed.value = 0;
return;
}
const elapsedTime = (Date.now() - (lastSpeedUpdate ?? 0)) / 1000;
const bytesSinceLastUpdate = sentBytes - oldSentBytes;
const currentSpeed = bytesSinceLastUpdate / elapsedTime;
recentSpeeds.push(currentSpeed);
if (recentSpeeds.length > 5) {
recentSpeeds.shift();
}
const recentSpeedsAverage =
recentSpeeds.reduce((acc, curr) => acc + curr) / recentSpeeds.length;
// Use the current speed for the first update to avoid smoothing lag
if (recentSpeeds.length === 1) {
speed.value = currentSpeed;
}
speed.value = recentSpeedsAverage * 0.2 + speed.value * 0.8;
lastSpeedUpdate = Date.now();
calculateEta();
};
const calculateEta = () => {
if (speed.value === 0) {
eta.value = Infinity;
return Infinity;
}
const remainingSize = uploadStore.totalBytes - uploadStore.sentBytes;
const speedBytesPerSecond = speed.value;
eta.value = remainingSize / speedBytesPerSecond;
};
watch(sentBytes, calculateSpeed);
watch(totalBytes, (totalBytes, oldTotalBytes) => {
if (oldTotalBytes !== 0) {
return;
}
// Mark the start time of a new upload batch
lastSpeedUpdate = Date.now();
});
const formattedETA = computed(() => {
if (!eta.value || eta.value === Infinity) {
return "--:--:--";
}
let totalSeconds = eta.value;
const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.round(totalSeconds % 60);
return `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
});
const toggle = () => {
open.value = !open.value;
};
const abortAll = () => {
if (confirm(t("upload.abortUpload"))) {
buttons.done("upload");
open.value = false;
uploadStore.abort();
fileStore.reload = true; // Trigger reload in the file store
}
};
</script>