Adds a repeat directive to manage a 3-state repeat button

It will cycle through "repeat the playing queue", "repeat the playing song" and "don't repeat anything".

- Adds a custom "loop-single" icon based on iconic's loop icon.
- Removes the LoopQueue setting which is now the "queue" value of Repeat
- Moves the Repeat setting to player-service.js. That way, it does not depend on saving the settings in any way. It also allows us to remove a dependency from player-service.js.
- Adds a player css which will hold all the player-related styles
- Removes the former png icons (we can always find them back thanks to Git)
This commit is contained in:
Hyzual 2015-05-14 21:41:20 +02:00
parent acd2ae67f8
commit efa604265d
19 changed files with 263 additions and 112 deletions

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="8" width="8" version="1.1" viewBox="0 0 8 8">
<path d="m6.002 0v1.0015c-1.7031 0.00345-3.4067-0.006907-5.1095 0.0052-0.54821 0.0425-0.96298 0.5856-0.8995 1.1241v0.87368h1.0015v-1.0015h5.0075v1.0015l2.0031-1.5021c-0.6677-0.5008-1.3356-1.0015-2.0031-1.5024zm0.086036 3.1845c-1.0618-0.075578-1.8347 0.96819-1.9789 1.823h-2.1132v-1.0015l-2.003 1.5023 2.003 1.5023v-1.0015h2.404c0.54902 1.1213 2.228 1.3563 3.0632 0.42782 0.45961-0.43735 0.56857-1.0803 0.54177-1.6849v-0.74587c-0.2578 0.0273-0.4787-0.0007-0.5932-0.2906-0.3515-0.3384-0.836-0.5324-1.3237-0.531zm0.066497 0.47534c0.29389-0.085418 0.27048 0.17533 0.25848 0.36687v2.0667c0.16688 0.00839 0.34015-0.017266 0.5028 0.01371 0.12894 0.19311 0.050198 0.52465-0.2269 0.43624h-1.3868c-0.1886-0.1444-0.0957-0.5528 0.1898-0.4499 0.1002-0.0281 0.3133 0.0567 0.3423-0.0426v-1.8196c-0.1923 0.0909-0.367 0.2378-0.5739 0.2842-0.1524-0.206-0.0164-0.4607 0.2048-0.5449 0.1713-0.097 0.3231-0.2382 0.5077-0.3049 0.0603-0.0053 0.1223-0.006 0.1817-0.0058z"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 B

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 8 8" id="loop-single"><path d="M6.002 0v1.002C4.3 1.004 2.595.995.892 1.006c-.548.042-.962.585-.9 1.124v.874H.996v-1h5.007v1l2.003-1.502c-.668-.5-1.336-1-2.003-1.502zm.086 3.184c-1.062-.075-1.835.97-1.98 1.823H1.997v-1l-2.003 1.5 2.003 1.504v-1H4.4c.55 1.12 2.228 1.355 3.063.427.46-.437.57-1.08.542-1.685v-.746c-.258.027-.48 0-.593-.29-.352-.34-.836-.533-1.324-.53zm.067.476c.293-.086.27.175.258.367v2.066c.167.01.34-.017.503.014.13.193.05.525-.227.436H5.3c-.188-.144-.096-.552.19-.45.1-.028.313.057.342-.042V4.23c-.192.092-.367.24-.574.285-.152-.206-.016-.46.205-.544.17-.096.323-.238.508-.304.06-.006.122-.006.182-.006z"/></symbol></svg>

After

Width:  |  Height:  |  Size: 782 B

View file

@ -18,6 +18,10 @@
<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" />
<!-- build:css(app) styles/concat.min.css -->
<link rel="stylesheet" href="player/player.css" />
<link rel="stylesheet" href="player/repeat-directive/repeat-directive.css" />
<!-- endbuild -->
</head> </head>
<body ui-keypress="{'32 179': 'togglePause($event)', '43 61 187': 'turnVolumeUp($event)', '45 95 189': 'turnVolumeDown($event)'}" ui-keydown="{'right 176': 'nextTrack($event)', 'left 177': 'previousTrack($event)'}"> <body ui-keypress="{'32 179': 'togglePause($event)', '43 61 187': 'turnVolumeUp($event)', '45 95 189': 'turnVolumeDown($event)'}" ui-keydown="{'right 176': 'nextTrack($event)', 'left 177': 'previousTrack($event)'}">
<div id="container"> <div id="container">
@ -112,6 +116,7 @@
<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="player/repeat-directive/repeat-directive.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>

