feat: add Video interface for typed video data handling

This commit is contained in:
Chneemann 2025-05-01 09:22:40 +02:00
parent 58110bc7c1
commit 3861464092
9 changed files with 113 additions and 71 deletions

View file

@ -11,8 +11,7 @@ import {
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { VideoListComponent } from './video-list/video-list.component'; import { VideoListComponent } from './video-list/video-list.component';
import { FilmGenreService } from '../../../services/film-genre.service'; import { FilmGenreService } from '../../../services/film-genre.service';
import { Subject, takeUntil } from 'rxjs'; import { Video } from '../../../interfaces/video.interface';
import { FilmGenre } from '../../../interfaces/film-genre.interface';
@Component({ @Component({
selector: 'app-categories', selector: 'app-categories',
@ -22,8 +21,8 @@ import { FilmGenre } from '../../../interfaces/film-genre.interface';
styleUrl: './categories.component.scss', styleUrl: './categories.component.scss',
}) })
export class CategoriesComponent implements AfterViewInit { export class CategoriesComponent implements AfterViewInit {
@Input() videos: any[] = []; @Input() videos: Video[] = [];
@Input() currentVideo: number = 0; @Input() currentVideo: Video | null = null;
@Input() favoriteVideos: any[] = []; @Input() favoriteVideos: any[] = [];
@Input() watchedVideos: any[] = []; @Input() watchedVideos: any[] = [];
@Output() currentVideoId = new EventEmitter<number>(); @Output() currentVideoId = new EventEmitter<number>();
@ -40,8 +39,11 @@ export class CategoriesComponent implements AfterViewInit {
} }
openCurrentVideo(videoId: number) { openCurrentVideo(videoId: number) {
this.currentVideo = videoId; const video = this.videos.find((v) => v.id === videoId);
this.currentVideoId.emit(videoId); if (video) {
this.currentVideo = video;
this.currentVideoId.emit(videoId);
}
} }
getAllVideos(filmGenre: string) { getAllVideos(filmGenre: string) {

View file

@ -4,7 +4,7 @@
<div class="video" (click)="openCurrentVideo(video.id)"> <div class="video" (click)="openCurrentVideo(video.id)">
<div class="banner"> <div class="banner">
<img <img
[ngClass]="{ selected: video.id === currentVideo }" [ngClass]="{ selected: video.id === currentVideo?.id }"
[src]="getThumbnailUrl(video.id, video.file_name)" [src]="getThumbnailUrl(video.id, video.file_name)"
alt="" alt=""
/> />

View file

@ -1,6 +1,7 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter } from '@angular/core';
import { environment } from '../../../../../environments/environment'; import { environment } from '../../../../../environments/environment';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Video } from '../../../../interfaces/video.interface';
@Component({ @Component({
selector: 'app-video-list', selector: 'app-video-list',
@ -10,19 +11,25 @@ import { CommonModule } from '@angular/common';
styleUrls: ['./video-list.component.scss'], styleUrls: ['./video-list.component.scss'],
}) })
export class VideoListComponent { export class VideoListComponent {
@Input() videos: any[] = []; @Input() videos: Video[] = [];
@Input() currentVideo: number = 0; @Input() currentVideo: Video | null = null;
@Input() watchedVideos: any[] = []; @Input() watchedVideos: any[] = [];
@Input() videoCategory: string = ''; @Input() videoCategory: string = '';
@Output() currentVideoId = new EventEmitter<number>(); @Output() currentVideoId = new EventEmitter<number>();
getThumbnailUrl(videoId: number, fileName: string): string { getThumbnailUrl(videoId: number | undefined, fileName: string): string {
return `${environment.baseUrl}/media/thumbnails/${videoId}/${fileName}_480p.jpg`; const id = videoId ?? 0;
return `${environment.baseUrl}/media/thumbnails/${id}/${fileName}_480p.jpg`;
} }
openCurrentVideo(videoId: number) { openCurrentVideo(videoId: number | undefined) {
this.currentVideo = videoId; if (videoId !== undefined) {
this.currentVideoId.emit(videoId); const video = this.videos.find((v) => v.id === videoId);
if (video) {
this.currentVideo = video;
this.currentVideoId.emit(videoId);
}
}
} }
scrollLeft(event: MouseEvent) { scrollLeft(event: MouseEvent) {

View file

@ -10,10 +10,14 @@
}" }"
> >
<!-- Fallback image is displayed before the video is loaded --> <!-- Fallback image is displayed before the video is loaded -->
<img *ngIf="!isVideoLoaded" [src]="thumbnailUrl" class="fallback-image" /> @if (!isVideoLoaded) {
<img [src]="thumbnailUrl" class="fallback-image" />
}
<!-- Video player -->
@if (currentVideo) {
<video <video
#videoElement #videoElement
*ngIf="currentVideo[0].id"
[src]="videoUrl" [src]="videoUrl"
poster="{{ thumbnailUrl }}" poster="{{ thumbnailUrl }}"
autoplay autoplay
@ -24,40 +28,42 @@
(canplay)="onVideoLoad()" (canplay)="onVideoLoad()"
(error)="onVideoError()" (error)="onVideoError()"
></video> ></video>
}
</div> </div>
@if (currentVideo.length > 0) { @if (currentVideo !== null) {
<div <div
class="content" class="content"
[ngClass]="{ [ngClass]="{
fullVhContent: !isWideScreen fullVhContent: !isWideScreen
}" }"
> >
<div class="title">{{ currentVideo[0].title }}</div> <div class="title">{{ currentVideo.title }}</div>
<div <div
class="description hide-scrollbar" class="description hide-scrollbar"
[ngClass]="{ [ngClass]="{
fullVhDescription: !isWideScreen fullVhDescription: !isWideScreen
}" }"
> >
{{ currentVideo[0].description }} {{ currentVideo.description }}
</div> </div>
<div class="buttons"> <div class="buttons">
@if (!isWideScreen) {
<app-btn-small <app-btn-small
*ngIf="!isWideScreen"
[imgPath]="'back'" [imgPath]="'back'"
[imgRadius]="'24px'" [imgRadius]="'24px'"
(click)="backToCategory([])" (click)="backToCategory()"
></app-btn-small> ></app-btn-small>
}
<app-btn-large <app-btn-large
[value]="'Play'" [value]="'Play'"
[imgPath]="'play'" [imgPath]="'play'"
[imgReverse]="true" [imgReverse]="true"
[disabled]="!isAnyResolutionAvailable()" [disabled]="!isAnyResolutionAvailable()"
(click)="playVideoId(playUrl, currentVideo[0].id)" (click)="playVideoId(playUrl, currentVideo)"
></app-btn-large> ></app-btn-large>
<div class="favorite-img" (click)="toggleLikeVideo(currentVideo[0].id)"> <div class="favorite-img" (click)="toggleLikeVideo(currentVideo)">
@if(checkLikeVideos(currentVideo[0].id)) { @if(checkLikeVideos(currentVideo)) {
<img <img
class="filled" class="filled"
src="./../../../../assets/img/favorite-filled.svg" src="./../../../../assets/img/favorite-filled.svg"
@ -74,7 +80,7 @@
@if (!isAnyResolutionAvailable()) { @if (!isAnyResolutionAvailable()) {
<p> <p>
(The video is being converted) (The video is being converted)
<a (click)="refreshPage(currentVideo)">Refresh</a> <a (click)="refreshPage()">Refresh</a>
</p> </p>
} }
</div> </div>

View file

@ -15,6 +15,7 @@ import { environment } from '../../../../environments/environment';
import { BtnSmallComponent } from '../../../shared/components/buttons/btn-small/btn-small.component'; import { BtnSmallComponent } from '../../../shared/components/buttons/btn-small/btn-small.component';
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';
@Component({ @Component({
selector: 'app-hero-banner', selector: 'app-hero-banner',
@ -25,15 +26,16 @@ import { ResolutionService } from '../../../services/resolution.service';
}) })
export class HeroBannerComponent implements OnChanges { export class HeroBannerComponent implements OnChanges {
@ViewChild('videoElement') videoElementRef!: ElementRef<HTMLVideoElement>; @ViewChild('videoElement') videoElementRef!: ElementRef<HTMLVideoElement>;
@Input() currentVideo: any[] = []; @Input() videos: Video[] = [];
@Input() currentVideo: Video | null = null;
@Input() isWideScreen: boolean = false; @Input() isWideScreen: boolean = false;
@Input() favoriteVideos: any[] = []; @Input() favoriteVideos: number[] = [];
@Input() watchedVideos: any[] = []; @Input() watchedVideos: number[] = [];
@Output() playVideo = new EventEmitter<string>(); @Output() playVideo = new EventEmitter<string>();
@Output() videoIsUploadedChange = new EventEmitter<{ @Output() videoIsUploadedChange = new EventEmitter<{
[resolution: string]: boolean; [resolution: string]: boolean;
}>(); }>();
@Output() refreshChange = new EventEmitter<any[]>(); @Output() refreshChange = new EventEmitter<Video[]>();
@Output() videosChange = new EventEmitter<any[]>(); @Output() videosChange = new EventEmitter<any[]>();
@Output() favoriteVideoChange = new EventEmitter<any[]>(); @Output() favoriteVideoChange = new EventEmitter<any[]>();
@ -69,14 +71,16 @@ export class HeroBannerComponent implements OnChanges {
} }
getVideoUrls() { getVideoUrls() {
this.playUrl = `${this.environmentBaseUrl}/media/videos/${this.currentVideo[0]?.id}/${this.currentVideo[0]?.file_name}`; if (this.currentVideo) {
this.thumbnailUrl = `${this.environmentBaseUrl}/media/thumbnails/${this.currentVideo[0]?.id}/${this.currentVideo[0]?.file_name}_1080p.jpg`; this.playUrl = `${this.environmentBaseUrl}/media/videos/${this.currentVideo.id}/${this.currentVideo.file_name}`;
this.videoUrl = `${this.environmentBaseUrl}/media/thumbnails/${this.currentVideo[0]?.id}/${this.currentVideo[0]?.file_name}_video-thumbnail.mp4`; this.thumbnailUrl = `${this.environmentBaseUrl}/media/thumbnails/${this.currentVideo.id}/${this.currentVideo.file_name}_1080p.jpg`;
this.videoUrl = `${this.environmentBaseUrl}/media/thumbnails/${this.currentVideo.id}/${this.currentVideo.file_name}_video-thumbnail.mp4`;
}
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes['currentVideo'] && this.currentVideo.length > 0) { if (changes['currentVideo'] && this.currentVideo !== null) {
const videoId = this.currentVideo[0]?.id; const videoId = this.currentVideo.id;
if (videoId) { if (videoId) {
this.videoService this.videoService
.isVideoResolutionUploaded(videoId) .isVideoResolutionUploaded(videoId)
@ -97,13 +101,19 @@ export class HeroBannerComponent implements OnChanges {
} }
} }
toggleLikeVideo(videoId: number): void { toggleLikeVideo(video: Video): void {
if (this.favoriteVideos.includes(videoId)) { if (video.id !== undefined) {
this.favoriteVideos = this.favoriteVideos.filter((id) => id !== videoId); const videoId = video.id;
} else {
this.favoriteVideos.push(videoId); if (this.favoriteVideos.includes(videoId)) {
this.favoriteVideos = this.favoriteVideos.filter(
(id) => id !== videoId
);
} else {
this.favoriteVideos.push(videoId);
}
this.favoriteVideoChange.emit(this.favoriteVideos);
} }
this.favoriteVideoChange.emit(this.favoriteVideos);
} }
toggleWatchedVideo(videoId: number): void { toggleWatchedVideo(videoId: number): void {
@ -122,24 +132,29 @@ export class HeroBannerComponent implements OnChanges {
this.userService.updateWatchedVideos(body); this.userService.updateWatchedVideos(body);
} }
checkLikeVideos(videoId: number) { checkLikeVideos(video: Video) {
return this.favoriteVideos.includes(videoId); if (video.id !== undefined) {
return this.favoriteVideos.includes(video.id);
}
return false;
} }
isAnyResolutionAvailable(): boolean { isAnyResolutionAvailable(): boolean {
return Object.values(this.videoIsUploaded).some((available) => available); return Object.values(this.videoIsUploaded).some((available) => available);
} }
refreshPage(newVideos: any[]) { refreshPage() {
this.refreshChange.emit(newVideos); this.refreshChange.emit(this.currentVideo ? [this.currentVideo] : []);
} }
backToCategory(newVideos: any[]) { backToCategory() {
this.videosChange.emit(newVideos); this.videosChange.emit([]);
} }
playVideoId(videoPath: string, videoId: number) { playVideoId(videoPath: string, video: Video) {
this.playVideo.emit(videoPath); if (video.id !== undefined) {
this.toggleWatchedVideo(videoId); this.playVideo.emit(videoPath);
this.toggleWatchedVideo(video.id);
}
} }
} }

View file

@ -10,9 +10,9 @@
<!-- Hero Banner --> <!-- Hero Banner -->
<app-hero-banner <app-hero-banner
*ngIf=" *ngIf="
currentVideo.length === 1 || currentVideo !== null || (this.isWideScreen() && currentVideo !== null)
(this.isWideScreen() && currentVideo.length > 1)
" "
[videos]="videos"
[isWideScreen]="isWideScreen()" [isWideScreen]="isWideScreen()"
[currentVideo]="currentVideo" [currentVideo]="currentVideo"
[favoriteVideos]="favoriteVideos" [favoriteVideos]="favoriteVideos"
@ -28,10 +28,10 @@
<!-- Video Categories --> <!-- Video Categories -->
<app-categories <app-categories
*ngIf=" *ngIf="
this.isWideScreen() || (!this.isWideScreen() && currentVideo.length === 0) this.isWideScreen() || (!this.isWideScreen() && currentVideo !== null)
" "
[videos]="videos" [videos]="videos"
[currentVideo]="currentVideo[0]?.id" [currentVideo]="currentVideo"
[favoriteVideos]="favoriteVideos" [favoriteVideos]="favoriteVideos"
[watchedVideos]="watchedVideos" [watchedVideos]="watchedVideos"
(currentVideoId)="currentVideoId($event)" (currentVideoId)="currentVideoId($event)"

View file

@ -9,6 +9,7 @@ import { BtnSmallComponent } from '../../shared/components/buttons/btn-small/btn
import { UploadVideoComponent } from './upload-video/upload-video.component'; import { UploadVideoComponent } from './upload-video/upload-video.component';
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';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -27,10 +28,10 @@ import { ResolutionService } from '../../services/resolution.service';
}) })
export class HomeComponent implements OnInit { export class HomeComponent implements OnInit {
@ViewChild(VideoPlayerComponent) videoPlayer!: VideoPlayerComponent; @ViewChild(VideoPlayerComponent) videoPlayer!: VideoPlayerComponent;
videos: any[] = []; videos: Video[] = [];
currentVideo: Video | null = null;
favoriteVideos: number[] = []; favoriteVideos: number[] = [];
watchedVideos: number[] = []; watchedVideos: number[] = [];
currentVideo: any[] = [];
playVideo: string = ''; playVideo: string = '';
isLoading: boolean = true; isLoading: boolean = true;
uploadVideoOverview: boolean = false; uploadVideoOverview: boolean = false;
@ -53,8 +54,8 @@ export class HomeComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
this.loadLikedAndWatchedVideos(); this.loadLikedAndWatchedVideos();
await this.loadAllVideos(); await this.loadAllVideos();
if (this.isWideScreen()) { if (this.isWideScreen() && !this.currentVideo) {
this.currentVideo.length === 0 ? this.loadRandomVideo() : null; this.loadRandomVideo();
} }
} }
@ -75,18 +76,19 @@ export class HomeComponent implements OnInit {
this.userService.updateLikedVideos(body); this.userService.updateLikedVideos(body);
} }
onRefreshPage(updatedVideos: any[]) { onRefreshPage(updatedVideos: Video[] | undefined) {
this.currentVideo = []; this.currentVideo = null;
setTimeout(() => { setTimeout(() => {
this.currentVideo = updatedVideos; this.currentVideo =
updatedVideos && updatedVideos.length > 0 ? updatedVideos[0] : null;
}, 1); }, 1);
} }
onVideosChange(updatedVideos: any[]) { onVideosChange(updatedVideos: Video[]) {
if (this.isWideScreen()) { if (this.isWideScreen()) {
this.loadRandomVideo(); this.loadRandomVideo();
} else { } else {
this.currentVideo = updatedVideos; this.currentVideo = updatedVideos[0] || null;
} }
} }
@ -98,7 +100,7 @@ export class HomeComponent implements OnInit {
this.videoIsUploaded = newStatus; this.videoIsUploaded = newStatus;
} }
onFavoriteVideoChange(favoriteVideos: any) { onFavoriteVideoChange(favoriteVideos: number[]) {
this.favoriteVideos = favoriteVideos; this.favoriteVideos = favoriteVideos;
this.updateLikeVideos(); this.updateLikeVideos();
} }
@ -130,14 +132,13 @@ export class HomeComponent implements OnInit {
loadRandomVideo(): void { loadRandomVideo(): void {
const randomIndex = Math.floor(Math.random() * this.videos.length); const randomIndex = Math.floor(Math.random() * this.videos.length);
this.currentVideo = [this.videos[randomIndex]]; this.currentVideo = this.videos[randomIndex] || null;
} }
currentVideoId(videoId: number) { currentVideoId(videoId: number) {
let index = this.videos.findIndex((video) => video.id === videoId); const video = this.videos.find((v) => v.id === videoId);
if (index !== -1) { if (video) {
this.currentVideo = []; this.currentVideo = video;
this.currentVideo.push(this.videos[index]);
} }
} }
@ -147,7 +148,7 @@ export class HomeComponent implements OnInit {
); );
} }
toggleUploadVideoOverview(value: any) { toggleUploadVideoOverview(value: boolean) {
this.uploadVideoOverview = value; this.uploadVideoOverview = value;
} }
} }

View file

@ -0,0 +1,10 @@
export interface Video {
id?: number;
creator: number;
created_at: string;
title: string;
description: string;
film_genre: string;
video_file: string;
file_name: string;
}

View file

@ -3,6 +3,7 @@ import { catchError, 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';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -33,7 +34,7 @@ export class VideoService {
* *
* @returns a promise resolving to an array of video objects * @returns a promise resolving to an array of video objects
*/ */
getAllVideos(): Promise<any> { getAllVideos(): Promise<Video[]> {
return firstValueFrom(this.apiService.get('/videos/', true)); return firstValueFrom(this.apiService.get('/videos/', true));
} }
@ -53,7 +54,7 @@ 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<any> { uploadVideo(formData: FormData): Promise<Video> {
return firstValueFrom( return firstValueFrom(
this.apiService.post('/video/upload/', formData, true) this.apiService.post('/video/upload/', formData, true)
); );