feat: add Video interface for typed video data handling
This commit is contained in:
parent
58110bc7c1
commit
3861464092
9 changed files with 113 additions and 71 deletions
|
|
@ -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,8 +39,11 @@ export class CategoriesComponent implements AfterViewInit {
|
|||
}
|
||||
|
||||
openCurrentVideo(videoId: number) {
|
||||
this.currentVideo = videoId;
|
||||
this.currentVideoId.emit(videoId);
|
||||
const video = this.videos.find((v) => v.id === videoId);
|
||||
if (video) {
|
||||
this.currentVideo = video;
|
||||
this.currentVideoId.emit(videoId);
|
||||
}
|
||||
}
|
||||
|
||||
getAllVideos(filmGenre: string) {
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,19 +11,25 @@ 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;
|
||||
this.currentVideoId.emit(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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,13 +101,19 @@ export class HeroBannerComponent implements OnChanges {
|
|||
}
|
||||
}
|
||||
|
||||
toggleLikeVideo(videoId: number): void {
|
||||
if (this.favoriteVideos.includes(videoId)) {
|
||||
this.favoriteVideos = this.favoriteVideos.filter((id) => id !== videoId);
|
||||
} else {
|
||||
this.favoriteVideos.push(videoId);
|
||||
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
|
||||
);
|
||||
} else {
|
||||
this.favoriteVideos.push(videoId);
|
||||
}
|
||||
this.favoriteVideoChange.emit(this.favoriteVideos);
|
||||
}
|
||||
this.favoriteVideoChange.emit(this.favoriteVideos);
|
||||
}
|
||||
|
||||
toggleWatchedVideo(videoId: number): void {
|
||||
|
|
@ -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) {
|
||||
this.playVideo.emit(videoPath);
|
||||
this.toggleWatchedVideo(videoId);
|
||||
playVideoId(videoPath: string, video: Video) {
|
||||
if (video.id !== undefined) {
|
||||
this.playVideo.emit(videoPath);
|
||||
this.toggleWatchedVideo(video.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
frontend/src/app/interfaces/video.interface.ts
Normal file
10
frontend/src/app/interfaces/video.interface.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue