Built entirely with...

First commit: April 9, 2016

iOS: primary focus.

Android: Not in original 1.0 release plan.

Decided to try it Aug. 6th.

Ready and submitted to App Store by July 6, 2016

< 3 months part time!

The Dream

July 6th, 2016

APP STORE REJECTED

REASON: Spotify IPv6 Support

New App Store Policy June 1st - What are the odds?!!

  REWIND!

Plugins

Native API's

Native Audio

CoreGraphics

Realtime Audio Waveform

Spotify Native SDK's

Toys

Toys

Toys

Toys

Toys

Toys

nativescript-spotify

Musical Toys

export class SpotifyDemo extends Observable {
  private _spotify: TNSSpotifyPlayer;

  constructor() {
    super();

    this._spotify = new TNSSpotifyPlayer();

    // when using iOS delegates that extend NSObject, TypeScript constructors are not used, therefore a separate `initPlayer()` exists
    this._spotify.initPlayer(true); // passing `true` lets player know you want it to emit events (sometimes it's not desired)

    // small sample of events (see Documentation below for full list)
    this._spotify.events.on('albumArtChange', (eventData) => {
      this.updateAlbumArt(eventData.data.url);
    });
    this._spotify.auth.events.on('authLoginSuccess', (eventData) => {
      this.loginSuccess();
    });
  }

  public login() {
    TNSSpotifyAuth.LOGIN();
  }

  public play(args?: EventData) {
    this._spotify.togglePlay('spotify:track:58s6EuEYJdlb0kO7awm3Vp').then((isPlaying: boolean) => {
      console.log(isPlaying ? 'Playing!' : 'Paused!');
    }, (error) => {
      console.log(`Playback error: ${error}`);
    });
  }

  private updateAlbumArt(url: string) {
    this.set(`currentAlbumUrl`, url);
  }

  private loginSuccess() {
    console.log(`loginSuccess!`);
  } 
}

Easy API to work with...

iOS Player

export class TNSSpotifyPlayer extends NSObject {
  public static ObjCProtocols = [SPTAudioStreamingDelegate, SPTAudioStreamingPlaybackDelegate];
  public player: any; // SPTAudioStreamingController
  
  public initPlayer(emitEvents?: boolean) {

    // init auth session
    TNSSpotifyAuth.VERIFY_SESSION().then(() => {
      this.setLoggedIn(true);
    }, () => {
      this.setLoggedIn(false);
    });
  }
  
  public togglePlay(track?: string, force?: boolean): Promise<any> {
    return new Promise((resolve, reject) => {
      if (track && (track !== this._loadedTrack)) {
        // first time play or changing track
        this.play(track).then(resolve, reject);
      } else if (this.player) {
        // toggling
        let playState = typeof force !== 'undefined' ? force : !this.player.isPlaying;
        this.player.setIsPlayingCallback(playState, (error) => {
          if (error != null) {
            console.log(`*** Pause/Resume playback got error:`);
            console.log(error);
            
            // try to verify/renew the session
            TNSSpotifyAuth.VERIFY_SESSION(TNSSpotifyAuth.SESSION).then(() => {
              let origResolve = resolve;
              this.togglePlay(track, force).then((isPlaying: boolean) => {
                origResolve(isPlaying);
              });
            }, () => {
              if (this.isLoginError(error.localizedDescription)) {
                this.loginError();
                reject('login');
              } else {
                reject(false);
              }
            });
            return;
          }
          resolve(this.player.isPlaying);
        });
      }
    });
  }

  public setVolume(val: number): Promise<any> {
    return new Promise((resolve, reject) => {
      if (this.player) {
        this.player.setVolumeCallback(val, (error) => {
          if (error !== null) {
            console.log(`Spotify Player volume adjust error:`, error);
            reject(error);
            return;
          }
          resolve();
        });
      } else {
        reject();
      }
    });
  }
  
  // Delegate methods
  public audioStreamingDidChangePlaybackStatus(controller: any, playing: boolean) {
    console.log(`DidChangePlaybackStatus: ${playing}`);
    if (this.events) {
      this._changedPlaybackStatus.data.playing = playing;
      this.events.notify(this._changedPlaybackStatus);  
    }
  }

  public audioStreamingDidChangePlaybackState(controller: any, state: any) {
    console.log(`DidChangePlaybackState: ${state}`);
    if (this.events) {
      this._changedPlaybackState.data.state = state;
      this.events.notify(this._changedPlaybackState);  
      this.updateCoverArt(state.currentTrack.albumUri);
      // this.updateCoverArt(state.currentTrack.albumCoverArtUri);     
    }
  }
  
  public audioStreamingDidSeekToOffset(controller: any, offset: any) {
    console.log(`DidSeekToOffset: ${offset}`);
    if (this.events) {
      this._seekedToOffset.data.offset = offset;
      this.events.notify(this._seekedToOffset);  
    }
  }
  
  public audioStreamingDidChangeVolume(controller: any, volume: any) {
    console.log(`DidChangeVolume: ${volume}`);
    if (this.events) {
      this._changedVolume.data.volume = volume;
      this.events.notify(this._changedVolume);  
    }
  }
  
  public audioStreamingDidChangeShuffleStatus(controller: any, isShuffled: boolean) {
    console.log(`DidChangeShuffleStatus: ${isShuffled}`);
    if (this.events) {
      this._changedShuffleStatus.data.shuffle = isShuffled;
      this.events.notify(this._changedShuffleStatus);  
    }
  }
  
  public audioStreamingDidChangeRepeatStatus(controller: any, isRepeated: boolean) {
    console.log(`DidChangeRepeatStatus: ${isRepeated}`);
    if (this.events) {
      this._changedRepeatStatus.data.repeat = isRepeated;
      this.events.notify(this._changedRepeatStatus);  
    }
  }
  
  public audioStreamingDidChangeToTrack(controller: any, trackMetadata: NSDictionary) {
    console.log(`DidChangeToTrack: ${trackMetadata}`);
    if (this.events) {
      this._changedToTrack.data.metadata = trackMetadata;
      this.events.notify(this._changedToTrack);  
    }
  }
  
  public audioStreamingDidFailToPlayTrack(controller: any, trackUri: NSURL) {
    console.log(`DidFailToPlayTrack: ${trackUri.absoluteString}`);
    if (this.events) {
      this._failedToPlayTrack.data.url = trackUri.absoluteString;
      this.events.notify(this._failedToPlayTrack);  
    }
  }
  
  public audioStreamingDidStartPlayingTrack(controller: any, trackUri: NSURL) {
    console.log(`DidStartPlayingTrack: ${trackUri.absoluteString}`);
    this.updateCoverArt(this.currentTrackMetadata().albumUri);
    if (this.events) {
      this._startedPlayingTrack.data.url = trackUri.absoluteString;
      this.events.notify(this._startedPlayingTrack);  
    }
  }
  
  public audioStreamingDidStopPlayingTrack(controller: any, trackUri: NSURL) {
    console.log(`DidStopPlayingTrack: ${trackUri.absoluteString}`);
    if (this.events) {
      this._stoppedPlayingTrack.data.url = trackUri.absoluteString;
      this.events.notify(this._stoppedPlayingTrack);  
    }
  }
  
  public audioStreamingDidSkipToNextTrack(controller: any) {
    console.log(`DidSkipToNextTrack`);
    if (this.events) {
      this.events.notify(this._skippedToNextTrack);  
    }
  }
  
  public audioStreamingDidSkipToPreviousTrack(controller: any) {
    console.log(`DidSkipToPreviousTrack`);
    if (this.events) {
      this.events.notify(this._skippedToPreviousTrack);  
    }
  }
  
