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 */ /* Declare app level module */
angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize', 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) { .config(['$routeProvider',function($routeProvider) {
'use strict';
$routeProvider $routeProvider
.when('/index', { redirectTo: '/library' }) .when('/index', { redirectTo: '/library' })
.when('/settings', { templateUrl: 'settings/settings.html', controller: 'SettingsController' }) .when('/settings', { templateUrl: 'settings/settings.html', controller: 'SettingsController' })
@ -21,8 +20,6 @@ angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize',
}]) }])
.config(['$httpProvider',function($httpProvider) { .config(['$httpProvider',function($httpProvider) {
'use strict';
$httpProvider.interceptors.push(['$rootScope', '$location', '$q', 'globals', function ($rootScope, $location, $q, globals) { $httpProvider.interceptors.push(['$rootScope', '$location', '$q', 'globals', function ($rootScope, $location, $q, globals) {
return { return {
'request': function (request) { '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') angular.module('JamStash')
.controller('AppController', ['$scope', '$rootScope', '$document', '$window', '$location', '$cookieStore', '$http', 'utils', 'globals', 'model', 'notifications', 'player', .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) { function($scope, $rootScope, $document, $window, $location, $cookieStore, $http, utils, globals, model, notifications, player, locker) {
'use strict'; 'use strict';
$rootScope.settings = globals.settings; $rootScope.settings = globals.settings;
@ -103,7 +103,7 @@
}; };
$scope.$watchCollection('queue', function(newItem, oldItem) { $scope.$watchCollection('queue', function(newItem, oldItem) {
if (oldItem.length != newItem.length if (oldItem.length != newItem.length
&& globals.settings.ShowQueue) { && globals.settings.ShowQueue) {
$rootScope.showQueue(); $rootScope.showQueue();
} }
@ -186,7 +186,7 @@
$rootScope.queue.splice(start, 1)[0]); $rootScope.queue.splice(start, 1)[0]);
$scope.$apply(); $scope.$apply();
}; };
$(document).on( 'click', 'message', function() { $(document).on( 'click', 'message', function() {
$(this).fadeOut(function () { $(this).remove(); }); $(this).fadeOut(function () { $(this).remove(); });
return false; return false;
}) })
@ -444,30 +444,22 @@
}; };
$scope.loadTrackPosition = function () { $scope.loadTrackPosition = function () {
if (utils.browserStorageCheck()) { // Load Saved Song
// Load Saved Song var song = locker.get('CurrentSong');
var song = angular.fromJson(localStorage.getItem('CurrentSong')); if (song) {
if (song) { player.load(song);
player.load(song);
}
} else {
if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); }
} }
}; };
$scope.loadQueue = function () { $scope.loadQueue = function () {
if(utils.browserStorageCheck()) { // load Saved queue
// load Saved queue var queue = locker.get('CurrentQueue');
var queue = angular.fromJson(localStorage.getItem('CurrentQueue')); if (queue) {
if (queue) { player.addSongs(queue);
player.queue = queue; if (player.queue.length > 0) {
if (player.queue.length > 0) { notifications.updateMessage(player.queue.length + ' Saved Song(s)', true);
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('Play Queue Loaded From localStorage: ' + player.queue.length + ' song(s)'); }
if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); }
} }
}; };

View file

@ -1,18 +1,18 @@
describe("Main controller", function() { describe("Main controller", function() {
'use strict'; 'use strict';
var scope, $rootScope, utils, globals, model, notifications, player; var scope, $rootScope, utils, globals, notifications, player, locker;
beforeEach(function() { beforeEach(function() {
module('JamStash'); 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_; $rootScope = _$rootScope_;
scope = $rootScope.$new(); scope = $rootScope.$new();
utils = _utils_; utils = _utils_;
globals = _globals_; globals = _globals_;
model = _model_;
notifications = _notifications_; notifications = _notifications_;
player = _player_; player = _player_;
locker = _locker_;
$controller('AppController', { $controller('AppController', {
$scope: scope, $scope: scope,
@ -23,9 +23,10 @@ describe("Main controller", function() {
$cookieStore: _$cookieStore_, $cookieStore: _$cookieStore_,
utils: utils, utils: utils,
globals: globals, globals: globals,
model: model, model: _model_,
notifications: notifications, notifications: notifications,
player: player player: player,
locker: locker
}); });
}); });
}); });
@ -62,7 +63,7 @@ describe("Main controller", function() {
beforeEach(function() { beforeEach(function() {
fakeStorage = {}; fakeStorage = {};
spyOn(localStorage, "getItem").and.callFake(function(key) { spyOn(locker, "get").and.callFake(function(key) {
return fakeStorage[key]; return fakeStorage[key];
}); });
spyOn(utils, "browserStorageCheck").and.returnValue(true); spyOn(utils, "browserStorageCheck").and.returnValue(true);
@ -86,13 +87,13 @@ describe("Main controller", function() {
scope.loadTrackPosition(); scope.loadTrackPosition();
expect(localStorage.getItem).toHaveBeenCalledWith('CurrentSong'); expect(locker.get).toHaveBeenCalledWith('CurrentSong');
expect(player.load).toHaveBeenCalledWith(song); expect(player.load).toHaveBeenCalledWith(song);
}); });
it("Given that we didn't save anything in local Storage, it doesn't load anything", function() { it("Given that we didn't save anything in local Storage, it doesn't load anything", function() {
scope.loadTrackPosition(); scope.loadTrackPosition();
expect(localStorage.getItem).toHaveBeenCalledWith('CurrentSong'); expect(locker.get).toHaveBeenCalledWith('CurrentSong');
expect(player.load).not.toHaveBeenCalled(); expect(player.load).not.toHaveBeenCalled();
}); });
}); });
@ -100,7 +101,10 @@ describe("Main controller", function() {
describe("loadQueue -", function() { describe("loadQueue -", function() {
beforeEach(function() { beforeEach(function() {
spyOn(notifications, "updateMessage"); 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() { 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(); scope.loadQueue();
expect(localStorage.getItem).toHaveBeenCalledWith('CurrentQueue'); expect(locker.get).toHaveBeenCalledWith('CurrentQueue');
expect(player.queue).toEqual(queue); expect(player.addSongs).toHaveBeenCalledWith(queue);
expect(notifications.updateMessage).toHaveBeenCalledWith('3 Saved Song(s)', true); 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() { it("Given that we didn't save anything in local Storage, it doesn't load anything", function() {
scope.loadQueue(); scope.loadQueue();
expect(localStorage.getItem).toHaveBeenCalledWith('CurrentQueue'); expect(locker.get).toHaveBeenCalledWith('CurrentQueue');
expect(player.queue).toEqual([]); expect(player.addSongs).not.toHaveBeenCalled();
expect(notifications.updateMessage).not.toHaveBeenCalled(); expect(notifications.updateMessage).not.toHaveBeenCalled();
}); });
}); });

View file

@ -3,9 +3,9 @@
* *
* Provides access to the notification UI. * 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'; 'use strict';
var msgIndex = 1; var msgIndex = 1;
@ -37,10 +37,12 @@ angular.module('jamstash.notifications', [])
if (this.hasNotificationPermission()) { if (this.hasNotificationPermission()) {
//closeAllNotifications() //closeAllNotifications()
var settings = {}; var settings = {};
if (bind = '#NextTrack') { if (bind === '#NextTrack') {
settings.notifyClick = function () { settings.notifyClick = function () {
$rootScope.nextTrack(); player.nextTrack();
this.close(); this.close();
//TODO: Hyz: This should be in a directive, so we wouldn't have to use this.
$rootScope.$apply();
}; };
} }
if (type === 'text') { if (type === 'text') {

View file

@ -1,131 +1,131 @@
<!DOCTYPE HTML> <!DOCTYPE HTML>
<html lang="en" ng-app="JamStash"> <html lang="en" ng-app="JamStash">
<head> <head>
<meta http-equiv="Content-type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-type" content="text/html; charset=UTF-8">
<meta name="description" content="HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming"> <meta name="description" content="HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming">
<meta name="keywords" content="Subsonic, Archive.org, Live Music Archive, HTML5 Audio, Music Streaming, Live Music"> <meta name="keywords" content="Subsonic, Archive.org, Live Music Archive, HTML5 Audio, Music Streaming, Live Music">
<meta property="og:image" content="http://jamstash.com/images/fbpreview.png"/> <meta property="og:image" content="http://jamstash.com/images/fbpreview.png"/>
<meta name=viewport content="width=device-width, initial-scale=1"> <meta name=viewport content="width=device-width, initial-scale=1">
<title>Jamstash</title> <title>Jamstash</title>
<link href="images/favicon_32x32.ico" rel="shortcut icon" /> <link href="images/favicon_32x32.ico" rel="shortcut icon" />
<link rel="icon" href="images/favicon_48x48.png" sizes="48x48"/> <link rel="icon" href="images/favicon_48x48.png" sizes="48x48"/>
<link rel="icon" href="images/favicon_32x32.png" sizes="32x32"/> <link rel="icon" href="images/favicon_32x32.png" sizes="32x32"/>
<!-- build:css(.) styles/vendor.min.css --> <!-- build:css(.) styles/vendor.min.css -->
<!-- bower:css --> <!-- bower:css -->
<link rel="stylesheet" href="bower_components/jplayer/dist/skin/pink.flag/jplayer.pink.flag.css" /> <link rel="stylesheet" href="bower_components/jplayer/dist/skin/pink.flag/jplayer.pink.flag.css" />
<link rel="stylesheet" href="bower_components/fancybox/source/jquery.fancybox.css" /> <link rel="stylesheet" href="bower_components/fancybox/source/jquery.fancybox.css" />
<!-- endbower --> <!-- endbower -->
<!-- endbuild --> <!-- 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/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="styles/Mobile.css" rel="stylesheet" type="text/css" data-name="main" /> <link href="" rel="stylesheet" type="text/css" data-name="theme" />
<link href="" rel="stylesheet" type="text/css" data-name="theme" /> </head>
</head> <body ng-controller="AppController">
<body ng-controller="AppController"> <div id="container">
<div id="container"> <div id="header">
<div id="header"> <div id="messages">
<div id="messages"> <span ng-attr-id="{{ 'msg_' + $index }}" class="message" ng-repeat="item in Messages track by $index" ng-bind-html="item"></span>
<span ng-attr-id="{{ 'msg_' + $index }}" class="message" ng-repeat="item in Messages track by $index" ng-bind-html="item"></span> </div>
</div> <div id="loading"></div>
<div id="loading"></div> <a id="jslogo" title="Jamstash" class="showQueue" href=""></a>
<a id="jslogo" title="Jamstash" class="showQueue" href=""></a> <a id="sslogo" target="_blank" ng-show="settings.Server" ng-href="{{settings.Server}}" title="{{settings.Server}}"></a>
<a id="sslogo" target="_blank" ng-show="settings.Server" ng-href="{{settings.Server}}" title="{{settings.Server}}"></a> <div id="globalactions">
<div id="globalactions"> <a href="" class="button" ng-click="toggleQueue()" title="Pin Queue"><img src="images/arrow_right_gl_8x8.png" /></a>
<a href="" class="button" ng-click="toggleQueue()" title="Pin Queue"><img src="images/arrow_right_gl_8x8.png" /></a> </div>
</div> <div id="nav">
<div id="nav"> <ul class="tabs">
<ul class="tabs"> <li><a href="#/library" class="first" id="action_Library" title="Library" ng-class="{'active': isActive('/library')}"><img src="images/headphones_gd_16x14.png" /></a></li>
<li><a href="#/library" class="first" id="action_Library" title="Library" ng-class="{'active': isActive('/library')}"><img src="images/headphones_gd_16x14.png" /></a></li> <li><a href="#/archive" id="action_Archive" class="" title="Archive.org - Live Music Archive" ng-class="{'active': isActive('/archive')}"><img src="images/archive_gd_16x16.png" /></a></li>
<li><a href="#/archive" id="action_Archive" class="" title="Archive.org - Live Music Archive" ng-class="{'active': isActive('/archive')}"><img src="images/archive_gd_16x16.png" /></a></li> <li><a href="#/settings" id="action_Settings" class="last" title="Settings" ng-class="{'active': isActive('/settings')}"><img src="images/cog_16x16.png" /></a></li>
<li><a href="#/settings" id="action_Settings" class="last" title="Settings" ng-class="{'active': isActive('/settings')}"><img src="images/cog_16x16.png" /></a></li> </ul>
</ul> </div>
</div> </div>
</div> <div id="content">
<div id="content"> <!-- Main -->
<!-- Main --> <div ng-view></div>
<div ng-view></div> <!-- Audio Player -->
<!-- Audio Player --> <div class="clear"></div>
<div class="clear"></div>
<div class="clear"></div>
<div class="clear"></div> </div><!-- end #content -->
</div><!-- end #content --> <div id="SideBar" ng-controller="QueueController">
<div id="SideBar" ng-controller="QueueController"> <div class="headeractions">
<div class="headeractions"> <a class="buttonimg" title="Shuffle Queue" ng-click="queueShuffle()"><img src="images/fork_gd_11x12.png"></a>
<a class="buttonimg" title="Shuffle Queue" ng-click="queueShuffle()"><img src="images/fork_gd_11x12.png"></a> <a class="buttonimg" id="action_Empty" title="Delete Queue" ng-click="queueEmpty()"><img src="images/trash_fill_gd_12x12.png"></a>
<a class="buttonimg" id="action_Empty" title="Delete Queue" ng-click="queueEmpty()"><img src="images/trash_fill_gd_12x12.png"></a> <a class="buttonimg" id="action_DeleteSelected" title="Remove Selected From Queue" ng-click="queueRemoveSelected()"><img src="images/x_11x11.png"></a>
<a class="buttonimg" id="action_DeleteSelected" title="Remove Selected From Queue" ng-click="queueRemoveSelected()"><img src="images/x_11x11.png"></a> </div>
</div> <div class="header">Queue</div>
<div class="header">Queue</div> <div id="SideQueue">
<div id="SideQueue"> <ul class="simplelist songlist noselect">
<ul class="simplelist songlist noselect"> <div ng-repeat="song in [player.queue] track by $index" class="songs" ng-include src="'common/songs_lite.html'" sortable></div>
<div ng-repeat="song in [player.queue] track by $index" class="songs" ng-include src="'common/songs_lite.html'" sortable></div> </ul>
</ul> <div class="colspacer"></div>
<div class="colspacer"></div> </div>
</div> <!--
<!-- <div id="NowPlaying">
<div id="NowPlaying"> <div class="header"><img src="images/rss_12x12.png" /> Now Playing</div>
<div class="header"><img src="images/rss_12x12.png" /> Now Playing</div> <div id="NowPlayingList"><span class="user">Loading...</span></div>
<div id="NowPlayingList"><span class="user">Loading...</span></div> </div>
</div> <div id="Chat">
<div id="Chat"> <div class="header"><img src="images/chat_alt_stroke_12x12.png" /> Chat</div>
<div class="header"><img src="images/chat_alt_stroke_12x12.png" /> Chat</div> <div id="ChatMsgs"></div>
<div id="ChatMsgs"></div> </div>
</div> <div class="submit"><img src="images/comment_stroke_gl_12x11.png" /><input type="text" id="ChatMsg" class="chat" title="Hit [Enter] to Post" /></div>
<div class="submit"><img src="images/comment_stroke_gl_12x11.png" /><input type="text" id="ChatMsg" class="chat" title="Hit [Enter] to Post" /></div> -->
--> </div>
</div> <!-- Player -->
<!-- Player --> <div ng-include src="'player/player.html'" ng-controller="PlayerController"></div>
<div ng-include src="'player/player.html'" ng-controller="PlayerController"></div> </div> <!-- End container -->
</div> <!-- End container --> <script>
<script> (function (i, s, o, g, r, a, m) {
(function (i, s, o, g, r, a, m) { i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments)
(i[r].q = i[r].q || []).push(arguments) }, i[r].l = 1 * new Date(); a = s.createElement(o),
}, i[r].l = 1 * new Date(); a = s.createElement(o), m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m) })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-40174100-1', 'jamstash.com');
ga('create', 'UA-40174100-1', 'jamstash.com'); ga('send', 'pageview');
ga('send', 'pageview'); </script>
</script> <!-- build:js({.,app}) scripts/vendor.min.js -->
<!-- build:js({.,app}) scripts/vendor.min.js --> <!-- bower:js -->
<!-- bower:js --> <script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/jquery/jquery.js"></script> <script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular/angular.js"></script> <script src="bower_components/angular-route/angular-route.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script> <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script> <script src="bower_components/angular-cookies/angular-cookies.js"></script>
<script src="bower_components/angular-cookies/angular-cookies.js"></script> <script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="bower_components/angular-resource/angular-resource.js"></script> <script src="bower_components/jquery-ui/ui/jquery-ui.js"></script>
<script src="bower_components/jquery-ui/ui/jquery-ui.js"></script> <script src="bower_components/jplayer/dist/jplayer/jquery.jplayer.js"></script>
<script src="bower_components/jplayer/dist/jplayer/jquery.jplayer.js"></script> <script src="bower_components/fancybox/source/jquery.fancybox.js"></script>
<script src="bower_components/fancybox/source/jquery.fancybox.js"></script> <script src="bower_components/notify.js/notify.js"></script>
<script src="bower_components/notify.js/notify.js"></script> <script src="bower_components/jquery.scrollTo/jquery.scrollTo.js"></script>
<script src="bower_components/jquery.scrollTo/jquery.scrollTo.js"></script> <script src="bower_components/underscore/underscore.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-underscore/angular-underscore.js"></script> <script src="bower_components/angular-locker/dist/angular-locker.min.js"></script>
<!-- endbower --> <!-- endbower -->
<script src="vendor/jquery.base64.js"></script> <script src="vendor/jquery.base64.js"></script>
<script src="vendor/jquery.dateFormat-1.0.js"></script> <script src="vendor/jquery.dateFormat-1.0.js"></script>
<!-- endbuild --> <!-- endbuild -->
<!-- our scripts --> <!-- our scripts -->
<!-- build:js(app) scripts/scripts.min.js --> <!-- build:js(app) scripts/scripts.min.js -->
<script src="app.js"></script> <script src="app.js"></script>
<script src="settings/settings.js"></script> <script src="settings/settings.js"></script>
<script src="settings/settings-service.js"></script> <script src="settings/settings-service.js"></script>
<script src="common/model-service.js"></script> <script src="common/model-service.js"></script>
<script src="common/utils-service.js"></script> <script src="common/utils-service.js"></script>
<script src="common/notification-service.js"></script> <script src="common/notification-service.js"></script>
<script src="common/main-controller.js"></script> <script src="common/main-controller.js"></script>
<script src="subsonic/subsonic.js"></script> <script src="subsonic/subsonic.js"></script>
<script src="subsonic/subsonic-service.js"></script> <script src="subsonic/subsonic-service.js"></script>
<script src="archive/archive.js"></script> <script src="archive/archive.js"></script>
<script src="archive/archive-service.js"></script> <script src="archive/archive-service.js"></script>
<script src="player/player.js"></script> <script src="player/player.js"></script>
<script src="player/player-directive.js"></script> <script src="player/player-directive.js"></script>
<script src="player/player-service.js"></script> <script src="player/player-service.js"></script>
<script src="queue/queue.js"></script> <script src="queue/queue.js"></script>
<script src="common/filters.js"></script> <script src="common/filters.js"></script>
<script src="common/directives.js"></script> <script src="common/directives.js"></script>
<!-- endbuild --> <!-- endbuild -->
</body> </body>
</html> </html>

View file

@ -4,15 +4,17 @@
* Encapsulates the jPlayer plugin. It watches the player service for the song to play, load or restart. * 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. * 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'; 'use strict';
return { return {
restrict: 'EA', restrict: 'EA',
template: '<div></div>', template: '<div></div>',
link: function(scope, element) { link: function(scope, element) {
var timerid;
var $player = element.children('div'); var $player = element.children('div');
var audioSolution = 'html,flash'; var audioSolution = 'html,flash';
if (globals.settings.ForceFlash) { if (globals.settings.ForceFlash) {
@ -41,12 +43,10 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas
duration: '#duration' duration: '#duration'
}, },
play: function() { play: function() {
console.log('jplayer play');
scope.revealControls(); scope.revealControls();
scope.scrobbled = false; scope.scrobbled = false;
}, },
ended: function() { ended: function() {
console.log('jplayer ended');
// We do this here and not on the service because we cannot create // We do this here and not on the service because we cannot create
// a circular dependency between the player and subsonic services // a circular dependency between the player and subsonic services
if(playerService.isLastSongPlaying() && globals.settings.AutoPlay) { 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 () { scope.$watch(function () {
return playerService.getPlayingSong(); return playerService.getPlayingSong();
}, function (newVal) { }, function (newSong) {
console.log('playingSong changed !'); scope.currentSong = newSong;
scope.currentSong = newVal; $player.jPlayer('setMedia', {'mp3': newSong.url});
$player.jPlayer('setMedia', {'mp3': newVal.url});
if(playerService.loadSong === true) { if(playerService.loadSong === true) {
// Do not play, only load // Do not play, only load
playerService.loadSong = false; playerService.loadSong = false;
scope.revealControls(); scope.revealControls();
$player.jPlayer('pause', newSong.position);
} else { } else {
$player.jPlayer('play'); $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; return playerService.restartSong;
}, function (newVal) { }, function (newVal) {
if(newVal === true) { if(newVal === true) {
console.log('restartSong changed !');
$player.jPlayer('play', 0); $player.jPlayer('play', 0);
playerService.restartSong = false; playerService.restartSong = false;
} }
@ -104,6 +101,51 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas
$('#songdetails').css('visibility', 'visible'); $('#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 } //end link
}; };
}]); }]);

View file

@ -1,7 +1,8 @@
describe("jplayer directive", function() { describe("jplayer directive", function() {
'use strict'; 'use strict';
var element, scope, playerService, mockGlobals, subsonic, $player, playingSong; var element, scope, $player, playingSong,
playerService, mockGlobals, subsonic, notifications, locker, $window;
beforeEach(function() { beforeEach(function() {
playingSong = {}; playingSong = {};
@ -20,15 +21,23 @@ describe("jplayer directive", function() {
$delegate.nextTrack = jasmine.createSpy('nextTrack'); $delegate.nextTrack = jasmine.createSpy('nextTrack');
$delegate.songEnded = jasmine.createSpy('songEnded'); $delegate.songEnded = jasmine.createSpy('songEnded');
$delegate.isLastSongPlaying = jasmine.createSpy('isLastSongPlaying'); $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; return $delegate;
}); });
$provide.value('globals', mockGlobals); $provide.value('globals', mockGlobals);
}); });
inject(function($rootScope, $compile, _player_, _subsonic_) { inject(function($rootScope, $compile, _player_, _subsonic_, _notifications_, _locker_, _$window_) {
playerService = _player_; playerService = _player_;
subsonic = _subsonic_; subsonic = _subsonic_;
notifications = _notifications_;
locker = _locker_;
$window = _$window_;
// Compile the directive // Compile the directive
scope = $rootScope.$new(); scope = $rootScope.$new();
element = '<div id="playdeck_1" jplayer></div>'; element = '<div id="playdeck_1" jplayer></div>';
@ -52,23 +61,35 @@ describe("jplayer directive", function() {
expect(scope.currentSong).toEqual(playingSong); 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"); spyOn(scope, "revealControls");
playingSong.position = 42.2784;
playerService.loadSong = true; playerService.loadSong = true;
scope.$apply(); scope.$apply();
expect($player.jPlayer).not.toHaveBeenCalledWith('play'); expect($player.jPlayer).not.toHaveBeenCalledWith('play');
expect($player.jPlayer).toHaveBeenCalledWith('pause', playingSong.position);
expect(playerService.loadSong).toBeFalsy(); expect(playerService.loadSong).toBeFalsy();
expect(scope.revealControls).toHaveBeenCalled(); expect(scope.revealControls).toHaveBeenCalled();
}); });
it("otherwise, it plays it", function() { describe("if the player service's loadSong flag is false,", function() {
playerService.loadSong = false; it("it plays the song", function() {
scope.$apply(); playerService.loadSong = false;
scope.$apply();
expect($player.jPlayer).toHaveBeenCalledWith('play'); expect($player.jPlayer).toHaveBeenCalledWith('play');
expect(playerService.loadSong).toBeFalsy(); 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(); 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) { if ($scope.settings.SaveTrackPosition) {
player.saveTrackPosition(); //TODO: Hyz: player.saveTrackPosition();
} else { } else {
player.deleteCurrentQueue(); player.deleteCurrentQueue();
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "jamstash", "name": "jamstash",
"version": "4.3", "version": "4.3.1",
"description": "HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming", "description": "HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming",
"authors": [ "authors": [
"tsquillario (https://github.com/tsquillario)", "tsquillario (https://github.com/tsquillario)",
@ -38,7 +38,8 @@
"notify.js": "<=1.2.2", "notify.js": "<=1.2.2",
"jquery.scrollTo": "~1.4.5", "jquery.scrollTo": "~1.4.5",
"underscore": "~1.7.0", "underscore": "~1.7.0",
"angular-underscore": "~0.5.0" "angular-underscore": "~0.5.0",
"angular-locker": "~1.0.2"
}, },
"overrides": { "overrides": {
"fancybox": { "fancybox": {

View file

@ -32,6 +32,7 @@ module.exports = function(config) {
'bower_components/jquery.scrollTo/jquery.scrollTo.js', 'bower_components/jquery.scrollTo/jquery.scrollTo.js',
'bower_components/underscore/underscore.js', 'bower_components/underscore/underscore.js',
'bower_components/angular-underscore/angular-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/angular-mocks/angular-mocks.js',
'bower_components/jasmine-promise-matchers/dist/jasmine-promise-matchers.js', 'bower_components/jasmine-promise-matchers/dist/jasmine-promise-matchers.js',
'bower_components/jasmine-fixture/dist/jasmine-fixture.js', 'bower_components/jasmine-fixture/dist/jasmine-fixture.js',

View file

@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "Jamstash", "name": "Jamstash",
"description": "HTML5 Player for Subsonic & Archive.org", "description": "HTML5 Player for Subsonic & Archive.org",
"version": "4.2.3", "version": "4.3.1",
"app": { "app": {
"launch": { "launch": {
"web_url": "http://jamstash.com" "web_url": "http://jamstash.com"

View file

@ -1,6 +1,6 @@
{ {
"name": "jamstash", "name": "jamstash",
"version": "4.3", "version": "4.3.1",
"description": "HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming", "description": "HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming",
"author": "Trevor Squillario (https://github.com/tsquillario)", "author": "Trevor Squillario (https://github.com/tsquillario)",
"contributors": [ "contributors": [