View file

@ -32,7 +32,6 @@ describe("jplayer directive", function() {
return $delegate; return $delegate;
}); });
$provide.value('globals', mockGlobals); $provide.value('globals', mockGlobals);
$provide.constant('jamstashVersion', '1.0.0');
}); });
spyOn($.fn, "jPlayer").and.callThrough(); spyOn($.fn, "jPlayer").and.callThrough();

View file

@ -3,9 +3,9 @@
* *
* Manages the player and playing queue. Use it to play a song, go to next track or add songs to the queue. * Manages the player and playing queue. Use it to play a song, go to next track or add songs to the queue.
*/ */
angular.module('jamstash.player.service', ['jamstash.settings.service', 'angular-underscore/utils']) angular.module('jamstash.player.service', ['angular-underscore/utils'])
.factory('player', ['globals', function (globals) { .factory('player', [function () {
'use strict'; 'use strict';
var playerVolume = 1.0; var playerVolume = 1.0;
@ -18,6 +18,10 @@ angular.module('jamstash.player.service', ['jamstash.settings.service', 'angular
pauseSong: false, pauseSong: false,
restartSong: false, restartSong: false,
loadSong: false, loadSong: false,
settings: {
repeat: "none",
repeatValues: ["queue", "song", "none"]
},
play: function (song) { play: function (song) {
// Find the song's index in the queue, if it's in there // Find the song's index in the queue, if it's in there
@ -56,11 +60,11 @@ angular.module('jamstash.player.service', ['jamstash.settings.service', 'angular
// Called from the player directive at the end of the current song // Called from the player directive at the end of the current song
songEnded: function () { songEnded: function () {
if (globals.settings.Repeat) { if (player.settings.repeat === "song") {
// repeat current track // repeat current track
player.restart(); player.restart();
} else if (player.isLastSongPlaying() === true) { } else if (player.isLastSongPlaying() === true) {
if (globals.settings.LoopQueue) { if (player.settings.repeat === "queue") {
// Loop to first track in queue // Loop to first track in queue
player.playFirstSong(); player.playFirstSong();
} }

View file

@ -1,21 +1,13 @@
describe("Player service -", function() { describe("Player service -", function() {
'use strict'; 'use strict';
var player, mockGlobals, firstSong, secondSong, thirdSong, newSong; var player, firstSong, secondSong, thirdSong, newSong;
beforeEach(function() { beforeEach(function() {
// We redefine globals because in some tests we need to alter the settings module('jamstash.player.service');
mockGlobals = {
settings: {
Repeat: false,
LoopQueue: false
}
};
module('jamstash.player.service', function ($provide) {
$provide.value('globals', mockGlobals);
});
inject(function (_player_) { inject(function (_player_) {
player = _player_; player = _player_;
}); });
player.settings.repeat = "none";
}); });
describe("Given that I have 3 songs in my playing queue,", function() { describe("Given that I have 3 songs in my playing queue,", function() {
@ -206,9 +198,9 @@ describe("Player service -", function() {
expect(player.nextTrack).toHaveBeenCalled(); expect(player.nextTrack).toHaveBeenCalled();
}); });
it("and that the 'Repeat song' setting is true, when the current song ends, it restarts it", function() { it("and that the 'Repeat' setting is set to 'song', when the current song ends, it restarts it", function() {
spyOn(player, "restart"); spyOn(player, "restart");
mockGlobals.settings.Repeat = true; player.settings.repeat = "song";
player.songEnded(); player.songEnded();
@ -220,9 +212,9 @@ describe("Player service -", function() {
player._playingIndex = 2; player._playingIndex = 2;
}); });
it("if the 'Repeat queue' setting is true, it plays the first song of the queue", function() { it("if the 'Repeat' setting is set to 'queue', it plays the first song of the queue", function() {
spyOn(player, "playFirstSong"); spyOn(player, "playFirstSong");
mockGlobals.settings.LoopQueue = true; player.settings.repeat = "queue";
player.songEnded(); player.songEnded();

9
app/player/player.css Normal file
View file

@ -0,0 +1,9 @@
.icon {
height: 12px;
width: 12px;
display: block;
}
.icon-wrap {
margin: 4px 2px;
float: left;
}

View file

@ -17,7 +17,7 @@
</div> </div>
<div id="songdetails"> <div id="songdetails">
<div id="coverart"> <div id="coverart">
<a ng-click="fancyboxOpenImage(getPlayingSong().coverartfull)"> <a ng-click="fancyboxOpenImage(getPlayingSong().coverartfull)">
<img ng-src="{{getPlayingSong().coverartthumb}}" src="images/albumdefault_60.jpg" height="30" width="30" /> <img ng-src="{{getPlayingSong().coverartthumb}}" src="images/albumdefault_60.jpg" height="30" width="30" />
</a> </a>
</div> </div>
@ -27,7 +27,7 @@
</ul> </ul>
<div id="songdetails_controls"> <div id="songdetails_controls">
<a href="" class="jukebox" title="Jukebox Mode [Beta]" ng-click="toggleSetting('Jukebox')" ng-class="{'hoverSelected': !settings.Jukebox }"></a> <a href="" class="jukebox" title="Jukebox Mode [Beta]" ng-click="toggleSetting('Jukebox')" ng-class="{'hoverSelected': !settings.Jukebox }"></a>
<a href="" class="loop" title="Repeat" ng-click="toggleSetting('Repeat')" ng-class="{'hoverSelected': !settings.Repeat }"></a> <jamstash-repeat selected-value="playerSettings.repeat" values="playerSettings.repeatValues"></jamstash-repeat>
<a href="" id="action_SaveProgress" class="lock" title="Save Track Position: On" ng-show="settings.SaveTrackPosition"></a> <a href="" id="action_SaveProgress" class="lock" title="Save Track Position: On" ng-show="settings.SaveTrackPosition"></a>
<a title="Favorite" href="" ng-class="{'favorite': getPlayingSong().starred, 'rate': !getPlayingSong().starred}" ng-click="toggleStar(getPlayingSong())" stop-event="click"></a> <a title="Favorite" href="" ng-class="{'favorite': getPlayingSong().starred, 'rate': !getPlayingSong().starred}" ng-click="toggleStar(getPlayingSong())" stop-event="click"></a>
<a href="" id="action_Mute" class="mute" title="Mute"></a> <a href="" id="action_Mute" class="mute" title="Mute"></a>

View file

@ -5,13 +5,15 @@
* Also provides the currently playing song's info through the scope so it can be displayed next to * Also provides the currently playing song's info through the scope so it can be displayed next to
* the player controls. * the player controls.
*/ */
angular.module('jamstash.player.controller', ['jamstash.player.service', 'jamstash.player.directive']) angular.module('jamstash.player.controller', ['jamstash.player.service', 'jamstash.player.directive', 'jamstash.repeat.directive'])
.controller('PlayerController', ['$scope', 'player', 'globals', .controller('PlayerController', ['$scope', 'player', 'globals',
function ($scope, player, globals) { function ($scope, player, globals) {
'use strict'; 'use strict';
$scope.getPlayingSong = player.getPlayingSong; $scope.getPlayingSong = player.getPlayingSong;
$scope.settings = globals.settings;
$scope.playerSettings = player.settings;
$scope.play = function () { $scope.play = function () {
if (globals.settings.Jukebox) { if (globals.settings.Jukebox) {

View file

@ -1,36 +1,62 @@
describe("Player controller", function() { describe("Player controller", function() {
'use strict'; 'use strict';
var player, scope; var player, scope, mockGlobals;
beforeEach(function() { beforeEach(function() {
// We redefine globals because in some tests we need to alter the settings
mockGlobals = {
settings: {
Jukebox: false
}
};
module('jamstash.player.controller'); module('jamstash.player.controller');
inject(function ($controller, $rootScope) { inject(function ($controller, $rootScope) {
scope = $rootScope.$new(); scope = $rootScope.$new();
player = jasmine.createSpyObj("player", ["getPlayingSong", "previousTrack", "nextTrack"]); player = jasmine.createSpyObj("player", [
"getPlayingSong",
"previousTrack",
"nextTrack",
"getRepeatValues",
"togglePause"
]);
$controller('PlayerController', { $controller('PlayerController', {
$scope: scope, $scope: scope,
player: player player: player,
globals: mockGlobals
}); });
}); });
}); });
it("When I get the currently playing song, it asks the player service", function() { it("When I play a song, the player service will be called", function() {
scope.play();
expect(player.togglePause).toHaveBeenCalled();
});
it("when I pause a song, the player service will be called", function() {
scope.pause();
expect(player.togglePause).toHaveBeenCalled();
});
it("When I get the currently playing song, the player service will be called", function() {
scope.getPlayingSong(); scope.getPlayingSong();
expect(player.getPlayingSong).toHaveBeenCalled(); expect(player.getPlayingSong).toHaveBeenCalled();
}); });
it("When I get the previous track, it uses the player service", function() { it("When I get the previous track, the player service will be called", function() {
scope.previousTrack(); scope.previousTrack();
expect(player.previousTrack).toHaveBeenCalled(); expect(player.previousTrack).toHaveBeenCalled();
}); });
it("When I get the next track, it uses the player service", function() { it("When I get the next track, the player service will be called", function() {
scope.nextTrack(); scope.nextTrack();
expect(player.nextTrack).toHaveBeenCalled(); expect(player.nextTrack).toHaveBeenCalled();

View file

@ -0,0 +1,6 @@
.icon-loop-queue, .icon-loop-single {
fill: #fff;
}
.icon-loop-none {
fill: #adadad;
}

View file

@ -0,0 +1,20 @@
<a href="javascript:void(0)" class="icon-wrap" ng-click="cycleRepeat()">
<span title="Repeat the playing queue" ng-show="selectedValue === 'queue'">
<svg class="icon">
<title>Repeat the playing queue</title>
<use xlink:href="images/sprite/iconic.svg#loop" class="icon-loop-queue"></use>
</svg>
</span>
<span title="Repeat the current song" ng-show="selectedValue === 'song'">
<svg class="icon">
<title>Repeat the current song</title>
<use xlink:href="images/sprite/jamstash-sprite.svg#loop-single" class="icon-loop-single"></use>
</svg>
</span>
<span title="Disable repeat" ng-show="selectedValue === 'none'">
<svg class="icon">
<title>Disable repeat</title>
<use xlink:href="images/sprite/iconic.svg#loop" class="icon-loop-none"></use>
</svg>
</span>
</a>

View file

@ -0,0 +1,29 @@
/**
* jamstash.repeat.directive Module
*
* Triple-state button to toggle between repeating the entire playing queue, the current playing song and disabling repeat
*/
angular.module('jamstash.repeat.directive', ['jamstash.notifications'])
.directive('jamstashRepeat', ['notifications', function (notifications) {
'use strict';
return {
restrict: 'E',
templateUrl: 'player/repeat-directive/repeat-directive.html',
replace: true,
scope: {
selectedValue: '=',
values: '='
},
link: function ($scope) {
$scope.$watch('selectedValue', function (newVal) {
$scope.selectedIndex = $scope.values.indexOf(newVal);
});
$scope.cycleRepeat = function () {
$scope.selectedIndex = ($scope.selectedIndex + 1) % $scope.values.length;
$scope.selectedValue = $scope.values[$scope.selectedIndex];
notifications.updateMessage('Repeat ' + $scope.selectedValue, true);
};
}
};
}]);

View file

@ -0,0 +1,69 @@
describe("repeat directive", function() {
'use strict';
var element, scope, isolateScope, notifications, mockGlobals;
beforeEach(module ('templates'));
beforeEach(function() {
// We redefine globals because in some tests we need to alter the settings
mockGlobals = {
settings: {
RepeatValues: ["queue", "song", "none"],
Repeat: "none"
}
};
module('jamstash.repeat.directive', function($provide) {
$provide.value('globals', mockGlobals);
// Mock the notifications service
$provide.decorator('notifications', function () {
return jasmine.createSpyObj("notifications", ["updateMessage"]);
});
});
inject(function ($rootScope, $compile, _notifications_) {
notifications = _notifications_;
// Compile the directive
scope = $rootScope.$new();
scope.settings = mockGlobals.settings;
element = '<jamstash-repeat selected-value="settings.Repeat" values="settings.RepeatValues"></jamstash-repeat>';
element = $compile(element)(scope);
scope.$digest();
isolateScope = element.isolateScope();
});
});
it("Given that the Repeat setting was set to 'none', when I cycle through the values, then the Repeat setting will be set to 'queue'", function() {
isolateScope.cycleRepeat();
isolateScope.$apply();
expect(mockGlobals.settings.Repeat).toBe('queue');
});
it("Given that the Repeat setting was set to 'queue', when I cycle through the values, then the Repeat setting will be set to 'song'", function() {
mockGlobals.settings.Repeat = 'queue';
isolateScope.$apply();
isolateScope.cycleRepeat();
isolateScope.$apply();
expect(mockGlobals.settings.Repeat).toBe('song');
});
it("Given that the Repeat setting was set to 'song', when I cycle through the values, then the Repeat setting will be set to 'none", function() {
mockGlobals.settings.Repeat = 'song';
isolateScope.$apply();
isolateScope.cycleRepeat();
isolateScope.$apply();
expect(mockGlobals.settings.Repeat).toBe('none');
});
it("When I cycle through the values, then the user will be notified with the new value", function() {
isolateScope.cycleRepeat();
isolateScope.$apply();
expect(notifications.updateMessage).toHaveBeenCalledWith('Repeat queue', true);
});
});

View file

@ -4,7 +4,7 @@
* Manages the playing queue. Gives access to the player service's queue-related functions, * Manages the playing queue. Gives access to the player service's queue-related functions,
* like adding, removing and shuffling the queue. * like adding, removing and shuffling the queue.
*/ */
angular.module('jamstash.queue.controller', ['jamstash.player.service']) angular.module('jamstash.queue.controller', ['jamstash.player.service', 'jamstash.settings.service'])
.controller('QueueController', ['$scope', 'globals', 'player', .controller('QueueController', ['$scope', 'globals', 'player',
function ($scope, globals, player) { function ($scope, globals, player) {

View file

@ -33,9 +33,6 @@
<div class="inputwrap"><input type="checkbox" id="AutoPlay" name="AutoPlay" value="1" title="When the Queue has ended, load random songs" ng-model="settings.AutoPlay" /></div> <div class="inputwrap"><input type="checkbox" id="AutoPlay" name="AutoPlay" value="1" title="When the Queue has ended, load random songs" ng-model="settings.AutoPlay" /></div>
<label for="AutoPlay">Auto Play</label> <label for="AutoPlay">Auto Play</label>
<div class="clear"></div> <div class="clear"></div>
<div class="inputwrap"><input type="checkbox" id="LoopQueue" name="LoopQueue" value="1" title="When the Queue has ended, start from beginning" ng-model="settings.LoopQueue" /></div>
<label for="LoopQueue">Loop Queue</label>
<div class="clear"></div>
<div class="inputwrap"><input type="checkbox" id="HideAZ" name="HideAZ" value="1" title="Hide A-Z Artist Picker (Tablet/Touch friendly feature)" ng-model="settings.HideAZ" /></div> <div class="inputwrap"><input type="checkbox" id="HideAZ" name="HideAZ" value="1" title="Hide A-Z Artist Picker (Tablet/Touch friendly feature)" ng-model="settings.HideAZ" /></div>
<label for="HideAZ">Hide A-Z</label> <label for="HideAZ">Hide A-Z</label>
<div class="clear"></div> <div class="clear"></div>

View file

@ -1474,15 +1474,6 @@ ul.songlist li:hover
display: block; display: block;
background: url('../images/lock_stroke_gl_9x12.png') 0 center no-repeat; background: url('../images/lock_stroke_gl_9x12.png') 0 center no-repeat;
} }
#songdetails a.loop
{
float: left;
margin: 4px 2px;
height: 9px;
width: 12px;
display: block;
background: url('../images/loop_alt3_w_12x9.png') 0 center no-repeat;
}
#songdetails a.jukebox #songdetails a.jukebox
{ {
float: left; float: left;
@ -1833,5 +1824,3 @@ legend
font-variant: small-caps; font-variant: small-caps;
font-weight: bold; font-weight: bold;
} }