UI improvements, save build feature partial implementation

This commit is contained in:
Colin McLeod
2015-05-02 00:04:57 -07:00
parent bca5ed899f
commit 71405e6cb7
21 changed files with 383 additions and 192 deletions

View File

@@ -23,11 +23,13 @@
</head>
<body>
<div id="bg"></div>
<shipyard-menu></shipyard-menu>
<shipyard-header></shipyard-header>
<div id="main" ui-view ng-click="bgClicked($event)"></div>
<footer>
<div class="right">Version <%= version %> - <%= date %></div>
<div class="right">
<a href="https://github.com/cmmcleod/ed-shipyard" target="_blank" title="Shipyard Github Project">Version <%= version %> - <%= date %></a>
</div>
<div class="left">
Coriolis Shipyard was created using assets and imagery from Elite: Dangerous, with the permission of Frontier Developments plc, for non-commercial purposes.<br>
It is not endorsed by nor reflects the views or opinions of Frontier Developments and no employee of Frontier Developments was involved in the making of it.

View File

@@ -1,44 +1,9 @@
angular.module('app', ['ui.router', 'shipyard', 'ngLodash', 'app.templates'])
.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', function($stateProvider, $urlRouterProvider, $locationProvider) {
$locationProvider.html5Mode(true);
$stateProvider
.state('outfit', {
url: '/outfit/:shipId/:code?bn',
params: {
// TODO: fix below, default, squash false not working
//shipId: { value: 'sidewinder', squash: false }, // Allow 'shipId' parameter to default to
code: { value: null, squash: true } // Allow 'code' parameter to be empty/optional
},
templateUrl: 'views/page-outfit.html',
controller: 'OutfitController',
resolve: {
shipId: ['$stateParams',function ($p) { // Ensure ship exists before loading controller
if (!DB.ships[$p.shipId]) {
throw { type: 404, message: 'Ship "' + $p.shipId + '" does not exist'};
}
}]
}
})
.state('shipyard', { url: '/', templateUrl: 'views/page-shipyard.html', controller: 'ShipyardController' })
.state('error', { params: {type:null, message:null, details: null }, templateUrl: 'views/page-error.html', controller: 'ErrorController' })
.state('notfound', { url: '*path', templateUrl: 'views/page-error.html', controller: 'ErrorController' });
}])
.config(['$provide',function($provide) {
// Global Error Handler, redirects uncaught errors to the error page
$provide.decorator('$exceptionHandler', ['$delegate', '$injector', function ($delegate, $injector) {
return function(exception, cause) {
$injector.get('$state').go('error', { details: exception }, {location:false, reload:true}); // Go to error state, reload the controller, keep the current URL
$delegate(exception, cause);
};
}]);
}])
.run(['$rootScope','$document','$state','commonArray','shipPurpose','shipSize','hardPointClass','internalGroupMap','hardpointsGroupMap', function ($rootScope, $doc, $state, CArr, shipPurpose, sz, hpc, igMap, hgMap) {
// Redirect any state transition errors to the error controller/state
$rootScope.$on('$stateChangeError', function(e, toState, toParams, fromState, fromParams, error){
e.preventDefault();
$state.go('error',error, {location:false, reload:true}); // Go to error state, reload the controller, keep the current URL
$state.go('error', error, {location:false, reload:true}); // Go to error state, reload the controller, keep the current URL
});
// Global Reference variables
@@ -47,7 +12,7 @@ angular.module('app', ['ui.router', 'shipyard', 'ngLodash', 'app.templates'])
$rootScope.SZ = sz;
$rootScope.HPC = hpc;
$rootScope.igMap = igMap;
window.hgmap = $rootScope.hgMap = hgMap;
$rootScope.hgMap = hgMap;
$rootScope.ships = DB.ships;
$rootScope.title = 'Coriolis';
@@ -61,11 +26,16 @@ angular.module('app', ['ui.router', 'shipyard', 'ngLodash', 'app.templates'])
// Global Event Listeners
$doc.bind('keyup', function (e) {
if(e.keyCode == 27) { // Escape Key
$rootScope.$broadcast('close', e);
$rootScope.$apply();
} else {
$rootScope.$broadcast('keyup', e);
}
});
$rootScope.bgClicked = function (e) {
$rootScope.$broadcast('bgClicked', e);
$rootScope.$broadcast('close', e);
}
}]);

