refactor: optimize HeroBannerComponent for better performance and readability

This commit is contained in:
Chneemann 2025-05-13 04:51:59 +02:00
parent 300cb97b2f
commit 77960560d3
4 changed files with 126 additions and 95 deletions

View file

@ -1,15 +1,6 @@
<section <section [class.fullVh]="!isWideScreen">
[ngClass]="{ <div class="video-banner" [class.fullVhBanner]="!isWideScreen">
fullVh: !isWideScreen <!-- Fallback image -->
}"
>
<div
class="video-banner"
[ngClass]="{
fullVhBanner: !isWideScreen
}"
>
<!-- Fallback image is displayed before the video is loaded -->
@if (!isVideoLoaded) { @if (!isVideoLoaded) {
<img [src]="thumbnailUrl" class="fallback-image" /> <img [src]="thumbnailUrl" class="fallback-image" />
} }
@ -19,31 +10,23 @@
<video <video
#videoElement #videoElement
[src]="previewClipUrl" [src]="previewClipUrl"
poster="{{ thumbnailUrl }}" [poster]="thumbnailUrl"
autoplay autoplay
muted muted
loop loop
playsinline playsinline
class="video-banner"
(canplay)="onVideoLoad()" (canplay)="onVideoLoad()"
(error)="onVideoError()" (error)="onVideoError()"
></video> ></video>
} }
</div> </div>
@if (currentVideo !== null) { @if (currentVideo) {
<div <div class="content" [class.fullVhContent]="!isWideScreen">
class="content"
[ngClass]="{
fullVhContent: !isWideScreen
}"
>
<div class="title">{{ currentVideo.title }}</div> <div class="title">{{ currentVideo.title }}</div>
<div <div
class="description hide-scrollbar" class="description hide-scrollbar"
[ngClass]="{ [class.fullVhDescription]="!isWideScreen"
fullVhDescription: !isWideScreen
}"
> >
{{ currentVideo.description }} {{ currentVideo.description }}
</div> </div>
@ -59,25 +42,30 @@
[value]="'Play'" [value]="'Play'"
[imgPath]="'play'" [imgPath]="'play'"
[imgReverse]="true" [imgReverse]="true"
[disabled]="!isAnyResolutionAvailable()" [disabled]="!isAnyResolutionAvailable"
(click)="playVideoId(videoUrl, currentVideo)" (click)="playVideoId(videoUrl, currentVideo)"
></app-btn-large> ></app-btn-large>
<div class="favorite-img" (click)="toggleLikeVideo(currentVideo)">
@if(checkLikeVideos(currentVideo)) { <!-- Favorite button -->
@let isLiked = checkLikeVideos(currentVideo);
<div
class="favorite-img"
(click)="toggleVideoStatus(currentVideo.id, 'favorite')"
>
<img <img
class="filled" [src]="
src="./../../../../assets/img/favorite-filled.svg" isLiked
alt="" ? './assets/img/favorite-filled.svg'
: './assets/img/favorite-outlined.svg'
"
[class.filled]="isLiked"
[class.outlined]="!isLiked"
alt="favorite"
/> />
} @else {
<img
class="outlined"
src="./../../../../assets/img/favorite-outlined.svg"
alt=""
/>
}
</div> </div>
@if (!isAnyResolutionAvailable()) {
<!-- Loading indicator -->
@if (!isAnyResolutionAvailable) {
<p> <p>
(The video is being converted) (The video is being converted)
<a (click)="refreshPage()">Refresh</a> <a (click)="refreshPage()">Refresh</a>

View file

@ -16,6 +16,11 @@ section {
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
video {
width: 100%;
height: 100%;
object-fit: cover;
}
&::after { &::after {
content: ""; content: "";
position: absolute; position: absolute;
@ -31,12 +36,6 @@ section {
} }
} }
.video-banner {
width: 100%;
height: 100%;
object-fit: cover;
}
.content { .content {
position: absolute; position: absolute;
width: 60%; width: 60%;

View file

@ -16,6 +16,7 @@ import { BtnSmallComponent } from '../../../shared/components/buttons/btn-small/
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { ResolutionService } from '../../../services/resolution.service'; import { ResolutionService } from '../../../services/resolution.service';
import { Video } from '../../../interfaces/video.interface'; import { Video } from '../../../interfaces/video.interface';
import { firstValueFrom, timeInterval } from 'rxjs';
@Component({ @Component({
selector: 'app-hero-banner', selector: 'app-hero-banner',
@ -51,6 +52,8 @@ export class HeroBannerComponent implements OnChanges {
availableResolutions: string[]; availableResolutions: string[];
videoIsUploaded: { [resolution: string]: boolean }; videoIsUploaded: { [resolution: string]: boolean };
private readonly PLAYBACK_SPEED = 0.5;
/** /**
* Initializes the HeroBannerComponent with the VideoService, ResolutionService and UserService. * Initializes the HeroBannerComponent with the VideoService, ResolutionService and UserService.
* Gets the available resolutions and video upload status. * Gets the available resolutions and video upload status.
@ -74,30 +77,27 @@ export class HeroBannerComponent implements OnChanges {
} }
/** /**
* Loads the resolution status of the video, sets the video speed and updates the video URLs. * Lifecycle hook called when the component's input properties have changed.
* @param changes SimpleChanges object containing the changed input properties. * Updates the video details if the current video has changed.
* @param changes The changes to the component's input properties.
*/ */
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes['currentVideo'] && this.currentVideo !== null) { if (changes['currentVideo']) {
const videoId = this.currentVideo.id; this.updateVideoDetails(this.currentVideo);
if (videoId) {
this.loadVideoResolutionStatus(videoId);
setTimeout(() => this.setVideoSpeed(), 0);
this.updateVideoUrls();
}
} }
} }
/** /**
* Updates the URLs for the video, thumbnail, and video thumbnail. * Updates the video details, resolution status, and playback speed
* for the hero banner component based on the given video.
* @param video The video to update the hero banner component with.
*/ */
private updateVideoUrls(): void { private updateVideoDetails(video: Video | null): void {
if (this.currentVideo) { if (video) {
const videoId = this.currentVideo.id; const videoId = video.id;
const fileName = this.currentVideo.file_name; this.loadVideoResolutionStatus(videoId);
this.videoUrl = `${this.environmentBaseUrl}/media/videos/${videoId}/${fileName}`; this.updateVideoUrls();
this.thumbnailUrl = `${this.environmentBaseUrl}/media/thumbnails/${videoId}/${fileName}_1080p.jpg`; this.setVideoSpeed();
this.previewClipUrl = `${this.environmentBaseUrl}/media/thumbnails/${videoId}/${fileName}_preview-clip.mp4`;
} }
} }
@ -105,22 +105,62 @@ export class HeroBannerComponent implements OnChanges {
* Loads the resolution status of the video. * Loads the resolution status of the video.
*/ */
private loadVideoResolutionStatus(videoId: number): void { private loadVideoResolutionStatus(videoId: number): void {
this.videoService firstValueFrom(this.videoService.isVideoResolutionUploaded(videoId))
.isVideoResolutionUploaded(videoId) .then((resolutions) => {
.subscribe((resolutions) => {
this.videoIsUploaded = resolutions; this.videoIsUploaded = resolutions;
this.videoIsUploadedChange.emit(this.videoIsUploaded); this.videoIsUploadedChange.emit(this.videoIsUploaded);
})
.catch((error) => {
console.error('Error loading the video resolution:', error);
}); });
} }
/**
* Updates the URLs for the video, thumbnail, and preview clip
* based on the current video. Sets the appropriate media paths
* for the video player and displays.
*/
private updateVideoUrls(): void {
if (this.currentVideo) {
const { id, file_name } = this.currentVideo;
this.videoUrl = this.getVideoMediaPath('video', id, file_name);
this.thumbnailUrl = this.getVideoMediaPath('thumbnail', id, file_name);
this.previewClipUrl = this.getVideoMediaPath('preview', id, file_name);
}
}
/**
* Generates a URL for a video, thumbnail, or preview clip given the type, id, and file name.
* @param type The type of media to generate the URL for.
* @param id The id of the video.
* @param file The file name of the video.
* @returns A URL pointing to the desired media.
*/
private getVideoMediaPath(
type: 'video' | 'thumbnail' | 'preview',
id: number,
file: string
): string {
switch (type) {
case 'video':
return `${this.environmentBaseUrl}/media/videos/${id}/${file}`;
case 'thumbnail':
return `${this.environmentBaseUrl}/media/thumbnails/${id}/${file}_1080p.jpg`;
case 'preview':
return `${this.environmentBaseUrl}/media/thumbnails/${id}/${file}_preview-clip.mp4`;
}
}
/** /**
* Sets the playback rate of the video. * Sets the playback rate of the video.
*/ */
private setVideoSpeed(): void { private setVideoSpeed(): void {
if (this.videoElementRef) { setInterval(() => {
const videoElement = this.videoElementRef.nativeElement; const videoElement = this.videoElementRef?.nativeElement;
videoElement.playbackRate = 0.5; if (videoElement) {
videoElement.playbackRate = this.PLAYBACK_SPEED;
} }
});
} }
/** /**
@ -138,26 +178,40 @@ export class HeroBannerComponent implements OnChanges {
} }
/** /**
* Toggles the like status of the video. * Plays the video and marks it as watched.
*/ */
toggleLikeVideo(video: Video): void { playVideoId(videoPath: string, video: Video): void {
if (video.id !== undefined) { this.playVideo.emit(videoPath);
const videoId = video.id; this.toggleVideoStatus(video.id, 'watched');
this.favoriteVideos = this.favoriteVideos.includes(videoId) }
? this.favoriteVideos.filter((id) => id !== videoId)
: [...this.favoriteVideos, videoId]; /**
* Toggles the status of a video (favorite or watched) and emits the updated
* favoriteVideos or watchedVideos array.
* @param videoId The id of the video to toggle the status for.
* @param statusType The type of status to toggle ('favorite' or 'watched').
*/
toggleVideoStatus(videoId: number, statusType: 'favorite' | 'watched'): void {
if (statusType === 'favorite') {
this.favoriteVideos = this.toggleArrayItem(this.favoriteVideos, videoId);
this.favoriteVideoChange.emit(this.favoriteVideos); this.favoriteVideoChange.emit(this.favoriteVideos);
} else if (statusType === 'watched') {
this.watchedVideos = this.toggleArrayItem(this.watchedVideos, videoId);
this.watchedVideoChange.emit(this.watchedVideos);
} }
} }
/** /**
* Toggles the watched status of the video. * Toggles the presence of an item in an array, returning a new array.
*
* @param array The array to modify.
* @param id The item to toggle.
* @returns A new array with the item toggled.
*/ */
toggleWatchedVideo(videoId: number): void { private toggleArrayItem(array: number[], id: number): number[] {
this.watchedVideos = this.watchedVideos.includes(videoId) return array.includes(id)
? this.watchedVideos.filter((id) => id !== videoId) ? array.filter((item) => item !== id)
: [...this.watchedVideos, videoId]; : [...array, id];
this.watchedVideoChange.emit(this.watchedVideos);
} }
/** /**
@ -167,13 +221,6 @@ export class HeroBannerComponent implements OnChanges {
return video.id !== undefined && this.favoriteVideos.includes(video.id); return video.id !== undefined && this.favoriteVideos.includes(video.id);
} }
/**
* Checks if any resolution is available.
*/
isAnyResolutionAvailable(): boolean {
return Object.values(this.videoIsUploaded).some((available) => available);
}
/** /**
* Refreshes the page with the current video. * Refreshes the page with the current video.
*/ */
@ -189,12 +236,9 @@ export class HeroBannerComponent implements OnChanges {
} }
/** /**
* Plays the video and marks it as watched. * Checks if any of the video resolutions are available to watch.
*/ */
playVideoId(videoPath: string, video: Video): void { get isAnyResolutionAvailable(): boolean {
if (video.id !== undefined) { return Object.values(this.videoIsUploaded).some((available) => available);
this.playVideo.emit(videoPath);
this.toggleWatchedVideo(video.id);
}
} }
} }

View file

@ -1,5 +1,5 @@
export interface Video { export interface Video {
id?: number; id: number;
creator: number; creator: number;
created_at: string; created_at: string;
title: string; title: string;