UNPKG

silk-gui

Version:

GUI for developers and Node OS

800 lines (662 loc) 24.5 kB
// main player class that is used by the player object to control the playing of songs function PlayState() { this.scrubberTimeout = null; this.names = { playpause: '#playpause', next: '#next', prev: '#prev', repeat: '#repeat', repeat_badge: '#repeat_badge', shuffle: '#shuffle', }; this.repeat_states = { all: 0, one: 1, none: 2, }; // search info this.searchText = ''; // currently viewed songs this.songs = []; // current song list sort state this.sort_asc = null; this.sort_col = null; // current pool of songs to play, in order from whatever list they were picked this.queue_pool = []; // same as above but shuffled, used to make sure a correct shuffle is done this.shuffle_pool = []; this.shuffle_idx = 0; // other state this.song_collection = null; this.playlist_collection = null; this.playing_id = null; this.is_playing = false; this.shuffle_state = false; this.repeat_state = 0; this.scrub = null; // keep track of play next and history this.play_history = []; this.play_history_idx = 0; // remote control data this.comp_name = null; this.init = function() { setInterval(function() { player.update(); }, 50); $(this.names.playpause).click(function() { player.togglePlayState(); }); $(this.names.next).click(function() { player.nextTrack(); }); $(this.names.prev).click(function() { player.prevTrack(); }); $(this.names.repeat).click(function() { player.toggleRepeat(); }); $(this.names.shuffle).click(function() { player.toggleShuffle(); }); this.shuffle_state = localStorage.getItem('shuffle') == 'true' || false; this.redrawShuffle(); this.repeat_state = localStorage.getItem('repeat') || this.repeat_states.all; this.redrawRepeat(); this.comp_name = localStorage.getItem('comp_name') || ''; this.volume = parseInt(localStorage.getItem('currentVolume')) || 100; this.PlayMethodAbstracter.setVolume(this.volume); this.onMobile = on_mobile; }; this.setupCollections = function() { this.song_collection = new SongCollection(); this.playlist_collection = new PlaylistCollection(); this.song_collection.fetch(); this.playlist_collection.fetch(); }; this.update = function() { if (this.is_playing && !this.isSeeking && this.scrub) { this.scrub.slider('setValue', this.PlayMethodAbstracter.getCurrentTime() / this.PlayMethodAbstracter.getDuration() * 100.0, false); var seconds = prettyPrintSeconds(this.PlayMethodAbstracter.getCurrentTime()); $('.current_time').html(seconds); } }; this.findSongIndex = function(id) { for (var i = 0; i < this.queue_pool.length; i++) { if (id == this.queue_pool[i].attributes._id) { return i; } } // fallback to all songs return null; }; this.updateSearch = function(searchText) { MusicApp.router.navigate('search/' + encodeURIComponent(searchText), true); }; this.searchItems = function(searchText) { this.searchText = searchText; if (searchText.length < 3) { return; } // set the search box to the search text if it isn't focuessed if ($('.search-input:focus').size() === 0) { $('.search-input').val(searchText); } // normalise the search text to lower case searchText = searchText.toLowerCase(); var tmpSongs = []; // counter for index in the song list var matched = 0; for (var i = 0; i < this.song_collection.length; i++) { var item = this.song_collection.models[i]; if (this.songMatches(item, searchText)) { // set the index item.attributes.index = matched++; // add it to the list of matched songs tmpSongs.push(item); } } this.songs = tmpSongs; // used for if they load the browser with a search page if (this.queue_pool.length === 0) { this.queue_pool = this.songs.slice(0); this.genShufflePool(); } // reset the sorting attributes player.sort_asc = player.sort_col = null; // create a mock playlist for the search results this.playlist = { title: 'Search Results for: \'' + this.searchText + '\'', editable: false, songs: deAttribute(this.songs), }; if (MusicApp.router.songview) { MusicApp.router.songview.render(); } else { MusicApp.router.songview = new SongView(); MusicApp.contentRegion.show(MusicApp.router.songview); } }; this.showMix = function(originalSong, similarSongs) { this.songs = similarSongs.map(function(songMeta) { return new SongModel(songMeta); }); // set the mix as the current pool of songs this.queue_pool = this.songs.slice(0); this.genShufflePool(); // create a mock playlist for the search results this.playlist = { title: 'Instant mix for: \'' + originalSong.title + '\' by \'' + originalSong.artist + '\'', editable: false, is_youtube: true, songs: deAttribute(this.songs), }; // musicapp should already be defined before they arrive here // so scroll it to the top and re-render it MusicApp.router.songview.scrollTop = 0; MusicApp.router.songview.render(); }; // note: this is a very expensive method of searching // it is used to match each term in the search against the title, album and artist this.songMatches = function(item, searchText) { item = item.attributes; if (!item.searchString) { item.searchString = ''; item.searchString += (item.title) ? item.title.toLowerCase() : ''; item.searchString += (item.album) ? item.album.toLowerCase() : ''; item.searchString += (item.display_artist) ? item.display_artist.toLowerCase() : ''; } var searchTextParts = searchText.split(/[ ]+/); if (searchMatchesSong(item.searchString, searchTextParts)) { return true; } return false; }; // sort the list of songs currently viewed by a certain column this.sortSongs = function(col) { if (this.sort_col == null || this.sort_asc == null || this.sort_col != col) { // start the sorting this.sort_col = col; this.sort_asc = true; } else if (this.sort_col == col) { // already sorted on this column, flip the direction this.sort_asc = !this.sort_asc; } // perform the sort player.songs.sort(this.songSortFunc); }; // sort the songs again (for when the underlying data has changed) this.resortSongs = function() { // perform the sort player.songs.sort(this.songSortFunc); }; // function to perform the sort based on current sorting attributes this.songSortFunc = function(a, b) { a = a.attributes; b = b.attributes; // function to determine sway based on two sides var decide = function(lhs, rhs, sortAsc) { if (lhs < rhs) { return (sortAsc) ? -1 : 1; } else if (rhs < lhs) { return (sortAsc) ? 1 : -1; } else { return 0; } }; // sort by the sorting column var deciderVal; // sorting based on original sorting column deciderVal = decide(a[player.sort_col], b[player.sort_col], player.sort_asc); if (deciderVal) { return deciderVal; } // if they share the same arist, sort by album, disc number, track number if (a.display_artist == b.display_artist) { // the albums are different, sort by them if (a.album != b.album) { deciderVal = decide(a.album, b.album, player.sort_asc); if (deciderVal) { return deciderVal; } } // if the above failed (they are the same), sort by disc deciderVal = decide(a.disc, b.disc, player.sort_asc); if (deciderVal) { return deciderVal; } // if they above still failed, sort by track deciderVal = decide(a.track, b.track, player.sort_asc); if (deciderVal) { return deciderVal; } } else { // otherwise sort by track title deciderVal = decide(a.title, b.title, player.sort_asc); if (deciderVal) { return deciderVal; } } return 0; }; this.durationChanged = function() { var seconds = prettyPrintSeconds(this.PlayMethodAbstracter.getDuration()); $('.duration').html(seconds); }; this.trackEnded = function() { // increment the playcount this.current_song.attributes.play_count++; socket.emit('update_play_count', { track_id: this.current_song.attributes._id, plays: this.current_song.attributes.play_count, }); // redraw that songs row (i.e. update it's play count) MusicApp.router.songview.redrawSong(this.current_song.attributes._id); // go to the next track this.nextTrack(); }; this.playSong = function(id, force_restart) { // remove the last playing song from the selection delFromSelection(this.playing_id); addToSelection(id, false); // set the current song var index_in_queue = this.findSongIndex(id); if (index_in_queue === null) { // song was played from outside the current queue, get it by id this.current_index = 0; this.current_song = this.song_collection.findBy_Id(id); } else { // song played was in the current queue, fetch the info from the queue this.current_index = index_in_queue; this.current_song = this.queue_pool[this.current_index]; } // only continue if the song was defined if (this.current_song) { // skip resetting the song if it's the same song playing and we don't need to force restart if (id == this.playing_id && !force_restart) { return; } else { this.playing_id = id; } // update the audio element this.PlayMethodAbstracter.playTrack(this.current_song); // set the state to playing this.setIsPlaying(true); // show the songs info info = new InfoView(); MusicApp.infoRegion.show(info); // update the selected item $('tr').removeClass('light-blue'); $('#' + id).addClass('light-blue'); // update the window title window.document.title = this.current_song.attributes.title + ' - ' + this.current_song.attributes.display_artist; // update the cover photo if it's showing fullscreen and the new song has cover art if (cover_is_visible && cover_is_current && this.current_song.attributes.cover_location) { showCover('cover/' + this.current_song.attributes.cover_location); } this.displayNotification(); } }; this.setIsPlaying = function(isPlaying) { this.is_playing = isPlaying; localStorage.setItem('last_play_state', isPlaying); $(this.names.playpause).removeClass('fa-play fa-pause'); if (this.is_playing) { $(this.names.playpause).addClass('fa-pause'); } else { $(this.names.playpause).addClass('fa-play'); } }; this.togglePlayState = function() { if (this.is_playing) { this.PlayMethodAbstracter.pause(); } else { this.PlayMethodAbstracter.play(); } this.setIsPlaying(!this.is_playing); }; this.toggleShuffle = function() { // toggle and save the value this.shuffle_state = !this.shuffle_state; localStorage.setItem('shuffle', this.shuffle_state); this.redrawShuffle(); // if we are viewing the queue, retrigger route if (this.playlist._id == 'QUEUE') { MusicApp.router.playlist('QUEUE'); } }; this.redrawShuffle = function() { // change the dom if (this.shuffle_state) { $(this.names.shuffle).addClass('blue'); } else { $(this.names.shuffle).removeClass('blue'); } }; this.toggleRepeat = function() { // change the state and save the value this.repeat_state = (this.repeat_state + 1) % 3; localStorage.setItem('repeat', this.repeat_state); this.redrawRepeat(); }; this.redrawRepeat = function() { $(this.names.repeat).addClass('blue'); $(this.names.repeat_badge).addClass('hidden'); if (this.repeat_state == this.repeat_states.one) { $(this.names.repeat_badge).removeClass('hidden'); } else if (this.repeat_state == this.repeat_states.none) { $(this.names.repeat).removeClass('blue'); } }; this.genShufflePool = function() { if (player.queue_pool.length > 1) { // create the shuffle pool, remove the current track, shuffle it and place // the current track at the start. This creates a correct shuffle. player.shuffle_pool = player.queue_pool.slice(0); // remove the current song var current_song = player.shuffle_pool.splice(player.current_index, 1); // shuffle the array player.shuffle_pool = shuffle_array(player.shuffle_pool); // re-add the current song at the start player.shuffle_pool.unshift(current_song[0]); // set the shuffle idx at the snog after this one player.shuffle_idx = 1; } else { player.shuffle_pool = player.queue_pool.slice(0); player.shuffle_idx = 0; } if (player.playlist && player.playlist._id == 'QUEUE') { player.songs = player.shuffle_pool; if (MusicApp.router.songview && player.shuffle_state) { MusicApp.router.songview.render(); } } }; this.nextTrack = function() { // repeat the current song if the repeat state is on one if (this.repeat_state == this.repeat_states.one) { this.PlayMethodAbstracter.setCurrentTime(0); this.PlayMethodAbstracter.play(); return; } // find the index we should move to var index = 0; if (this.play_history_idx > 0 && this.play_history.length >= this.play_history_idx) { // move forward a song in the history this.play_history_idx--; // play it and break this.playSong(this.play_history[this.play_history_idx], true); return; } else { if (this.shuffle_state) { if (this.shuffle_idx == this.shuffle_pool.length) { // reshuffle to make it seem more random this.genShufflePool(); } // play the next shuffle song this.play_history.unshift(this.shuffle_pool[this.shuffle_idx].attributes._id); this.playSong(this.shuffle_pool[this.shuffle_idx].attributes._id, true); // increment the shuffle idx for the next time `next` is pressed this.shuffle_idx++; } else { // they don't have shuffle turned on, play the next song in the queue index = this.current_index + 1; if (index == this.queue_pool.length) { index = 0; } // add the song to the history this.play_history.unshift(this.queue_pool[index].attributes._id); this.playSong(this.queue_pool[index].attributes._id, true); } } }; this.prevTrack = function() { // should we just start this song again if (this.PlayMethodAbstracter.getCurrentTime() > 5.00 || this.repeat_state == this.repeat_states.one) { this.PlayMethodAbstracter.setCurrentTime(0); this.PlayMethodAbstracter.play(); } else { // find the previous song if it exists if (this.play_history.length > 0 && this.play_history_idx + 1 < this.play_history.length) { // increment the history index marker this.play_history_idx++; // play the song from the history this.playSong(this.play_history[this.play_history_idx], true); } else { // move to the previous song in the playlist var index = this.current_index - 1; if (index == -1) { index = this.queue_pool.length - 1; } this.playSong(this.queue_pool[index].attributes._id, true); } } }; this.setScubElem = function(elem) { this.scrub = elem; this.scrub.slider() /* disable seeking as soon as slide / click starts. * this was added due to an issue causing the slider to update to the old * duration even when a click was triggered because of the delay for the * slideStop */ .on('slideStart', function() { player.isSeeking = true; }) .on('slide', function(slideEvt) { player.scrub_value = slideEvt.value; player.scrubTimeout(); }) .on('slideStop', function(slideEvt) { player.scrub_value = slideEvt.value; player.scrubTimeoutComplete(); }); }; this.setVolElem = function(elem) { this.vol = elem; this.vol.slider() .on('slide', function() { this.setVolume(this.vol.slider('getValue')); }.bind(this)) .on('slideStop', function() { this.setVolume(this.vol.slider('getValue'));}.bind(this)); // update the slider with the current known volume this.vol.slider('setValue', this.volume); }; // sets the current volume // @param volume: volume between 0 and 100 this.setVolume = function(volume) { this.PlayMethodAbstracter.setVolume(volume); // also update the slider if it's defined if (this.vol) { this.vol.slider('setValue', volume); } // commit it to local storage localStorage.setItem('currentVolume', volume); // keep the volume updated this.volume = volume; }; this.getVolume = function() { return this.volume; }; this.scrubTimeout = function() { if (this.scrubberTimeout !== null) { clearTimeout(this.scrubberTimeout); } this.scrubberTimeout = setTimeout(function() { player.scrubTimeoutComplete(); }, 1000); this.isSeeking = true; // update the time to show the current scrub value var seconds = prettyPrintSeconds(this.PlayMethodAbstracter.getDuration() * this.scrub_value / 100.00); $('.current_time').html(seconds); }; this.scrubTimeoutComplete = function() { clearTimeout(this.scrubberTimeout); this.isSeeking = false; this.scrubTo(this.scrub_value); }; // scrub to percentage in current track this.scrubTo = function(value) { var length = this.PlayMethodAbstracter.getDuration(); this.PlayMethodAbstracter.setCurrentTime(length * value / 100.00); }; this.setCompName = function(name) { // update the local data this.comp_name = name; localStorage.setItem('comp_name', this.comp_name); // update the name with the server socket.emit('set_comp_name', {name: this.comp_name}); }; this.displayNotification = function() { // send a song change notification to the desktop: if ('Notification' in window) { var showNotifiaction = function() { // build the notification data var notifTitle = 'Playing: ' + this.current_song.attributes.title; var notifOptions = { dir: 'auto', body: 'Album: ' + this.current_song.attributes.album + '\nArtist: ' + this.current_song.attributes.display_artist, icon: 'cover/' + this.current_song.attributes.cover_location, }; if (this.lastNotificationTimeout) { clearTimeout(this.lastNotificationTimeout); this.lastNotification.close(); } // show the notifiaction try { this.lastNotification = new Notification(notifTitle, notifOptions); // close the notification after a timeout this.lastNotificationTimeout = setTimeout(this.lastNotification.close.bind(this.lastNotification), 4321); } catch (exception) { console.log('Error using old notification style on device.'); } }; // check if we have permission, if not, ask for it if (Notification.permission === 'granted') { showNotifiaction.bind(this)(); } else if (Notification.permission !== 'denied') { Notification.requestPermission(function(permission) { if (permission === 'granted') { showNotifiaction.bind(this)(); } }); } } }; this.PlayMethodAbstracter = new (function() { this.isYoutubeElement = false; // html5 audio element this.audio_elem = document.getElementById('current_track'); this.srcElem = $('#current_src'); // youtube element this.ytplayer = null; // event listeners that dispatch the events this.audio_elem.addEventListener('ended', function() { if (this.endHandler) this.endHandler(); }.bind(this)); this.audio_elem.addEventListener('durationchange', function() { // fire the handler if (this.durationChangeHandler) this.durationChangeHandler(); }.bind(this)); this.playTrack = function(songInfo) { if (!songInfo.attributes.is_youtube) { // set the state this.isYT = false; // stop the youtube video if (this.ytplayer && this.ytplayer.stopVideo) { this.ytplayer.stopVideo(); } // load in the new audio track this.audio_elem.pause(); this.srcElem.attr('src', 'songs/' + songInfo.attributes._id); this.audio_elem.load(); this.audio_elem.play(); // only set this for tracks as youtube ones won't be avialable on refresh localStorage.setItem('last_playing_id', songInfo.attributes._id); } else { // set the state this.isYT = true; // pause the audio element this.audio_elem.pause(); // load in the youtube video this.ytplayer.loadVideoById(songInfo.attributes.youtube_id, 0, 'default'); } }; var lastYoutubeTime = -1; var lastYoutubeUpdate = (+new Date()); this.getCurrentTime = function() { if (this.isYT) { // get the time from youtube var youtubeTime = this.ytplayer.getCurrentTime(); // because it is jagged, update the time smoothly // by guessing the time in between updates if (youtubeTime != lastYoutubeTime) { lastYoutubeTime = youtubeTime; lastYoutubeUpdate = (+new Date()); return youtubeTime; } else { return lastYoutubeTime + ((+new Date()) - lastYoutubeUpdate) / 1000; } } else { // update the current time only if it's not a youtube video // since we will lose the youtube video on refresh localStorage.setItem('currentTime', this.audio_elem.currentTime); // return the current time return this.audio_elem.currentTime; } }; this.setCurrentTime = function(currentTime) { if (this.isYT) { this.ytplayer.seekTo(currentTime, true); } else { this.audio_elem.currentTime = currentTime; } }; this.getDuration = function() { if (this.isYT) { return this.ytplayer.getDuration(); } else { return this.audio_elem.duration; } }; this.pause = function() { if (this.isYT) { this.ytplayer.pauseVideo(); } else { this.audio_elem.pause(); } }; this.play = function() { if (this.isYT) { this.ytplayer.playVideo(); } else { this.audio_elem.play(); } }; // set the volume // @param volume: number between 0 and 100 this.setVolume = function(volume) { // set both of them, even if we aren't playing from them right now this.audio_elem.volume = volume / 100.00; // only the youtube player if it's defined if (this.ytplayer) { this.ytplayer.setVolume(volume); } }; this.onYoutubePlayerReady = function() { // call the duration changed handler this.durationChangeHandler(); this.setVolume(player.volume); }; this.onYoutubeStateChange = function(event) { // when the song ends, call the player function for onend if (event.data == YT.PlayerState.ENDED) { if (this.endHandler) this.endHandler(); } }; this.setupYoutube = function() { this.ytplayer = new YT.Player('player', { height: '480', width: '853', videoId: '', events: { onReady: this.onYoutubePlayerReady.bind(this), onStateChange: this.onYoutubeStateChange.bind(this), }, }); }; // force this method to be called with the current context }); // attach to the PlayMethodAbstracter events this.PlayMethodAbstracter.endHandler = this.trackEnded.bind(this); this.PlayMethodAbstracter.durationChangeHandler = this.durationChanged.bind(this); } var player = new PlayState(); player.init(); player.setupCollections(); // link in the youtube iframe API, this method must be global // so the youtube iframe API can call it when ready function onYouTubeIframeAPIReady() { player.PlayMethodAbstracter.setupYoutube(); }