56
app/js/config.js Normal file
View File

@@ -0,0 +1,56 @@
/**
* Sets up the routes and handlers before the Angular app is kicked off.
*/
angular.module('app').config(['$provide','$stateProvider', '$urlRouterProvider', '$locationProvider', function ($provide, $stateProvider, $urlRouterProvider, $locationProvider) {
// Use HTML5 push and replace state if possible
$locationProvider.html5Mode(true);
/**
* Set up all states and their routes.
*/
$stateProvider
.state('outfit', {
url: '/outfit/:shipId/:code?bn',
params: {
// TODO: Squash:false not working due to UI-router issue
shipId: { value: 'sidewinder', squash: false}, // Allow 'shipId' parameter to default to
code: { value: undefined, squash: true } // Allow 'code' parameter to be empty/optional
},
templateUrl: 'views/page-outfit.html',
controller: 'OutfitController',
resolve: {
shipId: ['$stateParams',function ($p) { // Ensure ship exists before loading controller
if (!DB.ships[$p.shipId]) {
throw { type: 'no-ship', message: $p.shipId };
}
}]
}
})
.state('shipyard', { url: '/', templateUrl: 'views/page-shipyard.html', controller: 'ShipyardController' })
.state('error', { params: {type:null, message:null, details: null }, templateUrl: 'views/page-error.html', controller: 'ErrorController' })
// Redirects
$urlRouterProvider.when('/outfit','/outfit/sidewinder/');
/**
* 404 Handler - Keep current URL/ do not redirect, change to error state.
*/
$urlRouterProvider.otherwise(function ($injector, $location) {
// Go to error state, reload the controller, keep the current URL
$injector.get('$state').go('error', { type: 404, message: null, details: null }, {location:false, reload:true});
return $location.path;
});
/**
* Global Error Handler. Decorates the existing error handler such that it
* redirects uncaught errors to the error page.
*
*/
$provide.decorator('$exceptionHandler', ['$delegate', '$injector', function ($delegate, $injector) {
return function(exception, cause) {
// Go to error state, reload the controller, keep the current URL
$injector.get('$state').go('error', { details: exception }, {location:false, reload:true});
$delegate(exception, cause);
};
}]);
}]);

View File

@@ -1,15 +1,29 @@
angular.module('app')
.controller('ErrorController', ['$rootScope','$scope','$stateParams', '$location', function ($rootScope, $scope, $p, $location) {
$rootScope.title = 'Error';
if ($p.path) { // If path is specified, 404
$scope.type = 404; // Deep Space Image...
$scope.message = ""
$scope.path = $p.path;
} else {
$scope.type = $p.type || 'unknown';
$scope.message = $p.message || "Uh, this is bad..";
$scope.path = $location.path();
$scope.type = $p.type || 'unknown';
switch ($scope.type) {
case 404:
$scope.msgPre = 'Page';
$scope.msgHighlight = $scope.path;
$scope.msgPost = 'Not Found';
$scope.image = 'deep-space';
break;
case 'no-ship':
$scope.msgPre = 'Ship';
$scope.msgHighlight = $p.message;
$scope.msgPost = 'does not exist';
$scope.image = 'thargoid';
break;
case 'build-fail':
$scope.msgPre = 'Build Failure!';
$scope.image = 'ship-explode';
break;
default:
$scope.msgPre = "Uh, this is bad..";
$scope.image = 'thargoid';
}
}]);

View File

@@ -1,7 +1,8 @@
angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$state', '$stateParams', 'Ship', 'Components', 'Serializer', 'Persist', function ($rootScope, $scope, $state, $p, Ship, Components, Serializer, Persist) {
var data = DB.ships[$p.shipId];
var data = DB.ships[$p.shipId]; // Retrieve the basic ship properties, slots and defaults
var ship = new Ship($p.shipId, data.properties, data.slots); // Create a new Ship instance
// Update the ship instance with the code (if provided) or the 'factory' defaults.
if ($p.code) {
Serializer.toShip(ship, $p.code); // Populate components from 'code' URL param
$scope.code = $p.code;
@@ -10,7 +11,7 @@ angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$s
}
$scope.buildName = $p.bn;
$rootScope.title = ship.name + $scope.buildName? ' - ' + $scope.buildName: '';
$rootScope.title = ship.name + ($scope.buildName? ' - ' + $scope.buildName: '');
$scope.ship = ship;
$scope.pp = ship.common[0]; // Power Plant
$scope.th = ship.common[1]; // Thruster
@@ -23,11 +24,17 @@ angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$s
$scope.internal = ship.internal;
$scope.availCS = Components.forShip(ship.id);
$scope.selectedSlot = null;
$scope.lastSaveCode = Persist.getBuild(ship.id, $scope.buildName);
$scope.savedCode = Persist.getBuild(ship.id, $scope.buildName);
// for debugging
window.myScope = $scope;
/**
* 'Opens' a select for component selection.
*
* @param {[type]} e The event object
* @param {[type]} slot The slot that is being 'opened' for selection
*/
$scope.selectSlot = function(e, slot) {
e.stopPropagation();
if ($scope.selectedSlot == slot) {
@@ -37,6 +44,14 @@ angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$s
}
};
/**
* Updates the ships build with the selected component for the
* specified slot. Prevents the click event from propagation.
*
* @param {string} type Shorthand key/string for identifying the slot & component type
* @param {[type]} slot The slot object belonging to the ship instance
* @param {[type]} e The event object
*/
$scope.select = function(type, slot, e) {
e.stopPropagation();
if (e.srcElement.id) {
@@ -54,7 +69,6 @@ angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$s
$scope.selectedSlot = null;
$scope.code = Serializer.fromShip(ship);
$state.go('outfit', {shipId: ship.id, code: $scope.code, bn: $scope.buildName}, {location:'replace', notify:false});
$scope.canSave = true;
}
}
@@ -62,21 +76,28 @@ angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$s
* Reload the build from the last save.
*/
$scope.reloadBuild = function() {
if ($scope.buildName && $scope.lastSaveCode) {
Serializer.toShip(ship, $scope.lastSaveCode); // Repopulate with components from last save
$scope.code = $scope.lastSaveCode;
$state.go('outfit', {shipId: ship.id, code: $scope.lastSaveCode, bn: $scope.buildName}, {location:'replace', notify:false});
if ($scope.buildName && $scope.savedCode) {
Serializer.toShip(ship, $scope.savedCode); // Repopulate with components from last save
$scope.code = $scope.savedCode;
$state.go('outfit', {shipId: ship.id, code: $scope.savedCode, bn: $scope.buildName}, {location:'replace', notify:false});
}
};
/**
* Save the current build. Will replace the saved build if there is one
* for this ship & with the exact name.
*/
$scope.saveBuild = function() {
if ($scope.code && $scope.code != $scope.lastSaveCode) {
if($scope.code != $scope.savedCode) {
Persist.saveBuild(ship.id, $scope.buildName, $scope.code);
$scope.lastSaveCode = $scope.code;
$rootScope.$broadcast('buildSaved', ship.id, $scope.buildName, $scope.code);
$scope.savedCode = $scope.code;
}
}
/**
* Permanently delete the current build and redirect/reload this controller
* with the 'factory' build of the current ship.
*/
$scope.deleteBuild = function() {
Persist.deleteBuild(ship.id, $scope.buildName);
$rootScope.$broadcast('buildDeleted', $scope.saveName, ship.id);
@@ -84,17 +105,20 @@ angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$s
}
$rootScope.$on('keyup', function (e, keyEvent) {
if(keyEvent.keyCode == 27) { // on Escape
$scope.selectedSlot = null;
$scope.$apply();
}
else if(keyEvent.keycode == 83 && keyEvent.ctrlKey){ // CTRL + S
// CTRL + S or CMD + S will override the default and save the build is possible
if (keyEvent.keycode == 83 && keyEvent.ctrlKey) {
e.preventDefault();
$scope.saveBuild();
}
});
$rootScope.$on('bgClicked', function (e, keyEvent) {
// Hide any open menu/slot/etc if escape key is pressed
$rootScope.$on('escape', function (e, keyEvent) {
$scope.selectedSlot = null;
$scope.$apply();
});
// Hide any open menu/slot/etc if the background is clicked
$rootScope.$on('close', function (e, keyEvent) {
$scope.selectedSlot = null;
});

View File

@@ -1,4 +1,3 @@
angular.module('app')
.controller('ShipyardController', ['$rootScope', function ($rootScope) {
angular.module('app').controller('ShipyardController', ['$rootScope', function ($rootScope) {
$rootScope.title = 'Coriolis - Shipyard';
}]);

View File

@@ -0,0 +1,37 @@
angular.module('app').directive('shipyardHeader', ['$rootScope', 'Persist', function ($rootScope, Persist) {
return {
restrict: 'E',
templateUrl: 'views/_header.html',
scope: true,
link: function (scope) {
scope.openedMenu = null;
scope.ships = DB.ships;
scope.allBuilds = Persist.builds;
scope.bs = Persist.state;
console.log(scope);
$rootScope.$on('$stateChangeStart',function(){
scope.openedMenu = null;
});
$rootScope.$on('close', function (e, keyEvent) {
scope.openedMenu = null;
});
scope.openMenu = function (menu) {
if(menu == scope.openedMenu) {
scope.openedMenu = null;
return;
}
if (menu == 'b' && !scope.bs.hasBuilds) {
scope.openedMenu = null;
return;
}
scope.openedMenu = menu;
};
}
};
}]);

View File

@@ -1,13 +0,0 @@
angular.module('app').directive('shipyardMenu', function () {
return {
restrict: 'E',
templateUrl: 'views/menu.html',
link: function () {
// TODO: Saved Ships: load, save, save as, delete, export
// TODO: Links: github, forum, etc
}
};
});

View File

@@ -1,3 +1,6 @@
/**
* [description]
*/
angular.module('app').service('Persist', ['lodash', function (_) {
var LS_KEY = 'builds';
@@ -9,6 +12,9 @@ angular.module('app').service('Persist', ['lodash', function (_) {
this.builds = {};
}
this.state = {
hasBuilds: Object.keys(this.builds).length > 0
}
/**
* Persist a ship build in local storage.
*
@@ -22,7 +28,9 @@ angular.module('app').service('Persist', ['lodash', function (_) {
}
this.builds[shipId][name] = code;
localStorage.setItem(LS_KEY, angular.toJson(this.builds)); // Persist updated build collection to localstorage
this.state.hasBuilds = true;
// Persist updated build collection to localstorage
localStorage.setItem(LS_KEY, angular.toJson(this.builds));
}
/**
@@ -48,11 +56,15 @@ angular.module('app').service('Persist', ['lodash', function (_) {
* @param {string} name The name of the build
*/
this.deleteBuild = function (shipId, name) {
delete build[shipId][name];
if (Object.keys(build[shipId]).length == 0) {
delete build[shipId];
if(this.builds[shipId][name]) {
delete this.builds[shipId][name];
if (Object.keys(this.builds[shipId]).length == 0) {
delete this.builds[shipId];
this.state.hasBuilds = Object.keys(this.builds).length > 0;
}
// Persist updated build collection to localstorage
localStorage.setItem(LS_KEY, angular.toJson(this.builds));
}
}
}]);

View File

@@ -3,8 +3,10 @@
* information or behavoir in Elite Dangerous.
*
* This file contains values and functions that can be reused across the app.
*
* @requires ngLodash is a dependency of this module.
*/
angular.module('shipyard', [])
angular.module('shipyard', ['ngLodash'])
.value('commonArray', [
'Power Plant',
'Thrusters',
@@ -67,19 +69,17 @@ angular.module('shipyard', [])
'Large',
'Huge'
])
.factory('calcJumpRange', function() {
/**
* Calculate the maximum single jump range based on mass and a specific FSD
*
* @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc
* @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass
* @param {number} fuel Optional - The fuel consumed during the jump (must be less than the drives max fuel per jump)
* @return {number} Distance in Light Years
*/
return function(mass, fsd, fuel) {
.value('calcJumpRange', function(mass, fsd, fuel) {
return Math.pow(Math.min(fuel || Infinity, fsd.maxfuel) / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass;
};
})
.factory('calcShieldStrength', function() {
/**
* Calculate the a ships shield strength based on mass, shield generator and shield boosters used.
*
@@ -90,7 +90,7 @@ angular.module('shipyard', [])
* @param {number} multiplier Shield multiplier for ship (1 + shield boosters if any)
* @return {number} Approximate shield strengh in MJ
*/
return function (mass, shields, sg, multiplier) {
.value('calcShieldStrength', function (mass, shields, sg, multiplier) {
if (!sg) {
return 0;
}
@@ -104,5 +104,4 @@ angular.module('shipyard', [])
return shields * multiplier * (sg.optmul + (mass - sg.optmass) / (sg.maxmass - sg.optmass) * (sg.maxmul - sg.optmul));
}
return shields * multiplier * sg.maxmul;
}
});

View File

@@ -2,14 +2,15 @@
@import 'fonts';
@import 'utilities';
@import 'icons';
@import 'header';
@import 'shipyard';
@import 'list';
@import 'slot';
@import 'ship';
@import 'outfit';
@import 'select';
@import 'charts';
@import 'meters';
@import 'error';
html, body {
@@ -65,44 +66,19 @@ body {
clear: both;
}
header {
background-color: @bg;
margin: 0;
height: 55px;
line-height: 55px;
font-family: @fTitle;
vertical-align: middle;
a {
vertical-align: middle;
color: @warning;
&:visited {
color: @warning;
}
&:hover {
color: teal;
}
}
.title {
font-size: 1.3em;
display: inline-block;
margin:0px;
text-transform: uppercase;
}
a, a:visited {
color: @fg;
}
footer {
font-size: 0.3em;
font-size: 0.6em;
color: #999;
padding: 10px 0;
width: 100%;
overflow: hidden;
}
header, footer {
footer {
.right {
float: right;
text-align: right;

View File

@@ -10,10 +10,10 @@
@disabled: #888;
@primary-disabled: darken(@primary, @disabledDarken);
@secondary-disabled: darken(@primary, @disabledDarken);
@warning-disabled: darken(@primary, @disabledDarken);
@secondary-disabled: darken(@secondary, @disabledDarken);
@warning-disabled: darken(@warning, @disabledDarken);
@bgBlack: rgba(0,0,0,0.6);
@bgBlack: rgba(0,0,0,0.9);
@primary-bg: fadeout(darken(@primary, 45%), 30%);
@secondary-bg: fadeout(darken(@secondary, @bgDarken), @bgTransparency); // Brown background

10
app/less/error.less Normal file
View File

@@ -0,0 +1,10 @@
.error {
width: 50%;
margin: 10% auto;
text-align: center;
small {
color: @primary-disabled;
}
}

85
app/less/header.less Normal file
View File

@@ -0,0 +1,85 @@
header {
background-color: @bg;
margin: 0;
padding: 0 1em;
height: 4em;
line-height: 4em;
font-family: @fTitle;
vertical-align: middle;
position: relative;
z-index: 2;
.user-select-none();
.menu {
position: relative;
z-index: 1;
cursor: default;
}
.menu-header {
height: 100%;
z-index: 2;
padding : 0 1em;
cursor: pointer;
color: @warning;
&.disabled {
color: @warning-disabled;
cursor: default;
&:hover {
background-color: transparent;
}
}
&:hover, &.selected {
background-color: @bgBlack;
}
}
.menu-list {
width: 200%;
font-family: @fStandard;
position: absolute;
padding: 0 1em 1em 0;
overflow: hidden;
background-color: @bgBlack;
font-size: 0.8em;
}
ul {
margin: 0;
padding: 0;
margin-top: 0.5em;
line-height: 2em;
}
li {
list-style: none;
margin-left: 1em;
line-height: 1.1em;
}
a {
vertical-align: middle;
color: @warning;
text-decoration: none;
white-space: nowrap;
&:visited {
color: @warning;
}
&:hover {
color: teal;
}
&.active {
color: @primary;
}
}
.title {
font-size: 1.3em;
display: inline-block;
margin:0px;
text-transform: uppercase;
}
}

View File

@@ -1,15 +1,24 @@
#overview {
margin: 0px auto;
text-align: center;
h1 {
font-family: @fTitle;
margin: 5px 0;
color: @primary;
float: left;
}
}
div {
display: inline-block;
margin: 0 5px;
#build {
float: right;
line-height: 2em;
input {
background: @primary-bg;
color: @fg;
border: none;
font-size: 0.8em;
line-height: 2em;
text-transform: uppercase;
}
}

View File

@@ -2,7 +2,6 @@
.slot-group {
float: left;
margin: 0 5px;
background-color: @bgBlack;
.user-select-none();
cursor: default;

21
app/views/_header.html Normal file
View File

@@ -0,0 +1,21 @@
<header>
<div class="l" style="margin-right: 2em;"><a ui-sref="shipyard" class="logo shipyard"></a></div>
<div class="l menu">
<div class="menu-header" ng-class="{selected: openedMenu=='b', disabled: !bs.hasBuilds}" ng-click="openMenu('b')">Builds</div>
<div class="menu-list" ng-if="openedMenu=='b'">
<ul class="left" ng-repeat="(shipId,builds) in allBuilds">
{{ships[shipId].properties.name}}
<li ng-repeat="(name, build) in builds">
<a ui-sref-active="active" ui-sref="outfit({shipId:shipId, code:build, bn:name})" ng-bind="name"></a>
</li>
</ul>
</div>
</div>
<div class="r">
<a class="logo github" href="https://github.com/cmmcleod/ed-shipyard" target="_blank" title="Shipyard Github Project"></a>
<a class="logo reddit" href="#" target="_blank" title="Reddit Thread"></a>
</div>
</header>

View File

@@ -1,11 +0,0 @@
<header>
<div class="left">
<a ui-sref="shipyard" class="logo shipyard"></a>
</div>
<div class="right">
<a class="logo github" href="https://github.com/cmmcleod/ed-shipyard" target="_blank" title="Shipyard Github Project"></a>
<a class="logo reddit" href="#" target="_blank" title="Reddit Thread"></a>
</div>
</header>

View File

@@ -1,12 +1,14 @@
<div class="error">
<h1>
<span ng-if="msgPre">{{msgPre}}</span>
<small ng-if="msgHighlight">{{msgHighlight}}</small>
<span ng-if="msgPost">{{msgPost}}</span>
</h1>
<!-- TODO: add awesome relevant SVG based on code /-->
<div>
<h1>Error {{type}}</h1>
<h2 ng-bind="message"></h2>
<p ng-if="path" ng-bind="path"></p>
<p ng-if="path" ng-bind="path"></p>
<p ng-if="type" ng-bind="type"></p>
</div>
<!-- TODO: formatting /-->
<!-- TODO: add awesome relevant SVG based on code /-->
<!-- TODO: Add github issue link /-->

View File

@@ -124,16 +124,16 @@
</div>
</div>
<div id="build">
<h1 ng-bind="ship.name"></h1>
<input ng-model="buildName" placeholder="Build Name" />
<button ng-click="saveBuild()" ng-disabled="code == lastSaveCode">Save</button>
<button ng-click="reloadBuild()" ng-disabled="!lastSaveCode || code == lastSaveCode">Reload</button>
<button ui-sref="outfit({shipId: ship.id,code:null})" ng-disabled="!code">Clear</button>
<button ng-click="deleteBuild()" ng-disabled="!lastSaveCode">Delete</button>
</div>
<div id="summary">
<div id="overview" class="list">
<h1 ng-bind="ship.name"></h1>
<div id="build">
<input ng-model="buildName" placeholder="Enter Build Name" />
<button ng-click="saveBuild()" ng-disabled="code == savedCode">Save</button>
<button ng-click="reloadBuild()" ng-disabled="!savedCode || code == savedCode">Reload</button>
<button ui-sref="outfit({shipId: ship.id,code:null})" ng-disabled="!code">Clear</button>
</div>
</div>
<div class="list">
<div class="header">Maneuverability</div>
<div class="summary">

View File

@@ -25,7 +25,7 @@
"dependencies": {
"d3": "~3.5.5",
"ng-lodash": "~0.2.0",
"angular-ui-router": "0.2.14"
"angular-ui-router": "^0.2.14"
},
"overrides": {}
}