├── .gitignore ├── README.md ├── images ├── arrow.jpg ├── artist_placeholder.png ├── extension_logo.png ├── extension_logo_playing.png ├── icon-add.png ├── icon-play.png ├── icon-search.png ├── icon-share.png ├── mute.png ├── next.png ├── next_click.png ├── pause.png ├── pause_click.png ├── play.png ├── play_click.png ├── prev.png ├── prev_click.png ├── remove.png ├── repeat_all.png ├── repeat_hover.png ├── repeat_once.png ├── sound.png ├── sound_click.png └── telehealth_overlay.jpg ├── index.html ├── package.json ├── src ├── .DS_Store ├── app.ts ├── interfaces │ ├── Events.ts │ ├── ISearch.ts │ ├── ISoundPlayer.ts │ ├── PlayerEvents.ts │ └── Song.ts ├── player │ ├── .DS_Store │ ├── Controls.ts │ ├── Player.ts │ ├── SongImage.ts │ ├── TimeInfo.ts │ ├── Volume.ts │ └── timeSeeker │ │ ├── .DS_Store │ │ ├── HorizontalDraggable.ts │ │ └── TimeSeeker.ts ├── services │ ├── .DS_Store │ ├── LocalStorage.ts │ ├── PlaylistService.ts │ ├── SearchFactory.ts │ ├── SoundCloudPlayer.ts │ ├── SoundCloudSearch.ts │ ├── SoundManager.ts │ └── SoundManagerSoundPlayer.ts ├── tabList │ ├── .DS_Store │ ├── Playlist.ts │ ├── SongItem.ts │ ├── TabList.ts │ └── searchTab │ │ ├── SearchBox.ts │ │ ├── SearchResult.ts │ │ └── SearchTab.ts └── tsconfig.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular 2 Sound Cloud player 2 | A simple music player made by Angular 2 3 | 4 | **Live demo: http://davidtran.github.io/ng2music/** 5 | 6 | ## Introduction 7 | Angular 2 is just coming out. This product is a demo product by myself with Angular 2. Through this product, we can learn more about: 8 | - Using Typescript in Angular 2 9 | - Dependency injection 10 | - Angular 2 component communication 11 | - RxJS pattern 12 | 13 | ![Angular 2 Sound Cloud music player](http://i.imgur.com/pc0BBn7.png?1) 14 | 15 | ## Features 16 | Currently, it just have some simple features: 17 | - Search music on SoundCloud 18 | - Play, pause, toggle volume 19 | - Add song to playlist 20 | 21 | ## Todos 22 | - Convert this app to a Google Chrome extension 23 | - Add more unit test 24 | 25 | ## How to use ? 26 | - git clone https://github.com/davidtran/angular2-soundcloud 27 | - npm install 28 | - npm start 29 | 30 | ## Known issues: 31 | - Sometimes SoundCloud api returns Access-Control-Allow-Origin, just need to refresh website few times. I will fix this issue soon. 32 | 33 | ## Author 34 | Nam Tran 35 | 36 | Website: http://jslancer.com 37 | 38 | ## License 39 | MIT 40 | 41 | -------------------------------------------------------------------------------- /images/arrow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/arrow.jpg -------------------------------------------------------------------------------- /images/artist_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/artist_placeholder.png -------------------------------------------------------------------------------- /images/extension_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/extension_logo.png -------------------------------------------------------------------------------- /images/extension_logo_playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/extension_logo_playing.png -------------------------------------------------------------------------------- /images/icon-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/icon-add.png -------------------------------------------------------------------------------- /images/icon-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/icon-play.png -------------------------------------------------------------------------------- /images/icon-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/icon-search.png -------------------------------------------------------------------------------- /images/icon-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/icon-share.png -------------------------------------------------------------------------------- /images/mute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/mute.png -------------------------------------------------------------------------------- /images/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/next.png -------------------------------------------------------------------------------- /images/next_click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/next_click.png -------------------------------------------------------------------------------- /images/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/pause.png -------------------------------------------------------------------------------- /images/pause_click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/pause_click.png -------------------------------------------------------------------------------- /images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/play.png -------------------------------------------------------------------------------- /images/play_click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/play_click.png -------------------------------------------------------------------------------- /images/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/prev.png -------------------------------------------------------------------------------- /images/prev_click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/prev_click.png -------------------------------------------------------------------------------- /images/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/remove.png -------------------------------------------------------------------------------- /images/repeat_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/repeat_all.png -------------------------------------------------------------------------------- /images/repeat_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/repeat_hover.png -------------------------------------------------------------------------------- /images/repeat_once.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/repeat_once.png -------------------------------------------------------------------------------- /images/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/sound.png -------------------------------------------------------------------------------- /images/sound_click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/sound_click.png -------------------------------------------------------------------------------- /images/telehealth_overlay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/images/telehealth_overlay.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Angular 2 Music 4 | 5 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 45 | 46 | 47 | Loading 48 | 49 | 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2music", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "angular2": "2.0.0-beta.3", 8 | "bootstrap": "^3.3.6", 9 | "es6-promise": "^3.0.2", 10 | "es6-shim": "^0.33.3", 11 | "live-server": "^0.9.1", 12 | "reflect-metadata": "0.1.2", 13 | "rxjs": "5.0.0-beta.0", 14 | "soundmanager2": "^2.97.20150601-a", 15 | "systemjs": "0.19.6", 16 | "typescript": "^1.6.2" 17 | }, 18 | "devDependencies": { 19 | "typescript": "^1.7.5" 20 | }, 21 | "scripts": { 22 | "test": "echo \"Error: no test specified\" && exit 1", 23 | "start": "node_modules/.bin/live-server" 24 | }, 25 | "author": "", 26 | "license": "ISC" 27 | } 28 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/src/.DS_Store -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | import {bootstrap} from 'angular2/platform/browser'; 3 | import {HTTP_PROVIDERS} from 'angular2/http'; 4 | 5 | import {SearchFactory} from './services/SearchFactory.ts'; 6 | import {PlaylistService} from './services/PlaylistService.ts'; 7 | import {SoundCloudSearch} from './services/SoundCloudSearch.ts'; 8 | import {SoundCloudPlayer} from './services/SoundCloudPlayer.ts'; 9 | import {SoundManagerSoundPlayer} from './services/SoundManagerSoundPlayer.ts'; 10 | import {SoundManager} from './services/SoundManager.ts'; 11 | import {PlayerCmp} from './player/Player.ts'; 12 | import {TabListCmp} from './tabList/TabList.ts'; 13 | import {LocalStorage} from './services/LocalStorage.ts'; 14 | import 'rxjs/Rx'; 15 | 16 | @Component({ 17 | selector: 'app', 18 | template: ` 19 |
20 | 21 | 22 |
23 | `, 24 | styles: [ 25 | ` 26 | .app { 27 | width: 320px; 28 | } 29 | 30 | ` 31 | ], 32 | directives: [TabListCmp, PlayerCmp], 33 | providers: [ 34 | HTTP_PROVIDERS, 35 | SoundCloudSearch, 36 | SearchFactory, 37 | PlaylistService, 38 | SoundCloudPlayer, 39 | SoundManagerSoundPlayer, 40 | SoundManager, 41 | LocalStorage 42 | ] 43 | }) 44 | export class AppCmp { 45 | 46 | } 47 | 48 | bootstrap(AppCmp); -------------------------------------------------------------------------------- /src/interfaces/Events.ts: -------------------------------------------------------------------------------- 1 | export class Events { 2 | public static ChangeSong = 1; 3 | public static Play = 2; 4 | public static PlayStart = 3; 5 | public static PlayResume =4; 6 | public static Pause = 5; 7 | public static Finish = 6; 8 | public static Seek = 7; 9 | public static Seeked = 8; 10 | public static BufferingStart = 9; 11 | public static BufferingEnd = 10; 12 | public static AudioError = 11; 13 | public static Time = 12; 14 | public static NoStreams = 13; 15 | public static NoProtocol = 14; 16 | public static NoConnection = 15; 17 | public static Volume = 16; 18 | } -------------------------------------------------------------------------------- /src/interfaces/ISearch.ts: -------------------------------------------------------------------------------- 1 | import {Song} from './Song.ts'; 2 | 3 | 4 | export interface ISearch 5 | { 6 | search: (keyword: string) => any; 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/ISoundPlayer.ts: -------------------------------------------------------------------------------- 1 | import {Song} from './Song.ts'; 2 | import {Events} from "./Events.ts"; 3 | export interface ISoundPlayer { 4 | initialize: (song: Song, calback: (e: Error) => void) => void; 5 | play: () => void; 6 | pause: () => void; 7 | seek: (time: number) => void; 8 | currentTime: () => number; 9 | totalTime: () => number; 10 | setVolume: (value: number) => void; 11 | getVolume: () => number; 12 | on: (event: number, handler: (data: any) => void) => void; 13 | } -------------------------------------------------------------------------------- /src/interfaces/PlayerEvents.ts: -------------------------------------------------------------------------------- 1 | enum PlayerEvents { 2 | ChangeSong, 3 | Play, 4 | PlayStart, 5 | PlayResume, 6 | Pause, 7 | Finish, 8 | Seek, 9 | Seeked, 10 | BufferingStart, 11 | BufferingEnd, 12 | AudioError, 13 | Time, 14 | NoStreams, 15 | NoProtocol, 16 | NoConnection 17 | } 18 | 19 | export default PlayerEvents; -------------------------------------------------------------------------------- /src/interfaces/Song.ts: -------------------------------------------------------------------------------- 1 | export interface Song 2 | { 3 | id: number, 4 | name: string, 5 | artist: string, 6 | streamUrl: string, 7 | provider: number, 8 | idFromProvider: string, 9 | duration: number, 10 | imageUrl: string, 11 | link: string 12 | } -------------------------------------------------------------------------------- /src/player/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/src/player/.DS_Store -------------------------------------------------------------------------------- /src/player/Controls.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "angular2/core"; 2 | //import {Song} from './interfaces/Song.ts'; 3 | import {Events} from '../interfaces/Events.ts'; 4 | import {SoundManager} from '../services/SoundManager.ts'; 5 | import {PlaylistService} from '../services/PlaylistService.ts'; 6 | import {NgIf} from 'angular2/common'; 7 | 8 | @Component({ 9 | selector: 'controls', 10 | template:` 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | `, 22 | styles: [ 23 | `#btnPrevious{ 24 | margin-right: 2px; 25 | } 26 | #btnPrevious img { 27 | width:30px; 28 | height:30px; 29 | margin-top:4px; 30 | } 31 | 32 | #btnPlayPause{ 33 | box-sizing: border-box; 34 | margin-right: 2px; 35 | } 36 | 37 | #btnPlayPause img{ 38 | width:40px; 39 | height:40px; 40 | } 41 | 42 | #btnNextSong{ 43 | position: relative; 44 | box-sizing: border-box; 45 | } 46 | 47 | #btnNextSong img{ 48 | margin-top: 4px; 49 | width: 30px; 50 | height: 30px; 51 | } 52 | ` 53 | ] 54 | directives: [NgIf] 55 | }) 56 | export class ControlsCmp { 57 | @Input("is-playing") isPlaying: boolean; 58 | @Input() song: any; 59 | 60 | constructor(private soundManager: SoundManager) { 61 | 62 | } 63 | 64 | togglePlayPause() { 65 | this.soundManager.togglePlayPause(); 66 | } 67 | 68 | next() { 69 | this.soundManager.next(); 70 | } 71 | 72 | previous() { 73 | this.soundManager.previous(); 74 | } 75 | } -------------------------------------------------------------------------------- /src/player/Player.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from 'angular2/core'; 2 | import {NgIf} from 'angular2/common'; 3 | import {SoundManager} from '../services/SoundManager.ts'; 4 | 5 | import {Song} from '../interfaces/Song.ts'; 6 | import {Events} from '../interfaces/Events.ts'; 7 | 8 | import {ControlsCmp} from "./Controls.ts"; 9 | import {VolumeCmp} from './Volume.ts'; 10 | import {SongImageCmp} from './SongImage.ts'; 11 | 12 | import {TimeSeekerCmp} from './timeSeeker/TimeSeeker.ts'; 13 | import {TimeInfoCmp} from './TimeInfo.ts'; 14 | 15 | @Component({ 16 | selector: 'player', 17 | template: ` 18 |
19 |
20 |
21 | 22 |
23 |
24 |