  public audioStreamingDidBecomeActivePlaybackDevice(controller: any) {
    console.log(`DidBecomeActivePlaybackDevice`);
    if (this.events) {
      this.events.notify(this._activePlaybackDevice);  
    }
  }
  
  public audioStreamingDidBecomeInactivePlaybackDevice(controller: any) {
    console.log(`DidBecomeInactivePlaybackDevice`);
    if (this.events) {
      this.events.notify(this._inactivePlaybackDevice);  
    }
  }
  
  public audioStreamingDidPopQueue(controller: any) {
    console.log(`DidPopQueue`);
    if (this.events) {
      this.events.notify(this._poppedQueue);  
    }
  }

  // SPTAudioStreamingDelegate
  public audioStreamingDidEncounterTemporaryConnectionError(controller: any) {
    console.log(`audioStreamingDidEncounterTemporaryConnectionError`);
    if (this.events) {
      this.events.notify(this._temporaryConnectionError);  
    }
  }

  public audioStreamingDidEncounterError(controller: any, error: any) {
    console.log(`audioStreamingDidEncounterTemporaryConnectionError`);
    console.log(error);
    if (this.events) {
      this._streamError.data.error = error;
      this.events.notify(this._streamError);  
    }
  }

  public audioStreamingDidReceiveMessage(controller: any, message: any) {
    console.log(`audioStreamingDidReceiveMessage`);
    if (this.events) {
      this._receivedMessage.data.message = message;
      this.events.notify(this._receivedMessage);  
    }
  }

  public audioStreamingDidDisconnect(controller: any) {
    console.log(`audioStreamingDidDisconnect`);
    if (this.events) {
      this.events.notify(this._streamDisconnected);  
    }
  }

  public audioStreamingDidLogin() {
    console.log(`audioStreamingDidLogin`);
    this._playerLoggedIn = true;
    let handlePromise = (success) => {
      if (this._loggingInPromise) {
        if (success) {
          this._loggingInPromise.resolve();
        } else {
          this._loggingInPromise.reject();
        }
        this._loggingInPromise = undefined;
      }
    };
    // check if user is non-premium
    TNSSpotifyAuth.CHECK_PREMIUM().then(() => {
      handlePromise(true);
    }, () => {
      handlePromise(false);
    });
  }

  public audioStreamingDidLogout(controller: any) {
    console.log(`audioStreamingDidLogout`);
    let errorRef = new interop.Reference();
    if (!this.player.stopWithError(errorRef)) {
      if (errorRef) {
        console.log(`stopWithError:`);
        for (let key in errorRef) {
          console.log(errorRef[key]);
        }
      }
    }
    this.player = undefined;
    TNSSpotifyAuth.LOGOUT();
  }   
  
  private play(track: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.checkPlayer().then(() => {
        this.playUri(track, resolve, reject);
      }, () => {
        reject('login');
      });
    });
  }
  
  private playUri(track: string, resolve: Function, reject: Function) {
    // https://developer.spotify.com/ios-sdk-docs/Documents/Classes/SPTAudioStreamingController.html
    console.log(track);
    // this.player.playURICallback(NSURL.URLWithString(track), (error) => {
    this.player.playURIStartingWithIndexCallback(NSURL.URLWithString(track), 0, (error) => {
      if (error != null) {
        console.log(`*** playURICallback got error:`);
        console.log(error);
        
        if (this.isLoginError(error.localizedDescription)) {
          this.loginError();
          reject('login');
        } else {
          reject(false);  
        }
        return;
      }
      this._loadedTrack = track;
      resolve(true);
    });
  }
  
  private checkPlayer(): Promise<boolean> {
    let printErrorRef = (ref: any, failure: boolean) => {
      console.log(`SPTAudioStreamingController.sharedInstance().startWithClientIdError Start${failure ? ' Failure' : ''}:`, ref);
      for (let key in ref) {
        console.log(ref[key]);
      }
    };

    return new Promise((resolve, reject) => {
      if (!this._started) {
        let errorRef = new interop.Reference();
        this.player = SPTAudioStreamingController.sharedInstance();
        // if (this.player.startWithClientIdError(TNSSpotifyConstants.CLIENT_ID, errorRef)) {
        if (this.player.startWithClientIdAudioControllerAllowCachingError(TNSSpotifyConstants.CLIENT_ID, null, false, errorRef)) {
          this._started = true;
          this.player.delegate = this;
          this.player.playbackDelegate = this;
          // this.player.diskCache = SPTDiskCache.alloc().initWithCapacity(1024 * 1024 * 64);
          
          if (errorRef) {
            console.log(errorRef.description);
            printErrorRef(errorRef, false);
          } 

          if (!this._playerLoggedIn) { 
            this._loggingInPromise = {
              resolve: resolve,
              reject: reject
            };
            this.player.loginWithAccessToken(TNSSpotifyAuth.SESSION.accessToken);
          } else {
            TNSSpotifyAuth.CHECK_PREMIUM().then(resolve, reject);
          }

        } else {
          this._started = false;
          if (errorRef) {
            printErrorRef(errorRef, true);
          }
          // check if user is non-premium
          // since this is real player error, reject both
          // but it will at least alert user if they are non-premium still
          TNSSpotifyAuth.CHECK_PREMIUM().then(reject, reject);
        }
      } else {
        resolve();
      }
    });
  }
  
  private updateCoverArt(albumUri: string): Promise<any> {
    return new Promise((resolve, reject) => {
      SPTAlbum.albumWithURISessionCallback(NSURL.URLWithString(albumUri), TNSSpotifyAuth.SESSION, (error, albumObj: any) => {
        if (error != null) {
          console.log(`*** albumWithURISessionCallback got error:`);
          console.log(error);
          reject();
          return;
        }
        
        // albumObj: SPTAlbum = https://developer.spotify.com/ios-sdk-docs/Documents/Classes/SPTAlbum.html
 
        this._currentAlbumImageUrl = albumObj.largestCover.imageURL.absoluteString;
        if (this.events) {
          this._albumArtChange.data.url = this._currentAlbumImageUrl;
          this.events.notify(this._albumArtChange);  
        }
        resolve();
      });
    });
  }
  
  private isLoginError(desc: string): boolean {
    if (desc.indexOf('invalid credentials') > -1 || desc.indexOf('NULL') > -1) {
      return true;
    } else {
      return false;
    }
  }
  
  private loginError() {
    this.setLoggedIn(false);
    Utils.alert('You need to login to renew your session.');
  }
  
  private setLoggedIn(value: boolean) {
    this._loggedIn = value;
    if (!value) {
      this._playerLoggedIn = false;
      if (this._started) {
        this._started = false;
        console.log(`streamingcontroller logout()`);
        this.player.logout();
        setTimeout(() => {
          // if delegate method did not get called, fallback to manual
          if (this.player) {
            this.audioStreamingDidLogout(null);
          }
        }, 1000);
      }
      
    }
  }
  
  private playerReady(): void {
    if (this.events) {
      this._playerReady.data.loggedIn = this._loggedIn;
      this.events.notify(this._playerReady);  
    }
  }
  
  private setupEvents() {
    // auth state
    this.auth.events.on('authLoginChange', (eventData: any) => {
      this.setLoggedIn(eventData.data.status);
    });      

    // player events    
    this.events = new Observable();
    this._albumArtChange = {
      eventName: 'albumArtChange',
      data: {
        url: ''
      }
    };
    this._playerReady = {
      eventName: 'playerReady',
      data: {
        loggedIn: false
      }
    };
    // delegate events
    this._changedPlaybackStatus = {
      eventName: 'changedPlaybackStatus',
      data: {
        playing: false
      }
    };
    this._changedPlaybackState = {
      eventName: 'changedPlaybackState',
      data: {
        state: {}
      }
    };
    this._seekedToOffset = {
      eventName: 'seekedToOffset',
      data: {
        offset: 0
      }
    };
    this._changedVolume = {
      eventName: 'changedVolume',
      data: {
        volume: 0
      }
    };
    this._changedShuffleStatus = {
      eventName: 'changedShuffleStatus',
      data: {
        shuffle: false
      }
    };
    this._changedRepeatStatus = {
      eventName: 'changedRepeatStatus',
      data: {
        repeat: false
      }
    };
    this._changedToTrack = {
      eventName: 'changedToTrack',
      data: {
        metadata: null
      }
    };
    this._failedToPlayTrack = {
      eventName: 'failedToPlayTrack',
      data: {
        url: null
      }
    };
    this._startedPlayingTrack = {
      eventName: 'startedPlayingTrack',
      data: {
        url: null
      }
    };
    this._stoppedPlayingTrack = {
      eventName: 'stoppedPlayingTrack',
      data: {
        url: null
      }
    };
    this._skippedToNextTrack = {
      eventName: 'skippedToNextTrack'
    };
    this._skippedToPreviousTrack = {
      eventName: 'skippedToPreviousTrack'
    };
    this._activePlaybackDevice = {
      eventName: 'activePlaybackDevice'
    };
    this._inactivePlaybackDevice = {
      eventName: 'inactivePlaybackDevice'
    };
    this._poppedQueue = {
      eventName: 'poppedQueue'
    };
    this._temporaryConnectionError = {
      eventName: 'temporaryConnectionError'
    };
    this._streamError = {
      eventName: 'streamError',
      data: {
        error: null
      }
    };
    this._receivedMessage = {
      eventName: 'receivedMessage',
      data: {
        message: null
      }
    };
    this._streamDisconnected = {
      eventName: 'streamDisconnected'
    };
  } 
}

Android Player

export class TNSSpotifyPlayer {
  public player: any;

  public initPlayer(emitEvents?: boolean) {

    // init auth session
    TNSSpotifyAuth.VERIFY_SESSION().then(() => {
      this.setLoggedIn(true);
    }, () => {
      this.setLoggedIn(false);
    });
  }

  public togglePlay(track?: string, force?: boolean): Promise<any> {
    return new Promise((resolve, reject) => {
      if (track && (track !== this._loadedTrack)) {
        // first time play or changing track
        this.play(track).then(resolve, reject);
      } else if (this.player) {
        // toggling
        this._playing = typeof force !== 'undefined' ? force : !this._playing;

        if (this._playing) {
          this.player.resume();
        } else {
          this.player.pause();
        }
        resolve(this._playing);
      }
    });
  }

  public setVolume(val: number): Promise<any> {
    return new Promise((resolve, reject) => {
      if (this._audioController) {
        this._audioController.setVolume(val);
        resolve();
      } else {
        reject();
      }
    });
  }

  private play(track: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.checkPlayer().then(() => {
        if (!this._playerLoggedIn) {
          this._playerLoggedIn = true;
        }
        this.playUri(track, resolve, reject);
      }, () => {
        reject('login');
      });
    });
  }

  private playUri(track: string, resolve: Function, reject: Function) {
    console.log(`playUri`, this.player);
    this.player.play(track);
    this._loadedTrack = track;
    this._playing = true;
    resolve(true);
  }

  private checkPlayer(): Promise<boolean> {
    return new Promise((resolve, reject) => {
      if (!this._started) {
        let activity = app.android.startActivity || app.android.foregroundActivity;
        let playerConfig: any = new Config(activity, TNSSpotifyAuth.SESSION, TNSSpotifyConstants.CLIENT_ID);
        let builder = new Builder(playerConfig);
        this._audioController = new CustomAudioController();
        builder.setAudioController(this._audioController);

        let observer = new Player.InitializationObserver({
          onError: (throwable) => {
            let msg = throwable.getMessage();
            console.log("MainActivity", "Could not initialize player: " + msg);
            reject(msg);
          },
          onInitialized: (player) => {
            console.log(`player initialized`, player);
            this._started = true;

            // check if user is non-premium
            TNSSpotifyAuth.CHECK_PREMIUM().then(() => {
              resolve();
            }, () => {
              reject();
            });
          }
        });

        this.player = builder.build(observer);
        this.player.addPlayerNotificationCallback(new PlayerNotificationCallback({

          onPlaybackEvent: (eventType, playerState) => {
            console.log('EVENT TYPE: ', eventType);
            console.log('PLAYER STATE: ', playerState);

            if (this._loadedTrack && (eventType == PlayerNotificationCallback.EventType.TRACK_CHANGED || eventType == PlayerNotificationCallback.EventType.END_OF_CONTEXT)) {
              eventType = eventType == PlayerNotificationCallback.EventType.END_OF_CONTEXT ? 'TRACK_END' : eventType;
              if (this.events && playerState.trackUri) {
                if (this._trackTimeout) {
                  clearTimeout(this._trackTimeout);
                }
                this._trackTimeout = setTimeout(() => {
                  let trackId = playerState.trackUri.split(':').slice(-1);
                  console.log(`trackId: ${trackId}`);
                  http.request({
                    url: `https://api.spotify.com/v1/tracks/${trackId}`,
                    method: 'GET',
                    headers: { 
                      "Content-Type": "application/json", 
                      "Authorization:": `Bearer ${TNSSpotifyAuth.SESSION}` }
                  }).then((res: any) => {
  
                    if (res && res.content) {
                      let track = JSON.parse(res.content);
                    
                      this._changedPlaybackState.data.state = {
                        currentTrack: {
                          uri: track.uri,
                          name: track.name,
                          albumName: track.album.name,
                          artistName: track.artists[0].name,
                          durationMs: track.duration_ms,
                          positionInMs: playerState.positionInMs,
                          eventType: eventType
                        }
                      };
                      this.events.notify(this._changedPlaybackState); 
                      if (track.album && track.album.images) {
                        this._albumArtChange.data.url = track.album.images[0].url;
                        this.events.notify(this._albumArtChange);  
                      }
                    }
                  }, (err: any) => {
                    console.log(`track data error:`, err);
                  });
                }, 500);
                
              }
            }
          },

          onPlaybackError: (errorType, errorDetails) => {
            console.log('ERROR TYPE: ', errorType);
            console.log('ERROR DETAILS: ', errorDetails);
            if (errorDetails) {
              for (let key in errorDetails) {
                console.log(`key: ${key}`, errorDetails[key]);
              }
            }
          }

        }));


      } else {
        resolve();
      }
    });
  }

  private isLoginError(desc: string): boolean {
    if (desc.indexOf('invalid credentials') > -1 || desc.indexOf('NULL') > -1) {
      return true;
    } else {
      return false;
    }
  }

  private loginError() {
    this.setLoggedIn(false);
    Utils.alert('You need to login to renew your session.');
  }

  private setLoggedIn(value: boolean) {
    this._loggedIn = value;
    if (!value) {
      this._playerLoggedIn = false;
      if (this._started) {
        this._started = false;
        console.log(`TODO: player dispose()`);
        this.player.logout();
        setTimeout(() => {
          // https://developer.spotify.com/android-sdk-docs/player/
          Spotify.destroyPlayer(this.player);
        }, 1000);
      }

    }
  }

  private playerReady(): void {
    if (this.events) {
      this._playerReady.data.loggedIn = this._loggedIn;
      this.events.notify(this._playerReady);
    }
  }

  private setupEvents() {
    // auth state
    this.auth.events.on('authLoginChange', (eventData: any) => {
      console.log(`this.auth.events.on('authLoginChange'`, eventData.data.status);
      this.setLoggedIn(eventData.data.status);
    });

    // // player events    
    this.events = new Observable();
    this._albumArtChange = {
      eventName: 'albumArtChange',
      data: {
        url: ''
      }
    };
    this._playerReady = {
      eventName: 'playerReady',
      data: {
        loggedIn: false
      }
    };
    // delegate events
    this._changedPlaybackStatus = {
      eventName: 'changedPlaybackStatus',
      data: {
        playing: false
      }
    };
    this._changedPlaybackState = {
      eventName: 'changedPlaybackState',
      data: {
        state: {}
      }
    };
    this._seekedToOffset = {
      eventName: 'seekedToOffset',
      data: {
        offset: 0
      }
    };
    this._changedVolume = {
      eventName: 'changedVolume',
      data: {
        volume: 0
      }
    };
    this._changedShuffleStatus = {
      eventName: 'changedShuffleStatus',
      data: {
        shuffle: false
      }
    };
    this._changedRepeatStatus = {
      eventName: 'changedRepeatStatus',
      data: {
        repeat: false
      }
    };
    this._changedToTrack = {
      eventName: 'changedToTrack',
      data: {
        metadata: null
      }
    };
    this._failedToPlayTrack = {
      eventName: 'failedToPlayTrack',
      data: {
        url: null
      }
    };
    this._startedPlayingTrack = {
      eventName: 'startedPlayingTrack',
      data: {
        url: null
      }
    };
    this._stoppedPlayingTrack = {
      eventName: 'stoppedPlayingTrack',
      data: {
        url: null
      }
    };
    this._skippedToNextTrack = {
      eventName: 'skippedToNextTrack'
    };
    this._skippedToPreviousTrack = {
      eventName: 'skippedToPreviousTrack'
    };
    this._activePlaybackDevice = {
      eventName: 'activePlaybackDevice'
    };
    this._inactivePlaybackDevice = {
      eventName: 'inactivePlaybackDevice'
    };
    this._poppedQueue = {
      eventName: 'poppedQueue'
    };
    this._temporaryConnectionError = {
      eventName: 'temporaryConnectionError'
    };
    this._streamError = {
      eventName: 'streamError',
      data: {
        error: null
      }
    };
    this._receivedMessage = {
      eventName: 'receivedMessage',
      data: {
        message: null
      }
    };
    this._streamDisconnected = {
      eventName: 'streamDisconnected'
    };
  }
}

