feat: add upload progress indicator in percent for video upload
This commit is contained in:
parent
0caabff4d7
commit
3f5d397dfa
8 changed files with 101 additions and 24 deletions
|
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
<!-- Loop through genres -->
|
<!-- Loop through genres -->
|
||||||
@for (genre of genres; track genre) {
|
@for (genre of genres; track genre) {
|
||||||
<option [value]="genre.name">{{ genre.name }}</option>
|
<option [value]="genre.code">{{ genre.name }}</option>
|
||||||
} }
|
} }
|
||||||
</select>
|
</select>
|
||||||
<div class="error-msg">
|
<div class="error-msg">
|
||||||
|
|
@ -96,5 +96,6 @@
|
||||||
@if(videoData.send){
|
@if(videoData.send){
|
||||||
<app-loading-dialog
|
<app-loading-dialog
|
||||||
loadingMsg="Video is uploading, please be patient a moment."
|
loadingMsg="Video is uploading, please be patient a moment."
|
||||||
|
[uploadProgress]="uploadProgress"
|
||||||
></app-loading-dialog>
|
></app-loading-dialog>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { VideoService } from '../../../services/video.service';
|
||||||
import { LoadingDialogComponent } from '../../../shared/components/loading-dialog/loading-dialog.component';
|
import { LoadingDialogComponent } from '../../../shared/components/loading-dialog/loading-dialog.component';
|
||||||
import { Video } from '../../../interfaces/video.interface';
|
import { Video } from '../../../interfaces/video.interface';
|
||||||
import { GenreService } from '../../../services/genre.service';
|
import { GenreService } from '../../../services/genre.service';
|
||||||
|
import { HttpEvent, HttpEventType } from '@angular/common/http';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-upload-video',
|
selector: 'app-upload-video',
|
||||||
|
|
@ -25,6 +26,8 @@ export class UploadVideoComponent {
|
||||||
@Output() uploadedVideo = new EventEmitter<Video>();
|
@Output() uploadedVideo = new EventEmitter<Video>();
|
||||||
|
|
||||||
errorMsgFileSize: string | null = null;
|
errorMsgFileSize: string | null = null;
|
||||||
|
uploadProgress: number | null = null;
|
||||||
|
|
||||||
readonly maxFileSizeMB = 20;
|
readonly maxFileSizeMB = 20;
|
||||||
|
|
||||||
videoData = {
|
videoData = {
|
||||||
|
|
@ -102,28 +105,51 @@ export class UploadVideoComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the video upload form submission.
|
* Handles form submission by validating the form and uploading the video.
|
||||||
* @param ngForm The video upload form
|
* @param ngForm - The submitted Angular form
|
||||||
*/
|
*/
|
||||||
async onSubmit(ngForm: NgForm): Promise<void> {
|
async onSubmit(ngForm: NgForm): Promise<void> {
|
||||||
if (ngForm.submitted && ngForm.form.valid) {
|
if (ngForm.submitted && ngForm.form.valid) {
|
||||||
try {
|
this.videoData.send = true;
|
||||||
this.videoData.send = true;
|
this.uploadProgress = 0;
|
||||||
const uploadedVideo = await this.videoService.uploadVideo(
|
|
||||||
this.createFormData()
|
this.videoService
|
||||||
);
|
.uploadVideoWithProgress(this.createFormData())
|
||||||
this.uploadedVideo.emit(uploadedVideo);
|
.subscribe({
|
||||||
ngForm.resetForm();
|
next: (event) => this.handleUploadEvent(event, ngForm),
|
||||||
this.closeVideoUploadOverview();
|
error: (err) => this.handleUploadError(err),
|
||||||
this.errorService.clearError();
|
});
|
||||||
} catch (error) {
|
|
||||||
this.errorService.handleError(error);
|
|
||||||
} finally {
|
|
||||||
this.videoData.send = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles upload progress and response events.
|
||||||
|
* @param event - The HTTP event from the upload observable
|
||||||
|
* @param ngForm - The form instance used for resetting after upload
|
||||||
|
*/
|
||||||
|
private handleUploadEvent(event: HttpEvent<any>, ngForm: NgForm): void {
|
||||||
|
if (event.type === HttpEventType.UploadProgress && event.total) {
|
||||||
|
this.uploadProgress = Math.round(100 * (event.loaded / event.total));
|
||||||
|
} else if (event.type === HttpEventType.Response) {
|
||||||
|
this.uploadedVideo.emit(event.body);
|
||||||
|
ngForm.resetForm();
|
||||||
|
this.closeVideoUploadOverview();
|
||||||
|
this.errorService.clearError();
|
||||||
|
this.uploadProgress = null;
|
||||||
|
this.videoData.send = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles upload errors and resets UI state.
|
||||||
|
* @param err - The error from the upload observable
|
||||||
|
*/
|
||||||
|
private handleUploadError(err: any): void {
|
||||||
|
this.errorService.handleError(err);
|
||||||
|
this.uploadProgress = null;
|
||||||
|
this.videoData.send = false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a FormData object containing the video data.
|
* Creates a FormData object containing the video data.
|
||||||
* @returns The FormData for the video upload
|
* @returns The FormData for the video upload
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpEvent, HttpHeaders } from '@angular/common/http';
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { Observable, throwError } from 'rxjs';
|
import { Observable, throwError } from 'rxjs';
|
||||||
|
|
@ -45,6 +45,30 @@ export class ApiService {
|
||||||
.pipe(catchError(this.handleError));
|
.pipe(catchError(this.handleError));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a POST request to the specified endpoint with a request body.
|
||||||
|
*
|
||||||
|
* @template T The expected type of the response.
|
||||||
|
* @param endpoint The API endpoint to send the request to.
|
||||||
|
* @param body The request payload to send.
|
||||||
|
* @param auth Whether to include authentication headers. Defaults to false.
|
||||||
|
* @param options Additional options for the request.
|
||||||
|
* @returns An Observable of type HttpEvent<T>.
|
||||||
|
*/
|
||||||
|
postWithProgress<T>(
|
||||||
|
endpoint: string,
|
||||||
|
body: any,
|
||||||
|
auth: boolean = false,
|
||||||
|
options: any = {}
|
||||||
|
): Observable<HttpEvent<T>> {
|
||||||
|
const baseOptions = auth ? { headers: this.getAuthHeaders() } : {};
|
||||||
|
const mergedOptions = { ...baseOptions, ...options };
|
||||||
|
|
||||||
|
return this.http
|
||||||
|
.post<T>(`${environment.baseUrl}${endpoint}`, body, mergedOptions)
|
||||||
|
.pipe(catchError(this.handleError));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a PUT request to the specified endpoint with a request body.
|
* Sends a PUT request to the specified endpoint with a request body.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||||
import { catchError, firstValueFrom, map, Observable, of } from 'rxjs';
|
import { catchError, firstValueFrom, map, Observable, of } from 'rxjs';
|
||||||
import { ApiService } from './api.service';
|
import { ApiService } from './api.service';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { catchError, firstValueFrom, map, Observable, of } from 'rxjs';
|
import { catchError, filter, firstValueFrom, map, Observable, of } from 'rxjs';
|
||||||
import { ApiService } from './api.service';
|
import { ApiService } from './api.service';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { ResolutionService } from './resolution.service';
|
import { ResolutionService } from './resolution.service';
|
||||||
import { Video } from '../interfaces/video.interface';
|
import { Video } from '../interfaces/video.interface';
|
||||||
|
import { HttpClient, HttpEvent, HttpEventType } from '@angular/common/http';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
|
@ -15,12 +16,13 @@ export class VideoService {
|
||||||
private availableResolutions: string[];
|
private availableResolutions: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the VideoService with ApiService, AuthService, and ResolutionService
|
* Initializes the VideoService with HttpClient,ApiService, AuthService, and ResolutionService
|
||||||
*
|
*
|
||||||
* It fetches the available resolutions from the ResolutionService and stores
|
* It fetches the available resolutions from the ResolutionService and stores
|
||||||
* them in the availableResolutions field.
|
* them in the availableResolutions field.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private resolutionService: ResolutionService
|
private resolutionService: ResolutionService
|
||||||
|
|
@ -54,9 +56,15 @@ export class VideoService {
|
||||||
* @param formData the FormData object representing the video to upload
|
* @param formData the FormData object representing the video to upload
|
||||||
* @returns a promise resolved when the upload is successful
|
* @returns a promise resolved when the upload is successful
|
||||||
*/
|
*/
|
||||||
uploadVideo(formData: FormData): Promise<Video> {
|
uploadVideoWithProgress(formData: FormData): Observable<any> {
|
||||||
return firstValueFrom(
|
return this.apiService.postWithProgress<any>(
|
||||||
this.apiService.post('/video/upload/', formData, true)
|
'/video/upload/',
|
||||||
|
formData,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
reportProgress: true,
|
||||||
|
observe: 'events',
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
<div class="loading-dialog">
|
<div class="loading-dialog">
|
||||||
<div class="loading-content">
|
<div class="loading-content">
|
||||||
<div id="loading" class="loader"></div>
|
<div id="loading" class="loader"></div>
|
||||||
|
|
||||||
|
<!-- Progress indicator -->
|
||||||
|
@if (uploadProgress !== null) {
|
||||||
|
<div class="loader-text">{{ uploadProgress }}%</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div id="loadingText" class="loading-text">
|
<div id="loadingText" class="loading-text">
|
||||||
<p>{{ loadingMsg }}</p>
|
<p>{{ loadingMsg }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
|
|
@ -34,6 +35,17 @@
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loader-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 20%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: $white;
|
||||||
|
text-shadow: 3px 3px 3px $black;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-text {
|
.loading-text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,5 @@ import { Component, Input } from '@angular/core';
|
||||||
})
|
})
|
||||||
export class LoadingDialogComponent {
|
export class LoadingDialogComponent {
|
||||||
@Input() loadingMsg: string = '';
|
@Input() loadingMsg: string = '';
|
||||||
|
@Input() uploadProgress: number | null = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue