diff --git a/example/README.md b/examples/basic_example/README.md similarity index 100% rename from example/README.md rename to examples/basic_example/README.md diff --git a/example/client.html b/examples/basic_example/client.html similarity index 99% rename from example/client.html rename to examples/basic_example/client.html index ac9422b..ddb4bd7 100644 --- a/example/client.html +++ b/examples/basic_example/client.html @@ -5,7 +5,7 @@ + + + + + React App + + + +
+ + + diff --git a/examples/react_example/public/manifest.json b/examples/react_example/public/manifest.json new file mode 100644 index 0000000..1f2f141 --- /dev/null +++ b/examples/react_example/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/react_example/public/napster.min.js b/examples/react_example/public/napster.min.js new file mode 100644 index 0000000..13d7aa9 --- /dev/null +++ b/examples/react_example/public/napster.min.js @@ -0,0 +1 @@ +!function(a,b,c){"use strict";function d(){this.frameReady=!1,this.ready=!1}function e(){return this.streamingPlayer=void 0,this.queued=[],this.played=[],this.repeat=!1,this.frameReady=!1,this.ready=!1,this}if(d.prototype.auth=function(){m.api.consumerKey&&m.member.accessToken&&m.windows(this.win).post("auth",{consumerKey:m.api.consumerKey,accessToken:m.member.accessToken})},d.prototype.play=function(a){return m.previewer.pause(),m.windows(this.win).post("play",a),this},d.prototype.pause=function(){return m.windows(this.win).post("pause"),this},d.prototype.resume=function(){},d.prototype.next=function(){m.windows(this.win).post("playNext")},d.prototype.previous=function(){m.windows(this.win).post("playPrevious")},d.prototype.queue=function(){return m.windows(this.win).post("queue",o),this},d.prototype.clearQueue=function(){m.windows(this.win).post("clearQueue")},d.prototype.toggleShuffle=function(){m.windows(this.win).post("toggleShuffle")},d.prototype.toggleClass=function(){m.windows(this.win).post("toggleRepeat")},d.prototype.seek=function(){m.windows(this.win).post("seek",t)},d.prototype.setVolume=function(){m.windows(this.win).post("setVolume",n)},d.prototype.fire=function(a){window.parent.postMessage({type:a},"*")},d.prototype.on=function(a,b){var c=this;return window.addEventListener("message",function(d){if("playerframeready"===d.data.type?c.frameReady=!0:"ready"===d.data.type?c.ready=!0:"playsessionexpired"===d.data.type&&(c.paused=!1,c.playing=!1),c.frameReady&&c.ready&&!c.authed&&(c.authed=!0,c.auth()),d.data.type===a){if(d.data.data&&d.data.data.id){d.data.data.id=d.data.data.id.replace("tra","Tra");var e=d.data.data.code,f="PlayStarted"===e||"PlayComplete"!==e&&"Paused"!==e&&"BufferEmpty"!==e&&"NetworkDropped"!==e&&"PlayInterrupted"!==e&&"IdleTimeout"!==e,g="Paused"===e||"NetworkDropped"===e||"PlayInterrupted"===e||"IdleTimeout"===e;c.playing=d.data.data.playing=f,c.paused=d.data.data.paused=g,c.currentTrack=c.playing||c.paused?d.data.data.id:null}b.call(this,d.data)}}),this},e.prototype.auth=function(){var a=this;this.streamingPlayer=new DrmStreamingPlayer({id:"napster-streaming-player",apikey:h,token:m.member.accessToken,bitrate:192,downgrade:!0,currentUser:{},env:"production"}),this.streamingPlayer.callbackHandler("trackEnded",function(){window.parent.postMessage({type:"playevent",data:{id:a.currentTrack,code:"PlayComplete",playing:!1}},"*"),!1===a.repeat?a.next():a.streamingPlayer.play(a.currentTrack,"UNKNOWN")}),this.streamingPlayer.callbackHandler("trackProgress",function(){window.parent.postMessage({type:"playtimer",data:{id:a.currentTrack,code:"trackProgress",currentTime:a.streamingPlayer.currentTime(),totalTime:a.streamingPlayer.track.duration}},"*")}),this.streamingPlayer.callbackHandler("sessionExpired",function(){window.parent.postMessage({type:"playsessionexpired",data:{id:a.currentTrack,code:"sessionExpired"}},"*")}),this.streamingPlayer.callbackHandler("trackLoaded",function(){window.parent.postMessage({type:"trackLoaded",data:{id:a.currentTrack,code:"trackLoaded"}},"*")})},e.prototype.play=function(a,b){"number"==typeof b?(this.streamingPlayer.pause(),this.streamingPlayer.seek(a,b,{context:"UNKNOWN"})):this.streamingPlayer.play(a,{context:"UNKNOWN"}),this.played.push(a),this.currentTrack=a,window.parent.postMessage({type:"playevent",data:{id:a,code:"PlayStarted",playing:!0}},"*")},e.prototype.pause=function(){this.streamingPlayer.pause(),window.parent.postMessage({type:"playevent",data:{id:this.currentTrack,code:"Paused",playing:!1}},"*")},e.prototype.resume=function(){this.streamingPlayer.resume(this.currentTrack,{context:"UNKNOWN"}),window.parent.postMessage({type:"playevent",data:{id:this.currentTrack,code:"PlayStarted",playing:!0}},"*")},e.prototype.next=function(){this.pause(),this.queued.length>=1&&this.play(this.queued.pop())},e.prototype.previous=function(){this.pause(),1===this.played.length?(this.streamingPlayer.play(this.played[0],{context:"UNKNOWN"}),window.parent.postMessage({type:"playevent",data:{id:this.played[0],code:"PlayStarted",playing:!0}},"*")):(this.queued.push(this.played.pop()),this.play(this.played.pop()))},e.prototype.queue=function(a){this.queued.push(a)},e.prototype.clearQueue=function(){this.queued=[],this.played=[]},e.prototype.toggleShuffle=function(){this.queued=this.queued.map(function(a){return[Math.random(),a]}).sort(function(a,b){return a[0]-b[0]}).map(function(a){return a[1]})},e.prototype.toggleRepeat=function(){this.repeat=!1===this.repeat},e.prototype.showQueue=function(){return this.queued},e.prototype.showPlayed=function(){return this.played},e.prototype.seek=function(a){this.streamingPlayer.seek(this.currentTrack,a)},e.prototype.setVolume=function(a){this.streamingPlayer.setVolume(a)},e.prototype.fire=function(a){window.parent.postMessage({type:a},"*")},e.prototype.on=function(a,b){var c=this;return window.addEventListener("message",function(d){if("playerframeready"===d.data.type?c.frameReady=!0:"ready"===d.data.type?c.ready=!0:"playsessionexpired"===d.data.type&&(c.paused=!1,c.playing=!1),c.frameReady&&c.ready&&!c.authed&&(c.authed=!0,c.auth()),d.data.type===a){if(d.data.data&&d.data.data.id){d.data.data.id=d.data.data.id.replace("tra","Tra");var e=d.data.data.code,f="PlayStarted"===e||"PlayComplete"!==e&&"Paused"!==e,g="Paused"===e;c.playing=d.data.data.playing=f,c.paused=d.data.data.paused=g,c.currentTrack=c.playing||c.paused?d.data.data.id:null}b.call(this,d.data)}}),this},a&&b&&b.ajax&&c){var f=c.stringify;c.stringify=function(a){return f(a,function(a,b){return"genre"===a?{id:b.id,name:b.name}:b})};var g,h,i=function(a,b,c){var d=a[b];a[b]=function(){return c.length===arguments.length?c.apply(this,arguments):"function"==typeof d?d.apply(this,arguments):void 0}},j="napster.member.accessToken",k="napster.member.refreshToken",l=function(a){for(var b in a)this[b]=a[b]},m={init:function(a){function c(){if(!0===a.isHTML5Compatible)return!0}function f(){if(!navigator.cookieEnabled)return!1;try{if("undefined"==typeof localStorage)return!1}catch(a){return!1}return!0}this.api.consumerKey=a.consumerKey,h=a.consumerKey,this.api.version=a.version||this.api.version,this.api.catalog=a.catalog||this.api.catalog;var i=a.player||"player-frame";if(i&&"string"==typeof i){var j=this,k=b("#"+i);if(c()){if(g="HTML5_PLAYER",!f())throw new Error("Cookies or localStorage is not enabled. Napster.js will not work properly without it.");j.player=new e,b("").appendTo(b(document.body)),b("#napster-streaming-player").css("display","none"),b.ajax({url:"https://api.napster.com/v2/streaming-player.js",dataType:"script",async:!0,success:function(){m.player.fire("ready")}})}else if(g="FLASH_PLAYER",j.player=new d,0===k.length)b(function(){var c=b("").attr("id",i).attr("name",i).attr("src","http://api.napster.com/v1.1/player/index.html?apikey="+a.consumerKey).attr("frameborder","no").attr("style","display:none;").appendTo(b(document.body)).load(function(){j.player.win=c.get(0)})});else{if(!(k.get(0)instanceof HTMLIFrameElement))throw new Error('The element "'+i+'" is not an HTMLIFrameElement.');j.player.win=k.get(0)}}}};m.api={host:"api.napster.com",catalog:"US",version:"v2.2",endpoint:function(a){return(a?"https://":"http://")+[this.host,this.version].join("/")},headers:function(a){var b={};return a&&m.member.accessToken&&(b.Authorization="Bearer "+m.member.accessToken),b},dataType:function(){return"json"},get:function(a,c,d){var e={apikey:this.consumerKey};b.ajax({type:"GET",dataType:this.dataType(),data:e,headers:this.headers(a),url:this.endpoint(a)+c,success:function(a,b,c){d(a)},error:function(a){d({status:a.status,error:a.statusText,response:a.responseJSON})}})},post:function(a,c,d,e){d||(d={}),b.ajax({type:d._method||"POST",data:d,dataType:this.dataType(),headers:this.headers(a),url:this.endpoint(a)+c+(a?"":"?apikey="+this.consumerKey),success:function(a,b,c){e(a)},error:function(a){e({status:a.status,error:a.statusText,response:a.responseJSON})}})},put:function(a,b,c,d){c._method="PUT",this.post.call(this,a,b,c,d)},del:function(a,b,c,d){c._method="DELETE",this.post.call(this,a,b,c,d)}},m.member=new function(){var b={};try{b=new l({accessToken:a.localStorage[j],refreshToken:a.localStorage[k]})}catch(a){throw new Error("Cookies or localStorage is not enabled. Napster.js will not work properly without it.")}return b},m.previewer={play:function(){return this},pause:function(){return this}},m.windows=function(a){return{post:function(b,c){if(!a)throw new Error("An iframe was not found at that reference.");a.contentWindow.postMessage({method:b,args:m.util.jsonClean(c||{})},"*")}}},m.on=function(a,b){window.addEventListener(a,b)},m.util={secondsToTime:function(a){if(!isNaN(a)){var b=Math.floor(a/60),c=Math.floor(a)%60;return b+":"+(c<10?"0"+c:c)}return"0:00"},jsonClean:function(a){return c.parse(c.stringify(a,function(a,b){return"genre"===a?{id:b.id,name:b.name}:b}))}},i(l.prototype,"set",function(b){b&&b.accessToken&&b.refreshToken&&(this.accessToken=a.localStorage[j]=b.accessToken,this.refreshToken=a.localStorage[k]=b.refreshToken,m.player.auth(b.accessToken))}),i(l.prototype,"unset",function(){this.accessToken=this.refreshToken=null,a.localStorage.removeItem(j),a.localStorage.removeItem(k)}),i(l.prototype,"load",function(){return this.accessToken=a.localStorage[j],this.refreshToken=a.localStorage[k],this}),i(l.prototype,"signedIn",function(){return null!=this.accessToken&&null!=this.refreshToken}),a.Napster=m,a.Member=l}}(window,jQuery,JSON); diff --git a/examples/react_example/src/App.css b/examples/react_example/src/App.css new file mode 100644 index 0000000..1655ab8 --- /dev/null +++ b/examples/react_example/src/App.css @@ -0,0 +1,18 @@ +.App { + text-align: center; +} + +#parent { + display: flex; +} +#narrow { + width: 450px; + background: black; + border-right: 20px solid grey; +} + +#wide { + flex: 1; + background: white; + text-align: center; +} diff --git a/examples/react_example/src/App.js b/examples/react_example/src/App.js new file mode 100644 index 0000000..2cd2937 --- /dev/null +++ b/examples/react_example/src/App.js @@ -0,0 +1,15 @@ +import React from 'react'; +import Auth from './Components/Auth'; +import './App.css'; +import './Css/Genre.css'; +import './Css/Track.css'; +import './Css/Player.css'; +import './Css/ProgressBar.css'; + +const App = () => ( +
+ +
+); + +export default App; diff --git a/examples/react_example/src/App.test.js b/examples/react_example/src/App.test.js new file mode 100644 index 0000000..b08ffa2 --- /dev/null +++ b/examples/react_example/src/App.test.js @@ -0,0 +1,7 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import App from './App'; + +it('renders without crashing', () => { + shallow(); +}); diff --git a/examples/react_example/src/Components/Auth.js b/examples/react_example/src/Components/Auth.js new file mode 100644 index 0000000..cd1911d --- /dev/null +++ b/examples/react_example/src/Components/Auth.js @@ -0,0 +1,59 @@ +import React from 'react'; +import Genre from './Genre'; + +const { Napster } = window; + + +export default class App extends React.Component { + constructor(props) { + super(props); + this.state = { + access_token: '', + }; + } + + componentDidMount() { + const detailURL = new URL(window.location); + const currentURL = detailURL.href; + const API_SECRET = process.env.REACT_APP_SECRET_KEY; + const API_KEY = process.env.REACT_APP_API_KEY; + + + if (detailURL.search === '') { + window.location = `https://api.napster.com/oauth/authorize?client_id=${API_KEY}&redirect_uri=${currentURL}&response_type=code`; + } else if (detailURL.search.includes('code')) { + Napster.init({ consumerKey: API_KEY, isHTML5Compatible: true }); + + const code = detailURL.search.substring(6); + + fetch(`https://api.napster.com/oauth/access_token?client_id=${API_KEY}&client_secret=${API_SECRET}&response_type=code&grant_type=authorization_code&redirect_uri=${currentURL}&code=${code}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(result => result.json()) + .then(result => { + Napster.player.on('ready', () => { + Napster.member.set({ + accessToken: result.access_token, + refreshToken: result.refresh_token + }); + }); + + + this.setState({ + access_token: result.access_token, + }); + }); + } + } + + render() { + return ( +
+ {this.state.access_token ? : null} +
+ ); + } +} diff --git a/examples/react_example/src/Components/Genre.js b/examples/react_example/src/Components/Genre.js new file mode 100644 index 0000000..31c61b7 --- /dev/null +++ b/examples/react_example/src/Components/Genre.js @@ -0,0 +1,195 @@ +import React from 'react'; +import Player from './Player'; +import GenreCalls from '../Models/GenreCalls'; +import TrackCalls from '../Models/TrackCalls'; + +let Napster; + +export default class Genre extends React.Component { + constructor(props) { + super(props); + this.state = { + genres: [], + tracks: [], + queue: [], + queueHolder: [], + selectedTrack: {}, + playing: false, + shuffle: false, + isShowing: false, + currentTime: 0, + totalTime: 0, + currentTrackId: "", + repeat: false, + autoplay: true, + }; + } + + componentDidMount() { + this.loadGenres(this.props.token); + Napster = window.Napster; + } + + loadGenres(token) { + return GenreCalls.getGenres(token) + .then(genres => { + if (this.state.genres !== genres) { + this.setState({ genres }); + } + }) + .catch(err => Error(err, "Loading Genres")); + } + + chooseTrackList(token, genre) { + return TrackCalls.getTracks(token, genre) + .then(tracks => { + if (this.state.tracks !== tracks) { + this.setState({ tracks }); + } + }) + .catch(err => Error(err, "Loading Tracks")); + } + + select(track) { + this.setState({ selectedTrack: track }, () => { + Napster.player.play(track.id); + this.isPlaying(true); + this.setState({ currentTrackId: track.id }); + const inQueue = this.state.queue.find(tr => track.id === tr.id); + if (!inQueue) { + this.setState({ queueHolder: this.state.tracks }); + this.setState({ queue: this.state.tracks }, () => { + if (this.state.shuffle) { + const shuffledQueue = [...this.state.queue].sort(() => Math.random() - 0.5); + this.updateQueue(shuffledQueue); + } + }); + } + }); + } + + isPlaying = cmd => { + this.setState({ playing: cmd }); + if (cmd === true) { + Napster.player.on('playtimer', e => { + this.setState({ + currentTime: e.data.currentTime, + totalTime: e.data.totalTime + }); + if (this.state.repeat) { + if (Math.floor(this.state.currentTime) === this.state.totalTime) { + Napster.player.play(this.state.selectedTrack.id); + } + } + if (this.state.autoplay) { + if (Math.floor(this.state.currentTime) === this.state.totalTime) { + const index = this.state.queue.map(q => q.id).indexOf(this.state.selectedTrack.id); + if (index !== 9) { + this.songMovement(this.state.queue[index + 1]); + this.currentTrack(this.state.selectedTrack.id); + Napster.player.play(this.state.queue[index + 1].id); + } else { + this.songMovement(this.state.queue[0]); + this.currentTrack(this.state.selectedTrack.id); + Napster.player.play(this.state.queue[0].id); + } + } + } + }); + } + } + + currentTrack = id => { this.setState({ currentTrackId: id }); } + + isShuffled = cmd => { this.setState({ shuffle: cmd }); } + + updateQueue = newQueue => { this.setState({ queue: newQueue }); } + + songMovement = index => { this.setState({ selectedTrack: index }); } + + songRepeat = cmd => { this.setState({ repeat: cmd }); } + + trackAutoplay = cmd => { this.setState({ autoplay: cmd }); } + + showQueue = () => { + if (this.state.selectedTrack.type === "track") { + if (this.state.isShowing === false) { + this.setState({ isShowing: true }); + } else { + this.setState({ isShowing: false }); + } + } else { + return ""; + } + } + + render() { + const genreList = this.state.genres.map(genre => ( +
{ this.chooseTrackList(this.props.token, genre.id); }} onKeyPress={this.handleKeyPress}> + Genre Art +

{genre.name.toUpperCase()}

+
+ )); + + const trackList = this.state.tracks.map(track => ( +
{ this.select(track); }} onKeyPress={this.handleKeyPress}> + Album Art +
+

{track.name}

+

{track.artistName}

+
+
+ )); + + const queueList = this.state.queue.map(track => ( +
{ this.select(track); }} onKeyPress={this.handleKeyPress}> + Album Art +
+

{track.name}

+

{track.artistName}

+
+
+ )); + + return ( + +
+
+ + {this.state.isShowing && ( +
+

Your Queue

+ {queueList} +
+ )} + {!this.state.isShowing && (
{trackList}
)} +
+
+

WELCOME

+

Select any genre to start listening!

+
+
    {genreList}
+
+
+ ); + } +} diff --git a/examples/react_example/src/Components/Genre.test.js b/examples/react_example/src/Components/Genre.test.js new file mode 100644 index 0000000..74caa7a --- /dev/null +++ b/examples/react_example/src/Components/Genre.test.js @@ -0,0 +1,191 @@ +import React from "react"; +import { shallow } from "enzyme"; +import Genre from './Genre'; +import GenreCalls from '../Models/GenreCalls'; +import TrackCalls from '../Models/TrackCalls'; +import NapsterMock from '../Test/NapsterMock'; + +describe('', () => { + let wrapper; + + const tracks = { + id: "1234", + name: "Hello", + artist: "Adele", + type: "track" + }; + const noTrack = { + type: "none" + }; + + const queue = [{ id: "1234", name: "Hello", artist: "Adele" }, { id: "1357", name: "Beautiful Girls", artist: "Sean Kingston" }]; + const genre = [{ id: "g.115", name: "Pop", links: { childGenres: { ids: ["g.1"] } } }, { id: "g.5", name: "Rock", links: { childGenres: { ids: ["g.1"] } } }]; + + beforeEach(() => { + GenreCalls.getGenres = jest.fn(() => Promise.resolve(genre)); + TrackCalls.getTracks = jest.fn(() => Promise.resolve(tracks)); + + window.Napster = new NapsterMock(); + + wrapper = shallow( + + ); + }); + + it('should render without crashing', () => { + expect(wrapper); + }); + + describe('loadGenres', () => { + it('should load genre data', async () => { + await wrapper.instance().loadGenres(); + expect(GenreCalls.getGenres).toHaveBeenCalled(); + expect(wrapper.state('genres')).toBe(genre); + }); + + it('should render the genre imgs and titles', () => { + expect(wrapper.find(`div[className^="genre-btn"]`).exists()).toBe(true); + }); + }); + + describe('chooseTrackList', () => { + it('should load track data', async () => { + expect(wrapper.state('tracks')).toEqual([]); + await wrapper.instance().chooseTrackList(); + expect(TrackCalls.getTracks).toHaveBeenCalled(); + expect(wrapper.state('tracks')).toEqual(tracks); + }); + }); + + describe('Select', () => { + const selectTestId = { + id: "1111", + }; + + it('should change the state for selected track data if track in queue', () => { + wrapper.setState({ autoplay: false }); + wrapper.instance().select(tracks); + expect(wrapper.state('selectedTrack')).toEqual(tracks); + expect(wrapper.state('currentTrackId')).toEqual(tracks.id); + }); + + it('should change the state for selected track data if track not in queue and shuffle is false', () => { + wrapper.setState({ autoplay: false }); + wrapper.setState({ tracks: queue }); + wrapper.instance().select(selectTestId); + expect(wrapper.state('selectedTrack')).toEqual(selectTestId); + expect(wrapper.state('currentTrackId')).toEqual(selectTestId.id); + expect(wrapper.state('queue')).toEqual(queue); + expect(wrapper.state('queueHolder')).toEqual(queue); + }); + }); + + describe('Showing Queue on Click (render logic)', () => { + it('should show queue and hide tracks when button is turned on', () => { + wrapper.setState({ isShowing: true }); + expect(wrapper.find(`div[id^='queue']`).exists()).toBe(true); + expect(wrapper.find(`div[id^='track']`).exists()).toBe(false); + }); + + it('should hide queue and show tracks when button is turned off', () => { + wrapper.setState({ isShowing: false }); + expect(wrapper.find(`div[id^='queue']`).exists()).toBe(false); + expect(wrapper.find(`div[id^='track']`).exists()).toBe(true); + }); + }); + + describe('IsPlaying', () => { + const currentTime = 5; + const totalTime = 5; + it('should turn repeat on', () => { + wrapper.setState({ + selectedTrack: tracks, + queue, + currentTime, + totalTime, + repeat: true, + autoplay: false + }); + expect(wrapper.state('repeat')).toBe(true); + expect(wrapper.state('selectedTrack')).toBe(tracks); + expect(wrapper.state('playing')).toBe(false); + wrapper.find('Player').props().isPlaying(true); + expect(wrapper.state('playing')).toBe(true); + }); + + it('should turn autoplay on', () => { + wrapper.setState({ + selectedTrack: tracks, + queue, + currentTime, + totalTime, + repeat: false + }); + expect(wrapper.state('selectedTrack')).toBe(tracks); + expect(wrapper.state('playing')).toBe(false); + wrapper.find('Player').props().isPlaying(true); + expect(wrapper.state('playing')).toBe(true); + expect(wrapper.state('selectedTrack')).toBe(queue[1]); + expect(wrapper.state('currentTrackId')).toBe("1357"); + }); + }); + + describe('Callback Functions', () => { + it('should change the state of currentTrackId', async () => { + expect(wrapper.state('currentTrackId')).toBe(""); + wrapper.find('Player').props().currentTrack(tracks.id); + expect(wrapper.state('currentTrackId')).toBe(tracks.id); + }); + + it('should change the state of shuffle', () => { + expect(wrapper.state('shuffle')).toBe(false); + wrapper.find('Player').props().isShuffled(true); + expect(wrapper.state('shuffle')).toBe(true); + }); + + it('should change the state of queue', () => { + expect(wrapper.state('queue')).toEqual([]); + wrapper.find('Player').props().updateQueue(queue); + expect(wrapper.state('queue')).toBe(queue); + }); + + it('should change the state of selectedTrack', () => { + expect(wrapper.state('selectedTrack')).toEqual({}); + wrapper.find('Player').props().songMovement(tracks); + expect(wrapper.state('selectedTrack')).toBe(tracks); + }); + + it('should change the state of repeat', () => { + expect(wrapper.state('repeat')).toBe(false); + wrapper.find('Player').props().songRepeat(true); + expect(wrapper.state('repeat')).toBe(true); + }); + + it('should change the state of autoplay', () => { + expect(wrapper.state('autoplay')).toBe(true); + wrapper.find('Player').props().trackAutoplay(false); + expect(wrapper.state('autoplay')).toBe(false); + }); + + describe('showQueue', () => { + it('should change the state of showing to true', () => { + wrapper.setState({ selectedTrack: tracks }); + expect(wrapper.state('isShowing')).toBe(false); + wrapper.find('Player').props().showQueue(); + expect(wrapper.state('isShowing')).toBe(true); + }); + + it('should change the state of showing to false', () => { + wrapper.setState({ selectedTrack: tracks }); + expect(wrapper.state('isShowing')).toBe(false); + wrapper.find('Player').props().showQueue(); + expect(wrapper.state('isShowing')).toBe(true); + }); + + it('should return " " if no track is selected', () => { + wrapper.setState({ selectedTrack: noTrack }); + expect(wrapper.instance().showQueue(false)).toEqual(''); + }); + }); + }); +}); diff --git a/examples/react_example/src/Components/Player.js b/examples/react_example/src/Components/Player.js new file mode 100644 index 0000000..5f82ef9 --- /dev/null +++ b/examples/react_example/src/Components/Player.js @@ -0,0 +1,136 @@ +import React from 'react'; +import ProgressBar from './ProgressBar'; +import 'font-awesome/css/font-awesome.min.css'; + +let Napster; + +export default class Player extends React.Component { + constructor(props) { + super(props); + this.state = { + }; + Napster = window.Napster; + } + + playPauseResume(track) { + if (track.type === "track") { + if (this.props.currentTrackId === track.id.toLowerCase()) { + if (this.props.playing === false) { + this.props.isPlaying(true); + Napster.player.resume(track.id); + } else if (this.props.playing === true) { + this.props.isPlaying(false); + Napster.player.pause(); + } + } else { + if (this.props.playing === true) { + this.props.isPlaying(false); + Napster.player.pause(); + } else { + this.props.currentTrack(track.id); + this.props.isPlaying(true); + Napster.player.play(track.id); + } + } + } else { + return ''; + } + } + + shuffle(trackList) { + if (this.props.selectedTrack.type === "track") { + if (this.props.shuffle === false) { + const shuffledQueue = [...trackList].sort(() => Math.random() - 0.5); + this.props.isShuffled(true); + this.props.updateQueue(shuffledQueue); + } else { + this.props.isShuffled(false); + this.props.updateQueue(this.props.queueHolder); + } + } else { + return ''; + } + } + + nextPrev(cmd, track) { + if (track.type === "track") { + const index = this.props.queue.map(e => e.id).indexOf(track.id); + if (cmd === "next") { + if (index !== 9) { + this.props.songMovement(this.props.queue[index + 1]); + this.props.isPlaying(true); + this.props.currentTrack(track.id); + Napster.player.play(this.props.queue[index + 1].id); + } else { + this.props.songMovement(this.props.queue[0]); + this.props.isPlaying(true); + this.props.currentTrack(track.id); + Napster.player.play(this.props.queue[0].id); + } + } else if (cmd === "prev") { + if (index !== 0) { + this.props.songMovement(this.props.queue[index - 1]); + this.props.isPlaying(true); + this.props.currentTrack(track.id); + Napster.player.play(this.props.queue[index - 1].id); + } else { + Napster.player.play(this.props.selectedTrack.id); + } + } + } else { + return ''; + } + } + + repeat() { + if (this.props.repeat === false) { + this.props.songRepeat(true); + this.props.trackAutoplay(false); + } else { + this.props.songRepeat(false); + this.props.trackAutoplay(true); + } + } + + + render() { + const select = ( +
+ Album Art +
+

{this.props.selectedTrack.name}

+

{this.props.selectedTrack.artistName}

+
+
+ ); + + const imgPlaceHolder = ( +
+ NapsterCat +
+

Track

+

Artist

+
+
+ ); + + return ( +
+ {this.props.selectedTrack.type === "track" ? (
    {select}
) : (
    {imgPlaceHolder}
)} + +
+ + + + + + +
+ ); + } +} diff --git a/examples/react_example/src/Components/Player.test.js b/examples/react_example/src/Components/Player.test.js new file mode 100644 index 0000000..27fa602 --- /dev/null +++ b/examples/react_example/src/Components/Player.test.js @@ -0,0 +1,226 @@ +import React from "react"; +import { shallow } from "enzyme"; +import Player from './Player'; +import NapsterMock from '../Test/NapsterMock'; + +describe('', () => { + let wrapper; + const track = { + id: "1234", + type: "track", + playbackSeconds: 200 + }; + const noTrack = { + type: "none" + }; + const queue = [{ id: "1234", name: "Hello", artist: "Adele" }, { id: "1357", name: "Beautiful Girls", artist: "Sean Kingston" }]; + + beforeEach(() => { + window.Napster = new NapsterMock(); + wrapper = shallow( + + ); + }); + + it('should render without crashing', () => { + expect(wrapper); + }); + + describe('Image Place Holder vs. Selected Track Image', () => { + it('should show place holder if no track selected', () => { + wrapper = shallow(); + expect(wrapper.find(`img[alt^="Album Art"]`).exists()).toBe(false); + expect(wrapper.find(`img[alt^="NapsterCat"]`).exists()).toBe(true); + }); + + it('should show track image if track selected', () => { + wrapper = shallow(); + expect(wrapper.find(`img[alt^="Album Art"]`).exists()).toBe(true); + expect(wrapper.find(`img[alt^="NapsterCat"]`).exists()).toBe(false); + }); + }); + + describe('Repeat function and button', () => { + it('should call repeat function and callbacks in Genre', () => { + const repeatMock = jest.fn(); + const autoplayMock = jest.fn(); + wrapper = shallow(); + wrapper.find(`button[title^='Repeat']`).simulate('click'); + expect(repeatMock).toHaveBeenCalledTimes(1); + expect(autoplayMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('Show Queue Button', () => { + it('should call show queue callback function in Genre', () => { + const mock = jest.fn(); + wrapper = shallow(); + wrapper.find(`button[title^='Show Queue']`).simulate('click'); + expect(mock).toHaveBeenCalledTimes(1); + }); + }); + + describe('NextPrev', () => { + it('should make the proper callbacks to Genre if track and next are selected', () => { + const mockSongMovement = jest.fn(); + const mockIsPlaying = jest.fn(); + const mockCurrentTrack = jest.fn(); + wrapper = shallow( + + ); + wrapper.find(`button[title^='Next Song']`).simulate('click'); + expect(mockSongMovement).toHaveBeenCalledTimes(1); + expect(mockIsPlaying).toHaveBeenCalledTimes(1); + expect(mockCurrentTrack).toHaveBeenCalledTimes(1); + }); + + it('should move backward in queue if track and prev are selected', () => { + const mockSongMovement = jest.fn(); + const mockIsPlaying = jest.fn(); + const mockCurrentTrack = jest.fn(); + wrapper = shallow( + + ); + wrapper.find(`button[title^='Next Song']`).simulate('click'); + expect(mockSongMovement).toHaveBeenCalledTimes(1); + expect(mockIsPlaying).toHaveBeenCalledTimes(1); + expect(mockCurrentTrack).toHaveBeenCalledTimes(1); + }); + + it('should return an empty string if no track selected', () => { + wrapper = shallow(); + wrapper.find(`button[title^='Previous Song']`).simulate('click'); + expect(wrapper.instance().nextPrev("prev", noTrack)).toEqual(''); + }); + }); + + describe('shuffle', () => { + it('should shuffle queue if track selected and shuffle initially false', () => { + const mockIsShuffled = jest.fn(); + const mockUpdateQueue = jest.fn(); + wrapper = shallow( + + ); + wrapper.instance().shuffle(queue); + expect(mockIsShuffled).toHaveBeenCalledTimes(1); + expect(mockUpdateQueue).toHaveBeenCalledTimes(1); + }); + + it('should shuffle queue if track selected and shuffle initially true', () => { + const mockIsShuffled = jest.fn(); + const mockUpdateQueue = jest.fn(); + wrapper = shallow( + + ); + wrapper.find(`button[title^='Shuffle']`).simulate('click'); + expect(mockIsShuffled).toHaveBeenCalledTimes(1); + expect(mockUpdateQueue).toHaveBeenCalledTimes(1); + }); + + it('should return an empty string if no track selected', () => { + wrapper = shallow(); + wrapper.find(`button[title^='Shuffle']`).simulate('click'); + expect(wrapper.instance().shuffle(queue)).toEqual(''); + }); + }); + + describe('playPauseResume', () => { + it('track selected, ids match, playing false', () => { + const mockIsPlaying = jest.fn(); + wrapper = shallow( + + ); + wrapper.find(`button[title^='Play']`).simulate('click'); + expect(mockIsPlaying).toHaveBeenCalledTimes(1); + }); + + it('track selected, ids match, playing true', () => { + const mockIsPlaying = jest.fn(); + wrapper = shallow( + + ); + wrapper.find(`button[title^='Pause']`).simulate('click'); + expect(mockIsPlaying).toHaveBeenCalledTimes(1); + }); + + it('track selected, ids dont match, playing true', () => { + const mockIsPlaying = jest.fn(); + wrapper = shallow( + + ); + wrapper.find(`button[title^='Pause']`).simulate('click'); + expect(mockIsPlaying).toHaveBeenCalledTimes(1); + }); + + it('track selected, ids dont match, playing false', () => { + const mockIsPlaying = jest.fn(); + const mockCurrentTrack = jest.fn(); + wrapper = shallow( + + ); + wrapper.find(`button[title^='Play']`).simulate('click'); + expect(mockIsPlaying).toHaveBeenCalledTimes(1); + }); + + it('should return an empty string if no track selected', () => { + wrapper = shallow(); + wrapper.find(`button[title^='Play']`).simulate('click'); + expect(wrapper.instance().playPauseResume(noTrack)).toEqual(''); + }); + }); +}); diff --git a/examples/react_example/src/Components/ProgressBar.js b/examples/react_example/src/Components/ProgressBar.js new file mode 100644 index 0000000..101c730 --- /dev/null +++ b/examples/react_example/src/Components/ProgressBar.js @@ -0,0 +1,63 @@ +import React from 'react'; + +let Napster; + +export default class ProgressBar extends React.Component { + constructor(props) { + super(props); + this.state = { + }; + Napster = window.Napster; + } + + calculateTotalValue(track) { + if (track.type === "track") { + const length = track.playbackSeconds; + const minutes = Math.floor(length / 60); + let seconds = track.playbackSeconds % 60; + if (seconds < 10) { + seconds = `0${seconds}`; + } + return `${minutes}:${seconds}`; + } else { + return "0:00"; + } + } + + normalizeTime = t => { + if (this.props.selectedTrack.type === "track") { + if (t === undefined) { + return "0:00"; + } else { + const time = Math.floor(t); + const minutes = Math.floor(time / 60); + let seconds = time % 60; + if (seconds < 10) { + seconds = `0${seconds}`; + } + const timeout = `${minutes}:${seconds}`; + return timeout; + } + } else { + return "0:00"; + } + } + + seek = event => { + if (this.props.playing) { + if (this.props.selectedTrack.type === "track") { + Napster.player.seek(event.target.value); + return event.target.value; + } + } + } + + render() { + return ( +
+ + {` ${this.normalizeTime(this.props.currentTime)}/${this.calculateTotalValue(this.props.selectedTrack)}`} +
+ ); + } +} diff --git a/examples/react_example/src/Components/ProgressBar.test.js b/examples/react_example/src/Components/ProgressBar.test.js new file mode 100644 index 0000000..955efa1 --- /dev/null +++ b/examples/react_example/src/Components/ProgressBar.test.js @@ -0,0 +1,93 @@ +import React from "react"; +import { shallow } from "enzyme"; +import ProgressBar from './ProgressBar'; +import NapsterMock from '../Test/NapsterMock'; + + +describe('', () => { + let wrapper; + + const track = { + id: "1234", + type: "track", + playbackSeconds: 200 + }; + const noTrack = { + type: "none" + }; + + beforeEach(() => { + wrapper = shallow( + + ); + window.Napster = new NapsterMock(); + }); + + it('should render without crashing', () => { + expect(wrapper); + }); + + it('should render progress bar on mount', () => { + expect(wrapper.find(`input`).exists()).toBe(true); + }); + + describe('calculateTotalValue', () => { + it('should print time in minutes and seconds format if there is a track selected', () => { + wrapper.instance().calculateTotalValue(track); + expect(wrapper.instance().calculateTotalValue(track)).toEqual("3:20"); + }); + + it('should print 0:00 if there is not a track selected', () => { + wrapper.instance().calculateTotalValue(noTrack); + expect(wrapper.instance().calculateTotalValue(noTrack)).toEqual("0:00"); + }); + }); + + describe('normalizeTime', () => { + it('should print time in minutes and seconds format if there is a track selected', () => { + wrapper.instance().normalizeTime(40); + expect(wrapper.instance().normalizeTime(40)).toEqual("0:40"); + }); + + it('should print 0:00 if currentTime is undefined', () => { + wrapper.instance().normalizeTime(null); + expect(wrapper.instance().normalizeTime(null)).toBe("0:00"); + }); + + it('should print 0:00 if there is not a track selected', () => { + wrapper = shallow(); + wrapper.instance().normalizeTime(1); + expect(wrapper.instance().normalizeTime(1)).toBe("0:00"); + }); + }); + + describe('seek', () => { + const event = { + target: { + value: 20 + } + }; + + it('should seek to new time if playing and if track is selected', () => { + wrapper = shallow(); + wrapper.find(`input`).simulate('change', event); + expect(wrapper.instance().seek(event)).toBe(event.target.value); + }); + + it('should do nothing if no track is selected', () => { + wrapper = shallow(); + wrapper.find(`input`).simulate('change', event); + expect(wrapper.instance().seek(event)).toEqual(); + }); + + it('should do nothing if not playing', () => { + wrapper.find(`input`).simulate('change', event); + expect(wrapper.instance().seek(event)).toEqual(); + }); + }); +}); diff --git a/examples/react_example/src/Css/Genre.css b/examples/react_example/src/Css/Genre.css new file mode 100644 index 0000000..bf6235c --- /dev/null +++ b/examples/react_example/src/Css/Genre.css @@ -0,0 +1,35 @@ +.genre-btn { + background-color: transparent; + cursor: default; + display: inline-block; + width: 20%; + height: 100px; +} + +.genre-btn img{ + object-fit: scale-down; + width: 200px; + height: 100px; + padding: none; + padding-top: 10px; +} + +.genre-btn h3{ + text-align: center; + color: #2ca6de; + font-size: 12px; +} + +.header{ + font-style: normal; + font-weight: bold; + font-size: 45px; + color: #2ca6de; +} + +.message{ + font-style: normal; + font-weight: normal; + font-size: 20px; + color: #2ca6de; +} diff --git a/examples/react_example/src/Css/Player.css b/examples/react_example/src/Css/Player.css new file mode 100644 index 0000000..c23032b --- /dev/null +++ b/examples/react_example/src/Css/Player.css @@ -0,0 +1,57 @@ +.player-btn { + background-color: black; + border: 2px solid white; + color: white; + padding: 12px 16px; + font-size:16px; + cursor: pointer; + border-radius: 50%; +} + +.player-toggle { + background-color: #2ca6de; + border: 2px solid white; + color: white; + padding: 12px 16px; + font-size:16px; + cursor: pointer; + border-radius: 50%; +} + +.select { + display: flex; + color: white; +} + +.select img { + margin-left: 25px; + margin-bottom: 3px; + display: block; + object-fit: scale-down; + width: 140px; + height: 140px; +} + +.select h3 { + margin: 0; + padding: 5px 10px; + font-size: 20px; +} + +.select p { + margin: 0; + padding: 5px 10px; + font-size: 15px; +} + +.demobox { + background-color: black; + position: relative; + align: center; + padding-left: 10px; + border: 5px solid black; + width: 430px; + height: 275px; + font-size: 14px; + text-align: center; +} diff --git a/examples/react_example/src/Css/ProgressBar.css b/examples/react_example/src/Css/ProgressBar.css new file mode 100644 index 0000000..ce2c7ae --- /dev/null +++ b/examples/react_example/src/Css/ProgressBar.css @@ -0,0 +1,92 @@ +input[type=range] { + height: 5px; + -webkit-appearance: none; + margin: 10px 0; + width: 50%; +} +input[type=range]:focus { + outline: none; +} +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 5px; + cursor: pointer; + box-shadow: 0px 0px 0px #2ca6de; + background: #2ca6de; + border-radius: 1px; + border: 0px solid #2ca6de; +} +input[type=range]::-webkit-slider-thumb { + box-shadow: 0px 0px 0px #2ca6de; + border: 1px solid #2497E3; + height: 18px; + width: 18px; + border-radius: 25px; + background: #B7D5E7; + cursor: pointer; + -webkit-appearance: none; + margin-top: -7px; +} +input[type=range]:focus::-webkit-slider-runnable-track { + background: #2ca6de; +} +input[type=range]::-moz-range-track { + width: 100%; + height: 5px; + cursor: pointer; + animate: 0.2s; + box-shadow: 0px 0px 0px #2ca6de; + background: #2497E3; + border-radius: 1px; + border: 0px solid #2ca6de; +} +input[type=range]::-moz-range-thumb { + box-shadow: 0px 0px 0px #2ca6de; + border: 1px solid #2497E3; + height: 18px; + width: 18px; + border-radius: 25px; + background: #B7D5E7; + cursor: pointer; +} +input[type=range]::-ms-track { + width: 100%; + height: 5px; + cursor: pointer; + background: transparent; + border-color: transparent; + color: transparent; +} +input[type=range]::-ms-fill-lower { + background: #2497E3; + border: 0px solid #2ca6de; + border-radius: 2px; + box-shadow: 0px 0px 0px #2ca6de; +} +input[type=range]::-ms-fill-upper { + background: #2ca6de; + border: 0px solid #2ca6de; + border-radius: 2px; + box-shadow: 0px 0px 0px #2ca6de; +} +input[type=range]::-ms-thumb { + margin-top: 1px; + box-shadow: 0px 0px 0px #2ca6de; + border: 1px solid #2497E3; + height: 18px; + width: 18px; + border-radius: 25px; + background: #B7D5E7; + cursor: pointer; +} +input[type=range]:focus::-ms-fill-lower { + background: #B7D5E7; +} +input[type=range]:focus::-ms-fill-upper { + background: #B7D5E7; +} + +.text{ + font-size: 14px; + color: white; +} diff --git a/examples/react_example/src/Css/Track.css b/examples/react_example/src/Css/Track.css new file mode 100644 index 0000000..21e02ad --- /dev/null +++ b/examples/react_example/src/Css/Track.css @@ -0,0 +1,39 @@ +.track-btn { + border: none; + background: none; + padding: 5px 10px; + border-radius: 5px; + font-size: 14px; + cursor: pointer; + display: inline-grid; + vertical-align: middle; + width: 103px; + height: 100px; +} + +.content { + display: flex; + color: white; +} + +.content img { + margin-left: 45px; + margin-bottom: 3px; + display: block; + object-fit: scale-down; + width: 70px; + height: 70px; +} + +.content h3, + +.content p { + margin: 0; + padding: 5px 10px; + font-size: 14px; + text-align: left; +} + +.queue { + color: white; +} diff --git a/examples/react_example/src/Models/GenreCalls.js b/examples/react_example/src/Models/GenreCalls.js new file mode 100644 index 0000000..a39149c --- /dev/null +++ b/examples/react_example/src/Models/GenreCalls.js @@ -0,0 +1,18 @@ +const GenreCalls = {}; + +GenreCalls.getGenres = function getGenres(access_token) { + const url = "https://api.napster.com/v2.2/genres?lang=en_US"; + return fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${access_token}`, + 'Content-Type': 'application/json' + } + }) + .then(result => result.json()) + .then(result => result.genres) + .catch(err => Error(err, "Loading Genres")); +}; + + +export default GenreCalls; diff --git a/examples/react_example/src/Models/TrackCalls.js b/examples/react_example/src/Models/TrackCalls.js new file mode 100644 index 0000000..392a870 --- /dev/null +++ b/examples/react_example/src/Models/TrackCalls.js @@ -0,0 +1,18 @@ +const TrackCalls = {}; + +TrackCalls.getTracks = function getTracks(access_token, genre) { + const url = `https://api.napster.com/v2.2/genres/${genre}/tracks/top?limit=10&isStreamableOnly=true`; + return fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${access_token}`, + 'Content-Type': 'application/json' + } + }) + .then(result => result.json()) + .then(result => result.tracks) + .catch(err => Error(err, "Loading Tracks")); +}; + + +export default TrackCalls; diff --git a/examples/react_example/src/Test/NapsterMock.js b/examples/react_example/src/Test/NapsterMock.js new file mode 100644 index 0000000..c7192a1 --- /dev/null +++ b/examples/react_example/src/Test/NapsterMock.js @@ -0,0 +1,14 @@ +class NapsterMock { + constructor() { + this.state = {}; + this.player = { + on: jest.fn().mockImplementation((a, b) => b({ data: { currentTime: 5, totalTime: 5 } })), + play: jest.fn(), + pause: jest.fn(), + resume: jest.fn(), + seek: jest.fn().mockImplementation(a => 0 + a), + }; + } +} + +export default NapsterMock; diff --git a/examples/react_example/src/flash_player.js b/examples/react_example/src/flash_player.js new file mode 100644 index 0000000..4331302 --- /dev/null +++ b/examples/react_example/src/flash_player.js @@ -0,0 +1,97 @@ +function FlashPlayer () { + this.frameReady = false; + this.ready = false; +}; + +FlashPlayer.prototype.auth = function auth() { + if (Napster.api.consumerKey && Napster.member.accessToken) { + Napster.windows(this.win).post('auth', { consumerKey: Napster.api.consumerKey, accessToken: Napster.member.accessToken }); + } +}; + +FlashPlayer.prototype.play = function play(o){ + Napster.previewer.pause(); + Napster.windows(this.win).post('play', o); + return this; +}; + +FlashPlayer.prototype.pause = function pause() { + Napster.windows(this.win).post('pause'); + return this; +}; + +FlashPlayer.prototype.resume = function resume() { + //TODO: figure out how flash does this/ if it is needed, etc. +}; +FlashPlayer.prototype.next = function next() { + Napster.windows(this.win).post('playNext'); +}; + +FlashPlayer.prototype.previous = function previous() { + Napster.windows(this.win).post('playPrevious'); +}; + +FlashPlayer.prototype.queue = function queue() { + Napster.windows(this.win).post('queue', o); + return this; +}; + +FlashPlayer.prototype.clearQueue = function clearQueue() { + Napster.windows(this.win).post('clearQueue'); +}; + +FlashPlayer.prototype.toggleShuffle = function toggleShuffle() { + Napster.windows(this.win).post('toggleShuffle'); +}; + +FlashPlayer.prototype.toggleClass = function toggleClass() { + Napster.windows(this.win).post('toggleRepeat'); +}; + +FlashPlayer.prototype.seek = function seek() { + Napster.windows(this.win).post('seek', t); +}; + +FlashPlayer.prototype.setVolume = function setVolume() { + Napster.windows(this.win).post('setVolume', n); +}; + +FlashPlayer.prototype.fire = function fire(eventName){ + window.parent.postMessage({ type: eventName }, "*"); +} + +FlashPlayer.prototype.on = function on(eventName, callback){ + var p = this; + + window.addEventListener('message', function(m) { + if (m.data.type === 'playerframeready') { + p.frameReady = true; + } + else if (m.data.type === 'ready') { + p.ready = true; + + } + else if (m.data.type === 'playsessionexpired') { + p.paused = false; + p.playing = false; + } + + if (p.frameReady && p.ready && !p.authed) { + p.authed = true; + p.auth(); + } + if (m.data.type === eventName) { + if (m.data.data && m.data.data.id) { + m.data.data.id = m.data.data.id.replace('tra', 'Tra'); + var c = m.data.data.code, + playing = (c === 'PlayStarted' || (c !== 'PlayComplete' && c !== 'Paused' && c !== 'BufferEmpty' && c !== 'NetworkDropped' && c !== 'PlayInterrupted' && c !== 'IdleTimeout')), + paused = (c === 'Paused' || c === 'NetworkDropped' || c === 'PlayInterrupted' || c === 'IdleTimeout'); + p.playing = m.data.data.playing = playing; + p.paused = m.data.data.paused = paused; + p.currentTrack = (p.playing || p.paused) ? m.data.data.id : null; + } + callback.call(this, m.data); + } + }); + return this; +}; diff --git a/examples/react_example/src/genreimages/g.115.jpg b/examples/react_example/src/genreimages/g.115.jpg new file mode 100755 index 0000000..b1d56f2 Binary files /dev/null and b/examples/react_example/src/genreimages/g.115.jpg differ diff --git a/examples/react_example/src/genreimages/g.146.jpg b/examples/react_example/src/genreimages/g.146.jpg new file mode 100755 index 0000000..02d7f69 Binary files /dev/null and b/examples/react_example/src/genreimages/g.146.jpg differ diff --git a/examples/react_example/src/genreimages/g.156.jpg b/examples/react_example/src/genreimages/g.156.jpg new file mode 100755 index 0000000..9b1f157 Binary files /dev/null and b/examples/react_example/src/genreimages/g.156.jpg differ diff --git a/examples/react_example/src/genreimages/g.18.jpg b/examples/react_example/src/genreimages/g.18.jpg new file mode 100755 index 0000000..85b06e4 Binary files /dev/null and b/examples/react_example/src/genreimages/g.18.jpg differ diff --git a/examples/react_example/src/genreimages/g.194.jpg b/examples/react_example/src/genreimages/g.194.jpg new file mode 100755 index 0000000..355bc42 Binary files /dev/null and b/examples/react_example/src/genreimages/g.194.jpg differ diff --git a/examples/react_example/src/genreimages/g.21.jpg b/examples/react_example/src/genreimages/g.21.jpg new file mode 100755 index 0000000..a833b33 Binary files /dev/null and b/examples/react_example/src/genreimages/g.21.jpg differ diff --git a/examples/react_example/src/genreimages/g.246.jpg b/examples/react_example/src/genreimages/g.246.jpg new file mode 100755 index 0000000..af606de Binary files /dev/null and b/examples/react_example/src/genreimages/g.246.jpg differ diff --git a/examples/react_example/src/genreimages/g.299.jpg b/examples/react_example/src/genreimages/g.299.jpg new file mode 100755 index 0000000..ec248fd Binary files /dev/null and b/examples/react_example/src/genreimages/g.299.jpg differ diff --git a/examples/react_example/src/genreimages/g.304.jpg b/examples/react_example/src/genreimages/g.304.jpg new file mode 100755 index 0000000..a1cbbfa Binary files /dev/null and b/examples/react_example/src/genreimages/g.304.jpg differ diff --git a/examples/react_example/src/genreimages/g.33.jpg b/examples/react_example/src/genreimages/g.33.jpg new file mode 100755 index 0000000..605e582 Binary files /dev/null and b/examples/react_example/src/genreimages/g.33.jpg differ diff --git a/examples/react_example/src/genreimages/g.383.jpg b/examples/react_example/src/genreimages/g.383.jpg new file mode 100755 index 0000000..10ccc8c Binary files /dev/null and b/examples/react_example/src/genreimages/g.383.jpg differ diff --git a/examples/react_example/src/genreimages/g.394.jpg b/examples/react_example/src/genreimages/g.394.jpg new file mode 100755 index 0000000..c1581e8 Binary files /dev/null and b/examples/react_example/src/genreimages/g.394.jpg differ diff --git a/examples/react_example/src/genreimages/g.4.jpg b/examples/react_example/src/genreimages/g.4.jpg new file mode 100755 index 0000000..9aeb9a4 Binary files /dev/null and b/examples/react_example/src/genreimages/g.4.jpg differ diff --git a/examples/react_example/src/genreimages/g.407.jpg b/examples/react_example/src/genreimages/g.407.jpg new file mode 100755 index 0000000..e2b0890 Binary files /dev/null and b/examples/react_example/src/genreimages/g.407.jpg differ diff --git a/examples/react_example/src/genreimages/g.437.jpg b/examples/react_example/src/genreimages/g.437.jpg new file mode 100755 index 0000000..19f5c03 Binary files /dev/null and b/examples/react_example/src/genreimages/g.437.jpg differ diff --git a/examples/react_example/src/genreimages/g.438.jpg b/examples/react_example/src/genreimages/g.438.jpg new file mode 100755 index 0000000..065e917 Binary files /dev/null and b/examples/react_example/src/genreimages/g.438.jpg differ diff --git a/examples/react_example/src/genreimages/g.446.jpg b/examples/react_example/src/genreimages/g.446.jpg new file mode 100755 index 0000000..d6ffd8a Binary files /dev/null and b/examples/react_example/src/genreimages/g.446.jpg differ diff --git a/examples/react_example/src/genreimages/g.453.jpg b/examples/react_example/src/genreimages/g.453.jpg new file mode 100755 index 0000000..aa3882c Binary files /dev/null and b/examples/react_example/src/genreimages/g.453.jpg differ diff --git a/examples/react_example/src/genreimages/g.470.jpg b/examples/react_example/src/genreimages/g.470.jpg new file mode 100755 index 0000000..4e489bb Binary files /dev/null and b/examples/react_example/src/genreimages/g.470.jpg differ diff --git a/examples/react_example/src/genreimages/g.488.jpg b/examples/react_example/src/genreimages/g.488.jpg new file mode 100755 index 0000000..6636689 Binary files /dev/null and b/examples/react_example/src/genreimages/g.488.jpg differ diff --git a/examples/react_example/src/genreimages/g.5.jpg b/examples/react_example/src/genreimages/g.5.jpg new file mode 100755 index 0000000..5ae7110 Binary files /dev/null and b/examples/react_example/src/genreimages/g.5.jpg differ diff --git a/examples/react_example/src/genreimages/g.510.jpg b/examples/react_example/src/genreimages/g.510.jpg new file mode 100755 index 0000000..f07d826 Binary files /dev/null and b/examples/react_example/src/genreimages/g.510.jpg differ diff --git a/examples/react_example/src/genreimages/g.69.jpg b/examples/react_example/src/genreimages/g.69.jpg new file mode 100755 index 0000000..4ee7dcd Binary files /dev/null and b/examples/react_example/src/genreimages/g.69.jpg differ diff --git a/examples/react_example/src/genreimages/g.71.jpg b/examples/react_example/src/genreimages/g.71.jpg new file mode 100755 index 0000000..ee6ea90 Binary files /dev/null and b/examples/react_example/src/genreimages/g.71.jpg differ diff --git a/examples/react_example/src/genreimages/g.75.jpg b/examples/react_example/src/genreimages/g.75.jpg new file mode 100755 index 0000000..9f9be30 Binary files /dev/null and b/examples/react_example/src/genreimages/g.75.jpg differ diff --git a/examples/react_example/src/genreimages/g.8223.jpg b/examples/react_example/src/genreimages/g.8223.jpg new file mode 100755 index 0000000..5af41c1 Binary files /dev/null and b/examples/react_example/src/genreimages/g.8223.jpg differ diff --git a/examples/react_example/src/genreimages/g.8224.jpg b/examples/react_example/src/genreimages/g.8224.jpg new file mode 100755 index 0000000..13b16e6 Binary files /dev/null and b/examples/react_example/src/genreimages/g.8224.jpg differ diff --git a/examples/react_example/src/genreimages/g.8245.jpg b/examples/react_example/src/genreimages/g.8245.jpg new file mode 100755 index 0000000..88fad9e Binary files /dev/null and b/examples/react_example/src/genreimages/g.8245.jpg differ diff --git a/examples/react_example/src/genreimages/g.8251.jpg b/examples/react_example/src/genreimages/g.8251.jpg new file mode 100755 index 0000000..061b041 Binary files /dev/null and b/examples/react_example/src/genreimages/g.8251.jpg differ diff --git a/examples/react_example/src/genreimages/g.8258.jpg b/examples/react_example/src/genreimages/g.8258.jpg new file mode 100755 index 0000000..240143b Binary files /dev/null and b/examples/react_example/src/genreimages/g.8258.jpg differ diff --git a/examples/react_example/src/html5_player.js b/examples/react_example/src/html5_player.js new file mode 100644 index 0000000..26f1fc6 --- /dev/null +++ b/examples/react_example/src/html5_player.js @@ -0,0 +1,161 @@ +function Html5Player () { + this.streamingPlayer = undefined; + this.queued = []; + this.played = []; + this.repeat = false; + this.frameReady = false; + this.ready = false; + return this; +}; + +Html5Player.prototype.auth = function auth() { + var that = this; + this.streamingPlayer = new DrmStreamingPlayer({ + id: 'napster-streaming-player', + apikey: API_KEY, + token: Napster.member.accessToken, + bitrate: 192, + downgrade: true, + currentUser: {}, + env: 'production' + }); + this.streamingPlayer.callbackHandler('trackEnded', function() { + window.parent.postMessage({ type: 'playevent', data: { id: that.currentTrack, code: 'PlayComplete', playing: false } }, "*") + if (that.repeat === false){ + that.next(); + } else { + that.streamingPlayer.play(that.currentTrack, 'UNKNOWN'); + } + }); + this.streamingPlayer.callbackHandler('trackProgress', function() { + window.parent.postMessage({ type: 'playtimer', data: { + id: that.currentTrack, + code: 'trackProgress', + currentTime: that.streamingPlayer.currentTime(), + totalTime: that.streamingPlayer.track.duration + } }, "*"); + }); + this.streamingPlayer.callbackHandler('sessionExpired', function() { + window.parent.postMessage({ + type: 'playsessionexpired', data: { + id: that.currentTrack, + code: 'sessionExpired' + } + }, "*"); + }); + + this.streamingPlayer.callbackHandler('trackLoaded', function() { + window.parent.postMessage({ type: 'trackLoaded', data: { + id: that.currentTrack, + code: 'trackLoaded' + } }, "*"); + }); +}; + + +Html5Player.prototype.play = function play(o){ + this.streamingPlayer.play(o, { context: 'UNKNOWN'}); + this.played.push(o) + this.currentTrack = o; + window.parent.postMessage({ type: 'playevent', data: { id: o, code: 'PlayStarted', playing: true } }, "*") +}; +Html5Player.prototype.pause = function pause() { + this.streamingPlayer.pause(); + window.parent.postMessage({ type: 'playevent', data: { id: this.currentTrack, code: 'Paused', playing: false } }, "*") +}; + +Html5Player.prototype.resume = function resume() { + this.streamingPlayer.resume(this.currentTrack, { context: 'UNKNOWN'}); + window.parent.postMessage({ type: 'playevent', data: { id: this.currentTrack, code: 'PlayStarted', playing: true } }, "*") +}; + +Html5Player.prototype.next = function next() { + this.pause(); + if (this.queued.length >= 1) { + // only do something if there are songs left in the queue + this.play(this.queued.pop()); + } +}; +Html5Player.prototype.previous = function previous() { + this.pause(); + if (this.played.length === 1) { + // when there are no songs left, the previous button will just restart the current track, and not do queue manipulation. + this.streamingPlayer.play(this.played[0], { context: 'UNKNOWN'}); + window.parent.postMessage({ type: 'playevent', data: { id: this.played[0], code: 'PlayStarted', playing: true } }, "*"); + } else { + this.queued.push(this.played.pop()); + this.play(this.played.pop()); + } + }; +Html5Player.prototype.queue = function queue(o) { + this.queued.push(o); +}; +Html5Player.prototype.clearQueue = function clearQueue() { + this.queued = []; + this.played = []; +}; +Html5Player.prototype.toggleShuffle = function toggleShuffle() { + this.queued = this.queued.map(function (a) { + return [Math.random(), a]; + }).sort(function (a, b) { + return a[0] - b[0]; + }).map(function (a) { + return a[1]; + }); +}; +Html5Player.prototype.toggleRepeat = function toggleRepeat() { + this.repeat = this.repeat === false ? true : false; +}; +Html5Player.prototype.showQueue = function showQueue() { + return this.queued; +}; +Html5Player.prototype.showPlayed = function showPlayed() { + return this.played; +}; +Html5Player.prototype.seek = function seek(t){ + this.streamingPlayer.seek(this.currentTrack, t); +}; +Html5Player.prototype.setVolume = function setVolume(n){ + this.streamingPlayer.setVolume(n); +}; + +Html5Player.prototype.fire = function fire(eventName){ + window.parent.postMessage({ type: eventName }, "*"); +} + +Html5Player.prototype.on = function on(eventName, callback){ + var p = this; + + window.addEventListener('message', function(m) { + if (m.data.type === 'playerframeready') { + p.frameReady = true; + } + else if (m.data.type === 'ready') { + p.ready = true; + + } + else if (m.data.type === 'playsessionexpired') { + p.paused = false; + p.playing = false; + } + + if (p.frameReady && p.ready && !p.authed) { + p.authed = true; + p.auth(); + } + if (m.data.type === eventName) { + if (m.data.data && m.data.data.id) { + m.data.data.id = m.data.data.id.replace('tra', 'Tra'); + var c = m.data.data.code, + playing = (c === 'PlayStarted' || (c !== 'PlayComplete' && c !== 'Paused')), + paused = (c === 'Paused'); + p.playing = m.data.data.playing = playing; + p.paused = m.data.data.paused = paused; + p.currentTrack = (p.playing || p.paused) ? m.data.data.id : null; + } + + callback.call(this, m.data); + } + }); + return this; +}; diff --git a/examples/react_example/src/index.css b/examples/react_example/src/index.css new file mode 100644 index 0000000..4a1df4d --- /dev/null +++ b/examples/react_example/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} diff --git a/examples/react_example/src/index.js b/examples/react_example/src/index.js new file mode 100644 index 0000000..a735dd2 --- /dev/null +++ b/examples/react_example/src/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import 'font-awesome/css/font-awesome.min.css'; +import * as serviceWorker from './serviceWorker'; + + +ReactDOM.render(, document.getElementById('root')); + +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://bit.ly/CRA-PWA +serviceWorker.unregister(); diff --git a/examples/react_example/src/logo.svg b/examples/react_example/src/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/examples/react_example/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/react_example/src/napster.js b/examples/react_example/src/napster.js new file mode 100644 index 0000000..6d09eba --- /dev/null +++ b/examples/react_example/src/napster.js @@ -0,0 +1,315 @@ +// Copyright (c) 2017 Napster / Rhapsody International +// Code released under the MIT license. +// See https://github.com/Napster/napster.js#the-mit-license-mit for more detail. +// The first thing you need to do, after including Napster.js in your app (but before using it), is initialize the Napster object with your application key. +// +// Napster.init({ +// consumerKey: 'foo' +// }); +// + +(function(exports, $, JSON) { + + 'use strict'; + + //= require 'flash_player.js' + //= require 'html5_player.js' + + if (!exports || !$ || !$.ajax || !JSON) return; + + var stringify = JSON.stringify; + JSON.stringify = function(o) { + return stringify(o, function(k, v) { + if (k === 'genre') { + return { + id: v.id, + name: v.name + }; + } + return v; + }); + }; + + var method = function(o, fname, f) { + var old = o[fname]; + o[fname] = function() { + if (f.length === arguments.length) { + return f.apply(this, arguments); // o -> this + } + else if (typeof old === 'function') { + return old.apply(this, arguments); + } + }; + }; + + var ACCESS_TOKEN_KEY = 'napster.member.accessToken', + REFRESH_TOKEN_KEY = 'napster.member.refreshToken', + streamingPlayer, + player, + API_KEY; + + var Member = function(obj) { + for (var k in obj) { + this[k] = obj[k]; + } + }; + + var Library = function(member) { + this.member = member; + }; + + function isFlash () { + return player === 'FLASH_PLAYER'; + }; + + var Napster = { + // ### Initialization Options + // Set your developer key and application ID here. You can also (optionally) specify which API and catalog versions you prefer. + // + // Napster.init({ + // consumerKey: options.consumerKey, + // version: 'v1', + // catalog: 'EN', + // isHTML5Compatible: true + // }); + + init: function(options) { + this.api.consumerKey = options.consumerKey; + API_KEY = options.consumerKey; + this.api.version = options.version || this.api.version; + this.api.catalog = options.catalog || this.api.catalog; + + function shouldLoadHTML5Engine() { + // Browser detection goes here. Override detection by setting the playback engine option. + + if (options.isHTML5Compatible === true) { + return true; + } + + // TODO: Detect browser + + // Logic should be written as follows. If in IE, return false + // If mobile, chrome, firefox, safari, return true + } + function checkRequirements() { + if (!navigator.cookieEnabled) { + return false; + } + try { + if (typeof localStorage === 'undefined') { + return false; + } + } catch(error) { + return false; + } + return true; + } + + var id = options.player || 'player-frame'; + + if (id && typeof id === 'string') { + var that = this, d = $('#' + id); + + if (shouldLoadHTML5Engine()) { + //Load HTML5 playback engine + player = 'HTML5_PLAYER'; + + if (!checkRequirements()) { + throw new Error('Cookies or localStorage is not enabled. Napster.js will not work properly without it.') + } + + that.player = new Html5Player(); + $("").appendTo($(document.body)); + $("#napster-streaming-player").css("display","none"); + $.ajax({ + url: 'https://api.napster.com/v2/streaming-player.js', + dataType: 'script', + async: true, + success: function () { + Napster.player.fire('ready'); + } + }); + } else { + //Fallback to flash + player = 'FLASH_PLAYER'; + that.player = new FlashPlayer(); + if (d.length === 0) { + $(function() { + var f = $('') + .attr('id', id) + .attr('name', id) + .attr('src', 'http://api.napster.com/v1.1/player/index.html?apikey=' + options.consumerKey) + .attr('frameborder', 'no') + .attr('style', 'display:none;') + .appendTo($(document.body)) + .load(function() { + that.player.win = f.get(0); + }); + }); + } + else if (d.get(0) instanceof HTMLIFrameElement) { + that.player.win = d.get(0); + } + else { + throw new Error('The element "' + id + '" is not an HTMLIFrameElement.') + } + } + } + } + } + + Napster.api = { + host: 'api.napster.com', + catalog: 'US', + version: 'v2.2', + endpoint: function(secure) { + return (secure ? 'https://' : 'http://') + [this.host, this.version].join('/'); + }, + headers: function(secure) { + var h = {}; + + if (secure && Napster.member.accessToken) { + h['Authorization'] = 'Bearer ' + Napster.member.accessToken; + } + + return h; + }, + dataType: function() { + return 'json'; + }, + + get: function(secure, path, cb) { + + var data = { apikey: this.consumerKey }; + + $.ajax({ + type: 'GET', + dataType: this.dataType(), + data: data, + headers: this.headers(secure), + url: this.endpoint(secure) + path, + success: function(data, textStatus, jqXHR) { + cb(data); + }, + error: function(jqXHR) { + cb({ status: jqXHR.status, error: jqXHR.statusText, response: jqXHR.responseJSON }); + } + }); + }, + + post: function(secure, path, data, cb) { + + if (!data) data = {}; + + $.ajax({ + type: data._method || 'POST', + data: data, + dataType: this.dataType(), + headers: this.headers(secure), + url: this.endpoint(secure) + path + (secure ? '' : '?apikey=' + this.consumerKey), + success: function(data, textStatus, jqXHR) { + cb(data); + }, + error: function(jqXHR) { + cb({ status: jqXHR.status, error: jqXHR.statusText, response: jqXHR.responseJSON }); + } + }); + }, + + put: function(secure, path, data, cb) { + data._method = 'PUT'; + this.post.call(this, secure, path, data, cb); + }, + + del: function(secure, path, data, cb) { + data._method = 'DELETE'; + this.post.call(this, secure, path, data, cb); + } + }; + + Napster.member = new function() { + var m = {}; + try { + m = new Member({ + accessToken: exports.localStorage[ACCESS_TOKEN_KEY], + refreshToken: exports.localStorage[REFRESH_TOKEN_KEY] + }); + } catch(error) { + throw new Error('Cookies or localStorage is not enabled. Napster.js will not work properly without it.'); + } + return m; + }; + Napster.previewer = { + play: function() { + return this; + }, + pause: function() { + return this; + } + }; + Napster.windows = function(win) { + return { + post: function(method, args) { + if (!win) { + throw new Error('An iframe was not found at that reference.'); + } + win.contentWindow.postMessage({ method: method, args: Napster.util.jsonClean(args || {}) }, "*"); + } + } + }; + Napster.on = function(eventName, callback) { + window.addEventListener(eventName, callback); + }; + Napster.util = { + secondsToTime: function(s) { + if (!isNaN(s)) { + var minutes = Math.floor(s / 60); + var seconds = Math.floor(s) % 60; + return minutes + ':' + ((seconds < 10) ? '0' + seconds : seconds); + } + return '0:00'; + }, + jsonClean: function(o) { + return JSON.parse(JSON.stringify(o, function(k, v) { + if (k === 'genre') return { id: v.id, name: v.name }; + return v; + })); + } + }; + // }; + + method(Member.prototype, 'set', function(creds) { + if (creds && creds.accessToken && creds.refreshToken) { + this.accessToken = exports.localStorage[ACCESS_TOKEN_KEY] = creds.accessToken; + this.refreshToken = exports.localStorage[REFRESH_TOKEN_KEY] = creds.refreshToken; + Napster.player.auth(creds.accessToken); + } + }); + + method(Member.prototype, 'unset', function() { + this.accessToken = this.refreshToken = null; + + exports.localStorage.removeItem(ACCESS_TOKEN_KEY); + exports.localStorage.removeItem(REFRESH_TOKEN_KEY); + }); + + method(Member.prototype, 'load', function() { + this.accessToken = exports.localStorage[ACCESS_TOKEN_KEY]; + this.refreshToken = exports.localStorage[REFRESH_TOKEN_KEY]; + + return this; + }); + + method(Member.prototype, 'signedIn', function() { + return (this.accessToken != null && this.refreshToken != null); + }); + + // Everyone listens to these events + // Napster.player + // .on('playevent', function(e) { }) + // .on('playtimer', function(e) { }) + + exports.Napster = Napster; + exports.Member = Member; + +})(window, jQuery, JSON); diff --git a/examples/react_example/src/serviceWorker.js b/examples/react_example/src/serviceWorker.js new file mode 100644 index 0000000..f8c7e50 --- /dev/null +++ b/examples/react_example/src/serviceWorker.js @@ -0,0 +1,135 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://bit.ly/CRA-PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export function register(config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://bit.ly/CRA-PWA' + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type'); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +} diff --git a/examples/react_example/src/setupTests.js b/examples/react_example/src/setupTests.js new file mode 100644 index 0000000..82edfc9 --- /dev/null +++ b/examples/react_example/src/setupTests.js @@ -0,0 +1,4 @@ +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +configure({ adapter: new Adapter() }); diff --git a/examples/react_native_example/reactnativeexample/.expo-shared/assets.json b/examples/react_native_example/reactnativeexample/.expo-shared/assets.json new file mode 100644 index 0000000..3a90770 --- /dev/null +++ b/examples/react_native_example/reactnativeexample/.expo-shared/assets.json @@ -0,0 +1,34 @@ +{ + "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, + "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true, + "4d5b1b3e96b582f0c6e903e0afc3917f00ce385bc3be6c16f74b8a884fac3b3d": true, + "d2ab2c5248a5e8b1ad78ab00ad1552aae73534262231e09b21254ecc6c41e098": true, + "379aafc455b56da2c71c0d02479bf280dc99f65eaa90bfa62c859b3526142cab": true, + "3f22c753799d7899acc3a2ca1c220db558cdd26321fac6093f41d192145de4b9": true, + "e5b2a2461eb6faddb36e5e1368da477da166aa3c271dc8276ad901b70d59d002": true, + "36fa2aca98dd097ad861430b1441a63406be110a6c08144f086619f31a068155": true, + "19db0e0afecf5c80c2c1edf8b6166482ddf5d7ce7e984c36ee91ef43eb67c91e": true, + "608a6a2ea08e2d334ed35fef4505664f3345dfc32ee29b94630053fea6581038": true, + "d58b66ea4173eb323eacf58766b233ef3a5b2e050bb8b1018edfb4a35ee44b12": true, + "99101f3e3aaa93a7c5e4db2d12b0310fa86f9bd326d70f3b4ca9071f40c3fda5": true, + "b72fb83b01c8f7dbaba860678d9d134911e4566a27894c3a842f48e9d5546d12": true, + "13cb4e9d525282bfb6e243d293b54beeac6b413a231136d21e7e56109a066a7f": true, + "0dcab205e05e90dce6d677a09ff366f2248dbe967672083ad299fe85a06991f7": true, + "d7fdd3ea3e20340e1624b2e8e7b52af9f45158529e3d8d7b1ba6f9f0d8ca4103": true, + "28fddc39e1c050db14b9f672f893af71b056b27c439895fd05da0ad2cd93f786": true, + "0c2f2b0442449687c2620880964706b74999544605fd22da9e0265ea0fc62be5": true, + "5a7712ad2e3ea7272e7ceb4f1dbcb1219e5f5d369d29aa08b6288e23a28d9378": true, + "48a7cee06b31ce68f9da4c8480a9077ec984261be483bd46ddec1b6c17c6c832": true, + "eb3ae6d9f8cec2cacc5cb536acba89d8b89381e2025849ee6ce366ec85bc4347": true, + "9c4a771dcc02a2dabac6474b835a36a2f5cbaaa3a169f17a5b3aa273cfe387d7": true, + "0714ce41fed7131d3122fbda95b5c903453a562e11571b50826a79199d38d5a1": true, + "6c0017e6ab1a6fe9a1439587843a6a8a62077660daefc7f4ec408b8dd3780b7a": true, + "efc67adc32d330805f50633cb2ac1cb5e53bb9a913a37d95ca36d2b06c97f195": true, + "f92e8ce2c682707876e93e25b483fe983b76dd3b6c01480428bd6680b08adf79": true, + "558ebb95886589ba5d5814c5a13b3389c4fe8f3761bc0f6959a4c4af66898aac": true, + "0c59fa292565bec6a214074e04077eac5be75c8f4f5eae47e177b4510b309175": true, + "6e027ebf801ed713dbf54c4677ddd0cb7d5f3bd8379f2182c63964de1a27cda8": true, + "0b1dd9caf78ff0b0d2ba914ba7110ecc0f0e0afbb6c83d1a3d7a172666bc4ed5": true, + "6c7787e26ea49c984da4b3f2b2f7c697f2a76bff53a985ee9f446e481d1b6ebe": true, + "7957cde06223d2c59022ced4b5b3593d3051007a4c115cc8049bf80c96fb1487": true +} \ No newline at end of file diff --git a/examples/react_native_example/reactnativeexample/.gitignore b/examples/react_native_example/reactnativeexample/.gitignore new file mode 100644 index 0000000..df8fd3a --- /dev/null +++ b/examples/react_native_example/reactnativeexample/.gitignore @@ -0,0 +1,11 @@ +node_modules/**/* +.expo/* +npm-debug.* +*.jks +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ +web-report/ +.env diff --git a/examples/react_native_example/reactnativeexample/.watchmanconfig b/examples/react_native_example/reactnativeexample/.watchmanconfig new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/examples/react_native_example/reactnativeexample/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/examples/react_native_example/reactnativeexample/App.js b/examples/react_native_example/reactnativeexample/App.js new file mode 100644 index 0000000..fac67e0 --- /dev/null +++ b/examples/react_native_example/reactnativeexample/App.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import {createStackNavigator, createAppContainer} from 'react-navigation'; +import Login from './Components/Login'; +import Genre from './Components/Genre'; +import Player from './Components/Player'; +import NavigationService from './Models/NavigationService'; + +const MainNavigator = createStackNavigator({ + Login: {screen: Login}, + Genre: {screen: Genre}, + Player: {screen: Player} +}); + +const AppContainer = createAppContainer(MainNavigator); + +export default class App extends React.Component { + render() { + return ( + { + NavigationService.setTopLevelNavigator(navigatorRef); + }} + /> + ); + } +} diff --git a/examples/react_native_example/reactnativeexample/Components/Genre.js b/examples/react_native_example/reactnativeexample/Components/Genre.js new file mode 100644 index 0000000..6777edb --- /dev/null +++ b/examples/react_native_example/reactnativeexample/Components/Genre.js @@ -0,0 +1,215 @@ +import React from 'react'; +import Player from './Player'; +import GenreCalls from '../Models/GenreCalls'; +import TrackCalls from '../Models/TrackCalls'; +import Images from '../Models/Images' +import NavigationService from '../Models/NavigationService'; +import { styles } from '../Styles/Genre.styles.js' +import { Text, View, Image, ScrollView, FlatList, Button, TouchableOpacity, BackHandler } from 'react-native'; + +export default class Genre extends React.Component { + static navigationOptions = ({ navigation }) => { + return { + headerRight: ( +