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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,9 +10,9 @@
<!-- Hero Banner -->
<app-hero-banner
*ngIf="
currentVideo.length === 1 ||
(this.isWideScreen() && currentVideo.length > 1)
currentVideo !== null || (this.isWideScreen() && currentVideo !== null)
"
[videos]="videos"
[isWideScreen]="isWideScreen()"
[currentVideo]="currentVideo"
[favoriteVideos]="favoriteVideos"
@ -28,10 +28,10 @@
<!-- Video Categories -->
<app-categories
*ngIf="
this.isWideScreen() || (!this.isWideScreen() && currentVideo.length === 0)
this.isWideScreen() || (!this.isWideScreen() && currentVideo !== null)
"
[videos]="videos"
[currentVideo]="currentVideo[0]?.id"
[currentVideo]="currentVideo"
[favoriteVideos]="favoriteVideos"
[watchedVideos]="watchedVideos"
(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 { UserService } from '../../services/user.service';
import { ResolutionService } from '../../services/resolution.service';
import { Video } from '../../interfaces/video.interface';
@Component({
selector: 'app-home',
@ -27,10 +28,10 @@ import { ResolutionService } from '../../services/resolution.service';
})
export class HomeComponent implements OnInit {
@ViewChild(VideoPlayerComponent) videoPlayer!: VideoPlayerComponent;
videos: any[] = [];
videos: Video[] = [];
currentVideo: Video | null = null;
favoriteVideos: number[] = [];
watchedVideos: number[] = [];
currentVideo: any[] = [];
playVideo: string = '';
isLoading: boolean = true;
uploadVideoOverview: boolean = false;
@ -53,8 +54,8 @@ export class HomeComponent implements OnInit {
async ngOnInit() {
this.loadLikedAndWatchedVideos();
await this.loadAllVideos();
if (this.isWideScreen()) {
this.currentVideo.length === 0 ? this.loadRandomVideo() : null;
if (this.isWideScreen() && !this.currentVideo) {
this.loadRandomVideo();
}
}
@ -75,18 +76,19 @@ export class HomeComponent implements OnInit {
this.userService.updateLikedVideos(body);
}
onRefreshPage(updatedVideos: any[]) {
this.currentVideo = [];
onRefreshPage(updatedVideos: Video[] | undefined) {
this.currentVideo = null;
setTimeout(() => {
this.currentVideo = updatedVideos;
this.currentVideo =
updatedVideos && updatedVideos.length > 0 ? updatedVideos[0] : null;
}, 1);
}
onVideosChange(updatedVideos: any[]) {
onVideosChange(updatedVideos: Video[]) {
if (this.isWideScreen()) {
this.loadRandomVideo();
} else {
this.currentVideo = updatedVideos;
this.currentVideo = updatedVideos[0] || null;
}
}
@ -98,7 +100,7 @@ export class HomeComponent implements OnInit {
this.videoIsUploaded = newStatus;
}
onFavoriteVideoChange(favoriteVideos: any) {
onFavoriteVideoChange(favoriteVideos: number[]) {
this.favoriteVideos = favoriteVideos;
this.updateLikeVideos();
}
@ -130,14 +132,13 @@ export class HomeComponent implements OnInit {
loadRandomVideo(): void {
const randomIndex = Math.floor(Math.random() * this.videos.length);
this.currentVideo = [this.videos[randomIndex]];
this.currentVideo = this.videos[randomIndex] || null;
}
currentVideoId(videoId: number) {
let index = this.videos.findIndex((video) => video.id === videoId);
if (index !== -1) {
this.currentVideo = [];
this.currentVideo.push(this.videos[index]);
const video = this.videos.find((v) => v.id === videoId);
if (video) {
this.currentVideo = video;
}
}
@ -147,7 +148,7 @@ export class HomeComponent implements OnInit {
);
}
toggleUploadVideoOverview(value: any) {
toggleUploadVideoOverview(value: boolean) {
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 { AuthService } from './auth.service';
import { ResolutionService } from './resolution.service';
import { Video } from '../interfaces/video.interface';
@Injectable({
providedIn: 'root',
@ -33,7 +34,7 @@ export class VideoService {
*
* @returns a promise resolving to an array of video objects
*/
getAllVideos(): Promise<any> {
getAllVideos(): Promise<Video[]> {
return firstValueFrom(this.apiService.get('/videos/', true));
}
@ -53,7 +54,7 @@ export class VideoService {
* @param formData the FormData object representing the video to upload
* @returns a promise resolved when the upload is successful
*/
uploadVideo(formData: FormData): Promise<any> {
uploadVideo(formData: FormData): Promise<Video> {
return firstValueFrom(
this.apiService.post('/video/upload/', formData, true)
);