{{ song.name }}

25 |

{{ song.artist }}

26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 | `, 52 | styles: [` 53 | .player{ 54 | padding-top:7px; 55 | padding-left:7px; 56 | padding-bottom: 18px; 57 | padding-right: 7px; 58 | background-color: #fff; 59 | } 60 | 61 | .song-title { 62 | font-size: 14px; 63 | margin-top:4px; 64 | padding-bottom: 0; 65 | color:#000; 66 | margin-bottom: 7px; 67 | } 68 | 69 | .song-artist{ 70 | font-size: 13px; 71 | margin-top: 6px; 72 | color:#939393; 73 | } 74 | .player-info { 75 | padding-left:0; 76 | } 77 | 78 | .controllerGroup{ 79 | display: block; 80 | margin-top: 15px; 81 | } 82 | 83 | .controllerGroup a { 84 | text-decoration: none; 85 | outline: none; 86 | } 87 | 88 | .controllerGroup a:focus { 89 | text-decoration: none; 90 | outline: none; 91 | } 92 | `], 93 | directives:[NgIf, ControlsCmp, VolumeCmp, SongImageCmp, TimeSeekerCmp, TimeInfoCmp] 94 | }) 95 | export class PlayerCmp implements OnInit { 96 | public song: Song; 97 | private isPlaying: boolean; 98 | private currentTime: number; 99 | private totalTime: number; 100 | private soundManager: SoundManager; 101 | 102 | constructor(soundManager: SoundManager) { 103 | this.song = null; 104 | this.soundManager = soundManager; 105 | this.soundManager.on(Events.ChangeSong, (song) => { 106 | this.song = song; 107 | this.totalTime = this.soundManager.getTotalTime(); 108 | }); 109 | } 110 | 111 | ngOnInit() { 112 | this.soundManager.on(Events.Pause, () => { 113 | this.isPlaying = false; 114 | }); 115 | 116 | this.soundManager.on(Events.Play, () => { 117 | this.isPlaying = true; 118 | }); 119 | 120 | this.soundManager.on(Events.PlayResume, () => { 121 | this.isPlaying = true; 122 | }); 123 | 124 | this.soundManager.on(Events.Time, (time) => { 125 | this.currentTime = time; 126 | this.totalTime = this.soundManager.getTotalTime(); 127 | }); 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /src/player/SongImage.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from 'angular2/core'; 2 | import {NgIf} from 'angular2/common'; 3 | import {Song} from '../interfaces/Song.ts'; 4 | 5 | @Component({ 6 | selector: 'song-image', 7 | template: ` 8 | 12 | `, 13 | styles: [` 14 | .artist-image{ 15 | border-radius: 100px; 16 | box-sizing:border-box; 17 | border: 5px solid #dedede; 18 | width:100%; 19 | } 20 | `], 21 | directives: [NgIf] 22 | }) 23 | export class SongImageCmp { 24 | @Input() song: any; 25 | 26 | private DefaultImageUrl = "/images/artist_placeholder.png"; 27 | 28 | getImageUrl() { 29 | if (this.song && this.song.imageUrl) { 30 | return this.song.imageUrl; 31 | } 32 | return this.DefaultImageUrl; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/player/TimeInfo.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from 'angular2/core'; 2 | import {NgIf} from 'angular2/common'; 3 | 4 | @Component({ 5 | selector: 'time-info', 6 | template: ` 7 |
8 | {{ formatTime(currentTime) }} / {{ formatTime(totalTime) }} 9 |
10 | `, 11 | directives: [NgIf] 12 | }) 13 | export class TimeInfoCmp { 14 | @Input('time') currentTime; 15 | @Input('total-time') totalTime; 16 | @Input('song') song; 17 | 18 | constructor() { 19 | 20 | } 21 | 22 | formatTime(time: number) { 23 | if (!this.song || !this.currentTime || !this.totalTime) { 24 | return '00:00'; 25 | } 26 | time = time / 1000; 27 | var minutes = Math.floor(time / 60); 28 | var seconds = Math.floor(time - minutes * 60); 29 | var minStr = minutes > 9 ? minutes.toString() : '0' + minutes.toString(); 30 | var secStr = seconds > 9 ? seconds.toString() : '0' + seconds.toString(); 31 | return minStr + ':' + secStr; 32 | } 33 | } -------------------------------------------------------------------------------- /src/player/Volume.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | import {NgIf} from 'angular2/common'; 3 | import {SoundManager} from '../services/SoundManager.ts'; 4 | import {Events} from '../interfaces/Events.ts'; 5 | 6 | @Component({ 7 | selector: 'volume', 8 | template: ` 9 | 10 | 11 | 12 | 13 | `, 14 | styles: [` 15 | 16 | #btnToggleVolume { 17 | width:20px; 18 | } 19 | #btnToggleVolume img{ 20 | width:20px; 21 | padding-top:15px; 22 | } 23 | 24 | #btnToggleVolume i{ 25 | margin-top:13px; 26 | color:#c7b4ab; 27 | } 28 | `], 29 | directives:[NgIf] 30 | }) 31 | export class VolumeCmp { 32 | 33 | private isMute = false; 34 | 35 | constructor(private soundManager: SoundManager) { 36 | this.soundManager.on(Events.Volume, (isMute) => { 37 | this.isMute = isMute; 38 | }); 39 | } 40 | 41 | toggleMute() { 42 | this.soundManager.toggleMute(); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/player/timeSeeker/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/src/player/timeSeeker/.DS_Store -------------------------------------------------------------------------------- /src/player/timeSeeker/HorizontalDraggable.ts: -------------------------------------------------------------------------------- 1 | import {Directive, HostListener, Output, EventEmitter} from 'angular2/core'; 2 | import {DOM} from 'angular2/src/platform/dom/dom_adapter'; 3 | import {ElementRef} from 'angular2/core'; 4 | 5 | @Directive({ 6 | selector: 'horizontal-draggable' 7 | }) 8 | export class HorizontalDraggable { 9 | @Output('position') position = new EventEmitter(); 10 | 11 | private mousedrag; 12 | 13 | private mouseup = new EventEmitter(); 14 | 15 | private mousedown = new EventEmitter(); 16 | 17 | private mousemove = new EventEmitter(); 18 | 19 | @HostListener('mouseup', ['$event']) 20 | onMouseup(event) { 21 | this.mouseup.next(event); 22 | } 23 | 24 | @HostListener('mousedown', ['$event']) 25 | onMousedown(event) { 26 | this.mousedown.next(event); 27 | } 28 | 29 | @HostListener('mousemove', ['$event']) 30 | onMousemove(event) { 31 | this.mousemove.next(event); 32 | } 33 | 34 | constructor(public element: ElementRef) { 35 | this.element.nativeElement.style.position = 'relative'; 36 | this.element.nativeElement.style.cursor = 'pointer'; 37 | 38 | this.mousedrag = this.mousedown 39 | .map(event => { 40 | event.preventDefault(); 41 | return { 42 | left: event.clientX - this.element.nativeElement.getBoundingClientRect().left, 43 | right: event.clientY - this.element.nativeElement.getBoundingClientRect().top 44 | } 45 | }) 46 | .flatMap(imageOffset => this.mousemove.map(pos => ({ 47 | top: pos.clientY - imageOffset.top, 48 | left: pos.clientX - imageOffset.left 49 | }))) 50 | .takeUntil(this.mouseup); 51 | } 52 | 53 | onInit() { 54 | this.mousedrag.subscribe({ 55 | next: pos => { 56 | this.element.nativeElement.style.left = pos.left + 'px'; 57 | } 58 | }) 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/player/timeSeeker/TimeSeeker.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef, OnInit, Input} from 'angular2/core'; 2 | import {NgIf} from 'angular2/common'; 3 | import {SoundManager} from '../../services/SoundManager.ts'; 4 | import {Events} from '../../interfaces/Events.ts'; 5 | 6 | @Component({ 7 | selector: 'time-seeker', 8 | template: ` 9 |
10 | 11 |
12 | `, 13 | styles: [` 14 | #sliderHandler { 15 | position: absolute; 16 | } 17 | 18 | #timeSlider{ 19 | position: relative; 20 | } 21 | 22 | #timeSlider{ 23 | position: relative; 24 | height:8px; 25 | background-color:#cfcfcf; 26 | background-image:none; 27 | border:none; 28 | width: 307px; 29 | float: right; 30 | border-radius: 4px; 31 | cursor: pointer !important; 32 | } 33 | 34 | #sliderHandler{ 35 | position: absolute; 36 | border-radius: 100px; 37 | background-image: none !important; 38 | background-color: #fff !important; 39 | border:1px solid #ff8b00 !important; 40 | top:-4px !important; 41 | width:15px !important; 42 | height:15px !important; 43 | box-sizing: border-box; 44 | } 45 | `] 46 | }) 47 | export class TimeSeekerCmp implements OnInit { 48 | @Input() time: number; 49 | @Input('total-time') duration: number; 50 | 51 | constructor(private soundManager: SoundManager, 52 | private element: ElementRef) { 53 | 54 | } 55 | 56 | calculatePositionByTime() { 57 | var percent = this.time * 100 / this.duration; 58 | var pos = percent * this.getTimeSliderWidth() / 100; 59 | return pos; 60 | } 61 | 62 | ngOnInit() { 63 | var offset = this.element.nativeElement.getBoundingClientRect(); 64 | var width = this.element.nativeElement.style.width; 65 | var height = this.element.nativeElement.style.height; 66 | } 67 | 68 | 69 | changePlaybackTime($event) { 70 | var time = this.calculateTimePercentOnClick($event); 71 | this.soundManager.seek(time); 72 | } 73 | 74 | private calculateTimePercentOnClick($event) { 75 | var parentX = this.getTimeSliderWidth(); 76 | var percent = $event.x * 100 / parentX; 77 | return percent; 78 | } 79 | 80 | private getTimeSliderWidth() { 81 | return parseInt(this.element.nativeElement.children[0].clientWidth); 82 | } 83 | } -------------------------------------------------------------------------------- /src/services/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/src/services/.DS_Store -------------------------------------------------------------------------------- /src/services/LocalStorage.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, OnInit} from 'angular2/core'; 2 | 3 | @Injectable() 4 | export class LocalStorage{ 5 | 6 | constructor() { 7 | 8 | } 9 | 10 | getObject(key) { 11 | try { 12 | return JSON.parse(localStorage[key]); 13 | } 14 | catch (e) { 15 | return null; 16 | } 17 | } 18 | 19 | setObject(key, data) { 20 | localStorage[key] = JSON.stringify(data); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/services/PlaylistService.ts: -------------------------------------------------------------------------------- 1 | import {Song} from '../interfaces/Song.ts'; 2 | import {Injectable} from 'angular2/core'; 3 | import {Subject} from 'rxjs/Subject'; 4 | import {Observable} from 'rxjs/Observable'; 5 | import {LocalStorage} from './LocalStorage.ts'; 6 | import * as Rx from 'rxjs/Rx'; 7 | @Injectable() 8 | export class PlaylistService { 9 | private _data: Song[]; 10 | private _subscribers: any[]; 11 | private currentSongIndex = -1; 12 | private $dataObservable; 13 | private $source; 14 | 15 | constructor(private localStorageService: LocalStorage) { 16 | this.$dataObservable = null; 17 | this._data = this.localStorageService.getObject('playlist_data'); 18 | if (null == this._data) { 19 | this._data = []; 20 | } 21 | this.$source = this._createDataObservable(); 22 | } 23 | 24 | add(song: Song) { 25 | var index = this._data.indexOf(song); 26 | if (index < 0) { 27 | this._data.push(song); 28 | this.syncWithLocalStorage(); 29 | this.publishChanges(); 30 | } 31 | } 32 | 33 | first(): Song { 34 | if (this._data.length == 0) return null; 35 | this.currentSongIndex = 0; 36 | return this._data[0]; 37 | } 38 | 39 | next(): Song { 40 | if (this._data.length == 0) return null; 41 | if (this.currentSongIndex < this._data.length - 1) { 42 | this.currentSongIndex++; 43 | } else { 44 | this.currentSongIndex = 0; 45 | } 46 | return this._data[this.currentSongIndex]; 47 | } 48 | 49 | previous(): Song { 50 | if (this._data.length == 0) return null; 51 | if (this.currentSongIndex > 0) { 52 | this.currentSongIndex--; 53 | } else { 54 | this.currentSongIndex = this._data.length - 1; 55 | } 56 | return this._data[this.currentSongIndex]; 57 | } 58 | 59 | remove(song: Song) { 60 | var index = this._data.indexOf(song); 61 | this._data.splice(index, 1); 62 | this.syncWithLocalStorage(); 63 | this.publishChanges(); 64 | } 65 | 66 | setIndexBySong(song: Song) { 67 | var index = this._data.indexOf(song); 68 | if (index > -1) { 69 | this.currentSongIndex = index; 70 | } 71 | } 72 | 73 | getAll(): Observable> { 74 | return this.$source; 75 | } 76 | 77 | private _createDataObservable() { 78 | return Rx.Observable.create(observer => this.$dataObservable = observer).share(); 79 | } 80 | 81 | publishChanges() { 82 | this.$dataObservable.next(this._data); 83 | } 84 | 85 | private syncWithLocalStorage() { 86 | this.localStorageService.setObject('playlist_data', this._data); 87 | } 88 | } -------------------------------------------------------------------------------- /src/services/SearchFactory.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'angular2/core'; 2 | import {ISearch} from '../interfaces/ISearch.ts'; 3 | import {SoundCloudSearch} from './SoundCloudSearch.ts'; 4 | 5 | @Injectable() 6 | export class SearchFactory { 7 | constructor(private soundCloudSearch: SoundCloudSearch) { 8 | 9 | } 10 | 11 | public getSearchClient(provider) : ISearch { 12 | if (provider == 1) { 13 | return this.soundCloudSearch; 14 | } 15 | return null; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/services/SoundCloudPlayer.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'angular2/core'; 2 | import {ISoundPlayer} from '../interfaces/ISoundPlayer.ts'; 3 | import {Song} from '../interfaces/Song.ts'; 4 | import {PlayerEvents} from "../interfaces/PlayerEvents.ts"; 5 | import {Events} from "../interfaces/Events.ts"; 6 | @Injectable() 7 | export class SoundCloudPlayer implements ISoundPlayer { 8 | 9 | private song: Song; 10 | private isPlaying: boolean; 11 | private scPlayer: any; 12 | private subscribers = {}; 13 | 14 | constructor() { 15 | 16 | } 17 | 18 | initialize(song: Song, callback: (e: Error) => void) { 19 | this.song = song; 20 | try { 21 | if (typeof(this.scPlayer) == 'object') this.scPlayer.pause(); 22 | this.scPlayer = null; 23 | SC.stream('/tracks/' + this.song.idFromProvider.toString()) 24 | .then((player) => { 25 | this.scPlayer = player; 26 | this.subscribers = {}; 27 | callback(null); 28 | }); 29 | } 30 | catch (e) { 31 | callback(e); 32 | } 33 | 34 | } 35 | 36 | play() { 37 | this.scPlayer.play(); 38 | this.isPlaying = true; 39 | } 40 | 41 | pause() { 42 | this.scPlayer.pause(); 43 | this.isPlaying = false; 44 | } 45 | 46 | seek(time: number) { 47 | this.scPlayer.seek(time); 48 | } 49 | 50 | currentTime(): number { 51 | return this.scPlayer.currentTime(); 52 | } 53 | 54 | setVolume(value: number) { 55 | this.scPlayer.setVolume(value); 56 | } 57 | 58 | getVolume(): number { 59 | return this.scPlayer.getVolume(); 60 | } 61 | 62 | on(event, handler: () => void) { 63 | if (!this.subscribers[event]) this.subscribers[event] = []; 64 | this.subscribers[event].push(handler); 65 | } 66 | 67 | publish(events) { 68 | if (events != null) { 69 | if (typeof(events.length) != "undefined" && events.length > 0) { 70 | 71 | } else { 72 | 73 | } 74 | } 75 | } 76 | 77 | private subscribeSoundCloudPlayerEvent() { 78 | this.scPlayer.on('play', () => this.publish(Events.Play)); 79 | this.scPlayer.on('play-start', () => this.publish(Events.PlayStart)); 80 | this.scPlayer.on('play-resume', () => this.publish(Events.PlayResume)); 81 | this.scPlayer.on('pause', () => this.publish(Events.Pause)); 82 | this.scPlayer.on('finish', () => this.publish(Events.Finish)); 83 | this.scPlayer.on('seek', () => this.publish(Events.Seek)); 84 | this.scPlayer.on('seeked', () => this.publish(Events.Seeked)); 85 | this.scPlayer.on('time', () => this.publish(Events.Time)); 86 | this.scPlayer.on('audio_error', () => this.publish(Events.AudioError)); 87 | this.scPlayer.on('no_streams', () => this.publish(Events.NoStreams)); 88 | } 89 | } -------------------------------------------------------------------------------- /src/services/SoundCloudSearch.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'angular2/core'; 2 | import {Song} from '../interfaces/Song.ts'; 3 | import {ISearch} from '../interfaces/ISearch.ts'; 4 | import {Http, Response} from 'angular2/http'; 5 | import {Observable} from 'rxjs/Observable'; 6 | 7 | @Injectable() 8 | export class SoundCloudSearch implements ISearch { 9 | 10 | private clientId: string = '8e1349e63dfd43dc67a63e0de3befc68'; 11 | private http: Http; 12 | 13 | constructor(http: Http) { 14 | this.http = http; 15 | console.log(this.http); 16 | } 17 | 18 | search(keyword: string): Song[] { 19 | var uri = this.makeSearchUri(keyword); 20 | 21 | return this.http.get(uri) 22 | .map(res => this.handleResponse(res)) 23 | .catch(this.handleError); 24 | } 25 | 26 | handleResponse(res: any): any{ 27 | var data = res.json(); 28 | var result = []; 29 | if (data && data.collection) { 30 | data.collection.forEach(function(item) { 31 | var song: Song = {}; 32 | song.streamUrl = item.stream_url; 33 | song.name = item.title; 34 | song.artist = item.user.username; 35 | song.provider = 1; 36 | song.idFromProvider = item.id; 37 | song.duration = item.duration; 38 | song.imageUrl = item.artwork_url; 39 | song.link = item.permalink_url; 40 | result.push(song); 41 | }); 42 | } 43 | return result; 44 | } 45 | 46 | handleError(e: Response) { 47 | console.log(e); 48 | } 49 | 50 | makeSearchUri(keyword: string) : string { 51 | return 'http://api.soundcloud.com/tracks?linked_partitioning=1&client_id=' + this.clientId + '&q=' + keyword; 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/services/SoundManager.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'angular2/core'; 2 | import {SoundCloudPlayer} from './SoundCloudPlayer.ts'; 3 | import {Song} from '../interfaces/Song.ts'; 4 | import {Events} from '../interfaces/Events.ts'; 5 | import {ISoundPlayer} from '../interfaces/ISoundPlayer.ts'; 6 | import {SoundManagerSoundPlayer} from './SoundManagerSoundPlayer.ts'; 7 | import {PlaylistService} from './PlaylistService.ts'; 8 | @Injectable() 9 | export class SoundManager { 10 | private soundPlayer: ISoundPlayer; 11 | private subscribers: Object = {}; 12 | private currentSong: Song; 13 | private isPlaying = false; 14 | private isMute = false; 15 | constructor(private soundCloudPlayer: SoundCloudPlayer, 16 | private soundManagerSoundPlayer: SoundManagerSoundPlayer, 17 | private playlistService: PlaylistService) { 18 | 19 | } 20 | 21 | private getSoundPlayer(song: Song) { 22 | return this.soundManagerSoundPlayer; 23 | } 24 | 25 | play(song: Song) { 26 | this.playlistService.add(song); //Auto add song to playlist 27 | this.playlistService.setIndexBySong(song); 28 | this.currentSong = song; 29 | 30 | if (!this.soundPlayer) { 31 | this.soundPlayer = this.getSoundPlayer(song); 32 | this.subscribSoundPlayerEvent(this.soundPlayer); 33 | } 34 | 35 | this.soundPlayer.initialize(song, (e: Error) => { 36 | if (!e) { 37 | this.soundPlayer.play(); 38 | this.publish(Events.ChangeSong, song); 39 | this.isPlaying = true; 40 | } else { 41 | alert(e.message); 42 | } 43 | }); 44 | } 45 | 46 | togglePlayPause() { 47 | if (this.currentSong != null) { 48 | if (!this.isPlaying) { 49 | this.soundPlayer.play(); 50 | } else { 51 | this.soundPlayer.pause(); 52 | } 53 | } else { 54 | var song = this.playlistService.first(); 55 | this.play(song); 56 | } 57 | } 58 | 59 | next() { 60 | var song = this.playlistService.next(); 61 | if (song) this.play(song); 62 | } 63 | 64 | previous() { 65 | var song = this.playlistService.previous(); 66 | if (song) { 67 | this.play(song); 68 | } 69 | } 70 | 71 | seek(time: number) { 72 | if (this.soundPlayer && this.currentSong) { 73 | this.soundPlayer.seek(time); 74 | } 75 | } 76 | 77 | toggleMute() { 78 | if (this.currentSong) { 79 | if (this.isMute) { 80 | this.soundPlayer.setVolume(100); 81 | this.isMute = false; 82 | this.publish(Events.Volume, false); 83 | } else { 84 | this.soundPlayer.setVolume(0); 85 | this.isMute = true; 86 | this.publish(Events.Volume, true); 87 | } 88 | } 89 | } 90 | 91 | getTotalTime() { 92 | if (this.soundPlayer && this.currentSong) { 93 | return this.soundPlayer.totalTime(); 94 | } 95 | return null; 96 | } 97 | 98 | on(event, handler: any) { 99 | if (!this.subscribers[event]) this.subscribers[event] = []; 100 | this.subscribers[event].push(handler); 101 | } 102 | 103 | private publish(event, data: any) { 104 | console.log('Publish event:', event, data); 105 | if (this.subscribers[event]) { 106 | this.subscribers[event].forEach(function(handler) { 107 | handler(data); 108 | }); 109 | } 110 | } 111 | 112 | getCurrentSong(): Song { 113 | return this.currentSong; 114 | } 115 | 116 | onSongFinish() { 117 | var nextSong = this.playlistService.next(); 118 | if (nextSong) { 119 | this.play(nextSong); 120 | } else { 121 | this.publish(Events.Finish, null); 122 | } 123 | } 124 | 125 | subscribSoundPlayerEvent(soundPlayer: ISoundPlayer) { 126 | 127 | soundPlayer.on(Events.Play, () => { 128 | this.publish(Events.Play, null); 129 | this.isPlaying = true; 130 | }); 131 | 132 | soundPlayer.on(Events.PlayStart, () => { 133 | this.publish(Events.PlayStart, null); 134 | this.isPlaying = true; 135 | }); 136 | 137 | soundPlayer.on(Events.PlayResume, () => { 138 | this.publish(Events.PlayResume, null); 139 | this.isPlaying = true; 140 | }); 141 | 142 | soundPlayer.on(Events.Pause, () => { 143 | this.publish(Events.Pause, null); 144 | this.isPlaying = false; 145 | }); 146 | 147 | soundPlayer.on(Events.Finish, () => { 148 | this.publish(Events.Finish, null); 149 | this.isPlaying = false; 150 | this.onSongFinish(); 151 | }); 152 | 153 | soundPlayer.on(Events.Seek, () => this.publish(Events.Seek, null)); 154 | 155 | soundPlayer.on(Events.Seeked, () => this.publish(Events.Seeked, null)); 156 | 157 | soundPlayer.on(Events.Time, (time) => { 158 | this.publish(Events.Time, time); 159 | }); 160 | 161 | soundPlayer.on(Events.AudioError, () => { 162 | this.publish(Events.AudioError, null); 163 | this.isPlaying = false; 164 | }); 165 | 166 | soundPlayer.on(Events.NoStreams, () => this.publish(Events.NoStreams, null)); 167 | 168 | } 169 | } -------------------------------------------------------------------------------- /src/services/SoundManagerSoundPlayer.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'angular2/core'; 2 | import {ISoundPlayer} from '../interfaces/ISoundPlayer.ts'; 3 | import {Song} from '../interfaces/Song.ts'; 4 | import {Events} from '../interfaces/Events.ts'; 5 | @Injectable() 6 | /** 7 | * This class take responsiblity to play a song. Just it. 8 | */ 9 | export class SoundManagerSoundPlayer implements ISoundPlayer { 10 | private soundObject: any; 11 | private subscribers: Object = {}; 12 | private lastSong: Song; 13 | constructor() { 14 | 15 | } 16 | 17 | initialize(song: Song, callback: (e: Error, data: any) => void) { 18 | if (this.lastSong) { 19 | soundManager.unload(this.lastSong.idFromProvider); 20 | soundManager.destroySound(this.lastSong.idFromProvider); 21 | } 22 | 23 | var soundObject = soundManager.getSoundById(song.id); 24 | if (!soundObject) { 25 | soundObject = soundManager.createSound({ 26 | url: song.streamUrl + '?client_id=' + soundCloudClientId, 27 | id: song.idFromProvider, 28 | volume: 100, 29 | onbufferchange: () => this.publish(Events.BufferingStart, null), 30 | ondataerror: () => this.publish(Events.AudioError, null), 31 | onfinish: () => this.publish(Events.Finish, null), 32 | onload: () => this.publish(Events.BufferingStart, null), 33 | onpause: () => this.publish(Events.Pause, null), 34 | onplay: () => this.publish(Events.Play, null), 35 | onresume: () => this.publish(Events.PlayResume, null), 36 | onstop: () => this.publish(Events.Finish, null), 37 | whileplaying: () => { 38 | var time = this.currentTime(); 39 | this.publish(Events.Time, time) 40 | }, 41 | }); 42 | 43 | if (!soundObject) { 44 | return callback(new Error('Error while create sound'), null); 45 | } 46 | 47 | this.lastSong = song; 48 | } 49 | 50 | soundObject.play(); 51 | this.soundObject = soundObject; 52 | return callback(null, song); 53 | } 54 | 55 | play() { 56 | if (this.soundObject) { 57 | this.soundObject.resume(); 58 | } 59 | } 60 | 61 | pause() { 62 | if (this.soundObject) { 63 | this.soundObject.pause(); 64 | } 65 | } 66 | 67 | seek(percent: number) { 68 | if (this.soundObject) { 69 | var time = this.soundObject.duration * percent / 100; 70 | this.soundObject.setPosition(time); 71 | } 72 | } 73 | 74 | currentTime(): number { 75 | if (!this.soundObject) return; 76 | return this.soundObject.position; 77 | } 78 | 79 | totalTime():number { 80 | if (!this.soundObject) return; 81 | return this.soundObject.duration; 82 | } 83 | 84 | setVolume(value: number) { 85 | if (!this.soundObject) return; 86 | this.soundObject.setVolume(value); 87 | } 88 | 89 | getVolume(): number { 90 | if (!this.soundObject) return; 91 | return this.soundObject.volume; 92 | } 93 | 94 | on(event, handler: () => void) { 95 | if (!this.subscribers[event]) this.subscribers[event] = []; 96 | this.subscribers[event].push(handler); 97 | } 98 | 99 | publish(event, data: any) { 100 | if (this.subscribers[event]) { 101 | this.subscribers[event].forEach(handler => { 102 | handler(data); 103 | }); 104 | } 105 | } 106 | 107 | private subscribeSoundCloudPlayerEvent() { 108 | if (!this.soundObject) return; 109 | 110 | //Do nothing because events are declared at createSoundOption 111 | } 112 | } -------------------------------------------------------------------------------- /src/tabList/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidtran/angular2-soundcloud/866bf7c2a33db14524455a1f225970dbae9931c2/src/tabList/.DS_Store -------------------------------------------------------------------------------- /src/tabList/Playlist.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from 'angular2/core'; 2 | import {NgFor} from 'angular2/common'; 3 | import {SongItemCmp} from './SongItem.ts'; 4 | import {PlaylistService} from '../services/PlaylistService.ts'; 5 | @Component({ 6 | selector: 'playlist', 7 | directives: [SongItemCmp, NgFor], 8 | template: ` 9 |
10 | 11 |
12 | `, 13 | styles: [` 14 | #playlistContainer{ 15 | padding:7px; 16 | max-height: 400px; 17 | overflow-y: scroll; 18 | } 19 | `] 20 | }) 21 | export class PlaylistCmp implements OnInit{ 22 | private data: Array; 23 | 24 | constructor(private playlistService: PlaylistService) { 25 | this.playlistService 26 | .getAll() 27 | .subscribe(playlistData => { 28 | this.data = playlistData; 29 | }); 30 | this.playlistService.publishChanges(); 31 | } 32 | 33 | ngOnInit() { 34 | 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/tabList/SongItem.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "angular2/core"; 2 | import {NgIf} from "angular2/common"; 3 | import {PlaylistService} from "../services/PlaylistService.ts"; 4 | import {Song} from "../interfaces/Song.ts"; 5 | import {SoundManager} from "../services/SoundManager.ts"; 6 | 7 | @Component({ 8 | selector: 'song-item', 9 | template: ` 10 |
11 |
12 | 17 |
18 | 19 |
20 |
{{song.name}}
23 |
24 | {{song.artist}} 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 | 38 | 39 | 40 | 45 | 46 | 47 | 52 | 53 | 54 |
55 |
56 | 57 | 58 |
59 |
60 | 61 | `, 62 | styles: [` 63 | .song-item{ 64 | border-top:1px solid #cfcfcf; 65 | padding: 7px 12px 7px 0px; 66 | overflow: hidden; 67 | } 68 | 69 | .song-item:hover, 70 | .musicchart > .media:hover, .song-item.active{ 71 | background-color: #EBEBEB; 72 | } 73 | 74 | .song-item:first-child{ 75 | border-top:none; 76 | } 77 | 78 | .song-list-information-column { 79 | padding-left: 5px; 80 | padding-right: 5px; 81 | } 82 | 83 | .playlist-item-control-group{ 84 | padding-left: 5px; 85 | padding-right: 5px; 86 | } 87 | 88 | .playlist-item-control-group i{ 89 | color: #8D8D8D; 90 | font-size: 16px; 91 | } 92 | 93 | .playlist-item-control-group i:hover{ 94 | color: #a18d93; 95 | } 96 | 97 | .icon-play { 98 | background-image: url(/images/icon-play.png); 99 | background-size: 20px; 100 | width: 20px; 101 | height: 20px; 102 | } 103 | 104 | .icon-share { 105 | background-image: url(/images/icon-share.png); 106 | background-size: 20px; 107 | width: 20px; 108 | height: 20px; 109 | } 110 | 111 | .icon-sound { 112 | background-image: url(/images/sound_click.png); 113 | background-size: 20px; 114 | width: 20px; 115 | height: 15px; 116 | } 117 | 118 | .icon-remove { 119 | background-image: url(/images/remove.png); 120 | background-size: 20px; 121 | width: 20px; 122 | height: 20px; 123 | } 124 | .icon-add { 125 | background-image: url(/images/icon-add.png); 126 | background-size: 20px; 127 | width: 20px; 128 | height: 20px; 129 | } 130 | 131 | .playlist-item-image{ 132 | width: 60px; 133 | cursor: pointer; 134 | border-radius: 5px; 135 | } 136 | `], 137 | directives: [NgIf] 138 | }) 139 | export class SongItemCmp 140 | { 141 | @Input('playing-song') playingSong = null; 142 | 143 | @Input() song; 144 | 145 | @Input("show-add") showAdd: boolean; 146 | 147 | @Input("show-delete") showDelete: boolean; 148 | 149 | @Input("show-play") showPlay: boolean; 150 | 151 | constructor(private playlistService: PlaylistService, 152 | private soundManager: SoundManager) { 153 | 154 | } 155 | 156 | addSongToPlaylist(song: Song) { 157 | this.playlistService.add(song); 158 | } 159 | 160 | play(song: Song) { 161 | this.soundManager.play(song); 162 | } 163 | 164 | delete(song) { 165 | this.playlistService.remove(song); 166 | } 167 | 168 | getSongImage(song: Song) { 169 | if (song.imageUrl != null) { 170 | return song.imageUrl; 171 | } 172 | return '/images/artist_placeholder.png'; 173 | } 174 | 175 | } -------------------------------------------------------------------------------- /src/tabList/TabList.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | import {SearchTabCmp} from './searchTab/SearchTab.ts'; 3 | import {PlaylistCmp} from './Playlist.ts'; 4 | import {PlaylistService} from '../services/PlaylistService.ts'; 5 | 6 | @Component({ 7 | selector: 'tablist', 8 | template: ` 9 |
10 |
11 |
12 | 22 | 23 |
24 | 25 | 26 |
27 | 28 |
29 |
30 |
31 | `, 32 | directives: [SearchTabCmp, PlaylistCmp], 33 | styles: [` 34 | .tab-content{ 35 | min-height: 300px; 36 | } 37 | .tab-content .col-xs-12{ 38 | padding-right: 11px; 39 | padding-left: 11px; 40 | } 41 | .nav-tabs>li{ 42 | width: 50%; 43 | box-sizing:border-box; 44 | font-size: 12px; 45 | } 46 | .nav-tabs { 47 | border: 1px solid #ff8b00; 48 | border-radius: 5px; 49 | margin: 0 5px; 50 | } 51 | 52 | .nav-tabs a{ 53 | color:#ff8b00; 54 | } 55 | .nav-tabs li > a:hover, 56 | .nav-tabs li.active > a:hover{ 57 | background: #ff8b00; 58 | color: #363636; 59 | } 60 | .nav-tabs li.active > a{ 61 | background: #ff8b00; 62 | color: #363636; 63 | } 64 | .nav-tabs>li.active>a, .nav-tabs>li.active>a:hover, .nav-tabs>li.active>a:focus{ 65 | border-bottom: 1px solid #ff8b00; 66 | border-top: none; 67 | border-left: none; 68 | border-right: none; 69 | background-color: #ff8b00; 70 | } 71 | .nav-tabs li > a{ 72 | border-radius: 0; 73 | border: 0; 74 | margin: 0; 75 | background: #fff; 76 | } 77 | .nav-tabs li:first-child > a{ 78 | border-radius: 5px 0 0 5px; 79 | } 80 | .nav-tabs li:last-child > a{ 81 | border-radius: 0 5px 5px 0; 82 | } 83 | .nav-tabs>li+li > a, 84 | .nav-tabs>li+li > a:hover{ 85 | border-left: 1px solid #ff8b00; 86 | } 87 | a:hover{ 88 | color: #613203; 89 | } 90 | a{ 91 | color: #000; 92 | } 93 | .nav-pills>li{ 94 | width: 32.3%; 95 | font-size: 13px; 96 | box-sizing: border-box; 97 | } 98 | `] 99 | }) 100 | export class TabListCmp { 101 | isShowSearchTab = false; 102 | isShowPlaylist = false; 103 | 104 | constructor(private playlistService: PlaylistService) { 105 | this.showSearchList(); 106 | } 107 | 108 | showPlaylist() { 109 | this.isShowPlaylist = true; 110 | this.isShowSearchTab = false; 111 | } 112 | 113 | showSearchList() { 114 | this.isShowPlaylist = false; 115 | this.isShowSearchTab = true; 116 | } 117 | } -------------------------------------------------------------------------------- /src/tabList/searchTab/SearchBox.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Output, OnInit} from 'angular2/core'; 2 | import {SearchFactory} from '../../services/SearchFactory.ts'; 3 | import {ISearch} from '../../interfaces/ISearch.ts'; 4 | import {NgFormControl, Control} from 'angular2/common'; 5 | 6 | @Component({ 7 | selector: 'search-box', 8 | template: ` 9 | 24 | `, 25 | styles: [` 26 | #searchbox { 27 | padding: 10px 10px 0 6px; 28 | } 29 | 30 | span.icon-search{ 31 | background-image: url(/images/icon-search.png); 32 | background-size: 17px; 33 | position: absolute; 34 | width: 17px; 35 | height: 16px; 36 | top: 8px; 37 | left: 21px; 38 | } 39 | 40 | button.search, 41 | input#form-field-search{ 42 | border-radius: 5px; 43 | border: 1px solid #ccc; 44 | color: #363636; 45 | box-shadow: none; 46 | padding: 6px 30px; 47 | height: 32px; 48 | } 49 | 50 | button.search{ 51 | background: #ffc803; 52 | } 53 | `], 54 | providers: [ 55 | SearchFactory 56 | ], 57 | directives: [NgFormControl] 58 | }) 59 | export class SearchBoxCmp implements OnInit { 60 | @Output() searchResult = new EventEmitter(); 61 | 62 | private searchFactory: SearchFactory; 63 | private searchClient: ISearch; 64 | private keyword = new Control(); 65 | 66 | constructor(searchFactory: SearchFactory) { 67 | this.searchFactory = searchFactory; 68 | this.searchClient = this.searchFactory.getSearchClient(1); 69 | 70 | this.keyword 71 | .valueChanges 72 | .debounceTime(400) 73 | .distinctUntilChanged() 74 | .flatMap(keywordStr => this.searchClient.search(keywordStr.toString())) 75 | .subscribe(data => { 76 | this.searchResult.emit(data); 77 | }); 78 | } 79 | 80 | ngOnInit() { 81 | 82 | } 83 | 84 | search(keyword: string) { 85 | this.searchClient 86 | .search(keyword) 87 | .subscribe(data => { 88 | this.searchResult.emit(data); 89 | }); 90 | } 91 | 92 | onInputKeyword($event: any) { 93 | if ($event.keyCode == 13) { 94 | return this.search($event.target.value); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/tabList/searchTab/SearchResult.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from 'angular2/core'; 2 | import {NgFor} from 'angular2/common'; 3 | import {PlaylistService} from '../../services/PlaylistService.ts'; 4 | import {Song} from '../../interfaces/Song.ts'; 5 | import {SongItemCmp} from '../SongItem.ts'; 6 | 7 | @Component({ 8 | selector: 'search-result', 9 | template: ` 10 | 11 |
12 | 15 | 16 |
17 | 18 |

Search your music on SoundCloud

19 |
20 | 21 |
22 | `, 23 | styles: [` 24 | #searchResult{ 25 | margin: 0 5px 0 7px; 26 | } 27 | 28 | #search-help{ 29 | opacity: 0.8 30 | } 31 | #search-help img{ 32 | margin-left:40px; 33 | margin-bottom: 15px; 34 | } 35 | 36 | #search-help p{ 37 | text-align: center; 38 | } 39 | `], 40 | directives: [NgFor, SongItemCmp] 41 | }) 42 | export class SearchResultCmp { 43 | 44 | @Input() result: Song[]; 45 | 46 | private showAdd: boolean = true; 47 | 48 | private playlistService: PlaylistService; 49 | 50 | constructor(playlistService: PlaylistService) { 51 | this.playlistService = playlistService; 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/tabList/searchTab/SearchTab.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | import {Song} from '../../interfaces/Song.ts'; 3 | import {SearchBoxCmp} from './SearchBox.ts'; 4 | import {SearchResultCmp} from './SearchResult.ts'; 5 | 6 | 7 | @Component({ 8 | selector: 'search-tab', 9 | template: ` 10 |
11 | 12 | 13 | `, 14 | styles: [` 15 | .search-tab { 16 | max-height: 400px; 17 | overflow-y: scroll; 18 | overflow-x: hidden; 19 | min-height: 400px; 20 | } 21 | `], 22 | directives: [SearchBoxCmp, SearchResultCmp] 23 | }) 24 | export class SearchTabCmp { 25 | 26 | public _searchResult: Song[] = []; 27 | 28 | constructor() { 29 | } 30 | 31 | onReceiveSearchResult(data) { 32 | this._searchResult = data; 33 | } 34 | } -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "removeComments": false, 9 | "noImplicitAny": false 10 | } 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "removeComments": false, 9 | "noImplicitAny": false 10 | } 11 | } --------------------------------------------------------------------------------