Skip to content

Commit b08ccc9

Browse files
committed
feat: add progress bar
Signed-off-by: Vitor Mattos <[email protected]>
1 parent 421d95b commit b08ccc9

4 files changed

Lines changed: 262 additions & 9 deletions

File tree

src/Components/Request/RequestPicker.vue

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
{{ t('libresign', 'Upload') }}
3535
</NcActionButton>
3636
</NcActions>
37+
<UploadProgress :is-uploading="isUploading"
38+
:upload-progress="uploadProgress"
39+
:uploaded-bytes="uploadedBytes"
40+
:total-bytes="totalBytes"
41+
:upload-start-time="uploadStartTime"
42+
@cancel="cancelUpload" />
3743
<FilePicker v-if="showFilePicker"
3844
:name="t('libresign', 'Select your file')"
3945
:multiselect="false"
@@ -90,6 +96,8 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
9096
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
9197
import NcTextField from '@nextcloud/vue/components/NcTextField'
9298
99+
import UploadProgress from '../UploadProgress.vue'
100+
93101
import { useActionsMenuStore } from '../../store/actionsmenu.js'
94102
import { useFilesStore } from '../../store/files.js'
95103
@@ -109,6 +117,7 @@ export default {
109117
NcTextField,
110118
PlusIcon,
111119
UploadIcon,
120+
UploadProgress,
112121
},
113122
props: {
114123
inline: {
@@ -133,6 +142,12 @@ export default {
133142
loading: false,
134143
openedMenu: false,
135144
canRequestSign: loadState('libresign', 'can_request_sign', false),
145+
uploadProgress: 0,
146+
isUploading: false,
147+
uploadAbortController: null,
148+
uploadedBytes: 0,
149+
totalBytes: 0,
150+
uploadStartTime: null,
136151
}
137152
},
138153
computed: {
@@ -186,28 +201,64 @@ export default {
186201
},
187202
async upload(files) {
188203
this.loading = true
204+
this.isUploading = true
205+
this.uploadProgress = 0
206+
this.uploadedBytes = 0
207+
this.totalBytes = 0
208+
this.uploadStartTime = Date.now()
189209
190210
const formData = new FormData()
191211
192212
if (files.length === 1) {
193213
const name = files[0].name.replace(/\.pdf$/i, '')
194214
formData.append('name', name)
195215
formData.append('file', files[0])
216+
this.totalBytes = files[0].size
196217
} else {
197218
formData.append('name', '')
219+
let totalSize = 0
198220
files.forEach((file) => {
199221
formData.append('files[]', file)
222+
totalSize += file.size
200223
})
224+
this.totalBytes = totalSize
201225
}
202226
203-
await this.filesStore.upload(formData)
227+
const abortController = new AbortController()
228+
this.uploadAbortController = abortController
229+
230+
await this.filesStore.upload(formData, {
231+
signal: abortController.signal,
232+
onUploadProgress: (progressEvent) => {
233+
if (progressEvent.total) {
234+
this.uploadedBytes = progressEvent.loaded
235+
this.uploadProgress = Math.round((progressEvent.loaded / progressEvent.total) * 100)
236+
}
237+
},
238+
})
204239
.then((nodeId) => {
205240
this.filesStore.selectFile(nodeId)
206241
})
207-
.catch(({ response }) => {
208-
showError(response.data.ocs.data.message)
242+
.catch((error) => {
243+
if (error.code === 'ERR_CANCELED') {
244+
return
245+
}
246+
if (error.response?.data?.ocs?.data?.message) {
247+
showError(error.response.data.ocs.data.message)
248+
} else {
249+
showError(t('libresign', 'Upload failed'))
250+
}
209251
})
210-
this.loading = false
252+
.finally(() => {
253+
this.loading = false
254+
this.isUploading = false
255+
this.uploadAbortController = null
256+
})
257+
},
258+
cancelUpload() {
259+
if (this.uploadAbortController) {
260+
this.uploadAbortController.abort()
261+
}
211262
},
212263
uploadFile() {
213264
this.openedMenu = false

src/Components/RightSidebar/EnvelopeFilesList.vue

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
<NcNoteCard v-if="errorMessage" type="error">
2121
{{ errorMessage }}
2222
</NcNoteCard>
23+
<div v-if="isUploading" class="upload-progress-wrapper">
24+
<UploadProgress :is-uploading="isUploading"
25+
:upload-progress="uploadProgress"
26+
:uploaded-bytes="uploadedBytes"
27+
:total-bytes="totalBytes"
28+
:upload-start-time="uploadStartTime"
29+
@cancel="cancelUpload" />
30+
</div>
2331
<NcEmptyContent v-if="files.length === 0 && !isLoadingFiles"
2432
:name="t('libresign', 'No files in envelope')"
2533
:description="t('libresign', 'Add files to get started')">
@@ -117,6 +125,8 @@ import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
117125
import NcListItem from '@nextcloud/vue/components/NcListItem'
118126
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
119127
128+
import UploadProgress from '../UploadProgress.vue'
129+
120130
import { SIGN_STATUS } from '../../domains/sign/enum.js'
121131
import { useFilesStore } from '../../store/files.js'
122132
@@ -134,6 +144,7 @@ export default {
134144
NcEmptyContent,
135145
NcListItem,
136146
NcNoteCard,
147+
UploadProgress,
137148
},
138149
props: {
139150
open: {
@@ -163,6 +174,12 @@ export default {
163174
message: '',
164175
action: null,
165176
},
177+
uploadProgress: 0,
178+
isUploading: false,
179+
uploadAbortController: null,
180+
uploadedBytes: 0,
181+
totalBytes: 0,
182+
uploadStartTime: null,
166183
}
167184
},
168185
computed: {
@@ -393,26 +410,56 @@ export default {
393410
if (!files || files.length === 0) return
394411
395412
this.hasLoading = true
413+
this.isUploading = true
414+
this.uploadProgress = 0
415+
this.uploadedBytes = 0
416+
this.totalBytes = 0
417+
this.uploadStartTime = Date.now()
418+
396419
const formData = new FormData()
420+
let totalSize = 0
397421
398422
for (const file of files) {
399423
formData.append('files[]', file)
424+
totalSize += file.size
400425
}
401426
402-
const result = await this.filesStore.addFilesToEnvelope(this.envelopeUuid, formData)
427+
this.totalBytes = totalSize
428+
429+
const abortController = new AbortController()
430+
this.uploadAbortController = abortController
431+
432+
const result = await this.filesStore.addFilesToEnvelope(this.envelopeUuid, formData, {
433+
signal: abortController.signal,
434+
onUploadProgress: (progressEvent) => {
435+
if (progressEvent.total) {
436+
this.uploadedBytes = progressEvent.loaded
437+
this.uploadProgress = Math.round((progressEvent.loaded / progressEvent.total) * 100)
438+
}
439+
},
440+
})
403441
404442
if (result.success) {
405443
this.showSuccess(this.t('libresign', result.message))
406444
this.files.push(...result.files)
407445
this.totalFiles = result.filesCount
408446
} else {
409-
this.showError(this.t('libresign', result.message))
447+
if (result.message !== 'Upload cancelled') {
448+
this.showError(this.t('libresign', result.message))
449+
}
410450
}
411451
412452
this.hasLoading = false
453+
this.isUploading = false
454+
this.uploadAbortController = null
413455
}
414456
input.click()
415457
},
458+
cancelUpload() {
459+
if (this.uploadAbortController) {
460+
this.uploadAbortController.abort()
461+
}
462+
},
416463
handleDelete(file) {
417464
this.deleteDialogConfig = {
418465
title: this.t('libresign', 'Delete'),
@@ -450,6 +497,14 @@ export default {
450497
overflow-y: auto;
451498
}
452499
500+
.upload-progress-wrapper {
501+
display: flex;
502+
justify-content: center;
503+
padding: 16px 0;
504+
margin-bottom: 16px;
505+
border-bottom: 1px solid var(--color-border);
506+
}
507+
453508
.files-list {
454509
display: flex;
455510
flex-direction: column;

src/Components/UploadProgress.vue

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 LibreCode coop and LibreCode contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<div v-if="isUploading" class="upload-picker-container">
7+
<div class="upload-picker__progress">
8+
<NcProgressBar :value="uploadProgress"
9+
:error="false"
10+
size="small"
11+
aria-label="Upload progress" />
12+
<p v-if="uploadEta">
13+
<span :title="uploadEta">{{ uploadEta }}</span>
14+
</p>
15+
</div>
16+
<NcButton class="upload-picker__cancel"
17+
type="tertiary"
18+
:aria-label="t('libresign', 'Cancel upload')"
19+
@click="$emit('cancel')">
20+
<template #icon>
21+
<CancelIcon :size="20" />
22+
</template>
23+
</NcButton>
24+
</div>
25+
</template>
26+
27+
<script>
28+
import CancelIcon from 'vue-material-design-icons/Cancel.vue'
29+
30+
import NcButton from '@nextcloud/vue/components/NcButton'
31+
import NcProgressBar from '@nextcloud/vue/components/NcProgressBar'
32+
33+
export default {
34+
name: 'UploadProgress',
35+
components: {
36+
CancelIcon,
37+
NcButton,
38+
NcProgressBar,
39+
},
40+
props: {
41+
isUploading: {
42+
type: Boolean,
43+
required: true,
44+
},
45+
uploadProgress: {
46+
type: Number,
47+
default: 0,
48+
},
49+
uploadedBytes: {
50+
type: Number,
51+
default: 0,
52+
},
53+
totalBytes: {
54+
type: Number,
55+
default: 0,
56+
},
57+
uploadStartTime: {
58+
type: Number,
59+
default: null,
60+
},
61+
},
62+
emits: ['cancel'],
63+
computed: {
64+
uploadEta() {
65+
if (!this.isUploading || !this.uploadStartTime || this.uploadedBytes === 0) {
66+
return ''
67+
}
68+
69+
const elapsed = Date.now() - this.uploadStartTime
70+
const rate = this.uploadedBytes / elapsed // bytes por ms
71+
const remaining = this.totalBytes - this.uploadedBytes
72+
const eta = remaining / rate // ms restantes
73+
74+
if (eta < 1000) {
75+
return t('libresign', 'a few seconds left')
76+
} else if (eta < 60000) {
77+
const seconds = Math.ceil(eta / 1000)
78+
return t('libresign', '{seconds} seconds left', { seconds })
79+
} else {
80+
const minutes = Math.ceil(eta / 60000)
81+
return t('libresign', '{minutes} minutes left', { minutes })
82+
}
83+
},
84+
},
85+
}
86+
</script>
87+
88+
<style lang="scss" scoped>
89+
.upload-picker-container {
90+
display: inline-flex;
91+
align-items: center;
92+
gap: 0;
93+
height: var(--default-clickable-area);
94+
}
95+
96+
.upload-picker__progress {
97+
width: 200px;
98+
max-width: 0;
99+
transition: max-width var(--animation-quick) ease-in-out;
100+
margin-top: 8px;
101+
102+
:deep(.progress-bar) {
103+
height: 6px;
104+
}
105+
106+
p {
107+
margin: 0;
108+
padding: 0;
109+
font-size: 13px;
110+
color: var(--color-text-maxcontrast);
111+
overflow: hidden;
112+
white-space: nowrap;
113+
text-overflow: ellipsis;
114+
115+
span {
116+
display: inline-block;
117+
}
118+
}
119+
}
120+
121+
.upload-picker-container .upload-picker__progress {
122+
max-width: 200px;
123+
margin-right: 20px;
124+
margin-left: 8px;
125+
}
126+
</style>

0 commit comments

Comments
 (0)