Refactor queue.js

- Added angular-ui's ui-sortable directive
- Removed our custom drag & drop handlers, they are replaced by ui-sortable.
- Refactored queue.js to better comply with https://github.com/johnpapa/angular-styleguide#controllers. Having the exposed
This commit is contained in:
Hyzual 2015-07-11 14:11:34 +02:00
parent 5d385149f3
commit 9f24576d04
9 changed files with 230 additions and 169 deletions

View file

@ -20,7 +20,6 @@ angular.module('JamStash', [
$routeProvider
.when('/index', { redirectTo: '/library' })
.when('/settings', { templateUrl: 'settings/settings.html', controller: 'SettingsController' })
.when('/queue', { templateUrl: 'queue/queue.html', controller: 'QueueController' })
.when('/library', { templateUrl: 'subsonic/subsonic.html', controller: 'SubsonicController' })
.when('/library/:artistId', { templateUrl: 'subsonic/subsonic.html', controller: 'SubsonicController', reloadOnSearch: false })
.when('/library/:artistId/:albumId', { templateUrl: 'subsonic/subsonic.html', controller: 'SubsonicController', reloadOnSearch: false })

View file

@ -53,7 +53,7 @@
<div class="clear"></div>
</div><!-- end #content -->
<div id="SideBar" ng-include src="'queue/queue.html'" ng-controller="QueueController">
<div id="SideBar" ng-include src="'queue/queue.html'" ng-controller="QueueController as vm">
<!--
<div id="NowPlaying">
<div class="header"><img src="images/rss_12x12.png" /> Now Playing</div>
@ -97,6 +97,7 @@
<script src="bower_components/angular-locker/dist/angular-locker.min.js"></script>
<script src="bower_components/angular-ui-utils/keypress.js"></script>
<script src="bower_components/ng-lodash/build/ng-lodash.js"></script>
<script src="bower_components/angular-ui-sortable/sortable.js"></script>
<!-- endbower -->
<!-- endbuild -->
<!-- our scripts -->

View file

@ -27,7 +27,7 @@ angular.module('jamstash.player.service', ['ngLodash'])
// Find the song's index in the queue, if it's in there
var index = player.indexOfSong(song);
player._playingIndex = (index !== undefined) ? index : -1;
if(player._playingSong === song) {
if (player._playingSong === song) {
// We call restart because the _playingSong hasn't changed and the directive won't
// play the song again
player.restart();
@ -78,7 +78,7 @@ angular.module('jamstash.player.service', ['ngLodash'])
var index = player.indexOfSong(player._playingSong);
player._playingIndex = (index !== undefined) ? index : -1;
if((player._playingIndex + 1) < player.queue.length) {
if ((player._playingIndex + 1) < player.queue.length) {
var nextTrack = player.queue[player._playingIndex + 1];
player._playingIndex++;
player.play(nextTrack);
@ -90,7 +90,7 @@ angular.module('jamstash.player.service', ['ngLodash'])
var index = player.indexOfSong(player._playingSong);
player._playingIndex = (index !== undefined) ? index : -1;
if((player._playingIndex - 1) > 0) {
if ((player._playingIndex - 1) > 0) {
var previousTrack = player.queue[player._playingIndex - 1];
player._playingIndex--;
player.play(previousTrack);
@ -107,7 +107,7 @@ angular.module('jamstash.player.service', ['ngLodash'])
shuffleQueue: function () {
var shuffled = _.without(player.queue, player._playingSong);
shuffled = _.shuffle(shuffled);
if(player._playingSong !== undefined) {
if (player._playingSong !== undefined) {
shuffled.unshift(player._playingSong);
player._playingIndex = 0;
}
@ -136,12 +136,22 @@ angular.module('jamstash.player.service', ['ngLodash'])
return player;
},
reorderQueue: function (oldIndex, newIndex) {
if (oldIndex < 0 || oldIndex >= player.queue.length || newIndex < 0 || newIndex >= player.queue.length) {
return player;
}
var song = player.queue[oldIndex];
player.queue.splice(oldIndex, 1);
player.queue.splice(newIndex, 0, song);
return player;
},
getPlayingSong: function () {
return player._playingSong;
},
isLastSongPlaying: function () {
return ((player._playingIndex +1) === player.queue.length);
return ((player._playingIndex + 1) === player.queue.length);
},
indexOfSong: function (song) {
@ -178,8 +188,11 @@ angular.module('jamstash.player.service', ['ngLodash'])
},
setVolume: function (volume) {
if (volume > 1) { volume = 1; }
else if(volume < 0) { volume = 0; }
if (volume > 1) {
volume = 1;
} else if (volume < 0) {
volume = 0;
}
playerVolume = Math.round(volume * 100) / 100;
return player;
}

View file

@ -297,6 +297,77 @@ describe("Player service -", function () {
});
});
describe("reorderQueue() -", function () {
it("Given an old index and a new index, when I move a song from the old index in the queue to the new, then it will be removed from its previous index and inserted at the given new index and the service itself will be returned to allow chaining", function () {
player.queue = [
{ id: 9116 },
{ id: 5568 },
{ id: 2010 }
];
var result = player.reorderQueue(0, 2);
expect(player.queue).toEqual([
{ id: 5568 },
{ id: 2010 },
{ id: 9116 }
]);
expect(result).toBe(player);
});
it("Given a negative old index, when I try to move a song from the old index to the new, then the queue won't be modified", function () {
var untouchedQueue = [
{ id: 8178 },
{ id: 406 },
{ id: 2413 }
];
player.queue = untouchedQueue;
player.reorderQueue(-1, 2);
expect(player.queue).toBe(untouchedQueue);
});
it("Given an old index that is outside the queue's bounds, when I try to move a song from the old index to the new, then the queue won't be modified", function () {
var untouchedQueue = [
{ id: 3843 },
{ id: 1519 },
{ id: 1271 }
];
player.queue = untouchedQueue;
player.reorderQueue(3, 2);
expect(player.queue).toBe(untouchedQueue);
});
it("Given a negative new index, when I try to move a song from the old index to the new, then the queue won't be modified", function () {
var untouchedQueue = [
{ id: 692 },
{ id: 9079 },
{ id: 8620 }
];
player.queue = untouchedQueue;
player.reorderQueue(0, -1);
expect(player.queue).toBe(untouchedQueue);
});
it("Given a new index that is outside the queue's bounds, when I try to move a song from the old index to the new, then the queue won't be modified", function () {
var untouchedQueue = [
{ id: 6318 },
{ id: 3905 },
{ id: 9826 }
];
player.queue = untouchedQueue;
player.reorderQueue(0, 3);
expect(player.queue).toBe(untouchedQueue);
});
});
describe("When I turn the volume up,", function () {
it("it sets the player's volume up by 10%", function () {
player.setVolume(0.5);

View file

@ -1,20 +1,54 @@
<div class="headeractions">
<a class="buttonimg" title="Shuffle Queue" ng-click="shuffleQueue()"><img src="images/fork_gd_11x12.png"></a>
<a class="buttonimg" title="Delete Queue" ng-click="emptyQueue()"><img src="images/trash_fill_gd_12x12.png"></a>
<a class="buttonimg" title="Remove Selected From Queue" ng-click="removeSelectedSongsFromQueue()"><img src="images/x_11x11.png"></a>
<a
class="buttonimg"
title="Shuffle Queue"
ng-click="vm.shuffleQueue()"
><img src="images/fork_gd_11x12.png"></a>
<a
class="buttonimg"
title="Delete Queue"
ng-click="vm.emptyQueue()"
><img src="images/trash_fill_gd_12x12.png"></a>
<a
class="buttonimg"
title="Remove Selected From Queue"
ng-click="vm.removeSelectedSongsFromQueue()"
><img src="images/x_11x11.png"></a>
</div>
<div class="header">Queue</div>
<div id="SideQueue">
<ul class="simplelist songlist noselect">
<div ng-repeat="song in [player.queue] track by $index" class="songs" sortable>
<li class="row song id{{o.id}}" ng-repeat="o in song" ng-click="toggleSelection(o)" ng-dblclick="playSong(o)" ng-class="{'selected': o.selected, 'playing': isPlayingSong(o)}">
<div
class="songs"
ui-sortable
ng-model="vm.player.queue"
>
<li
ng-repeat="song in vm.player.queue track by $index"
class="row song id{{song.id}}"
ng-click="vm.toggleSelection(song)"
ng-dblclick="vm.playSong(song)"
ng-class="{'selected': song.selected, 'playing': vm.isPlayingSong(song)}"
>
<div class="itemactions">
<a class="remove" href="" title="Remove Song" ng-click="removeSongFromQueue(o)" stop-event="click"></a>
<a href="" title="Star" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="toggleStar(o)" stop-event="click"></a>
<a
class="remove"
href=""
title="Remove Song"
ng-click="vm.removeSongFromQueue(song)"
stop-event="click"
></a>
<a
href=""
title="Star"
ng-class="{'favorite': song.starred, 'rate': ! song.starred}"
ng-click="vm.toggleStar(song)"
stop-event="click"
></a>
<div class="clear"></div>
</div>
<div class="title floatleft" title="{{o.description}}" ng-bind-html="o.name"></div>
<div class="time floatleft">{{o.time}}</div>
<div class="title floatleft" title="{{ song.description }}" ng-bind-html="song.name"></div>
<div class="time floatleft">{{ song.time }}</div>
<div class="clear"></div>
</li>

View file

@ -4,69 +4,69 @@
* Manages the playing queue. Gives access to the player service's queue-related functions,
* like adding, removing and shuffling the queue.
*/
angular.module('jamstash.queue.controller', ['jamstash.player.service', 'jamstash.settings.service', 'jamstash.selectedsongs'])
angular.module('jamstash.queue.controller', [
'ngLodash',
'jamstash.player.service',
'jamstash.selectedsongs',
'ui.sortable'
])
.controller('QueueController', ['$scope', 'globals', 'player', 'SelectedSongs',
function ($scope, globals, player, SelectedSongs) {
'use strict';
$scope.settings = globals.settings;
$scope.player = player;
.controller('QueueController', QueueController);
$scope.playSong = function (song) {
player.play(song);
};
QueueController.$inject = [
'$scope',
'lodash',
'player',
'SelectedSongs'
];
$scope.emptyQueue = function () {
function QueueController(
$scope,
_,
player,
SelectedSongs
) {
'use strict';
var self = this;
_.extend(self, {
player: player,
addSongToQueue : player.addSong,
emptyQueue : emptyQueue,
isPlayingSong : isPlayingSong,
playSong : player.play,
removeSelectedSongsFromQueue: removeSelectedSongsFromQueue,
removeSongFromQueue : player.removeSong,
shuffleQueue : shuffleQueue,
toggleSelection : SelectedSongs.toggle
});
function emptyQueue() {
player.emptyQueue();
//TODO: Hyz: Shouldn't it be in a directive ?
// TODO: Hyz: Shouldn't it be in a directive ?
$.fancybox.close();
};
}
$scope.shuffleQueue = function () {
player.shuffleQueue();
//TODO: Hyz: Shouldn't it be in a directive ?
$('#SideBar').stop().scrollTo('.header', 400);
};
$scope.addSongToQueue = function (song) {
player.addSong(song);
};
$scope.removeSongFromQueue = function (song) {
player.removeSong(song);
};
$scope.removeSelectedSongsFromQueue = function () {
player.removeSongs(SelectedSongs.get());
};
$scope.isPlayingSong = function (song) {
function isPlayingSong(song) {
return angular.equals(song, player.getPlayingSong());
};
}
function removeSelectedSongsFromQueue() {
player.removeSongs(SelectedSongs.get());
}
function shuffleQueue() {
player.shuffleQueue();
// TODO: Hyz: Shouldn't it be in a directive ?
$('#SideBar').stop().scrollTo('.header', 400);
}
$scope.$watch(function () {
return player.getPlayingSong();
}, function (newSong) {
if(newSong !== undefined) {
//TODO: Hyz: Shouldn't it be in a directive ?
if (newSong !== undefined) {
// TODO: Hyz: Shouldn't it be in a directive ?
$('#SideBar').stop().scrollTo('.song.id' + newSong.id, 400);
}
});
/**
* Change the queue's order through jQuery UI's sortable
*/
$scope.dragStart = function (e, ui) {
ui.item.data('start', ui.item.index());
};
$scope.dragEnd = function (e, ui) {
var start = ui.item.data('start'),
end = ui.item.index();
player.queue.splice(end, 0, player.queue.splice(start, 1)[0]);
};
$scope.toggleSelection = function (song) {
SelectedSongs.toggle(song);
};
}]);
}

View file

@ -2,7 +2,7 @@
describe("Queue controller", function () {
'use strict';
var player, scope, SelectedSongs;
var QueueContoller, $scope, player, SelectedSongs;
var song;
beforeEach(function () {
@ -10,133 +10,74 @@ describe("Queue controller", function () {
SelectedSongs = jasmine.createSpyObj("SelectedSongs", ["get"]);
inject(function ($controller, $rootScope, globals, _player_) {
scope = $rootScope.$new();
player = _player_;
player = jasmine.createSpyObj("player", [
"emptyQueue",
"getPlayingSong",
"removeSongs",
"reorderQueue",
"shuffleQueue"
]);
$controller('QueueController', {
$scope: scope,
globals: globals,
player: player,
inject(function ($controller, $rootScope) {
$scope = $rootScope.$new();
QueueContoller = $controller('QueueController', {
$scope : $scope,
player : player,
SelectedSongs: SelectedSongs
});
});
song = { id: 7310 };
player.queue = [];
});
it("When I play a song, it calls play in the player service", function () {
spyOn(player, "play");
scope.playSong(song);
expect(player.play).toHaveBeenCalledWith(song);
});
it("When I empty the queue, it calls emptyQueue in the player service and closes fancybox", function () {
spyOn(player, "emptyQueue");
it("emptyQueue() - When I empty the queue, then the player service's emptyQueuewill be called and fancybox will be closed", function () {
spyOn($.fancybox, "close");
scope.emptyQueue();
QueueContoller.emptyQueue();
expect(player.emptyQueue).toHaveBeenCalled();
expect($.fancybox.close).toHaveBeenCalled();
});
it("When I shuffle the queue, then the player's shuffleQueue will be called and the queue will be scrolled back to the first element", function () {
spyOn(player, "shuffleQueue");
it("shuffleQueue() - When I shuffle the queue, then the player's shuffleQueue will be called and the queue will be scrolled back to the first element", function () {
spyOn($.fn, 'scrollTo');
scope.shuffleQueue();
QueueContoller.shuffleQueue();
expect(player.shuffleQueue).toHaveBeenCalled();
expect($.fn.scrollTo).toHaveBeenCalledWith('.header', jasmine.any(Number));
});
it("When I add one song to the queue, it calls addSong in the player service", function () {
spyOn(player, "addSong");
scope.addSongToQueue(song);
expect(player.addSong).toHaveBeenCalledWith(song);
it("removeSelectedSongsFromQueue() - When I remove all the selected songs from the queue, then the player's removeSongs will be called with the selected songs", function () {
SelectedSongs.get.and.returnValue([
{ id: 6390 },
{ id: 2973 }
]);
QueueContoller.removeSelectedSongsFromQueue();
expect(player.removeSongs).toHaveBeenCalledWith([
{ id: 6390 },
{ id: 2973 }
]);
});
it("When I remove a song from the queue, it calls removeSong in the player service", function () {
spyOn(player, "removeSong");
scope.removeSongFromQueue(song);
expect(player.removeSong).toHaveBeenCalledWith(song);
});
it("isPlayingSong() - Given a song, when I want to know if it's playing, then the player service will be called and its return will be compared with the given song", function () {
song = { id: 2537 };
player.getPlayingSong.and.returnValue(song);
it("When I remove all the selected songs from the queue, it calls removeSongs in the player service", function () {
spyOn(player, "removeSongs");
var secondSong = { id: 6791 };
SelectedSongs.get.and.returnValue([song, secondSong]);
scope.removeSelectedSongsFromQueue();
expect(player.removeSongs).toHaveBeenCalledWith([song, secondSong]);
});
expect(QueueContoller.isPlayingSong(song)).toBeTruthy();
it("asks the player service if a given song is the currently playing song", function () {
spyOn(player, "getPlayingSong").and.returnValue(song);
expect(scope.isPlayingSong(song)).toBeTruthy();
expect(player.getPlayingSong).toHaveBeenCalled();
});
it("when the player service's current song changes, it scrolls the queue to display it", function () {
spyOn(player, "getPlayingSong").and.callFake(function () {
return song;
});
it("When the player service's current song changes, then the queue will be scrolled down to display the new playing song", function () {
song = { id: 5239 };
player.getPlayingSong.and.returnValue(song);
spyOn($.fn, "scrollTo");
scope.$apply();
$scope.$apply();
expect($.fn.scrollTo).toHaveBeenCalled();
});
describe("reorders the queue by drag and drop - ", function () {
var mockUI;
beforeEach(function () {
player.queue = [
{ id: 2246 },
{ id: 8869 },
{ id: 285 }
];
mockUI = {
item: {}
};
});
it("given a song in the queue, when I start dragging it, it records what its starting position in the queue was", function () {
mockUI.item.index = jasmine.createSpy("index").and.returnValue('1');
mockUI.item.data = jasmine.createSpy("data");
scope.dragStart({}, mockUI);
expect(mockUI.item.index).toHaveBeenCalled();
expect(mockUI.item.data).toHaveBeenCalledWith('start', '1');
});
it("given a song in the queue that I started dragging, when I drop it, its position in the queue has changed", function () {
mockUI.item.index = jasmine.createSpy("index").and.returnValue('0');
mockUI.item.data = jasmine.createSpy("data").and.returnValue('1');
scope.dragEnd({}, mockUI);
expect(mockUI.item.index).toHaveBeenCalled();
expect(mockUI.item.data).toHaveBeenCalledWith('start');
// The second song should now be first
expect(player.queue).toEqual([
{ id: 8869 },
{ id: 2246 },
{ id: 285 }
]);
});
// TODO: Hyz: Maybe it should be an end-to-end test
it("given that the player is playing the second song (B), when I swap the first (A) and the second song (B), the player's next song should be A", function () {
player.play({ id: 8869 });
mockUI.item.index = jasmine.createSpy("index").and.returnValue('0');
mockUI.item.data = jasmine.createSpy("data").and.returnValue('1');
scope.dragEnd({}, mockUI);
player.nextTrack();
expect(player._playingIndex).toBe(1);
expect(player._playingSong).toEqual({ id: 2246 });
});
});
});

View file

@ -41,7 +41,8 @@
"angular-locker": "~2.0.1",
"angular-ui-utils": "bower-keypress",
"open-iconic": "~1.1.1",
"ng-lodash": "~0.2.3"
"ng-lodash": "~0.2.3",
"angular-ui-sortable": "~0.13.4"
},
"overrides": {
"fancybox": {

View file

@ -34,6 +34,7 @@ module.exports = function (config) {
'bower_components/angular-locker/dist/angular-locker.min.js',
'bower_components/angular-ui-utils/keypress.js',
'bower_components/ng-lodash/build/ng-lodash.js',
'bower_components/angular-ui-sortable/sortable.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',