Thanks to BRAD MARTIN !!

16 community plugins

nativescript-audio nativescript-plugin-firebase
nativescript-coachmarks nativescript-slides
nativescript-ezaudio nativescript-social-share
nativescript-fancyalert nativescript-splashscreen
nativescript-gif nativescript-spotify
nativescript-loading-indicator nativescript-swiss-army-knife
nativescript-ng2-fonticon nativescript-telerik-ui-pro
nativescript-permissions nativescript-themes

3 iOS only

11 contributed on or authored

?

iOS

Android

iOS

Android

Android

Conditional Plugin Requiring

Architecture

Angular Native

Firebase

Redux via ngrx/store

ngrx/store - 29 Unique Actions   w/  6 Reducers

ngrx/effects - help facilitate side effects

Fast-forward

Aug. 17th

Spotify IPv6 support lands!

Deployment

- First build: 130 Mb

- Primarily 2 difficult issues

   to solve:

1. Angular `moduleId: module.id`

2. Font registration for release builds

Webpack with Angular Native

templateUrl full relative path

Webpack with Angular Native

Custom font registration for iOS

Webpack to rescue

... and Nathanael Anderson!

Final build:

~ 29.1 Mb

- Android - WIP

Open Source as of today!

- iOS

ShoutOutPlay

as of today...

Community thank you:

Nathanael Anderson -

Alex Vakrilov -

Brad Martin -

Josh Sommer -

Osei Fortune -

Jen Looper -

TJ VanToll -

Angular thank you:

- Rob Wormald

- Mike Ryan

- Brandon Roberts

- Brian Troncone

ngrx inspiration!

Nathan Walker

@wwwalkerrun

ShoutOutPlay

By Nathan Walker

ShoutOutPlay

  • 1,997