diff --git a/app/app.js b/app/app.js index ebb0bea..6664920 100755 --- a/app/app.js +++ b/app/app.js @@ -1,11 +1,10 @@ +'use strict'; /* Declare app level module */ angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize', - 'jamstash.subsonic.controller', 'jamstash.archive.controller', 'jamstash.player.controller', 'jamstash.queue.controller']) + 'jamstash.subsonic.controller', 'jamstash.archive.controller', 'jamstash.player.controller', 'jamstash.queue.controller', 'angular-locker']) .config(['$routeProvider',function($routeProvider) { - 'use strict'; - $routeProvider .when('/index', { redirectTo: '/library' }) .when('/settings', { templateUrl: 'settings/settings.html', controller: 'SettingsController' }) @@ -21,8 +20,6 @@ angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize', }]) .config(['$httpProvider',function($httpProvider) { - 'use strict'; - $httpProvider.interceptors.push(['$rootScope', '$location', '$q', 'globals', function ($rootScope, $location, $q, globals) { return { 'request': function (request) { @@ -51,4 +48,10 @@ angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize', } }; }]); +}]) + +.config(['lockerProvider', function (lockerProvider) { + lockerProvider.setDefaultDriver('local') + .setDefaultNamespace('jamstash') + .setEventsEnabled(false); }]); diff --git a/app/common/main-controller.js b/app/common/main-controller.js index 465ee3e..f15cbc1 100644 --- a/app/common/main-controller.js +++ b/app/common/main-controller.js @@ -1,6 +1,6 @@ -angular.module('JamStash') -.controller('AppController', ['$scope', '$rootScope', '$document', '$window', '$location', '$cookieStore', '$http', 'utils', 'globals', 'model', 'notifications', 'player', - function($scope, $rootScope, $document, $window, $location, $cookieStore, $http, utils, globals, model, notifications, player) { +angular.module('JamStash') +.controller('AppController', ['$scope', '$rootScope', '$document', '$window', '$location', '$cookieStore', '$http', 'utils', 'globals', 'model', 'notifications', 'player', 'locker', + function($scope, $rootScope, $document, $window, $location, $cookieStore, $http, utils, globals, model, notifications, player, locker) { 'use strict'; $rootScope.settings = globals.settings; @@ -103,7 +103,7 @@ }; $scope.$watchCollection('queue', function(newItem, oldItem) { - if (oldItem.length != newItem.length + if (oldItem.length != newItem.length && globals.settings.ShowQueue) { $rootScope.showQueue(); } @@ -186,7 +186,7 @@ $rootScope.queue.splice(start, 1)[0]); $scope.$apply(); }; - $(document).on( 'click', 'message', function() { + $(document).on( 'click', 'message', function() { $(this).fadeOut(function () { $(this).remove(); }); return false; }) @@ -444,30 +444,22 @@ }; $scope.loadTrackPosition = function () { - if (utils.browserStorageCheck()) { - // Load Saved Song - var song = angular.fromJson(localStorage.getItem('CurrentSong')); - if (song) { - player.load(song); - } - } else { - if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); } + // Load Saved Song + var song = locker.get('CurrentSong'); + if (song) { + player.load(song); } }; $scope.loadQueue = function () { - if(utils.browserStorageCheck()) { - // load Saved queue - var queue = angular.fromJson(localStorage.getItem('CurrentQueue')); - if (queue) { - player.queue = queue; - if (player.queue.length > 0) { - notifications.updateMessage(player.queue.length + ' Saved Song(s)', true); - } - if (globals.settings.Debug) { console.log('Play Queue Loaded From localStorage: ' + player.queue.length + ' song(s)'); } + // load Saved queue + var queue = locker.get('CurrentQueue'); + if (queue) { + player.addSongs(queue); + if (player.queue.length > 0) { + notifications.updateMessage(player.queue.length + ' Saved Song(s)', true); } - } else { - if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); } + if (globals.settings.Debug) { console.log('Play Queue Loaded From localStorage: ' + player.queue.length + ' song(s)'); } } }; diff --git a/app/common/main-controller_test.js b/app/common/main-controller_test.js index bf6e350..6435084 100644 --- a/app/common/main-controller_test.js +++ b/app/common/main-controller_test.js @@ -1,18 +1,18 @@ describe("Main controller", function() { 'use strict'; - var scope, $rootScope, utils, globals, model, notifications, player; + var scope, $rootScope, utils, globals, notifications, player, locker; beforeEach(function() { module('JamStash'); - inject(function ($controller, _$rootScope_, _$document_, _$window_, _$location_, _$cookieStore_, _utils_, _globals_, _model_, _notifications_, _player_) { + inject(function ($controller, _$rootScope_, _$document_, _$window_, _$location_, _$cookieStore_, _utils_, _globals_, _model_, _notifications_, _player_, _locker_) { $rootScope = _$rootScope_; scope = $rootScope.$new(); utils = _utils_; globals = _globals_; - model = _model_; notifications = _notifications_; player = _player_; + locker = _locker_; $controller('AppController', { $scope: scope, @@ -23,9 +23,10 @@ describe("Main controller", function() { $cookieStore: _$cookieStore_, utils: utils, globals: globals, - model: model, + model: _model_, notifications: notifications, - player: player + player: player, + locker: locker }); }); }); @@ -62,7 +63,7 @@ describe("Main controller", function() { beforeEach(function() { fakeStorage = {}; - spyOn(localStorage, "getItem").and.callFake(function(key) { + spyOn(locker, "get").and.callFake(function(key) { return fakeStorage[key]; }); spyOn(utils, "browserStorageCheck").and.returnValue(true); @@ -86,13 +87,13 @@ describe("Main controller", function() { scope.loadTrackPosition(); - expect(localStorage.getItem).toHaveBeenCalledWith('CurrentSong'); + expect(locker.get).toHaveBeenCalledWith('CurrentSong'); expect(player.load).toHaveBeenCalledWith(song); }); it("Given that we didn't save anything in local Storage, it doesn't load anything", function() { scope.loadTrackPosition(); - expect(localStorage.getItem).toHaveBeenCalledWith('CurrentSong'); + expect(locker.get).toHaveBeenCalledWith('CurrentSong'); expect(player.load).not.toHaveBeenCalled(); }); }); @@ -100,7 +101,10 @@ describe("Main controller", function() { describe("loadQueue -", function() { beforeEach(function() { spyOn(notifications, "updateMessage"); - player.queue = []; + spyOn(player, "addSongs").and.callFake(function (songs) { + // Update the queue length so that notifications work + player.queue.length += songs.length; + }); }); it("Given that we previously saved the playing queue in local Storage, it fills the player's queue with what we saved and notifies the user", function() { @@ -115,16 +119,16 @@ describe("Main controller", function() { scope.loadQueue(); - expect(localStorage.getItem).toHaveBeenCalledWith('CurrentQueue'); - expect(player.queue).toEqual(queue); + expect(locker.get).toHaveBeenCalledWith('CurrentQueue'); + expect(player.addSongs).toHaveBeenCalledWith(queue); expect(notifications.updateMessage).toHaveBeenCalledWith('3 Saved Song(s)', true); }); it("Given that we didn't save anything in local Storage, it doesn't load anything", function() { scope.loadQueue(); - expect(localStorage.getItem).toHaveBeenCalledWith('CurrentQueue'); - expect(player.queue).toEqual([]); + expect(locker.get).toHaveBeenCalledWith('CurrentQueue'); + expect(player.addSongs).not.toHaveBeenCalled(); expect(notifications.updateMessage).not.toHaveBeenCalled(); }); }); diff --git a/app/common/notification-service.js b/app/common/notification-service.js index 5716892..dff787a 100644 --- a/app/common/notification-service.js +++ b/app/common/notification-service.js @@ -3,9 +3,9 @@ * * Provides access to the notification UI. */ -angular.module('jamstash.notifications', []) +angular.module('jamstash.notifications', ['jamstash.player.service']) -.service('notifications', ['$rootScope', 'globals', function($rootScope, globals) { +.service('notifications', ['$rootScope', 'globals', 'player', function($rootScope, globals, player) { 'use strict'; var msgIndex = 1; @@ -37,10 +37,12 @@ angular.module('jamstash.notifications', []) if (this.hasNotificationPermission()) { //closeAllNotifications() var settings = {}; - if (bind = '#NextTrack') { + if (bind === '#NextTrack') { settings.notifyClick = function () { - $rootScope.nextTrack(); + player.nextTrack(); this.close(); + //TODO: Hyz: This should be in a directive, so we wouldn't have to use this. + $rootScope.$apply(); }; } if (type === 'text') { diff --git a/app/index.html b/app/index.html index 9240824..74c65c5 100755 --- a/app/index.html +++ b/app/index.html @@ -1,131 +1,131 @@ - - - - - - - - - Jamstash - - - - - - - - - - - - - - - -
- -
- -
- -
- -
-
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + Jamstash + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/player/player-directive.js b/app/player/player-directive.js index 9f6293e..fa8c416 100644 --- a/app/player/player-directive.js +++ b/app/player/player-directive.js @@ -4,15 +4,17 @@ * Encapsulates the jPlayer plugin. It watches the player service for the song to play, load or restart. * It also enables jPlayer to attach event handlers to our UI through css Selectors. */ -angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstash.settings', 'jamstash.subsonic.service']) +angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstash.settings', 'jamstash.subsonic.service', 'jamstash.notifications', 'jamstash.utils', 'angular-locker']) -.directive('jplayer', ['player', 'globals', 'subsonic', function(playerService, globals, subsonic) { +.directive('jplayer', ['player', 'globals', 'subsonic', 'notifications', 'utils', 'locker', '$window', + function(playerService, globals, subsonic, notifications, utils, locker, $window) { 'use strict'; return { restrict: 'EA', template: '
', link: function(scope, element) { + var timerid; var $player = element.children('div'); var audioSolution = 'html,flash'; if (globals.settings.ForceFlash) { @@ -41,12 +43,10 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas duration: '#duration' }, play: function() { - console.log('jplayer play'); scope.revealControls(); scope.scrobbled = false; }, ended: function() { - console.log('jplayer ended'); // We do this here and not on the service because we cannot create // a circular dependency between the player and subsonic services if(playerService.isLastSongPlaying() && globals.settings.AutoPlay) { @@ -69,23 +69,21 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas }); }; - updatePlayer(); - - scope.currentSong = {}; - scope.scrobbled = false; - scope.$watch(function () { return playerService.getPlayingSong(); - }, function (newVal) { - console.log('playingSong changed !'); - scope.currentSong = newVal; - $player.jPlayer('setMedia', {'mp3': newVal.url}); + }, function (newSong) { + scope.currentSong = newSong; + $player.jPlayer('setMedia', {'mp3': newSong.url}); if(playerService.loadSong === true) { // Do not play, only load playerService.loadSong = false; scope.revealControls(); + $player.jPlayer('pause', newSong.position); } else { $player.jPlayer('play'); + if(globals.settings.NotificationSong) { + notifications.showNotification(newSong.coverartthumb, utils.toHTML.un(newSong.name), utils.toHTML.un(newSong.artist + ' - ' + newSong.album), 'text', '#NextTrack'); + } } }); @@ -93,7 +91,6 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas return playerService.restartSong; }, function (newVal) { if(newVal === true) { - console.log('restartSong changed !'); $player.jPlayer('play', 0); playerService.restartSong = false; } @@ -104,6 +101,51 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas $('#songdetails').css('visibility', 'visible'); }; + scope.saveTrackPosition = function () { + var audio = $player.data('jPlayer'); + if (audio !== undefined && scope.currentSong !== undefined) { + var position = audio.status.currentTime; + if (position !== null) { + scope.currentSong.position = position; + locker.put('CurrentSong', scope.currentSong); + if (globals.settings.Debug) { console.log('Saving Current Position: ', scope.currentSong); } + } + } + }; + + scope.saveQueue = function () { + locker.put('CurrentQueue', playerService.queue); + if (globals.settings.Debug) { console.log('Saving Queue: ' + playerService.queue.length + ' songs'); } + }; + + scope.startSavePosition = function () { + if (globals.settings.SaveTrackPosition) { + if (timerid !== 0) { + $window.clearInterval(timerid); + } + timerid = $window.setInterval(function () { + var audio = $player.data('jPlayer'); + if (globals.settings.SaveTrackPosition && audio.status.currentTime > 0 && audio.status.paused === false) { + $('#action_SaveProgress') + .fadeTo("slow", 0).delay(500) + .fadeTo("slow", 1).delay(500) + .fadeTo("slow", 0).delay(500) + .fadeTo("slow", 1); + scope.saveTrackPosition(); + scope.saveQueue(); + } + }, 30000); + } + }; + + // Startup + timerid = 0; + scope.currentSong = {}; + scope.scrobbled = false; + + updatePlayer(); + scope.startSavePosition(); + } //end link }; }]); diff --git a/app/player/player-directive_test.js b/app/player/player-directive_test.js index 9015af2..bb42735 100644 --- a/app/player/player-directive_test.js +++ b/app/player/player-directive_test.js @@ -1,7 +1,8 @@ describe("jplayer directive", function() { 'use strict'; - var element, scope, playerService, mockGlobals, subsonic, $player, playingSong; + var element, scope, $player, playingSong, + playerService, mockGlobals, subsonic, notifications, locker, $window; beforeEach(function() { playingSong = {}; @@ -20,15 +21,23 @@ describe("jplayer directive", function() { $delegate.nextTrack = jasmine.createSpy('nextTrack'); $delegate.songEnded = jasmine.createSpy('songEnded'); $delegate.isLastSongPlaying = jasmine.createSpy('isLastSongPlaying'); - + return $delegate; + }); + //TODO: Hyz: We shouldn't have to know the utils service just for that. Remove these calls and deal with this in the Notifications service. + // Mock the utils service + $provide.decorator('utils', function ($delegate) { + $delegate.toHTML.un = jasmine.createSpy('un'); return $delegate; }); $provide.value('globals', mockGlobals); }); - inject(function($rootScope, $compile, _player_, _subsonic_) { + inject(function($rootScope, $compile, _player_, _subsonic_, _notifications_, _locker_, _$window_) { playerService = _player_; subsonic = _subsonic_; + notifications = _notifications_; + locker = _locker_; + $window = _$window_; // Compile the directive scope = $rootScope.$new(); element = '
'; @@ -52,23 +61,35 @@ describe("jplayer directive", function() { expect(scope.currentSong).toEqual(playingSong); }); - it("if the player service's loadSong flag is true, it does not play the song and it displays the player controls", function() { + it("if the player service's loadSong flag is true, it does not play the song, it displays the player controls and sets the player to the song's supplied position", function() { spyOn(scope, "revealControls"); + playingSong.position = 42.2784; playerService.loadSong = true; scope.$apply(); expect($player.jPlayer).not.toHaveBeenCalledWith('play'); + expect($player.jPlayer).toHaveBeenCalledWith('pause', playingSong.position); expect(playerService.loadSong).toBeFalsy(); expect(scope.revealControls).toHaveBeenCalled(); }); - it("otherwise, it plays it", function() { - playerService.loadSong = false; - scope.$apply(); + describe("if the player service's loadSong flag is false,", function() { + it("it plays the song", function() { + playerService.loadSong = false; + scope.$apply(); - expect($player.jPlayer).toHaveBeenCalledWith('play'); - expect(playerService.loadSong).toBeFalsy(); + expect($player.jPlayer).toHaveBeenCalledWith('play'); + expect(playerService.loadSong).toBeFalsy(); + }); + + it("if the global setting NotificationSong is true, it displays a notification", function() { + spyOn(notifications, "showNotification"); + mockGlobals.settings.NotificationSong = true; + scope.$apply(); + + expect(notifications.showNotification).toHaveBeenCalled(); + }); }); }); @@ -168,4 +189,79 @@ describe("jplayer directive", function() { expect(scope.scrobbled).toBeTruthy(); }); }); + + describe("save to localStorage -", function() { + beforeEach(function() { + spyOn(locker, "put"); + }); + + it("it saves the current song and its position to localStorage", function() { + var position = 48.0773; + $player.data('jPlayer').status.currentTime = position; + scope.currentSong = { + id: 419 + }; + + scope.saveTrackPosition(); + + expect(scope.currentSong.position).toBe(position); + expect(locker.put).toHaveBeenCalledWith('CurrentSong', scope.currentSong); + }); + + it("it saves the player queue to localStorage", function() { + var queue = [ + {id: 2313}, + {id: 4268}, + {id: 5470} + ]; + playerService.queue = queue; + + scope.saveQueue(); + + expect(locker.put).toHaveBeenCalledWith('CurrentQueue', queue); + }); + + describe("Given that the global setting SaveTrackPosition is true,", function() { + beforeEach(function() { + jasmine.clock().install(); + mockGlobals.settings.SaveTrackPosition = true; + spyOn(scope, "saveTrackPosition"); + spyOn(scope, "saveQueue"); + }); + + afterEach(function() { + jasmine.clock().uninstall(); + }); + + it("every 30 seconds, it saves the current song and queue", function() { + $player.data('jPlayer').status.currentTime = 35.3877; + $player.data('jPlayer').status.paused = false; + + scope.startSavePosition(); + jasmine.clock().tick(30001); + + expect(scope.saveTrackPosition).toHaveBeenCalled(); + expect(scope.saveQueue).toHaveBeenCalled(); + }); + + it("if the song is not playing, it does not save anything", function() { + $player.data('jPlayer').status.currentTime = 0.0; + $player.data('jPlayer').status.paused = true; + + scope.startSavePosition(); + jasmine.clock().tick(30001); + + expect(scope.saveTrackPosition).not.toHaveBeenCalled(); + expect(scope.saveQueue).not.toHaveBeenCalled(); + }); + + it("if there was already a watcher, it clears it before watching", function() { + spyOn($window, "clearInterval"); + + scope.startSavePosition(); + scope.startSavePosition(); + expect($window.clearInterval).toHaveBeenCalled(); + }); + }); + }); }); diff --git a/app/settings/settings.js b/app/settings/settings.js index 997093d..581ea05 100644 --- a/app/settings/settings.js +++ b/app/settings/settings.js @@ -47,7 +47,7 @@ } } if ($scope.settings.SaveTrackPosition) { - player.saveTrackPosition(); + //TODO: Hyz: player.saveTrackPosition(); } else { player.deleteCurrentQueue(); } diff --git a/bower.json b/bower.json index 41d6b29..0875e67 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "jamstash", - "version": "4.3", + "version": "4.3.1", "description": "HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming", "authors": [ "tsquillario (https://github.com/tsquillario)", @@ -38,7 +38,8 @@ "notify.js": "<=1.2.2", "jquery.scrollTo": "~1.4.5", "underscore": "~1.7.0", - "angular-underscore": "~0.5.0" + "angular-underscore": "~0.5.0", + "angular-locker": "~1.0.2" }, "overrides": { "fancybox": { diff --git a/karma.conf.js b/karma.conf.js index 9617475..b0e2828 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -32,6 +32,7 @@ module.exports = function(config) { 'bower_components/jquery.scrollTo/jquery.scrollTo.js', 'bower_components/underscore/underscore.js', 'bower_components/angular-underscore/angular-underscore.js', + 'bower_components/angular-locker/dist/angular-locker.min.js', 'bower_components/angular-mocks/angular-mocks.js', 'bower_components/jasmine-promise-matchers/dist/jasmine-promise-matchers.js', 'bower_components/jasmine-fixture/dist/jasmine-fixture.js', diff --git a/manifest.json b/manifest.json index 6e2f9de..0a6a943 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "Jamstash", "description": "HTML5 Player for Subsonic & Archive.org", - "version": "4.2.3", + "version": "4.3.1", "app": { "launch": { "web_url": "http://jamstash.com" diff --git a/package.json b/package.json index 7becdee..6169e0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jamstash", - "version": "4.3", + "version": "4.3.1", "description": "HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming", "author": "Trevor Squillario (https://github.com/tsquillario)", "contributors": [