├── .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 | 
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 |
32 |
33 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 | }
--------------------------------------------------------------------------------