feat: add upload progress indicator in percent for video upload

This commit is contained in:
Chneemann 2025-05-04 18:39:06 +02:00
parent 0caabff4d7
commit 3f5d397dfa
8 changed files with 101 additions and 24 deletions

View file

@ -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>
} }

View file

@ -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

View file

@ -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.
* *

View file

@ -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({

View file

@ -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',
}
); );
} }

View file

@ -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>

View file

@ -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;

View file

@ -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;
} }