Refactor media keys and fix play/pause
- Using space to play/pause was broken due to an issue with data binding. Running `$scope.$apply()` after updating the `player.pauseSong` property using `player.togglePause()` seems to have fixed this. - All of the key event processing has been moved into the JS. This means that all the key event logic is all in one spot (easier to understand IMO). Additionally, this removes the need to redefine functions for each shortcut. - Removed the now-unused `angular-ui-utils/keypress.js` package. - Fixed key events being active when editing dropdown/checkbox controls in the settings menu - Removed the custom `[Home]` shortcut since this is done natively by the browser already. - Removed the 1-6 shortcuts for the tabs. Logic being that it seems pretty rare that you would want to rapidly switch between them (you would have to use the mouse to do anything on all tabs except the subsonic one anyway), and they were already broken without anyone complaining. - Fixed tests
This commit is contained in:
parent
dbf5010745
commit
8c279093df
8 changed files with 69 additions and 115 deletions
|
@ -6,7 +6,6 @@ angular.module('JamStash', [
|
||||||
'ngRoute',
|
'ngRoute',
|
||||||
'ngSanitize',
|
'ngSanitize',
|
||||||
'ngLodash',
|
'ngLodash',
|
||||||
'ui.keypress',
|
|
||||||
'jamstash.subsonic.controller',
|
'jamstash.subsonic.controller',
|
||||||
'jamstash.archive.controller',
|
'jamstash.archive.controller',
|
||||||
'jamstash.player.controller',
|
'jamstash.player.controller',
|
||||||
|
|
|
@ -171,39 +171,51 @@ angular.module('JamStash')
|
||||||
$(this).fadeOut(function () { $(this).remove(); });
|
$(this).fadeOut(function () { $(this).remove(); });
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
$document.keydown(function (e) {
|
|
||||||
$scope.scrollToIndex(e);
|
// Shortcut processing
|
||||||
|
$(document).keydown(function (e) {
|
||||||
|
$scope.processKeyEvent(e);
|
||||||
});
|
});
|
||||||
$scope.scrollToIndex = function (e) {
|
$scope.scrollToIndex = function (e) {
|
||||||
var source = e.target.id;
|
$scope.processKeyEvent(e);
|
||||||
if (e.target.tagName.toUpperCase() != 'INPUT') {
|
return true;
|
||||||
var unicode = e.charCode ? e.charCode : e.keyCode;
|
};
|
||||||
if (globals.settings.Debug) { console.log('Keycode Triggered: ' + unicode); }
|
$scope.processKeyEvent = function (e) {
|
||||||
if (unicode == 49) { // 1
|
if (e.isDefaultPrevented() ||
|
||||||
$('#action_Queue').click();
|
e.repeat ||
|
||||||
} else if (unicode == 50) {
|
e.altKey || e.metaKey || e.ctrlKey ||
|
||||||
$('#action_Library').click();
|
(e.target && _.contains(['input', 'textarea', 'select'], e.target.tagName.toLowerCase()))) {
|
||||||
} else if (unicode == 51) {
|
return;
|
||||||
$('#action_Archive').click();
|
|
||||||
} else if (unicode == 52) {
|
|
||||||
$('#action_Settings').click();
|
|
||||||
} else if (unicode == 53) {
|
|
||||||
} else if (unicode == 54) { // 6
|
|
||||||
}
|
}
|
||||||
if (unicode >= 65 && unicode <= 90 && $('#tabLibrary').is(':visible')) { // a-z
|
|
||||||
var key = utils.findKeyForCode(unicode);
|
var key = e.key;
|
||||||
if (key == 'x' || key == 'y' || key == 'z') {
|
if (globals.settings.Debug) { console.log('Key pressed: ' + key); }
|
||||||
|
if (key == "Esc" || key == "Escape") {
|
||||||
|
$rootScope.hideQueue();
|
||||||
|
} else if (key == " " || key == "Space") {
|
||||||
|
player.togglePause();
|
||||||
|
} else if (key == "ArrowLeft" || key == "Left") {
|
||||||
|
player.previousTrack();
|
||||||
|
} else if (key == "ArrowRight" || key == "Right") {
|
||||||
|
player.nextTrack();
|
||||||
|
} else if (key == "-" || key == "_") {
|
||||||
|
persistence.saveVolume(player.turnVolumeDown());
|
||||||
|
} else if (key == "=" || key == "+") {
|
||||||
|
persistence.saveVolume(player.turnVolumeUp());
|
||||||
|
} else if (/^[a-z]$/i.test(key) && $('#tabLibrary').is(':visible')) {
|
||||||
|
if (/^[x-z]$/i.test(key)) {
|
||||||
key = 'x-z';
|
key = 'x-z';
|
||||||
}
|
}
|
||||||
var el = '#' + key.toUpperCase();
|
var el = '#' + key.toUpperCase();
|
||||||
if ($(el).length > 0) {
|
if ($(el).length > 0) {
|
||||||
$('#left-component').stop().scrollTo(el, 400);
|
$('#left-component').stop().scrollTo(el, 400);
|
||||||
}
|
}
|
||||||
} else if (unicode == 36 && $('#tabLibrary').is(':visible')) { // home
|
|
||||||
$('#left-component').stop().scrollTo('#MusicFolders', 400);
|
|
||||||
}
|
}
|
||||||
|
else{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return true;
|
$scope.$apply();
|
||||||
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
$scope.scrollToIndexName = function (index) {
|
$scope.scrollToIndexName = function (index) {
|
||||||
var el = '#' + index;
|
var el = '#' + index;
|
||||||
|
@ -246,53 +258,6 @@ angular.module('JamStash')
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the target of this event is an input
|
|
||||||
* @param {jQuery event} event
|
|
||||||
* @return {Boolean}
|
|
||||||
*/
|
|
||||||
function isTargetInput (event) {
|
|
||||||
return (event && event.target.tagName === "INPUT");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We define player-related methods here instead of in player controller
|
|
||||||
in order to bind keypresses to <body> and have global shortcuts.
|
|
||||||
We also check the event so we don't do anything if it's on an input */
|
|
||||||
$scope.togglePause = function (event) {
|
|
||||||
if(!isTargetInput(event)) {
|
|
||||||
if(globals.settings.Jukebox) {
|
|
||||||
$scope.sendToJukebox('stop');
|
|
||||||
} else {
|
|
||||||
player.togglePause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.turnVolumeUp = function (event) {
|
|
||||||
if(!isTargetInput(event)) {
|
|
||||||
var volume = player.turnVolumeUp();
|
|
||||||
persistence.saveVolume(volume);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.turnVolumeDown = function (event) {
|
|
||||||
if(!isTargetInput(event)) {
|
|
||||||
var volume = player.turnVolumeDown();
|
|
||||||
persistence.saveVolume(volume);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.nextTrack = function (event) {
|
|
||||||
if(!isTargetInput(event)) {
|
|
||||||
player.nextTrack();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$scope.previousTrack = function (event) {
|
|
||||||
if(!isTargetInput(event)) {
|
|
||||||
player.previousTrack();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$rootScope.addToJukebox = function (id) {
|
$rootScope.addToJukebox = function (id) {
|
||||||
if (globals.settings.Debug) { console.log("LOAD JUKEBOX"); }
|
if (globals.settings.Debug) { console.log("LOAD JUKEBOX"); }
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
|
|
@ -3,7 +3,7 @@ describe("Main controller", function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var controllerParams, $controller, $q, scope, mockGlobals, player, utils, persistence, subsonic, notifications,
|
var controllerParams, $controller, $q, scope, mockGlobals, player, utils, persistence, subsonic, notifications,
|
||||||
deferred;
|
deferred, mockKeypress;
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockGlobals = {
|
mockGlobals = {
|
||||||
settings: {
|
settings: {
|
||||||
|
@ -18,6 +18,16 @@ describe("Main controller", function () {
|
||||||
$provide.value('globals', mockGlobals);
|
$provide.value('globals', mockGlobals);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mock a keypress to the application
|
||||||
|
mockKeypress = function(scope, key, target){
|
||||||
|
scope.processKeyEvent({
|
||||||
|
key: key,
|
||||||
|
target: target,
|
||||||
|
isDefaultPrevented: function(){},
|
||||||
|
preventDefault: function(){}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Mock the player service
|
// Mock the player service
|
||||||
player = jasmine.createSpyObj("player", [
|
player = jasmine.createSpyObj("player", [
|
||||||
"togglePause",
|
"togglePause",
|
||||||
|
@ -78,10 +88,6 @@ describe("Main controller", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
xdescribe("toggleSetting -", function () {
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("", function () {
|
describe("", function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$controller('AppController', controllerParams);
|
$controller('AppController', controllerParams);
|
||||||
|
@ -99,24 +105,16 @@ describe("Main controller", function () {
|
||||||
expect(scope.showQueue).toHaveBeenCalled();
|
expect(scope.showQueue).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("When I toggle pause,", function () {
|
describe("When I toggle pause using the keyboard shortcut,", function () {
|
||||||
it("given that we're using the Jukebox mode, it sends a 'stop' command to the jukebox", function () {
|
it("it toggles pause on the player service", function () {
|
||||||
mockGlobals.settings.Jukebox = true;
|
mockKeypress(scope, ' ');
|
||||||
spyOn(scope, "sendToJukebox");
|
|
||||||
|
|
||||||
scope.togglePause();
|
|
||||||
expect(scope.sendToJukebox).toHaveBeenCalledWith('stop');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("it toggles pause using the player service", function () {
|
|
||||||
scope.togglePause();
|
|
||||||
expect(player.togglePause).toHaveBeenCalled();
|
expect(player.togglePause).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("When I turn the volume up, it sets the player's volume up and saves it using the persistence service", function () {
|
it("When I turn the volume up, it sets the player's volume up and saves it using the persistence service", function () {
|
||||||
player.turnVolumeUp.and.returnValue(0.6);
|
player.turnVolumeUp.and.returnValue(0.6);
|
||||||
scope.turnVolumeUp();
|
mockKeypress(scope, '+');
|
||||||
|
|
||||||
expect(player.turnVolumeUp).toHaveBeenCalled();
|
expect(player.turnVolumeUp).toHaveBeenCalled();
|
||||||
expect(persistence.saveVolume).toHaveBeenCalledWith(0.6);
|
expect(persistence.saveVolume).toHaveBeenCalledWith(0.6);
|
||||||
|
@ -124,52 +122,49 @@ describe("Main controller", function () {
|
||||||
|
|
||||||
it("When I turn the volume down, it sets the player's volume down and saves it using the persistence service", function () {
|
it("When I turn the volume down, it sets the player's volume down and saves it using the persistence service", function () {
|
||||||
player.turnVolumeDown.and.returnValue(0.4);
|
player.turnVolumeDown.and.returnValue(0.4);
|
||||||
scope.turnVolumeDown();
|
mockKeypress(scope, '-');
|
||||||
|
|
||||||
expect(player.turnVolumeDown).toHaveBeenCalled();
|
expect(player.turnVolumeDown).toHaveBeenCalled();
|
||||||
expect(persistence.saveVolume).toHaveBeenCalledWith(0.4);
|
expect(persistence.saveVolume).toHaveBeenCalledWith(0.4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("When I go to the next track, it calls next track on the player", function () {
|
it("When I go to the next track, it calls next track on the player", function () {
|
||||||
scope.nextTrack();
|
mockKeypress(scope, 'ArrowRight');
|
||||||
expect(player.nextTrack).toHaveBeenCalled();
|
expect(player.nextTrack).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("When I go to the previous track, it calls previous track on the player", function () {
|
it("When I go to the previous track, it calls previous track on the player", function () {
|
||||||
scope.previousTrack();
|
mockKeypress(scope, 'ArrowLeft');
|
||||||
expect(player.previousTrack).toHaveBeenCalled();
|
expect(player.previousTrack).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Given that I am targeting an input,", function () {
|
describe("Given that I am targeting an input,", function () {
|
||||||
var event;
|
var target = { 'tagName': "iNPUt" } ;
|
||||||
beforeEach(function () {
|
|
||||||
event = { target: { tagName: "INPUT" } };
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when I use a shortcut to toggle pause, it doesn't do anything", function () {
|
it("when I use a shortcut to toggle pause, it doesn't do anything", function () {
|
||||||
scope.togglePause(event);
|
mockKeypress(scope, ' ', target);
|
||||||
expect(player.togglePause).not.toHaveBeenCalled();
|
expect(player.togglePause).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when I use a shortcut to turn the volume up, it doesn't do anything", function () {
|
it("when I use a shortcut to turn the volume up, it doesn't do anything", function () {
|
||||||
scope.turnVolumeUp(event);
|
mockKeypress(scope, '+', target);
|
||||||
expect(player.turnVolumeUp).not.toHaveBeenCalled();
|
expect(player.turnVolumeUp).not.toHaveBeenCalled();
|
||||||
expect(persistence.saveVolume).not.toHaveBeenCalled();
|
expect(persistence.saveVolume).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when I use a shortcut to turn the volume down, it doesn't do anything", function () {
|
it("when I use a shortcut to turn the volume down, it doesn't do anything", function () {
|
||||||
scope.turnVolumeDown(event);
|
mockKeypress(scope, '-', target);
|
||||||
expect(player.turnVolumeDown).not.toHaveBeenCalled();
|
expect(player.turnVolumeDown).not.toHaveBeenCalled();
|
||||||
expect(persistence.saveVolume).not.toHaveBeenCalled();
|
expect(persistence.saveVolume).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when I use a shortcut to go to the next track, it doesn't do anything", function () {
|
it("when I use a shortcut to go to the next track, it doesn't do anything", function () {
|
||||||
scope.nextTrack(event);
|
mockKeypress(scope, 'RightArrow', target);
|
||||||
expect(player.nextTrack).not.toHaveBeenCalled();
|
expect(player.nextTrack).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when I use a shortcut to go to the previous track, it doesn't do anything", function () {
|
it("when I use a shortcut to go to the previous track, it doesn't do anything", function () {
|
||||||
scope.previousTrack(event);
|
mockKeypress(scope, 'LeftArrow', target);
|
||||||
expect(player.previousTrack).not.toHaveBeenCalled();
|
expect(player.previousTrack).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<link rel="stylesheet" href="subsonic/breadcrumbs-directive/breadcrumbs-directive.css" />
|
<link rel="stylesheet" href="subsonic/breadcrumbs-directive/breadcrumbs-directive.css" />
|
||||||
<!-- endbuild -->
|
<!-- 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>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<div id="messages">
|
<div id="messages">
|
||||||
|
@ -97,7 +97,6 @@
|
||||||
<script src="bower_components/jquery.scrollTo/jquery.scrollTo.js"></script>
|
<script src="bower_components/jquery.scrollTo/jquery.scrollTo.js"></script>
|
||||||
<script src="bower_components/jquery-dateFormat/dist/jquery-dateFormat.js"></script>
|
<script src="bower_components/jquery-dateFormat/dist/jquery-dateFormat.js"></script>
|
||||||
<script src="bower_components/angular-locker/dist/angular-locker.min.js"></script>
|
<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/ng-lodash/build/ng-lodash.js"></script>
|
||||||
<script src="bower_components/angular-ui-sortable/sortable.js"></script>
|
<script src="bower_components/angular-ui-sortable/sortable.js"></script>
|
||||||
<!-- endbower -->
|
<!-- endbower -->
|
||||||
|
|
|
@ -85,7 +85,7 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas
|
||||||
var p = event.jPlayer.status.currentPercentAbsolute;
|
var p = event.jPlayer.status.currentPercentAbsolute;
|
||||||
var isPlaying = !event.jPlayer.status.paused;
|
var isPlaying = !event.jPlayer.status.paused;
|
||||||
if (!scope.scrobbled && p > 30 && isPlaying) {
|
if (!scope.scrobbled && p > 30 && isPlaying) {
|
||||||
if (globals.settings.Debug) { console.log('LAST.FM SCROBBLE - Percent Played: ' + p); }
|
if (globals.settings.Debug) { console.log('Scrobbling - Percent Played: ' + p); }
|
||||||
subsonic.scrobble(scope.currentSong);
|
subsonic.scrobble(scope.currentSong);
|
||||||
scope.scrobbled = true;
|
scope.scrobbled = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,9 +114,7 @@
|
||||||
<h3 class="title">Keyboard Shortcuts</h3>
|
<h3 class="title">Keyboard Shortcuts</h3>
|
||||||
<ul class="preferences">
|
<ul class="preferences">
|
||||||
<li><em>Esc</em> Hide Queue</li>
|
<li><em>Esc</em> Hide Queue</li>
|
||||||
<li><em>[1-6]</em> Switch to corresponding tab</li>
|
|
||||||
<li><em>[a-z]</em> Use to Quickly Browse to an Artist</li>
|
<li><em>[a-z]</em> Use to Quickly Browse to an Artist</li>
|
||||||
<li><em>Home</em> Scroll to Top of Artist List</li>
|
|
||||||
<li><em>Spacebar</em> Play/Pause</li>
|
<li><em>Spacebar</em> Play/Pause</li>
|
||||||
<li><em>→</em> Next Track</li>
|
<li><em>→</em> Next Track</li>
|
||||||
<li><em>←</em> Previous Track</li>
|
<li><em>←</em> Previous Track</li>
|
||||||
|
|
|
@ -39,7 +39,6 @@
|
||||||
"jquery.scrollTo": "~1.4.5",
|
"jquery.scrollTo": "~1.4.5",
|
||||||
"jquery-dateFormat": "~1.0.2",
|
"jquery-dateFormat": "~1.0.2",
|
||||||
"angular-locker": "~2.0.1",
|
"angular-locker": "~2.0.1",
|
||||||
"angular-ui-utils": "bower-keypress",
|
|
||||||
"open-iconic": "~1.1.1",
|
"open-iconic": "~1.1.1",
|
||||||
"ng-lodash": "~0.2.3",
|
"ng-lodash": "~0.2.3",
|
||||||
"angular-ui-sortable": "~0.13.4"
|
"angular-ui-sortable": "~0.13.4"
|
||||||
|
|
|
@ -33,7 +33,6 @@ module.exports = function (config) {
|
||||||
'bower_components/jquery.scrollTo/jquery.scrollTo.js',
|
'bower_components/jquery.scrollTo/jquery.scrollTo.js',
|
||||||
'bower_components/jquery-dateFormat/dist/jquery-dateFormat.js',
|
'bower_components/jquery-dateFormat/dist/jquery-dateFormat.js',
|
||||||
'bower_components/angular-locker/dist/angular-locker.min.js',
|
'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/ng-lodash/build/ng-lodash.js',
|
||||||
'bower_components/angular-ui-sortable/sortable.js',
|
'bower_components/angular-ui-sortable/sortable.js',
|
||||||
'bower_components/angular-mocks/angular-mocks.js',
|
'bower_components/angular-mocks/angular-mocks.js',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue