Adds angular-locker dependency.

- It makes it easier to use localStorage and sessionStorage "the angular way". It also does all the error handling so we don't need to.
- Adds back the automatic saving of the current track's position and playing queue in localStorage. It's fully unit tested.
- Adds back the notifications. Every time we change songs (if the setting is true), it displays a notification. Clicking on it goes to the next song, just like before.
- Bumps up the versions to the actual value on the various json files.
This commit is contained in:
Hyzual 2015-01-03 01:36:12 +01:00
parent 2e97e25f25
commit 83869b7808
12 changed files with 346 additions and 205 deletions

View file

@ -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);
}]);

View file

@ -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;
@ -444,31 +444,23 @@
};
$scope.loadTrackPosition = function () {
if (utils.browserStorageCheck()) {
// Load Saved Song
var song = angular.fromJson(localStorage.getItem('CurrentSong'));
var song = locker.get('CurrentSong');
if (song) {
player.load(song);
}
} else {
if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); }
}
};
$scope.loadQueue = function () {
if(utils.browserStorageCheck()) {
// load Saved queue
var queue = angular.fromJson(localStorage.getItem('CurrentQueue'));
var queue = locker.get('CurrentQueue');
if (queue) {
player.queue = queue;
player.addSongs(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)'); }
}
} else {
if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); }
}
};
/* Launch on Startup */

View file

@ -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();
});
});

View file

@ -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') {

View file

@ -16,7 +16,6 @@
<link rel="stylesheet" href="bower_components/fancybox/source/jquery.fancybox.css" />
<!-- endbower -->
<!-- endbuild -->
<!--<link href="vendor/jquery-split-pane.css" rel="stylesheet" />-->
<link href="styles/Style.css" rel="stylesheet" type="text/css" data-name="main" />
<link href="styles/Mobile.css" rel="stylesheet" type="text/css" data-name="main" />
<link href="" rel="stylesheet" type="text/css" data-name="theme" />
@ -103,6 +102,7 @@
<script src="bower_components/jquery.scrollTo/jquery.scrollTo.js"></script>
<script src="bower_components/underscore/underscore.js"></script>
<script src="bower_components/angular-underscore/angular-underscore.js"></script>
<script src="bower_components/angular-locker/dist/angular-locker.min.js"></script>
<!-- endbower -->
<script src="vendor/jquery.base64.js"></script>
<script src="vendor/jquery.dateFormat-1.0.js"></script>

View file

@ -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: '<div></div>',
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
};
}]);

View file

@ -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 = '<div id="playdeck_1" jplayer></div>';
@ -52,24 +61,36 @@ 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() {
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();
});
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();
});
});
});
it("When the player service's restartSong flag is true, it restarts the current song and resets the flag to false", function() {
@ -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();
});
});
});
});

View file

@ -47,7 +47,7 @@
}
}
if ($scope.settings.SaveTrackPosition) {
player.saveTrackPosition();
//TODO: Hyz: player.saveTrackPosition();
} else {
player.deleteCurrentQueue();
}

View file

@ -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": {

View file

@ -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',

View file

@ -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"

View file

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