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": [