├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── index.ts │ ├── main │ │ ├── index.ts │ │ ├── main.component.css │ │ ├── main.component.html │ │ ├── main.component.ts │ │ ├── video-player │ │ │ ├── video-player.component.css │ │ │ ├── video-player.component.html │ │ │ └── video-player.component.ts │ │ ├── videos-list │ │ │ ├── videos-list.component.css │ │ │ ├── videos-list.component.html │ │ │ └── videos-list.component.ts │ │ ├── videos-playlist │ │ │ ├── videos-playlist.component.css │ │ │ ├── videos-playlist.component.html │ │ │ └── videos-playlist.component.ts │ │ └── videos-search │ │ │ ├── videos-search.component.css │ │ │ ├── videos-search.component.html │ │ │ └── videos-search.component.ts │ └── shared │ │ ├── constants.ts │ │ ├── directives │ │ └── lazy-scroll │ │ │ └── lazy-scroll.directive.ts │ │ ├── pipes │ │ ├── video-duration.pipe.ts │ │ ├── video-likes-views.pipe.ts │ │ └── video-name.pipe.ts │ │ └── services │ │ ├── browser-notification.service.ts │ │ ├── notification.service.ts │ │ ├── playlist-store.service.ts │ │ ├── youtube-api.service.ts │ │ └── youtube-player.service.ts ├── assets │ ├── logo.png │ └── logo_git.png ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css └── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = false 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /tmp 4 | 5 | # dependencies 6 | /node_modules 7 | /bower_components 8 | 9 | # IDEs and editors 10 | /.idea 11 | .project 12 | .classpath 13 | *.launch 14 | .settings/ 15 | 16 | # misc 17 | /.sass-cache 18 | /connect.lock 19 | /coverage/* 20 | /libpeerconnection.log 21 | npm-debug.log 22 | testem.log 23 | /typings 24 | /src/app/lib 25 | 26 | 27 | # e2e 28 | /e2e/*.js 29 | /e2e/*.map 30 | 31 | #System Files 32 | .DS_Store 33 | Thumbs.db 34 | 35 | /.vscode 36 | 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 'stable' 5 | notifications: 6 | email: false 7 | script: 8 | - npm run lint 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SamirH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ngx-youtube-player-logo](https://raw.githubusercontent.com/SamirHodzic/ngx-youtube-player/master/src/assets/logo_git.png) 2 | 3 | # ngx-YouTube-Player 4 | 5 | > YouTube player app built with Angular 7 (latest 7.1.4). 6 | 7 | ## Quickstart 8 | 9 | **Note**: Require Node 4+ together with Npm 3+, also be sure to install 10 | 11 | **1- Install [Angular-CLI](https://github.com/angular/angular-cli) (latest 7.1.4) :** 12 | 13 | ```bash 14 | $ npm install -g @angular/cli@latest 15 | ``` 16 | 17 | **2- Clone the project:** 18 | 19 | ```bash 20 | $ git clone https://github.com/SamirHodzic/ngx-youtube-player 21 | $ cd ngx-youtube-player 22 | ``` 23 | 24 | **3- Install the npm packages described in the package.json :** 25 | 26 | ```bash 27 | $ npm install 28 | ``` 29 | 30 | **4- Transpile typescript into javascript, host the app and monitor the changes :** 31 | 32 | ```bash 33 | $ ng serve 34 | ``` 35 | 36 | Visit http://localhost:4200 and enjoy! 37 | 38 | ## Dependencies 39 | - [Angular](https://angular.io/) with [Typescript](https://www.typescriptlang.org/) 40 | - [Angular CLI](https://cli.angular.io/) 41 | - [Material Design Lite](https://github.com/google/material-design-lite/) 42 | 43 | ## Features 44 | - Play music while searching 45 | - Extended controls 46 | - Shuffle/Repeat options for your playlists 47 | - Browser notifications when new song is going to start 48 | - Different type for video displaying 49 | - Create local playlist without authorization 50 | - Simple Import/Export playlists as JSON 51 | 52 | ## TODO 53 | - ~~'Now playing' when video is minimized~~ 54 | - ~~Update UI to be fully responsive for mobile/tablet~~ 55 | - ~~Browser notification interface when new song is going to start~~ 56 | - Save multiple playlists and switch between them 57 | - Write tests 58 | - ... 59 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-youtube-player": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": ["src/assets", "src/favicon.ico"], 20 | "styles": [ 21 | "src/styles.css", 22 | "node_modules/material-design-lite/dist/material.min.css" 23 | ], 24 | "scripts": ["node_modules/material-design-lite/material.min.js"] 25 | }, 26 | "configurations": { 27 | "production": { 28 | "optimization": true, 29 | "outputHashing": "all", 30 | "sourceMap": false, 31 | "extractCss": true, 32 | "namedChunks": false, 33 | "aot": true, 34 | "extractLicenses": true, 35 | "vendorChunk": false, 36 | "buildOptimizer": true 37 | } 38 | } 39 | }, 40 | "serve": { 41 | "builder": "@angular-devkit/build-angular:dev-server", 42 | "options": { 43 | "browserTarget": "ngx-youtube-player:build" 44 | }, 45 | "configurations": {} 46 | }, 47 | "extract-i18n": { 48 | "builder": "@angular-devkit/build-angular:extract-i18n", 49 | "options": { 50 | "browserTarget": "ngx-youtube-player:build" 51 | } 52 | }, 53 | "lint": { 54 | "builder": "@angular-devkit/build-angular:tslint", 55 | "options": { 56 | "tsConfig": [], 57 | "exclude": [] 58 | } 59 | } 60 | } 61 | }, 62 | "ngx-youtube-player-e2e": { 63 | "root": "e2e", 64 | "sourceRoot": "e2e", 65 | "projectType": "application" 66 | } 67 | }, 68 | "defaultProject": "ngx-youtube-player", 69 | "cli": { 70 | "warnings": { 71 | "typescriptMismatch": false 72 | } 73 | }, 74 | "schematics": { 75 | "@schematics/angular:component": { 76 | "inlineStyle": false, 77 | "inlineTemplate": false, 78 | "prefix": "app", 79 | "styleext": "css" 80 | }, 81 | "@schematics/angular:directive": { 82 | "prefix": "app" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-youtube-player", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "start": "ng serve", 8 | "lint": "tslint \"src/**/*.ts\"", 9 | "gh-deploy": "ng build --prod && npx angular-cli-ghpages" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/SamirHodzic/ngx-youtube-player" 14 | }, 15 | "private": false, 16 | "dependencies": { 17 | "@angular/common": "^7.0.0", 18 | "@angular/compiler": "^7.0.0", 19 | "@angular/core": "^7.0.0", 20 | "@angular/forms": "^7.0.0", 21 | "@angular/http": "^7.0.0", 22 | "@angular/platform-browser": "^7.0.0", 23 | "@angular/platform-browser-dynamic": "^7.0.0", 24 | "@angular/router": "^7.0.0", 25 | "core-js": "^2.6.1", 26 | "material-design-lite": "^1.3.0", 27 | "rxjs": "^6.3.0", 28 | "rxjs-compat": "^6.3.3", 29 | "tslib": "^1.9.0", 30 | "zone.js": "^0.8.26" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~0.11.0", 34 | "@angular/cli": "^7.0.0", 35 | "@angular/compiler-cli": "^7.0.0", 36 | "@types/node": "^10.0.50", 37 | "angular-cli-ghpages": "^0.5.3", 38 | "codelyzer": "^4.5.0", 39 | "ts-node": "^4.0.0", 40 | "tslint": "^5.8.0", 41 | "typescript": "~3.1.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamirHodzic/ngx-youtube-player/030bd859c64b4cccb6ba19e72a6e18d475e2626b/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app', 5 | templateUrl: 'app.component.html', 6 | styleUrls: ['app.component.css'] 7 | }) 8 | 9 | export class AppComponent { } 10 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpModule } from '@angular/http'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | // Components 6 | import { AppComponent } from './app.component'; 7 | import { MainComponent } from './main/main.component'; 8 | import { VideosListComponent } from './main/videos-list/videos-list.component'; 9 | import { VideosPlaylistComponent } from './main/videos-playlist/videos-playlist.component'; 10 | import { VideosSearchComponent } from './main/videos-search/videos-search.component'; 11 | import { VideoPlayerComponent } from './main/video-player/video-player.component'; 12 | // Services 13 | import { YoutubeApiService } from './shared/services/youtube-api.service'; 14 | import { YoutubePlayerService } from './shared/services/youtube-player.service'; 15 | import { PlaylistStoreService } from './shared/services/playlist-store.service'; 16 | import { NotificationService } from './shared/services/notification.service'; 17 | import { BrowserNotificationService } from './shared/services/browser-notification.service'; 18 | // Pipes 19 | import { VideoDurationPipe } from './shared/pipes/video-duration.pipe'; 20 | import { VideoLikesViewsPipe } from './shared/pipes/video-likes-views.pipe'; 21 | import { VideoNamePipe } from './shared/pipes/video-name.pipe'; 22 | import { LazyScrollDirective } from './shared/directives/lazy-scroll/lazy-scroll.directive'; 23 | 24 | @NgModule({ 25 | imports: [ 26 | BrowserModule, 27 | HttpModule, 28 | ReactiveFormsModule 29 | ], 30 | declarations: [ 31 | AppComponent, 32 | MainComponent, 33 | 34 | VideosListComponent, 35 | VideosSearchComponent, 36 | VideoPlayerComponent, 37 | VideosPlaylistComponent, 38 | 39 | VideoDurationPipe, 40 | VideoLikesViewsPipe, 41 | VideoNamePipe, 42 | 43 | LazyScrollDirective 44 | ], 45 | bootstrap: [ 46 | AppComponent 47 | ], 48 | providers: [ 49 | YoutubeApiService, 50 | YoutubePlayerService, 51 | PlaylistStoreService, 52 | NotificationService, 53 | BrowserNotificationService 54 | ] 55 | }) 56 | export class AppModule { 57 | } 58 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.module'; 2 | export * from './app.component'; 3 | -------------------------------------------------------------------------------- /src/app/main/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main.component'; 2 | -------------------------------------------------------------------------------- /src/app/main/main.component.css: -------------------------------------------------------------------------------- 1 | .mdl-navigation { 2 | width: 100%; 3 | } 4 | 5 | .mdl-layout-title { 6 | margin-right: 10px; 7 | } 8 | 9 | .mdl-layout-title>img { 10 | height: 35px; 11 | } 12 | 13 | .mdl-layout__header { 14 | height: 64px; 15 | background-color: rgb(79, 111, 144); 16 | } 17 | 18 | .mdl-layout__content { 19 | width: 95%; 20 | } 21 | 22 | .mdl-layout__header-row { 23 | padding: 0 40px 0 16px; 24 | } 25 | 26 | .mdl-layout__drawer-button { 27 | right: 0; 28 | left: inherit; 29 | } 30 | 31 | .blur-main-playlist-opened { 32 | opacity: 0.4; 33 | transition: all 0.3s ease; 34 | -moz-transition: all 0.3s ease; 35 | -ms-transition: all 0.3s ease; 36 | -webkit-transition: all 0.3s ease; 37 | -o-transition: all 0.3s ease; 38 | } 39 | 40 | .mdl-js-snackbar { 41 | bottom: 0; 42 | z-index: 999; 43 | right: 0; 44 | background-color: rgba(196, 48, 43, 0.85); 45 | } 46 | 47 | @media (max-width: 850px) { 48 | .mdl-layout-title>img { 49 | height: 30px; 50 | } 51 | } 52 | 53 | @media (max-width: 1150px) { 54 | .mdl-layout__content { 55 | width: 100%; 56 | } 57 | .yt-player, 58 | .ytp-cued-thumbnail-overlay { 59 | width: 100px; 60 | } 61 | } -------------------------------------------------------------------------------- /src/app/main/main.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | 7 |
8 | 12 | 15 |
16 |
17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 | 31 |
32 | 33 | 35 | 36 | 37 |
38 |
39 | 40 |
-------------------------------------------------------------------------------- /src/app/main/main.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, AfterViewInit } from '@angular/core'; 2 | import { YoutubeApiService } from '../shared/services/youtube-api.service'; 3 | import { YoutubePlayerService } from '../shared/services/youtube-player.service'; 4 | import { PlaylistStoreService } from '../shared/services/playlist-store.service'; 5 | import { NotificationService } from '../shared/services/notification.service'; 6 | 7 | @Component({ 8 | selector: 'main-block', 9 | templateUrl: 'main.component.html', 10 | styleUrls: ['main.component.css'] 11 | }) 12 | 13 | export class MainComponent implements AfterViewInit { 14 | public videoList = []; 15 | public videoPlaylist = []; 16 | public loadingInProgress = false; 17 | public playlistToggle = false; 18 | public playlistNames = false; 19 | public repeat = false; 20 | public shuffle = false; 21 | public playlistElement: any; 22 | private pageLoadingFinished = false; 23 | 24 | constructor( 25 | private youtubeService: YoutubeApiService, 26 | private youtubePlayer: YoutubePlayerService, 27 | private playlistService: PlaylistStoreService, 28 | private notificationService: NotificationService 29 | ) { 30 | this.videoPlaylist = this.playlistService.retrieveStorage().playlists; 31 | } 32 | 33 | ngAfterViewInit() { 34 | this.playlistElement = document.getElementById('playlist'); 35 | } 36 | 37 | playFirstInPlaylist(): void { 38 | if (this.videoPlaylist[0]) { 39 | this.playlistElement.scrollTop = 0; 40 | this.youtubePlayer.playVideo(this.videoPlaylist[0].id, this.videoPlaylist[0].snippet.title); 41 | } 42 | } 43 | 44 | handleSearchVideo(videos: Array): void { 45 | this.videoList = videos; 46 | } 47 | 48 | checkAddToPlaylist(video: any): void { 49 | if (!this.videoPlaylist.some((e) => e.id === video.id)) { 50 | this.videoPlaylist.push(video); 51 | this.playlistService.addToPlaylist(video); 52 | 53 | let inPlaylist = this.videoPlaylist.length - 1; 54 | 55 | setTimeout(() => { 56 | let topPos = document.getElementById(this.videoPlaylist[inPlaylist].id).offsetTop; 57 | this.playlistElement.scrollTop = topPos - 100; 58 | }); 59 | } 60 | } 61 | 62 | repeatActive(val: boolean): void { 63 | this.repeat = val; 64 | this.shuffle = false; 65 | } 66 | 67 | shuffleActive(val: boolean): void { 68 | this.shuffle = val; 69 | this.repeat = false; 70 | } 71 | 72 | togglePlaylist(): void { 73 | this.playlistToggle = !this.playlistToggle; 74 | setTimeout(() => { 75 | this.playlistNames = !this.playlistNames; 76 | }, 200); 77 | } 78 | 79 | searchMore(): void { 80 | if (this.loadingInProgress || this.pageLoadingFinished || this.videoList.length < 1) { 81 | return; 82 | } 83 | 84 | this.loadingInProgress = true; 85 | this.youtubeService.searchNext() 86 | .then(data => { 87 | this.loadingInProgress = false; 88 | if (data.length < 1 || data.status === 400) { 89 | setTimeout(() => { 90 | this.pageLoadingFinished = true; 91 | setTimeout(() => { 92 | this.pageLoadingFinished = false; 93 | }, 10000); 94 | }) 95 | return; 96 | } 97 | data.forEach((val) => { 98 | this.videoList.push(val); 99 | }); 100 | }).catch(error => { 101 | this.loadingInProgress = false; 102 | }) 103 | } 104 | 105 | nextVideo(): void { 106 | this.playPrevNext(true); 107 | } 108 | 109 | prevVideo(): void { 110 | this.playPrevNext(false); 111 | } 112 | 113 | playPrevNext(value): void { 114 | let current = this.youtubePlayer.getCurrentVideo(); 115 | let inPlaylist; 116 | 117 | this.videoPlaylist.forEach((video, index) => { 118 | if (video.id === current) { 119 | inPlaylist = index; 120 | } 121 | }); 122 | 123 | // if-else hell 124 | if (inPlaylist !== undefined) { 125 | let topPos = document.getElementById(this.videoPlaylist[inPlaylist].id).offsetTop; 126 | if (this.shuffle) { 127 | let shuffled = this.videoPlaylist[this.youtubePlayer.getShuffled(inPlaylist, this.videoPlaylist.length)]; 128 | this.youtubePlayer.playVideo(shuffled.id, shuffled.snippet.title); 129 | this.playlistElement.scrollTop = document.getElementById(shuffled.id).offsetTop - 100; 130 | } else { 131 | if (value) { 132 | if (this.videoPlaylist.length - 1 === inPlaylist) { 133 | this.youtubePlayer.playVideo(this.videoPlaylist[0].id, this.videoPlaylist[0].snippet.title); 134 | this.playlistElement.scrollTop = 0; 135 | } else { 136 | this.youtubePlayer.playVideo(this.videoPlaylist[inPlaylist + 1].id, this.videoPlaylist[inPlaylist + 1].snippet.title) 137 | this.playlistElement.scrollTop = topPos - 100; 138 | } 139 | } else { 140 | if (inPlaylist === 0) { 141 | this.youtubePlayer.playVideo(this.videoPlaylist[this.videoPlaylist.length - 1].id, 142 | this.videoPlaylist[this.videoPlaylist.length - 1].snippet.title); 143 | this.playlistElement.scrollTop = this.playlistElement.offsetHeight; 144 | } else { 145 | this.youtubePlayer.playVideo(this.videoPlaylist[inPlaylist - 1].id, this.videoPlaylist[inPlaylist - 1].snippet.title) 146 | this.playlistElement.scrollTop = topPos - 230; 147 | } 148 | } 149 | } 150 | } else { 151 | this.playFirstInPlaylist(); 152 | } 153 | } 154 | 155 | closePlaylist(): void { 156 | this.playlistToggle = false; 157 | this.playlistNames = false; 158 | } 159 | 160 | clearPlaylist(): void { 161 | this.videoPlaylist = []; 162 | this.playlistService.clearPlaylist(); 163 | this.notificationService.showNotification('Playlist cleared.'); 164 | } 165 | 166 | exportPlaylist(): void { 167 | if (this.videoPlaylist.length < 1) { 168 | this.notificationService.showNotification('Nothing to export.'); 169 | return; 170 | } 171 | 172 | let data = JSON.stringify(this.videoPlaylist); 173 | let a = document.createElement('a'); 174 | let file = new Blob([data], { type: 'text/json' }); 175 | 176 | a.href = URL.createObjectURL(file); 177 | a.download = 'playlist.json'; 178 | a.click(); 179 | this.notificationService.showNotification('Playlist exported.'); 180 | } 181 | 182 | importPlaylist(playlist: any): void { 183 | this.videoPlaylist = playlist; 184 | this.playlistService.importPlaylist(this.videoPlaylist); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/app/main/video-player/video-player.component.css: -------------------------------------------------------------------------------- 1 | .main-player-block { 2 | position: absolute; 3 | width: 100%; 4 | height: 50px; 5 | bottom: 0; 6 | background-color: rgba(79, 111, 144, 1); 7 | z-index: 992; 8 | } 9 | 10 | .player-containter { 11 | transition: all 300ms ease-in-out; 12 | width: 440px; 13 | height: 250px; 14 | position: fixed; 15 | z-index: 992; 16 | bottom: 16px; 17 | left: 16px; 18 | background-color: #000; 19 | border: 3px solid rgba(79, 111, 144, 0.75); 20 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); 21 | } 22 | 23 | .player-containter.minimized { 24 | transform: translate3d(-20%, 20%, 0) scale(0.6); 25 | } 26 | 27 | .player-containter.super-minimized { 28 | transform: translate3d(-42.5%, 46.5%, 0) scale(0.15); 29 | } 30 | 31 | .player-view-controls { 32 | position: absolute; 33 | color: white; 34 | background-color: rgb(196, 48, 43); 35 | top: -27px; 36 | left: -3px; 37 | font-size: 30px; 38 | border-top-left-radius: 3px; 39 | border-top-right-radius: 3px; 40 | } 41 | 42 | .player-view-controls i { 43 | cursor: pointer; 44 | } 45 | 46 | .player-view-controls .minimize { 47 | margin-left: -7px; 48 | } 49 | 50 | .player-controls-block { 51 | margin-top: 5px; 52 | text-align: center; 53 | color: #fff; 54 | -webkit-touch-callout: none; 55 | -webkit-user-select: none; 56 | -moz-user-select: none; 57 | -ms-user-select: none; 58 | user-select: none; 59 | } 60 | 61 | .mdl-button--icon.play { 62 | width: 42px; 63 | height: 42px; 64 | } 65 | 66 | .mdl-button--icon.play i { 67 | transform: translate(-20px, -12px); 68 | font-size: 40px; 69 | } 70 | 71 | .mdl-button--icon.prev { 72 | width: 30px; 73 | height: 30px; 74 | } 75 | 76 | .mdl-button--icon.prev i { 77 | transform: translate(-15px, -12px); 78 | font-size: 28px; 79 | } 80 | 81 | .mdl-button--icon.next { 82 | width: 30px; 83 | height: 30px; 84 | } 85 | 86 | .mdl-button--icon.next i { 87 | transform: translate(-14px, -12px); 88 | font-size: 28px; 89 | } 90 | 91 | .repeat-shuffle-block { 92 | margin-left: 40px; 93 | position: absolute; 94 | top: 10px; 95 | } 96 | 97 | .repeat-shuffle-block button.active { 98 | background-color: rgba(196, 48, 43, 0.85); 99 | } 100 | 101 | .mute-block { 102 | margin-left: -80px; 103 | position: absolute; 104 | top: 10px; 105 | } 106 | 107 | .mute-block button.active { 108 | background-color: rgba(196, 48, 43, 0.85); 109 | } 110 | 111 | .playlist-drop-button { 112 | position: absolute; 113 | color: #fff; 114 | right: 12px; 115 | top: 10px; 116 | } 117 | 118 | .main-yt-player-block { 119 | position: relative; 120 | text-align: center; 121 | -webkit-touch-callout: none; 122 | -webkit-user-select: none; 123 | -moz-user-select: none; 124 | -ms-user-select: none; 125 | user-select: none; 126 | } 127 | 128 | .main-yt-player-block .material-icons { 129 | position: absolute; 130 | color: white; 131 | font-size: 255px; 132 | left: 0; 133 | right: 0; 134 | margin: 0 auto; 135 | cursor: pointer; 136 | display: none; 137 | text-shadow: 2px 2px 2px #000; 138 | } 139 | 140 | .main-yt-player-block:hover .material-icons { 141 | display: block; 142 | } 143 | 144 | .player-fullscreen { 145 | bottom: 50px; 146 | left: 0; 147 | width: 95%; 148 | height: calc(100% - 114px); 149 | border: none; 150 | box-shadow: none; 151 | } 152 | 153 | .current-playing-text { 154 | max-width: 320px; 155 | white-space: nowrap; 156 | text-overflow: ellipsis; 157 | overflow: hidden; 158 | position: absolute; 159 | left: 10.5em; 160 | top: 1.4em; 161 | font-size: 12px; 162 | text-align: left; 163 | text-align: left; 164 | -webkit-touch-callout: none; 165 | -webkit-user-select: none; 166 | -khtml-user-select: none; 167 | -moz-user-select: none; 168 | -ms-user-select: none; 169 | user-select: none; 170 | pointer-events: none; 171 | } 172 | 173 | .now-playing-label { 174 | opacity: 0.75; 175 | white-space: nowrap; 176 | overflow: hidden; 177 | } 178 | 179 | @media (max-width: 850px) { 180 | .player-containter { 181 | bottom: 60px; 182 | } 183 | .current-playing-text { 184 | display: none !important; 185 | } 186 | .player-controls-block { 187 | left: 0; 188 | } 189 | .repeat-shuffle-block { 190 | margin-left: 10px; 191 | } 192 | } 193 | 194 | @media (max-width: 1150px) { 195 | .player-fullscreen { 196 | bottom: 50px; 197 | width: 100%; 198 | } 199 | .current-playing-text { 200 | max-width: 160px; 201 | } 202 | } -------------------------------------------------------------------------------- /src/app/main/video-player/video-player.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Now playing: 5 | {{ currentVideoText | videoName: [56, 51] }} 6 |
7 | 8 | 12 |
13 | Fullscreen 14 |
15 |
16 | 19 |
20 | Previous 21 |
22 | 25 | 28 | 31 |
32 | Next 33 |
34 | 35 | 39 |
40 | Repeat one 41 |
42 | 46 |
47 | Shuffle 48 |
49 |
50 |
51 |
52 |
53 | arrow_drop_down 54 | arrow_drop_up 55 | remove 56 |
57 |
58 | zoom_out_map 59 |
60 |
61 |
62 | 63 | 66 | 67 | 73 | 74 | 75 |
-------------------------------------------------------------------------------- /src/app/main/video-player/video-player.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, AfterContentInit, Output, EventEmitter } from '@angular/core'; 2 | import { YoutubePlayerService } from '../../shared/services/youtube-player.service'; 3 | import { NotificationService } from '../../shared/services/notification.service'; 4 | import { BrowserNotificationService } from '../../shared/services/browser-notification.service'; 5 | 6 | @Component({ 7 | selector: 'video-player', 8 | templateUrl: 'video-player.component.html', 9 | styleUrls: ['video-player.component.css'] 10 | }) 11 | 12 | export class VideoPlayerComponent implements AfterContentInit { 13 | public currentVideoText = 'None'; 14 | public playingEvent = 'pause'; 15 | public minPlayer = true; 16 | public superMinPlayer = false; 17 | public shuffle = false; 18 | public repeat = false; 19 | public fullscreenActive = false; 20 | public notifications = false; 21 | 22 | @Output() repeatActive = new EventEmitter(); 23 | @Output() shuffleActive = new EventEmitter(); 24 | @Output() nextVideoEvent = new EventEmitter(); 25 | @Output() prevVideoEvent = new EventEmitter(); 26 | @Output() playFirstInPlaylist = new EventEmitter(); 27 | @Output() clearPlaylist = new EventEmitter(); 28 | @Output() exportPlaylist = new EventEmitter(); 29 | @Output() importPlaylist = new EventEmitter(); 30 | @Output() closePlaylist = new EventEmitter(); 31 | 32 | constructor( 33 | private youtubePlayer: YoutubePlayerService, 34 | private notificationService: NotificationService, 35 | private browserNotification: BrowserNotificationService 36 | ) { 37 | this.youtubePlayer.playPauseEvent.subscribe(event => this.playingEvent = event); 38 | this.youtubePlayer.currentVideoText.subscribe(event => this.currentVideoText = event || 'None'); 39 | } 40 | 41 | ngAfterContentInit() { 42 | let doc = window.document; 43 | let playerApi = doc.createElement('script'); 44 | playerApi.type = 'text/javascript'; 45 | playerApi.src = 'https://www.youtube.com/iframe_api'; 46 | doc.body.appendChild(playerApi); 47 | 48 | this.youtubePlayer.createPlayer(); 49 | } 50 | 51 | toggleFullscreen(): void { 52 | this.minPlayer = false; 53 | this.superMinPlayer = false; 54 | this.fullscreenActive = !this.fullscreenActive; 55 | 56 | let width = this.fullscreenActive ? window.innerWidth - 70 : 440; 57 | let height = this.fullscreenActive ? window.innerHeight - 120 : 250; 58 | this.youtubePlayer.resizePlayer(width, height); 59 | } 60 | 61 | playPause(event: string): void { 62 | this.playingEvent = event; 63 | 64 | if (!this.youtubePlayer.getCurrentVideo()) { 65 | this.playFirstInPlaylist.emit(); 66 | return; 67 | } 68 | 69 | event === 'pause' ? this.youtubePlayer.pausePlayingVideo() : this.youtubePlayer.playPausedVideo(); 70 | } 71 | 72 | nextVideo(): void { 73 | this.nextVideoEvent.emit(); 74 | } 75 | 76 | prevVideo(): void { 77 | this.prevVideoEvent.emit(); 78 | } 79 | 80 | togglePlayer(): void { 81 | this.minPlayer = !this.minPlayer; 82 | this.superMinPlayer = false; 83 | } 84 | 85 | minimizePlayer(): void { 86 | this.superMinPlayer = !this.superMinPlayer; 87 | } 88 | 89 | toggleRepeat(): void { 90 | this.repeat = !this.repeat; 91 | this.shuffle = false; 92 | this.repeatActive.emit(this.repeat); 93 | } 94 | 95 | toggleShuffle(): void { 96 | this.shuffle = !this.shuffle; 97 | this.repeat = false; 98 | this.shuffleActive.emit(this.shuffle); 99 | } 100 | 101 | openClosedPlaylist(): void { 102 | this.closePlaylist.emit(); 103 | } 104 | 105 | clearPlaylistAction(): void { 106 | this.clearPlaylist.emit(); 107 | } 108 | 109 | exportPlaylistAction(): void { 110 | this.exportPlaylist.emit(); 111 | } 112 | 113 | importPlaylistAction(): void { 114 | let import_button = document.getElementById('import_button'); 115 | import_button.click(); 116 | } 117 | 118 | handleInputChange(e: any): void { 119 | let file = e.dataTransfer ? e.dataTransfer.files[0] : e.target.files[0]; 120 | 121 | if (file.name.split('.').pop() !== 'json') { 122 | this.notificationService.showNotification('File not supported.'); 123 | return; 124 | } 125 | 126 | let reader = new FileReader(); 127 | let me = this; 128 | 129 | reader.readAsText(file); 130 | reader.onload = function (ev) { 131 | let list; 132 | try { 133 | list = JSON.parse(ev.target['result']); 134 | } catch (exc) { 135 | list = null; 136 | } 137 | if (!list || list.length < 1) { 138 | me.notificationService.showNotification('Playlist not valid.'); 139 | return; 140 | } 141 | 142 | me.importPlaylist.emit(list); 143 | me.notificationService.showNotification('Playlist imported.'); 144 | document.getElementById('import_button')['value'] = ''; 145 | } 146 | } 147 | 148 | toggleNotifications(): void { 149 | this.notifications ? 150 | ( 151 | this.notifications = false, 152 | this.browserNotification.disable() 153 | ) : 154 | this.browserNotification.checkNotification().then(async res => { 155 | this.notifications = res === 'granted' ? true : ( 156 | this.notificationService.showNotification('Browser notifications blocked.'), 157 | false 158 | ); 159 | }); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/app/main/videos-list/videos-list.component.css: -------------------------------------------------------------------------------- 1 | .loader-progress { 2 | position: relative; 3 | width: 100%; 4 | margin-bottom: 60px; 5 | top: 0; 6 | left: 0; 7 | } 8 | 9 | .loader-progress>.loading { 10 | margin: 0 auto; 11 | } 12 | 13 | .demo-card-square.mdl-card { 14 | height: 210px; 15 | width: 100%; 16 | } 17 | 18 | .custom-cell.mdl-cell--2-col { 19 | width: calc(20% - 16px); 20 | } 21 | 22 | .custom-cell { 23 | -webkit-touch-callout: none; 24 | -webkit-user-select: none; 25 | -moz-user-select: none; 26 | -ms-user-select: none; 27 | user-select: none; 28 | } 29 | 30 | .demo-card-square>.mdl-card__title { 31 | color: #fff; 32 | } 33 | 34 | .mdl-card__title { 35 | position: relative; 36 | transition: all 0.3s ease; 37 | -moz-transition: all 0.3s ease; 38 | -ms-transition: all 0.3s ease; 39 | -webkit-transition: all 0.3s ease; 40 | -o-transition: all 0.3s ease; 41 | } 42 | 43 | .mdl-card--expand:hover { 44 | box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.3); 45 | background-size: 130% !important; 46 | cursor: pointer; 47 | } 48 | 49 | .mdl-card__supporting-text { 50 | text-overflow: ellipsis; 51 | white-space: nowrap; 52 | padding: 6px 2px 2px 2px; 53 | color: rgb(196, 48, 43); 54 | text-align: right; 55 | width: 97%; 56 | } 57 | 58 | .mdl-card__supporting-text .material-icons { 59 | cursor: pointer; 60 | font-size: 20px; 61 | } 62 | 63 | .video-name-block { 64 | font-size: 11px; 65 | font-weight: normal; 66 | position: absolute; 67 | left: 0; 68 | top: 0; 69 | height: 40px; 70 | width: 100%; 71 | background-color: rgba(0, 0, 0, 0.45); 72 | } 73 | 74 | .video-info-block { 75 | font-size: 11px; 76 | font-weight: normal; 77 | position: absolute; 78 | left: 0; 79 | bottom: 0; 80 | height: 30px; 81 | width: 100%; 82 | background-color: rgba(0, 0, 0, 0.45); 83 | } 84 | 85 | .video-informations { 86 | padding: 8px; 87 | text-align: center; 88 | } 89 | 90 | .video-informations i { 91 | font-size: 11px; 92 | } 93 | 94 | .video-play-button { 95 | left: 0; 96 | top: 0; 97 | right: 0; 98 | bottom: 0; 99 | margin: auto; 100 | } 101 | 102 | .video-play-button i { 103 | font-size: 50px; 104 | visibility: hidden; 105 | } 106 | 107 | .mdl-card--expand:hover .video-play-button i { 108 | visibility: visible; 109 | } 110 | 111 | .last-item { 112 | margin-bottom: 45px; 113 | } 114 | 115 | @media (max-width: 479px) { 116 | .custom-cell.mdl-cell--2-col { 117 | width: calc(100% - 16px); 118 | } 119 | } 120 | 121 | @media (max-width: 839px) and (min-width: 480px) { 122 | .custom-cell.mdl-cell--2-col { 123 | width: calc(50% - 16px); 124 | } 125 | } -------------------------------------------------------------------------------- /src/app/main/videos-list/videos-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 | thumb_up 8 | {{ video.statistics?.likeCount | videoLikesViews }} 9 | 10 | 11 | remove_red_eye 12 | {{ video.statistics?.viewCount | videoLikesViews }} 13 | 14 | 15 | access_time 16 | {{ video.contentDetails?.duration | videoDuration }} 17 | 18 |
19 |
20 |
21 |
22 | {{ video.snippet?.title | videoName: [65, 62] }} 23 |
24 |
25 |
26 | play_circle_filled 27 |
28 |
29 |
30 | playlist_add 31 |
32 |
33 |
34 | 35 |
36 |
37 |
-------------------------------------------------------------------------------- /src/app/main/videos-list/videos-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | import { YoutubePlayerService } from '../../shared/services/youtube-player.service'; 3 | import { PlaylistStoreService } from '../../shared/services/playlist-store.service'; 4 | 5 | @Component({ 6 | selector: 'videos-list', 7 | templateUrl: 'videos-list.component.html', 8 | styleUrls: ['videos-list.component.css'] 9 | }) 10 | 11 | export class VideosListComponent { 12 | @Input() videoList; 13 | @Input() loadingInProgress; 14 | @Output() videoPlaylist = new EventEmitter(); 15 | 16 | constructor( 17 | private youtubePlayer: YoutubePlayerService, 18 | private playlistService: PlaylistStoreService 19 | ) { } 20 | 21 | play(video: any): void { 22 | this.youtubePlayer.playVideo(video.id, video.snippet.title); 23 | this.addToPlaylist(video); 24 | } 25 | 26 | addToPlaylist(video: any): void { 27 | this.videoPlaylist.emit(video); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/main/videos-playlist/videos-playlist.component.css: -------------------------------------------------------------------------------- 1 | .playlist { 2 | height: calc(100% - 113px); 3 | width: 5%; 4 | max-width: 70px; 5 | position: fixed; 6 | top: 64px; 7 | right: 0; 8 | background-color: rgba(49, 68, 86, 1); 9 | overflow-x: hidden; 10 | transition: 0.1s; 11 | box-shadow: -2px 2px 2px 0 rgba(0, 0, 0, .14), -2px 3px 1px -2px rgba(0, 0, 0, .2), -2px 1px 5px 0 rgba(0, 0, 0, .12); 12 | -webkit-touch-callout: none; 13 | -webkit-user-select: none; 14 | -moz-user-select: none; 15 | -ms-user-select: none; 16 | user-select: none; 17 | } 18 | 19 | .playlist.opened { 20 | width: 25%; 21 | max-width: 25%; 22 | z-index: 993; 23 | height: calc(100% - 114px); 24 | box-shadow: none; 25 | } 26 | 27 | .playlist-thumbnail { 28 | height: 55px; 29 | width: 55px; 30 | background: #000; 31 | margin: 5px; 32 | display: inline-block; 33 | position: relative; 34 | } 35 | 36 | .playist-item.playing { 37 | background: rgba(196, 48, 43, 0.95); 38 | } 39 | 40 | .playist-item-empty { 41 | color: #fff; 42 | } 43 | 44 | .playist-item-empty .playlist-thumbnail { 45 | text-align: center; 46 | } 47 | 48 | .playist-item-empty i { 49 | margin-top: 10px; 50 | font-size: 35px; 51 | } 52 | 53 | .playist-item:hover { 54 | background: rgba(196, 48, 43, 0.5); 55 | cursor: pointer; 56 | } 57 | 58 | .playist-item:hover .delete-from-playlist { 59 | display: block; 60 | } 61 | 62 | .no-in-playlist { 63 | color: #fff; 64 | text-shadow: 2px 2px 2px #000; 65 | } 66 | 67 | .video-duration { 68 | color: #fff; 69 | text-shadow: 2px 2px 2px #000; 70 | position: absolute; 71 | bottom: -4px; 72 | left: 1px; 73 | font-size: 9px; 74 | } 75 | 76 | .opened-item-info { 77 | display: inline-block; 78 | color: rgba(255, 255, 255, 0.8); 79 | position: absolute; 80 | left: 70px; 81 | margin-top: 8px; 82 | } 83 | 84 | .opened-item-info.closed { 85 | display: none; 86 | } 87 | 88 | .delete-from-playlist { 89 | position: absolute; 90 | display: none; 91 | bottom: 0; 92 | right: 0; 93 | color: #f44542; 94 | background-color: rgba(0, 0, 0, 0.65); 95 | font-size: 16px; 96 | } 97 | 98 | @media (max-width: 1150px) { 99 | .playlist { 100 | width: 0; 101 | } 102 | .playlist.opened { 103 | width: 40%; 104 | max-width: 40%; 105 | } 106 | } 107 | 108 | @media (max-width: 620px) { 109 | .playlist.opened { 110 | width: 70%; 111 | max-width: 70%; 112 | } 113 | } -------------------------------------------------------------------------------- /src/app/main/videos-playlist/videos-playlist.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | {{ i + 1 }} 6 | {{ video.contentDetails.duration | videoDuration }} 7 | cancel 8 |
9 | 10 |
11 | {{ video.snippet.title | videoName: [65, 62] }} 12 |
13 |
14 | 15 |
16 |
17 | block 18 |
19 |
20 | Playlist is empty 21 |
22 |
23 | 24 |
-------------------------------------------------------------------------------- /src/app/main/videos-playlist/videos-playlist.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { YoutubePlayerService } from '../../shared/services/youtube-player.service'; 3 | import { PlaylistStoreService } from '../../shared/services/playlist-store.service'; 4 | 5 | @Component({ 6 | selector: 'videos-playlist', 7 | templateUrl: 'videos-playlist.component.html', 8 | styleUrls: ['videos-playlist.component.css'] 9 | }) 10 | 11 | export class VideosPlaylistComponent { 12 | @Input() playlistToggle; 13 | @Input() videoPlaylist; 14 | @Input() playlistNames; 15 | @Input() repeat; 16 | @Input() shuffle; 17 | 18 | constructor( 19 | private youtubePlayer: YoutubePlayerService, 20 | private playlistService: PlaylistStoreService 21 | ) { 22 | this.youtubePlayer.videoChangeEvent.subscribe(event => event ? this.playNextVideo() : false); 23 | } 24 | 25 | play(id: string): void { 26 | let videoText = 'None'; 27 | 28 | this.videoPlaylist.forEach((video, index) => { 29 | if (video.id === id) { 30 | videoText = video.snippet.title; 31 | } 32 | }); 33 | 34 | this.youtubePlayer.playVideo(id, videoText); 35 | } 36 | 37 | currentPlaying(id: string): boolean { 38 | return this.youtubePlayer.getCurrentVideo() === id; 39 | } 40 | 41 | removeFromPlaylist(video: Object): void { 42 | this.videoPlaylist.splice(this.videoPlaylist.indexOf(video), 1); 43 | this.playlistService.removeFromPlaylist(video); 44 | } 45 | 46 | playNextVideo(): void { 47 | let current = this.youtubePlayer.getCurrentVideo(); 48 | let inPlaylist; 49 | 50 | if (this.repeat) { 51 | this.play(current); 52 | return; 53 | } 54 | 55 | this.videoPlaylist.forEach((video, index) => { 56 | if (video.id === current) { 57 | inPlaylist = index; 58 | } 59 | }); 60 | 61 | if (inPlaylist !== undefined) { 62 | let topPos = document.getElementById(this.videoPlaylist[inPlaylist].id).offsetTop; 63 | let playlistEl = document.getElementById('playlist'); 64 | if (this.shuffle) { 65 | let shuffled = this.videoPlaylist[this.youtubePlayer.getShuffled(inPlaylist, this.videoPlaylist.length)]; 66 | this.youtubePlayer.playVideo(shuffled.id, shuffled.snippet.title); 67 | playlistEl.scrollTop = document.getElementById(shuffled).offsetTop - 100; 68 | } else { 69 | if (this.videoPlaylist.length - 1 === inPlaylist) { 70 | this.youtubePlayer.playVideo(this.videoPlaylist[0].id, this.videoPlaylist[0].snippet.title); 71 | playlistEl.scrollTop = 0; 72 | } else { 73 | this.youtubePlayer.playVideo(this.videoPlaylist[inPlaylist + 1].id, this.videoPlaylist[inPlaylist + 1].snippet.title) 74 | playlistEl.scrollTop = topPos - 100; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/main/videos-search/videos-search.component.css: -------------------------------------------------------------------------------- 1 | .mdl-textfield--floating-label.is-focused>.mdl-textfield__label { 2 | color: #fff; 3 | } 4 | 5 | .mdl-textfield__input { 6 | border-bottom: 1px solid rgba(255, 255, 255, .12); 7 | } 8 | 9 | .mdl-textfield__label { 10 | color: rgba(255, 255, 255, .26); 11 | } 12 | 13 | .mdl-textfield__label:after { 14 | background-color: #fff; 15 | } 16 | 17 | @media (max-width: 750px) { 18 | .mdl-textfield { 19 | width: 140px; 20 | } 21 | } -------------------------------------------------------------------------------- /src/app/main/videos-search/videos-search.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | 9 |
-------------------------------------------------------------------------------- /src/app/main/videos-search/videos-search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | import { FormBuilder, Validators } from '@angular/forms'; 3 | import { YoutubeApiService } from '../../shared/services/youtube-api.service'; 4 | import { YoutubePlayerService } from '../../shared/services/youtube-player.service'; 5 | import { NotificationService } from '../../shared/services/notification.service'; 6 | 7 | @Component({ 8 | selector: 'videos-search', 9 | templateUrl: 'videos-search.component.html', 10 | styleUrls: ['videos-search.component.css'] 11 | }) 12 | 13 | export class VideosSearchComponent { 14 | @Output() videosUpdated = new EventEmitter(); 15 | @Input() loadingInProgress; 16 | 17 | private last_search: string; 18 | 19 | public searchForm = this.fb.group({ 20 | query: ['', Validators.required] 21 | }); 22 | 23 | constructor( 24 | public fb: FormBuilder, 25 | private youtubeService: YoutubeApiService, 26 | private youtubePlayer: YoutubePlayerService, 27 | private notificationService: NotificationService 28 | ) { 29 | this.youtubeService.searchVideos('') 30 | .then(data => { 31 | this.videosUpdated.emit(data); 32 | }) 33 | } 34 | 35 | doSearch(event): void { 36 | if (this.loadingInProgress || 37 | (this.searchForm.value.query.trim().length === 0) || 38 | (this.last_search && this.last_search === this.searchForm.value.query)) { 39 | return; 40 | } 41 | 42 | this.videosUpdated.emit([]); 43 | this.last_search = this.searchForm.value.query; 44 | 45 | this.youtubeService.searchVideos(this.last_search) 46 | .then(data => { 47 | if (data.length < 1) { 48 | this.notificationService.showNotification('No matches found.'); 49 | } 50 | this.videosUpdated.emit(data); 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export const YOUTUBE_API_KEY = 'AIzaSyAsMiGn7Z09Yh1zYyJlmPf0ak8XwZ7lFJY'; 2 | -------------------------------------------------------------------------------- /src/app/shared/directives/lazy-scroll/lazy-scroll.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[LazyScroll]', 5 | host: { 6 | '(scroll)': 'onScroll($event)' 7 | } 8 | }) 9 | export class LazyScrollDirective { 10 | public _element: any; 11 | public _count: number; 12 | 13 | @Input('ScrollDistance') scrollTrigger: number; 14 | @Output() OnScrollMethod = new EventEmitter(); 15 | 16 | constructor( 17 | public element: ElementRef 18 | ) { 19 | this._element = this.element.nativeElement; 20 | if (!this.scrollTrigger) { 21 | this.scrollTrigger = 1; 22 | } 23 | } 24 | 25 | onScroll() { 26 | this._count++; 27 | if (this._element.scrollTop + this._element.clientHeight >= this._element.scrollHeight) { 28 | this.OnScrollMethod.emit(null); 29 | } else { 30 | if (this._count % this.scrollTrigger === 0) { 31 | this.OnScrollMethod.emit(null); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/shared/pipes/video-duration.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'videoDuration' 5 | }) 6 | 7 | export class VideoDurationPipe implements PipeTransform { 8 | transform(value: any, args?: any[]): any { 9 | const time = value; 10 | if (!time) { 11 | return '...'; 12 | } 13 | return ['PT', 'H', 'M', 'S'].reduce((prev, cur, i, arr) => { 14 | const now = prev.rest.split(cur); 15 | if (cur !== 'PT' && cur !== 'H' && !prev.rest.match(cur)) { 16 | prev.new.push('00'); 17 | } 18 | if (now.length === 1) { 19 | return prev; 20 | } 21 | prev.new.push(now[0]); 22 | return { 23 | rest: now[1].replace(cur, ''), 24 | new: prev.new 25 | }; 26 | }, { rest: time, new: [] }) 27 | .new.filter(_time => _time !== '') 28 | .map(_time => _time.length === 1 ? `0${_time}` : _time) 29 | .join(':'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/shared/pipes/video-likes-views.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'videoLikesViews' 5 | }) 6 | 7 | export class VideoLikesViewsPipe implements PipeTransform { 8 | transform(value: any, args?: any[]): any { 9 | return parseInt(value, 10).toLocaleString('en'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/pipes/video-name.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'videoName' 5 | }) 6 | 7 | export class VideoNamePipe implements PipeTransform { 8 | transform(value: any, args: any[]): any { 9 | const dots = '...'; 10 | 11 | if (value.length > args[0]) { 12 | value = value.substring(0, args[1]) + dots; 13 | } 14 | 15 | return value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/shared/services/browser-notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | let _window: any = window; 4 | 5 | @Injectable() 6 | export class BrowserNotificationService { 7 | private notifSupported; 8 | private enabled = false; 9 | 10 | constructor() { 11 | this.notifSupported = (window).Notification && (Notification).permission !== 'denied' ? true : false; 12 | } 13 | 14 | async checkNotification(): Promise { 15 | if (!this.enabled) { 16 | return Notification.requestPermission((result) => { 17 | return result === 'granted' ? ( 18 | this.enabled = true 19 | ) : false; 20 | }); 21 | } 22 | } 23 | 24 | public disable(): void { 25 | this.enabled = false; 26 | } 27 | 28 | public show(name: string): void { 29 | if (!this.notifSupported || !this.enabled) { 30 | return; 31 | } 32 | 33 | Notification.requestPermission((status) => { 34 | let n = new Notification('Now playing', { 35 | body: name, 36 | icon: 'assets/logo_git.png' 37 | }); 38 | }); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/app/shared/services/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class NotificationService { 5 | private timeoutDuration = 3500; 6 | 7 | constructor() { } 8 | 9 | public showNotification(message: string): void { 10 | let notification = document.querySelector('.mdl-js-snackbar'); 11 | let data = { 12 | message: message, 13 | timeout: this.timeoutDuration 14 | }; 15 | 16 | notification['MaterialSnackbar'].showSnackbar(data); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/services/playlist-store.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class PlaylistStoreService { 5 | private ngxYTPlayer = 'ngx_yt_player'; 6 | private playlists_template: Object = { 7 | 'playlists': [] 8 | }; 9 | 10 | constructor() { } 11 | 12 | private init(): void { 13 | localStorage.setItem(this.ngxYTPlayer, JSON.stringify(this.playlists_template)); 14 | } 15 | 16 | public retrieveStorage() { 17 | let storedPlaylist = this.parse(); 18 | 19 | if (!storedPlaylist) { 20 | this.init(); 21 | storedPlaylist = this.parse(); 22 | } 23 | 24 | return storedPlaylist; 25 | } 26 | 27 | public addToPlaylist(video: Object): void { 28 | let store = this.parse(); 29 | store.playlists.push(video); 30 | localStorage.setItem(this.ngxYTPlayer, JSON.stringify(store)); 31 | } 32 | 33 | public removeFromPlaylist(video: any): void { 34 | let store = this.parse(); 35 | store.playlists = store.playlists.filter(item => item.id !== video.id); 36 | localStorage.setItem(this.ngxYTPlayer, JSON.stringify(store)); 37 | } 38 | 39 | private parse() { 40 | return JSON.parse(localStorage.getItem(this.ngxYTPlayer)); 41 | } 42 | 43 | public clearPlaylist() { 44 | this.init(); 45 | } 46 | 47 | public importPlaylist(videos: any): void { 48 | let store = this.parse(); 49 | store.playlists = videos; 50 | localStorage.setItem(this.ngxYTPlayer, JSON.stringify(store)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/shared/services/youtube-api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response } from '@angular/http'; 3 | import 'rxjs/add/operator/toPromise'; 4 | import 'rxjs/add/operator/map'; 5 | import { NotificationService } from './notification.service'; 6 | import { YOUTUBE_API_KEY } from '../constants'; 7 | 8 | @Injectable() 9 | export class YoutubeApiService { 10 | base_url = 'https://www.googleapis.com/youtube/v3/'; 11 | max_results = 50; 12 | 13 | public nextToken: string; 14 | public lastQuery: string; 15 | 16 | constructor( 17 | private http: Http, 18 | private notificationService: NotificationService 19 | ) { } 20 | 21 | searchVideos(query: string): Promise { 22 | const url = `${this.base_url}search?q=${query}&maxResults=${this.max_results}&type=video&part=snippet,id&key=${YOUTUBE_API_KEY}&videoEmbeddable=true`; // tslint:disable-line 23 | 24 | return this.http.get(url) 25 | .map(response => { 26 | let jsonRes = response.json(); 27 | let res = jsonRes['items']; 28 | this.lastQuery = query; 29 | this.nextToken = jsonRes['nextPageToken'] ? jsonRes['nextPageToken'] : undefined; 30 | 31 | let ids = []; 32 | 33 | res.forEach((item) => { 34 | ids.push(item.id.videoId); 35 | }); 36 | 37 | return this.getVideos(ids); 38 | }) 39 | .toPromise() 40 | .catch(this.handleError) 41 | } 42 | 43 | searchNext(): Promise { 44 | const url = `${this.base_url}search?q=${this.lastQuery}&pageToken=${this.nextToken}&maxResults=${this.max_results}&type=video&part=snippet,id&key=${YOUTUBE_API_KEY}&videoEmbeddable=true`; // tslint:disable-line 45 | 46 | return this.http.get(url) 47 | .map(response => { 48 | let jsonRes = response.json(); 49 | let res = jsonRes['items']; 50 | this.nextToken = jsonRes['nextPageToken'] ? jsonRes['nextPageToken'] : undefined; 51 | let ids = []; 52 | 53 | res.forEach((item) => { 54 | ids.push(item.id.videoId); 55 | }); 56 | 57 | return this.getVideos(ids); 58 | }) 59 | .toPromise() 60 | .catch(this.handleError) 61 | } 62 | 63 | getVideos(ids): Promise { 64 | const url = `${this.base_url}videos?id=${ids.join(',')}&maxResults=${this.max_results}&type=video&part=snippet,contentDetails,statistics&key=${YOUTUBE_API_KEY}`; // tslint:disable-line 65 | 66 | return this.http.get(url) 67 | .map(results => { 68 | return results.json()['items']; 69 | }) 70 | .toPromise() 71 | .catch(this.handleError) 72 | } 73 | 74 | private handleError(error: Response | any) { 75 | let errMsg: string; 76 | if (error instanceof Response) { 77 | const body = error.json() || ''; 78 | const err = body.error || JSON.stringify(body); 79 | errMsg = `${error.status} - ${error.statusText || ''} ${err}`; 80 | } else { 81 | errMsg = error.message ? error.message : error.toString(); 82 | } 83 | 84 | this.notificationService.showNotification(errMsg); 85 | return Promise.reject(errMsg); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/app/shared/services/youtube-player.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Output, EventEmitter } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { NotificationService } from './notification.service'; 4 | import { BrowserNotificationService } from './browser-notification.service'; 5 | 6 | let _window: any = window; 7 | 8 | @Injectable() 9 | export class YoutubePlayerService { 10 | public yt_player; 11 | private currentVideoId: string; 12 | 13 | @Output() videoChangeEvent: EventEmitter = new EventEmitter(true); 14 | @Output() playPauseEvent: EventEmitter = new EventEmitter(true); 15 | @Output() currentVideoText: EventEmitter = new EventEmitter(true); 16 | 17 | constructor( 18 | public notificationService: NotificationService, 19 | public browserNotification: BrowserNotificationService 20 | ) { } 21 | 22 | createPlayer(): void { 23 | let interval = setInterval(() => { 24 | if ((typeof _window.YT !== 'undefined') && _window.YT && _window.YT.Player) { 25 | this.yt_player = new _window.YT.Player('yt-player', { 26 | width: '440', 27 | height: '250', 28 | playerVars: { 29 | iv_load_policy: '3', 30 | rel: '0' 31 | }, 32 | events: { 33 | onStateChange: (ev) => { 34 | this.onPlayerStateChange(ev); 35 | } 36 | } 37 | }); 38 | clearInterval(interval); 39 | } 40 | }, 100); 41 | } 42 | 43 | onPlayerStateChange(event: any) { 44 | const state = event.data; 45 | switch (state) { 46 | case 0: 47 | this.videoChangeEvent.emit(true); 48 | this.playPauseEvent.emit('pause'); 49 | break; 50 | case 1: 51 | this.playPauseEvent.emit('play'); 52 | break; 53 | case 2: 54 | this.playPauseEvent.emit('pause'); 55 | break; 56 | } 57 | } 58 | 59 | playVideo(videoId: string, videoText?: string): void { 60 | if (!this.yt_player) { 61 | this.notificationService.showNotification('Player not ready.'); 62 | return; 63 | } 64 | this.yt_player.loadVideoById(videoId); 65 | this.currentVideoId = videoId; 66 | this.currentVideoText.emit(videoText); 67 | this.browserNotification.show(videoText); 68 | } 69 | 70 | pausePlayingVideo(): void { 71 | this.yt_player.pauseVideo(); 72 | } 73 | 74 | playPausedVideo(): void { 75 | this.yt_player.playVideo(); 76 | } 77 | 78 | getCurrentVideo(): string { 79 | return this.currentVideoId; 80 | } 81 | 82 | resizePlayer(width: number, height: number) { 83 | this.yt_player.setSize(width, height); 84 | } 85 | 86 | getShuffled(index: number, max: number): number { 87 | if (max < 2) { 88 | return; 89 | } 90 | 91 | let i = Math.floor(Math.random() * max); 92 | return i !== index ? i : this.getShuffled(index, max); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamirHodzic/ngx-youtube-player/030bd859c64b4cccb6ba19e72a6e18d475e2626b/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo_git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamirHodzic/ngx-youtube-player/030bd859c64b4cccb6ba19e72a6e18d475e2626b/src/assets/logo_git.png -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamirHodzic/ngx-youtube-player/030bd859c64b4cccb6ba19e72a6e18d475e2626b/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ngx-youtube-player 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { AppModule } from './app/'; 4 | 5 | enableProdMode(); 6 | 7 | platformBrowserDynamic().bootstrapModule(AppModule); 8 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6/symbol'; 2 | import 'core-js/es6/object'; 3 | import 'core-js/es6/function'; 4 | import 'core-js/es6/parse-int'; 5 | import 'core-js/es6/parse-float'; 6 | import 'core-js/es6/number'; 7 | import 'core-js/es6/math'; 8 | import 'core-js/es6/string'; 9 | import 'core-js/es6/date'; 10 | import 'core-js/es6/array'; 11 | import 'core-js/es6/regexp'; 12 | import 'core-js/es6/map'; 13 | import 'core-js/es6/set'; 14 | import 'core-js/es6/reflect'; 15 | 16 | 17 | import 'zone.js/dist/zone'; 18 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #EAEBED; 3 | } 4 | 5 | .loader { 6 | width: 40px; 7 | height: 40px; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | bottom: 0; 12 | right: 0; 13 | margin: auto; 14 | } 15 | 16 | .loading { 17 | margin-left: 10px; 18 | border-radius: 50%; 19 | width: 40px; 20 | height: 40px; 21 | border: 0.2rem solid rgba(217, 30, 24, 0.5); 22 | border-top-color: rgb(52, 73, 94); 23 | -webkit-animation: spin 1s infinite linear; 24 | animation: spin 1s infinite linear; 25 | } 26 | 27 | @-webkit-keyframes spin { 28 | 0% { 29 | -webkit-transform: rotate(0deg); 30 | transform: rotate(0deg); 31 | } 32 | 100% { 33 | -webkit-transform: rotate(360deg); 34 | transform: rotate(360deg); 35 | } 36 | } 37 | 38 | @keyframes spin { 39 | 0% { 40 | -webkit-transform: rotate(0deg); 41 | transform: rotate(0deg); 42 | } 43 | 100% { 44 | -webkit-transform: rotate(360deg); 45 | transform: rotate(360deg); 46 | } 47 | } -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "", 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": ["es6", "dom"], 8 | "mapRoot": "./", 9 | "module": "es6", 10 | "moduleResolution": "node", 11 | "outDir": "../docs/out-tsc", 12 | "sourceMap": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "../node_modules/@types" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "max-line-length": [ 20 | true, 21 | 140 22 | ], 23 | "member-access": false, 24 | "member-ordering": [ 25 | true, 26 | "static-before-instance", 27 | "variables-before-functions" 28 | ], 29 | "no-arg": true, 30 | "no-bitwise": true, 31 | "no-console": [ 32 | true, 33 | "debug", 34 | "info", 35 | "time", 36 | "timeEnd", 37 | "trace" 38 | ], 39 | "no-construct": true, 40 | "no-debugger": true, 41 | "no-duplicate-variable": true, 42 | "no-empty": false, 43 | "no-eval": true, 44 | "no-inferrable-types": true, 45 | "no-shadowed-variable": true, 46 | "no-string-literal": false, 47 | "no-switch-case-fall-through": true, 48 | "no-trailing-whitespace": true, 49 | "no-unused-expression": true, 50 | "no-var-keyword": true, 51 | "object-literal-sort-keys": false, 52 | "one-line": [ 53 | true, 54 | "check-open-brace", 55 | "check-catch", 56 | "check-else", 57 | "check-whitespace" 58 | ], 59 | "quotemark": [ 60 | true, 61 | "single" 62 | ], 63 | "radix": true, 64 | "semicolon": [ 65 | "always" 66 | ], 67 | "triple-equals": [ 68 | true, 69 | "allow-null-check" 70 | ], 71 | "typedef-whitespace": [ 72 | true, 73 | { 74 | "call-signature": "nospace", 75 | "index-signature": "nospace", 76 | "parameter": "nospace", 77 | "property-declaration": "nospace", 78 | "variable-declaration": "nospace" 79 | } 80 | ], 81 | "variable-name": false, 82 | "whitespace": [ 83 | true, 84 | "check-branch", 85 | "check-decl", 86 | "check-operator", 87 | "check-separator", 88 | "check-type" 89 | ], 90 | "use-input-property-decorator": true, 91 | "use-output-property-decorator": true, 92 | "use-host-property-decorator": false, 93 | "no-input-rename": false, 94 | "no-output-rename": true, 95 | "use-life-cycle-interface": true, 96 | "use-pipe-transform-interface": true, 97 | "component-class-suffix": true, 98 | "directive-class-suffix": true 99 | } 100 | } --------------------------------------------------------------------------------