Merge pull request #246 from pR0Ps/bugfix/update-and-ui-fixes

Mouse and media key fixes
This commit is contained in:
Carey Metcalfe 2019-01-16 02:06:54 -05:00 committed by GitHub
commit 2afc90ef06
17 changed files with 2683 additions and 1978 deletions

View file

@ -25,6 +25,9 @@ module.exports = function (grunt) {
dist: 'dist'
};
// Serve static files
var serveStatic = require('serve-static');
// Paths to ssh config & private key
var sshConfigFile = '.ssh/testServer.json';
var sshKeyFile = '.ssh/test-server-key/';
@ -87,9 +90,9 @@ module.exports = function (grunt) {
return [
connect().use(
'/bower_components',
connect.static('./bower_components')
serveStatic('./bower_components')
),
connect.static(appConfig.app)
serveStatic(appConfig.app)
];
}
}

View file

@ -6,7 +6,6 @@ angular.module('JamStash', [
'ngRoute',
'ngSanitize',
'ngLodash',
'ui.keypress',
'jamstash.subsonic.controller',
'jamstash.archive.controller',
'jamstash.player.controller',

View file

@ -1,6 +1,7 @@
'use strict';
angular.module('JamStash').directive('sortable', function () {
angular.module('JamStash')
.directive('sortable', function () {
return {
link: function (scope, elm, attrs) {
elm.sortable({
@ -46,30 +47,6 @@ angular.module('JamStash').directive('sortable', function () {
}
};
}])
.directive('stopEvent', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
element.bind(attr.stopEvent, function (e) {
e.stopPropagation();
});
}
};
})
.directive('ngEnter', function () {
return {
scope: { onEnter: '&' },
link: function (scope, element) {
console.log(scope);
element.bind("keydown keypress", function (event) {
if (event.which === 13) {
scope.onEnter();
scope.$apply();
}
});
}
};
})
.directive('ngDownload', ['$compile', function ($compile) {
return {
restrict: 'E',
@ -92,16 +69,20 @@ angular.module('JamStash').directive('sortable', function () {
}
};
}])
.directive('stopEvent', function () {
.directive('stopEvent', ['lodash', function (_) {
return {
restrict: 'A',
link: function (scope, element, attr) {
element.bind(attr.stopEvent, function (e) {
e.stopPropagation();
});
if (attr && attr.stopEvent) {
_.forEach(attr.stopEvent.split(','), function (eventName) {
element.bind(eventName, function (e) {
e.stopPropagation();
});
});
}
}
};
})
}])
.directive('ngEnter', function () {
return function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
@ -114,22 +95,4 @@ angular.module('JamStash').directive('sortable', function () {
}
});
};
})
.directive("ngMsgs", function() {
/* Not Using */
return {
restrict: 'E',
transclude : false,
scope: {
msgs: "="
},
template: '<span id="msg_{{$index}}" class="message">{{ item }}</span>',
link: function (scope, elm, attrs) {
scope.$watch(scope.Messages, function () {
var content = $compile((template)(scope));
elm.append(content);
$(elm).parent().fadeIn();
});
}
};
});
});

View file

@ -154,9 +154,9 @@ angular.module('JamStash')
});
};
$(document).on("click", ".message", function(){
$(this).remove();
});
$(document).on("click", ".message", function(){
$(this).remove();
});
// Global Functions
window.onbeforeunload = function () {
@ -171,39 +171,51 @@ angular.module('JamStash')
$(this).fadeOut(function () { $(this).remove(); });
return false;
})
$document.keydown(function (e) {
$scope.scrollToIndex(e);
// Shortcut processing
$(document).keydown(function (e) {
$scope.processKeyEvent(e);
});
$scope.scrollToIndex = function (e) {
var source = e.target.id;
if (e.target.tagName.toUpperCase() != 'INPUT') {
var unicode = e.charCode ? e.charCode : e.keyCode;
if (globals.settings.Debug) { console.log('Keycode Triggered: ' + unicode); }
if (unicode == 49) { // 1
$('#action_Queue').click();
} else if (unicode == 50) {
$('#action_Library').click();
} else if (unicode == 51) {
$('#action_Archive').click();
} else if (unicode == 52) {
$('#action_Settings').click();
} else if (unicode == 53) {
} else if (unicode == 54) { // 6
$scope.processKeyEvent(e);
return true;
};
$scope.processKeyEvent = function (e) {
if (e.isDefaultPrevented() ||
e.repeat ||
e.altKey || e.metaKey || e.ctrlKey ||
(e.target && _.contains(['input', 'textarea', 'select'], e.target.tagName.toLowerCase()))) {
return;
}
var key = e.key;
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';
}
if (unicode >= 65 && unicode <= 90 && $('#tabLibrary').is(':visible')) { // a-z
var key = utils.findKeyForCode(unicode);
if (key == 'x' || key == 'y' || key == 'z') {
key = 'x-z';
}
var el = '#' + key.toUpperCase();
if ($(el).length > 0) {
$('#left-component').stop().scrollTo(el, 400);
}
} else if (unicode == 36 && $('#tabLibrary').is(':visible')) { // home
$('#left-component').stop().scrollTo('#MusicFolders', 400);
var el = '#' + key.toUpperCase();
if ($(el).length > 0) {
$('#left-component').stop().scrollTo(el, 400);
}
}
return true;
else{
return;
}
$scope.$apply();
e.preventDefault();
};
$scope.scrollToIndexName = function (index) {
var el = '#' + index;
@ -246,82 +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) {
if (globals.settings.Debug) { console.log("LOAD JUKEBOX"); }
$.ajax({
url: globals.BaseURL() + '/jukeboxControl.view?' + globals.BaseParams() + '&action=set&id=' + id,
method: 'GET',
dataType: globals.settings.Protocol,
timeout: globals.settings.Timeout,
success: function (data) {
/*
if (data["subsonic-response"].podcasts.channel !== undefined) {
}
deferred.resolve(podcasts);
*/
$.get(globals.BaseURL() + '/jukeboxControl.view?' + globals.BaseParams() + '&action=start');
}
});
};
$rootScope.sendToJukebox = function (action) {
if (globals.settings.Debug) { console.log("SEND JUKEBOX " + action); }
$.ajax({
url: globals.BaseURL() + '/jukeboxControl.view?' + globals.BaseParams() + '&action=' + action,
method: 'GET',
dataType: globals.settings.Protocol,
timeout: globals.settings.Timeout,
success: function (data) {
}
});
};
$scope.toggleStar = function (item) {
subsonic.toggleStar(item).then(function (newStarred) {
item.starred = newStarred;

View file

@ -3,7 +3,7 @@ describe("Main controller", function () {
'use strict';
var controllerParams, $controller, $q, scope, mockGlobals, player, utils, persistence, subsonic, notifications,
deferred;
deferred, mockKeypress;
beforeEach(function () {
mockGlobals = {
settings: {
@ -18,6 +18,16 @@ describe("Main controller", function () {
$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
player = jasmine.createSpyObj("player", [
"togglePause",
@ -78,10 +88,6 @@ describe("Main controller", function () {
});
});
xdescribe("toggleSetting -", function () {
});
describe("", function () {
beforeEach(function () {
$controller('AppController', controllerParams);
@ -99,24 +105,16 @@ describe("Main controller", function () {
expect(scope.showQueue).toHaveBeenCalled();
});
describe("When I toggle pause,", function () {
it("given that we're using the Jukebox mode, it sends a 'stop' command to the jukebox", function () {
mockGlobals.settings.Jukebox = true;
spyOn(scope, "sendToJukebox");
scope.togglePause();
expect(scope.sendToJukebox).toHaveBeenCalledWith('stop');
});
it("it toggles pause using the player service", function () {
scope.togglePause();
describe("When I toggle pause using the keyboard shortcut,", function () {
it("it toggles pause on the player service", function () {
mockKeypress(scope, ' ');
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 () {
player.turnVolumeUp.and.returnValue(0.6);
scope.turnVolumeUp();
mockKeypress(scope, '+');
expect(player.turnVolumeUp).toHaveBeenCalled();
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 () {
player.turnVolumeDown.and.returnValue(0.4);
scope.turnVolumeDown();
mockKeypress(scope, '-');
expect(player.turnVolumeDown).toHaveBeenCalled();
expect(persistence.saveVolume).toHaveBeenCalledWith(0.4);
});
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();
});
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();
});
describe("Given that I am targeting an input,", function () {
var event;
beforeEach(function () {
event = { target: { tagName: "INPUT" } };
});
var target = { 'tagName': "iNPUt" } ;
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();
});
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(persistence.saveVolume).not.toHaveBeenCalled();
});
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(persistence.saveVolume).not.toHaveBeenCalled();
});
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();
});
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();
});
});

View file

@ -1,10 +1,10 @@
<li class="row song" ng-repeat="o in song" ng-click="toggleSelection(o)" ng-dblclick="playFrom($index)" ng-class="{'selected': o.selected, 'playing': o.playing}">
<div class="itemactions">
<a class="add" href="" title="Add To Queue" ng-click="addSongToQueue(o)" stop-event="click"></a>
<!--<a class="remove" href="" title="Remove Song" ng-click="removeSongFromQueue(o)" stop-event="click"></a>-->
<a class="play" href="" title="Play this song" ng-click="playSong(o)" stop-event="click"></a>
<!--<a class="download" href="" title="Download Song" ng-click="download(o.id)"></a>-->
<a href="" title="Star" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="toggleStar(o)" stop-event="click"></a>
<a class="add" href="" title="Add To Queue" ng-click="addSongToQueue(o)" stop-event="click,dblclick"></a>
<!--<a class="remove" href="" title="Remove Song" ng-click="removeSongFromQueue(o)" stop-event="click,dblclick"></a>-->
<a class="play" href="" title="Play this song" ng-click="playSong(o)" stop-event="click,dblclick"></a>
<!--<a class="download" href="" title="Download Song" ng-click="download(o.id)" stop-event="click,dblclick"></a>-->
<a href="" title="Star" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="toggleStar(o)" stop-event="click,dblclick"></a>
<div class="clear"></div>
</div>
<div class="track floatleft" ng-bind-html="o.track"></div>

View file

@ -26,7 +26,7 @@
<link rel="stylesheet" href="subsonic/breadcrumbs-directive/breadcrumbs-directive.css" />
<!-- endbuild -->
</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="header">
<div id="messages">
@ -84,20 +84,19 @@
<!-- build:js({.,app}) scripts/vendor.min.js -->
<!-- bower:js -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/jquery-ui/jquery-ui.js"></script>
<script src="bower_components/jplayer/dist/jplayer/jquery.jplayer.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-sanitize/angular-sanitize.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/jquery-ui/jquery-ui.js"></script>
<script src="bower_components/jplayer/dist/jplayer/jquery.jplayer.js"></script>
<script src="bower_components/jquery-mousewheel/jquery.mousewheel.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/jquery.scrollTo/jquery.scrollTo.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-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 -->

View file

@ -85,7 +85,7 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas
var p = event.jPlayer.status.currentPercentAbsolute;
var isPlaying = !event.jPlayer.status.paused;
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);
scope.scrobbled = true;
}
@ -123,7 +123,7 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas
$player.jPlayer('setMedia', media);
if (globals.settings.Jukebox) {
$player.jPlayer('mute', true);
scope.addToJukebox(newSong.id);
subsonic.addToJukebox(newSong);
}
if (playerService.loadSong === true || globals.settings.Jukebox) {
// Do not play, only load
@ -152,10 +152,9 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas
scope.$watch(function () {
return playerService.pauseSong;
}, function (newVal) {
if(newVal === true) {
$player.jPlayer('pause');
} else {
$player.jPlayer('play');
$player.jPlayer(newVal ? 'pause' : 'play');
if(globals.settings.Jukebox){
subsonic.sendToJukebox(newVal ? 'stop' : 'start');
}
});

View file

@ -76,11 +76,12 @@ describe("jplayer directive", function () {
it("if the global setting Jukebox is true, it mutes jPlayer and adds the song to subsonic's Jukebox", function () {
mockGlobals.settings.Jukebox = true;
scope.addToJukebox = jasmine.createSpy("addToJukebox");
spyOn(subsonic, "addToJukebox")
scope.$apply();
expect($player.jPlayer).toHaveBeenCalledWith('mute', true);
expect(scope.addToJukebox).toHaveBeenCalledWith(playingSong.id);
expect(subsonic.addToJukebox).toHaveBeenCalledWith(
jasmine.objectContaining({ id: playingSong.id}))
});
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 () {
@ -142,6 +143,7 @@ describe("jplayer directive", function () {
describe("", function () {
beforeEach(function () {
$.fn.jPlayer.and.stub();
spyOn(subsonic, "sendToJukebox")
});
it("When the player service's restartSong flag is true, it restarts the current song, resets the restart flag to false and resets the scrobbled flag to false", function () {
@ -170,6 +172,25 @@ describe("jplayer directive", function () {
expect($player.jPlayer).toHaveBeenCalledWith('play');
});
it("When the player service's pauseSong is true and jukebox is enabled, 'stop' is sent to the jukebox", function () {
mockGlobals.settings.Jukebox = true;
playerService.pauseSong = true;
scope.$apply();
expect(subsonic.sendToJukebox).toHaveBeenCalledWith('stop');
});
it("Given that the current song is paused and jukebox is enabled, 'start' is sent to the jukebox when it's unpaused", function () {
mockGlobals.settings.Jukebox = true;
playerService.pauseSong = true;
scope.$apply();
playerService.pauseSong = false;
scope.$apply();
expect(subsonic.sendToJukebox).toHaveBeenCalledWith('start');
});
it("When the player service's volume changes, it sets jPlayer's volume", function () {
playerService.getVolume.and.returnValue(0.2034);
scope.$apply();

View file

@ -15,22 +15,8 @@ angular.module('jamstash.player.controller', ['jamstash.player.service', 'jamsta
$scope.settings = globals.settings;
$scope.playerSettings = player.settings;
$scope.play = function () {
if (globals.settings.Jukebox) {
$scope.sendToJukebox('start');
} else {
player.togglePause();
}
};
$scope.pause = function () {
if (globals.settings.Jukebox) {
$scope.sendToJukebox('stop');
} else {
player.togglePause();
}
};
$scope.play = player.togglePause;
$scope.pause = player.togglePause;
$scope.previousTrack = player.previousTrack;
$scope.nextTrack = player.nextTrack;
}]);

View file

@ -46,14 +46,14 @@
href=""
title="Remove Song"
ng-click="vm.removeSongFromQueue(song)"
stop-event="click"
stop-event="click,dblclick"
></a>
<a
href=""
title="Star"
ng-class="{'favorite': song.starred, 'rate': ! song.starred}"
ng-click="vm.toggleStar(song)"
stop-event="click"
stop-event="click,dblclick"
></a>
<div class="clear"></div>
</div>

View file

@ -114,9 +114,7 @@
<h3 class="title">Keyboard Shortcuts</h3>
<ul class="preferences">
<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>Home</em> Scroll to Top of Artist List</li>
<li><em>Spacebar</em> Play/Pause</li>
<li><em>&rarr;</em> Next Track</li>
<li><em>&larr;</em> Previous Track</li>

View file

@ -53,7 +53,9 @@ function subsonicService(
scrobble : scrobble,
search : search,
subsonicRequest : subsonicRequest,
toggleStar : toggleStar
toggleStar : toggleStar,
addToJukebox : addToJukebox,
sendToJukebox : sendToJukebox
});
// TODO: Hyz: Remove when refactored
@ -570,4 +572,27 @@ function subsonicService(
});
return promise;
}
function addToJukebox(song) {
if (globals.settings.Debug) { console.log("Load Jukebox"); }
var promise = self.subsonicRequest('jukeboxControl.view', {
params: {
action: 'set',
id: song.id
}
}).then(function () {
self.sendToJukebox('start');
});
return promise;
}
function sendToJukebox(action) {
if (globals.settings.Debug) { console.log("Send Jukebox " + action); }
var promise = self.subsonicRequest('jukeboxControl.view', {
params: {
action: action
}
})
return promise;
}
}

View file

@ -26,20 +26,19 @@
},
"main": "app/index.html",
"dependencies": {
"jquery": "~2.1.4",
"jquery-ui": "~1.11.4",
"jplayer": "~2.9.2",
"angular": "~1.4.1",
"angular-route": "~1.4.1",
"angular-sanitize": "~1.4.1",
"angular-cookies": "~1.4.1",
"angular-resource": "~1.4.1",
"jquery": "~2.1.4",
"jquery-ui": "~1.11.4",
"jplayer": "~2.9.2",
"fancybox": "~2.1.4",
"notify.js": "<=1.2.2",
"jquery.scrollTo": "~1.4.5",
"jquery-dateFormat": "~1.0.2",
"angular-locker": "~2.0.1",
"angular-ui-utils": "bower-keypress",
"open-iconic": "~1.1.1",
"ng-lodash": "~0.2.3",
"angular-ui-sortable": "~0.13.4"

View file

@ -20,20 +20,19 @@ module.exports = function (config) {
files: [
// bower:
'bower_components/jquery/dist/jquery.js',
'bower_components/jquery-ui/jquery-ui.js',
'bower_components/jplayer/dist/jplayer/jquery.jplayer.js',
'bower_components/angular/angular.js',
'bower_components/angular-route/angular-route.js',
'bower_components/angular-sanitize/angular-sanitize.js',
'bower_components/angular-cookies/angular-cookies.js',
'bower_components/angular-resource/angular-resource.js',
'bower_components/jquery-ui/jquery-ui.js',
'bower_components/jplayer/dist/jplayer/jquery.jplayer.js',
'bower_components/jquery-mousewheel/jquery.mousewheel.js',
'bower_components/fancybox/source/jquery.fancybox.js',
'bower_components/notify.js/notify.js',
'bower_components/jquery.scrollTo/jquery.scrollTo.js',
'bower_components/jquery-dateFormat/dist/jquery-dateFormat.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/angular-ui-sortable/sortable.js',
'bower_components/angular-mocks/angular-mocks.js',

4232
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -28,32 +28,33 @@
"main": "app/index.html",
"dependencies": {},
"devDependencies": {
"grunt": "^0.4.5",
"grunt-bump": "^0.3.1",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-connect": "^0.10.1",
"grunt-contrib-copy": "^0.8.0",
"grunt-contrib-cssmin": "^0.12.3",
"grunt-contrib-htmlmin": "^0.4.0",
"grunt": "^1.0.1",
"grunt-bump": "^0.8.0",
"grunt-contrib-clean": "^1.0.0",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-connect": "^1.0.2",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-cssmin": "^1.0.2",
"grunt-contrib-htmlmin": "^2.0.0",
"grunt-contrib-imagemin": "^1.0.1",
"grunt-contrib-uglify": "^0.9.1",
"grunt-contrib-watch": "^0.6.1",
"grunt-contrib-uglify": "^2.0.0",
"grunt-contrib-watch": "^1.0.0",
"grunt-filerev": "^2.3.1",
"grunt-karma": "^0.10.1",
"grunt-karma": "^2.0.0",
"grunt-notify": "^0.4.1",
"grunt-ssh": "^0.12.3",
"grunt-svg-sprite": "^1.1.2",
"grunt-usemin": "^3.0.0",
"grunt-wiredep": "^2.0.0",
"jasmine-core": "^2.6.1",
"jit-grunt": "^0.9.1",
"karma": "^0.12.32",
"karma-chrome-launcher": "^0.1.12",
"karma-coverage": "^0.3.1",
"karma-jasmine": "^0.3.5",
"karma-ng-html2js-preprocessor": "^0.1.2",
"karma-notify-reporter": "^0.1.1",
"grunt-wiredep": "^3.0.1",
"jasmine-core": "^2.5.2",
"jit-grunt": "^0.10.0",
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.1.1",
"karma-jasmine": "^1.0.2",
"karma-ng-html2js-preprocessor": "^1.0.0",
"karma-notify-reporter": "^1.0.1",
"serve-static": "^1.13.1",
"time-grunt": "^1.2.1"
},
"engines": {