Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

user should be able to make private playlist #12

Merged
merged 3 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/app/components/player/player.component.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
@defer {
<section class="player-wrapper" #player_wrapper>
@if(currentVideoInfo){
<div class="player-src" id="pip_container">
<iframe id="pip_element" allow="picture-in-picture; autoplay; clipboard-write; gyroscope" *ngIf="safeURL"
width="100%" height="100%" [src]="safeURL" allowfullscreen webkitallowfullscreen></iframe>
</div>
}
<div class="player-info">
<div class="info">
@if (currentVideoInfo) {
<h4 class="player-info-title">{{ currentVideoInfo.title }}</h4>
}
<ng-content select="form-slot"></ng-content>
</div>
@if (platform.BLINK && !platform.ANDROID && !platform.IOS) {
@if (platform.BLINK && !platform.ANDROID && !platform.IOS && videos?.length) {
<div class="actions">
<button mat-button (click)="enablePip()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
Expand All @@ -27,7 +29,6 @@ <h4 class="player-info-title">{{ currentVideoInfo.title }}</h4>
<div class="player-list-wrapper" [style.top]="player_wrapper | adjustHeight">
<ng-container *ngTemplateOutlet="player_playlist"></ng-container>
</div>

}


Expand All @@ -39,7 +40,7 @@ <h4 class="player-info-title">{{ currentVideoInfo.title }}</h4>
<mat-expansion-panel-header>
<mat-panel-title>
<p class="accordion-title">
Public playlist
{{playlist_title}}
</p>
</mat-panel-title>
</mat-expansion-panel-header>
Expand Down
39 changes: 20 additions & 19 deletions src/app/components/player/player.component.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import {ChangeDetectionStrategy, Component, inject, Input, OnChanges, SimpleChanges} from '@angular/core';
import {MatExpansionModule} from '@angular/material/expansion';
import {ReactiveFormsModule} from '@angular/forms';
import {CommonModule} from '@angular/common';
import {AdjustHeightPipe} from "../../../pipes/adjust-height.pipe";
import {MatButtonModule} from "@angular/material/button";
import {ThumbnailPipe} from "../../../pipes/thumbnail.pipe";
import {YoutubeService} from "@services/youtube.service";
import {DOMService} from "@services/dom.service";
import {InputComponent} from "@components/input/input.component";
import {ButtonComponent} from "@components/button/button.component";
import {VideoInfo} from "@interface/video-info.interface";
import {Platform} from "@angular/cdk/platform";
import {SafeResourceUrl} from "@angular/platform-browser";
import {YoutubeUtil} from "@utils/youtube.util";
import { ChangeDetectionStrategy, Component, inject, Input, OnChanges, SimpleChanges } from '@angular/core';
import { MatExpansionModule } from '@angular/material/expansion';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { AdjustHeightPipe } from "../../../pipes/adjust-height.pipe";
import { MatButtonModule } from "@angular/material/button";
import { ThumbnailPipe } from "../../../pipes/thumbnail.pipe";
import { DOMService } from "@services/dom.service";
import { InputComponent } from "@components/input/input.component";
import { ButtonComponent } from "@components/button/button.component";
import { VideoInfoResponse } from "@interface/video-info.interface";
import { Platform } from "@angular/cdk/platform";
import { SafeResourceUrl } from "@angular/platform-browser";
import { YoutubeUtil } from "@utils/youtube.util";

@Component({
selector: 'app-player',
Expand All @@ -24,23 +23,25 @@ import {YoutubeUtil} from "@utils/youtube.util";
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PlayerComponent implements OnChanges {
@Input({required: true}) videos: VideoInfo[] | null = null;
@Input({ required: true }) videos: VideoInfoResponse[] | null = null;
@Input({ required: true }) playlist_title!: string;

platform = inject(Platform);
domService = inject(DOMService);
youtubeUtil = inject(YoutubeUtil);
panelOpenState = true;
protected safeURL!: SafeResourceUrl;
protected currentVideoInfo!: VideoInfo;
protected currentVideoInfo!: VideoInfoResponse;

ngOnChanges(changes: SimpleChanges): void {
if (this.videos) {
this.setCurrentVideo(this.videos[0]);
}
}
enablePip = async () => await this.domService.enterPiP({containerId: '#pip_container', pipElementId: '#pip_element'});
enablePip = async () => await this.domService.enterPiP({ containerId: '#pip_container', pipElementId: '#pip_element' });

setCurrentVideo(videInfo: VideoInfo) {
setCurrentVideo(videInfo: VideoInfoResponse) {
if (!videInfo) return;
this.currentVideoInfo = videInfo;
this.safeURL = this.youtubeUtil.convertSafeYoutubeUrl(this.currentVideoInfo.videoId)
}
Expand Down
35 changes: 34 additions & 1 deletion src/app/pages/personal/personal.component.html
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
<p>personal works!</p>
<app-player [videos]="videoInfos$ | async" playlist_title="Personal Playlist">
<ng-container *ngTemplateOutlet="form_accordion" ngProjectAs="form-slot"></ng-container>
</app-player>
<ng-template #form_accordion>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<p class="accordion-title">
@if (!authUser()) {
Login to add videos to private playlist
} @else {
Add your favourite video to private playlist
}
</p>
</mat-expansion-panel-header>
@if (authUser()) {
<ng-container *ngTemplateOutlet="private_form"></ng-container>
}
</mat-expansion-panel>
</mat-accordion>
</ng-template>

<ng-template #private_form>
<div class="player-limited">
@if (privateForm) {
<form [formGroup]="privateForm" data-cy="form-group" class="player-limited-form" (ngSubmit)="submit()">
<app-input formControlName="title" placeholder="Naruto Shippuden" label="Title" data-cy="title"></app-input>
<app-input formControlName="videoId" placeholder="https://youtu.be/8ofCZObsnOo?list=RDgW2hE-DRx7Q"
label="Youtube Video link" data-cy="videoId"></app-input>
<button mat-raised-button type="submit" color="accent" [disabled]="privateForm.invalid"> Submit</button>
</form>
}
</div>
</ng-template>
60 changes: 57 additions & 3 deletions src/app/pages/personal/personal.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,66 @@
import { Component } from '@angular/core';
import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatExpansionModule } from '@angular/material/expansion';
import { InputComponent } from '@components/input/input.component';
import { PlayerComponent } from '@components/player/player.component';
import { VideoInfo, VideoInfoResponse } from '@interface/video-info.interface';
import { HotToastService } from '@ngneat/hot-toast';
import { AuthService } from '@services/auth.service';
import { YoutubeService } from '@services/youtube.service';
import { YoutubeUtil } from '@utils/youtube.util';
import { youtubeUrlValidator } from '@validators/youtube.validators';
import { Observable } from 'rxjs';

@Component({
selector: 'app-personal',
standalone: true,
imports: [],
imports: [PlayerComponent, InputComponent, ReactiveFormsModule, MatExpansionModule, NgTemplateOutlet, AsyncPipe],
templateUrl: './personal.component.html',
styleUrl: './personal.component.scss'
})
export class PersonalComponent {
export class PersonalComponent implements OnInit {


youtubeService = inject(YoutubeService);
youtubeUtil = inject(YoutubeUtil);
authUser = inject(AuthService).authUser;
toastService = inject(HotToastService);
videoInfos$ !: Observable<VideoInfoResponse[]>;
private formBuilder = inject(FormBuilder);

privateForm = this.formBuilder.group({
title: new FormControl('', [Validators.required, Validators.minLength(3)]),
videoId: new FormControl('', [Validators.required, youtubeUrlValidator()]),
})

ngOnInit(): void {
try {
const authUserEmail = this.authUser()?.email;
if (authUserEmail) {
this.videoInfos$ = this.youtubeService.getVideos(authUserEmail, 'privateVideos')
}
} catch (err) {
this.toastService.error('Failed to fetch videos.')
}
}

async submit() {
const authUser = this.authUser();
if (!authUser) {
this.toastService.error('You must have to login before submit');
return;
}
const validateVideoInfo = await this.youtubeUtil.getValidVideoInfoFromForm(this.privateForm, authUser);
if (!validateVideoInfo.isValid && validateVideoInfo.error) {
this.toastService.error(validateVideoInfo.error.toString());
return;
}

if (validateVideoInfo.isValid && validateVideoInfo.data && authUser.email) {
await this.youtubeService.saveVideo(validateVideoInfo.data, authUser.email);
this.privateForm.reset();
this.toastService.success('Added to private playlist');
}
}
}
2 changes: 1 addition & 1 deletion src/app/pages/public/public.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<app-player [videos]="(videoInfos$ | async)">
<app-player [videos]="(videoInfos$ | async)" playlist_title="Public Playlist">
<ng-container *ngTemplateOutlet="form_accordion" ngProjectAs="form-slot"></ng-container>
</app-player>
<ng-template #form_accordion>
Expand Down
8 changes: 4 additions & 4 deletions src/app/pages/public/public.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { YoutubeUtil } from "@utils/youtube.util";
import { AuthService } from "@services/auth.service";
import { HotToastService } from "@ngneat/hot-toast";
import { Observable } from "rxjs";
import { VideoInfo } from "@interface/video-info.interface";
import { VideoInfoResponse } from "@interface/video-info.interface";
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { AsyncPipe, NgTemplateOutlet } from "@angular/common";
import { InputComponent } from "@components/input/input.component";
Expand Down Expand Up @@ -38,7 +38,7 @@ export class PublicComponent implements OnInit, AfterViewInit {
authUser = inject(AuthService).authUser;
toastService = inject(HotToastService);

videoInfos$ !: Observable<VideoInfo[]>;
videoInfos$ !: Observable<VideoInfoResponse[]>;
private formBuilder = inject(FormBuilder);


Expand All @@ -50,7 +50,7 @@ export class PublicComponent implements OnInit, AfterViewInit {

ngOnInit(): void {
try {
this.videoInfos$ = this.youtubeService.getVideos()
this.videoInfos$ = this.youtubeService.getVideos('videos', 'publicVideos')
} catch (err) {
this.toastService.error('Failed to fetch videos.')
}
Expand All @@ -69,7 +69,7 @@ export class PublicComponent implements OnInit, AfterViewInit {
}

if (validateVideoInfo.isValid && validateVideoInfo.data) {
await this.youtubeService.saveVideo(validateVideoInfo.data);
await this.youtubeService.saveVideo(validateVideoInfo.data, 'videos');
this.publicForm.reset();
this.toastService.success('Added to public playlist');
}
Expand Down
2 changes: 1 addition & 1 deletion src/guards/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const authGuard: CanActivateFn = (route, state) => {
const authUser = inject(AuthService)?.authUser();

if (authUser) {
return inject(Router).navigateByUrl('/player');
return inject(Router).navigateByUrl('/public');
}

return true;
Expand Down
5 changes: 3 additions & 2 deletions src/guards/user.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { AuthService } from '@services/auth.service';

export const userGuard: CanActivateFn = (route, state) => {
const authUser = inject(AuthService)?.authUser();

if (!authUser) {
return inject(Router).navigateByUrl('/not-found', { skipLocationChange: true });
}
if (authUser && route.queryParams['uid'] !== authUser.uid) {
return inject(Router).navigateByUrl('/not-found', { skipLocationChange: true });
}

return true;
};
20 changes: 6 additions & 14 deletions src/indexDB/db.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import Dexie, { Table } from 'dexie';
import { VideoInfo } from "@interface/video-info.interface";
import { VideoInfoResponse } from "@interface/video-info.interface";
export class AppDB extends Dexie {
videos!: Table<VideoInfo, "title">;
publicVideos!: Table<VideoInfoResponse, "title">;
privateVideos!: Table<VideoInfoResponse, "title">;

constructor() {
super('ReTube');
this.version(1).stores({
videos: 'title'
publicVideos: 'title',
privateVideos: 'title'
})

this.open()
.then(() => console.log('Opened'))
.catch((err) => console.error(err))
.finally(() => {
if (this.isOpen()) {
console.log('Connected to IndexDB');
} else {
console.log('IndexDB instance error');
}
});
}
}
export const db = new AppDB()
2 changes: 2 additions & 0 deletions src/interfaces/video-info.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export interface VideoInfo {
title: string;
userName: string;
}

export type VideoInfoResponse = Omit<VideoInfo, 'userName' | 'userId'>
27 changes: 17 additions & 10 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Platform} from '@angular/cdk/platform';
import {inject, Injectable, signal} from '@angular/core';
import {Router} from '@angular/router';
import {HotToastService} from '@ngneat/hot-toast';
import { Platform } from '@angular/cdk/platform';
import { inject, Injectable, signal } from '@angular/core';
import { Router } from '@angular/router';
import { HotToastService } from '@ngneat/hot-toast';
import {
Auth,
getAuth,
Expand All @@ -13,6 +13,7 @@ import {
signOut,
User
} from "firebase/auth";
import { db } from 'indexDB/db';

const provider = new GoogleAuthProvider();

Expand All @@ -36,7 +37,7 @@ export class AuthService {
}
} catch (e) {
console.error(e);
this.toastService.error(`Something went wrong`, {position: 'bottom-center'})
this.toastService.error(`Something went wrong`, { position: 'bottom-center' })
}
}

Expand All @@ -45,9 +46,15 @@ export class AuthService {
const auth = getAuth();
await signOut(auth);
this.authUser.set(null);
this.toastService.success(`Successfully Log Out`, {position: 'bottom-center'})
this.toastService.success(`Successfully Log Out`, { position: 'bottom-center' });
} catch (e) {
this.toastService.error(`Something went wrong`, {position: 'bottom-center'})
this.toastService.error(`Something went wrong`, { position: 'bottom-center' })
} finally {
/**
* TODO: Perform user data persistance cleanup
*/
this.router.navigateByUrl('/public');
db.privateVideos.clear()
}
}

Expand All @@ -58,7 +65,7 @@ export class AuthService {
if (result) {
const user = result.user;
this.authUser.set(user);
await this.router.navigateByUrl('/player')
await this.router.navigateByUrl('/public')
}
} catch (error: any) {
// Handle Errors here.
Expand All @@ -70,7 +77,7 @@ export class AuthService {
const credential = GoogleAuthProvider.credentialFromError(error);
// ...
console.error(error);
this.toastService.error(`Something went wrong. Please try later.`, {position: 'bottom-center'})
this.toastService.error(`Something went wrong. Please try later.`, { position: 'bottom-center' })

}
}
Expand All @@ -92,7 +99,7 @@ export class AuthService {
// The AuthCredential type that was used.
const credential = GoogleAuthProvider.credentialFromError(error);
console.error(error);
this.toastService.error(`Something went wrong. Please try later.`, {position: 'bottom-center'})
this.toastService.error(`Something went wrong. Please try later.`, { position: 'bottom-center' })
}
}
}
Expand Down
Loading