Merge branch '4.5.0' into develop

This commit is contained in:
Hyzual 2015-05-23 12:51:42 +02:00
commit ab1954b3ba
48 changed files with 2037 additions and 1020 deletions

View file

@ -1,17 +1,10 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 4
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

View file

@ -1,38 +1,32 @@
{
"esnext": true,
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"indent": 2,
"esnext": true,
"freeze": true,
"latedef": true,
"newcap": true,
"noarg": true,
"quotmark": "true",
"regexp": true,
"nonbsp": true,
"nonew": true,
"notypeof": true,
"shadow": false,
"strict": true,
"undef": true,
"unused": true,
"strict": true,
"trailing": true,
"smarttabs": true,
"globals": {
"_": false,
"affix": false,
"after": false,
"afterEach": false,
"angular": false,
"before": false,
"beforeEach": false,
"browser": false,
"describe": false,
"console": false,
"expect": false,
"inject": false,
"it": false,
"jasmine": false,
"spyOn": false
"installPromiseMatchers": false,
"module": false
},
"browser": true,
"jquery": true,
"node": true
"jasmine": true
}

View file

@ -39,7 +39,7 @@ module.exports = function (grunt) {
watch: {
bower: {
files: ['bower.json'],
tasks: ['wiredep']
tasks: ['wiredep', 'copy:svg']
},
js: {
files: ['<%= yeoman.app %>/**/*.js', '!<%= yeoman.app %>/**/*_test.js'],
@ -52,12 +52,17 @@ module.exports = function (grunt) {
files: ['<%= yeoman.app %>/**/*_test.js'],
tasks: ['karma:continuous:run']
},
svg: {
files: ['<%= yeoman.app %>/images/**/*.svg', '!<%= yeoman.app %>/images/sprite/**'],
tasks: ['svg_sprite:dist']
},
gruntfile: {
files: ['Gruntfile.js']
},
livereload: {
files: [
'<%= yeoman.app %>/**/*.html',
'<%= yeoman.app %>/**/*.css',
'<%= yeoman.app %>/styles/{,*/}*.css',
'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
]
@ -89,20 +94,6 @@ module.exports = function (grunt) {
}
}
},
test: {
options: {
port: 9001,
middleware: function (connect) {
return [
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
}
},
coverage: {
options: {
open: true,
@ -123,7 +114,7 @@ module.exports = function (grunt) {
// Test settings
karma: {
options: {
configFile: './karma.conf.js',
configFile: './karma.conf.js'
},
unit: {
singleRun: true,
@ -161,22 +152,6 @@ module.exports = function (grunt) {
}
},
// Make sure code styles are up to par and there are no obvious mistakes
jshint: {
options: {
jshintrc: '.jshintrc',
reporter: require('jshint-stylish'),
force: true //TODO: while I work on correcting those errors, don't block the build
},
all: {
src: [
'Gruntfile.js',
'<%= yeoman.app %>/**/*.js',
'!<%= yeoman.app %>/vendor/**/*.js'
]
}
},
// Empties folders to start fresh
clean: {
dist: {
@ -218,7 +193,7 @@ module.exports = function (grunt) {
// Performs rewrites based on filerev and the useminPrepare configuration
usemin: {
html: ['<%= yeoman.dist %>/{,*/}*.html'],
html: ['<%= yeoman.dist %>/**/*.html'],
css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
js: ['<%= yeoman.dist %>/scripts/*.js'],
options: {
@ -264,6 +239,22 @@ module.exports = function (grunt) {
}
},
svg_sprite: {
options: {
mode: {
symbol: {
dest: '',
sprite: 'jamstash-sprite.svg'
}
}
},
dist: {
cwd: '<%= yeoman.app %>/images',
src: ['**/*.svg', '!sprite/**/*.svg'],
dest: '<%= yeoman.app %>/images/sprite'
}
},
// Minify our CSS files but do not merge them, we still want to have two
cssmin: {
styles: {
@ -271,7 +262,7 @@ module.exports = function (grunt) {
expand: true,
cwd: '.tmp/styles',
src: ['*.css', '!*.min.css'],
dest: '<%= yeoman.dist %>/styles',
dest: '<%= yeoman.dist %>/styles'
}]
}
},
@ -309,6 +300,12 @@ module.exports = function (grunt) {
],
dest: '<%= yeoman.dist %>'
},
{
expand: true,
cwd: '<%= yeoman.app %>',
src: ['images/sprite/*.svg'],
dest: '<%= yeoman.dist %>'
},
{
expand: true,
cwd: '<%= yeoman.app %>',
@ -324,6 +321,13 @@ module.exports = function (grunt) {
'bower_components/fancybox/source/*.{png,gif}'
],
dest: '.tmp/styles'
}
]
},
svg: {
files: [{
src: ['bower_components/open-iconic/sprite/sprite.svg'],
dest: '<%= yeoman.app %>/images/sprite/iconic.svg'
}]
}
},
@ -365,7 +369,7 @@ module.exports = function (grunt) {
sftp: {
test: {
files: {
'./': ['<%= yeoman.dist %>/{,*/}*', '<%= yeoman.dist %>/.git*']
'./': ['<%= yeoman.dist %>/**/*', '<%= yeoman.dist %>/.git*']
},
options: {
path: '/var/www/jamstash',
@ -388,7 +392,7 @@ module.exports = function (grunt) {
}
});
grunt.registerTask('serve', 'Compile then start a connect web server', function(target) {
grunt.registerTask('serve', 'Compile then start a connect web server', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'connect:dist:keepalive']);
}
@ -401,14 +405,13 @@ module.exports = function (grunt) {
]);
});
grunt.registerTask('test', 'Run unit tests and jshint', function() {
grunt.registerTask('test', 'Run unit tests and jshint', function () {
return grunt.task.run([
'karma:unit',
'jshint'
'karma:unit'
]);
});
grunt.registerTask('coverage', 'Run unit tests and display test coverage results on browser', function() {
grunt.registerTask('coverage', 'Run unit tests and display test coverage results on browser', function () {
return grunt.task.run([
'clean:coverage',
'karma:unit',
@ -416,10 +419,11 @@ module.exports = function (grunt) {
]);
});
grunt.registerTask('build', 'Concatenate all JS files, minify all JS, CSS, HTML and image files and version all static assets', function() {
grunt.registerTask('build', 'Concatenate all JS files, minify all JS, CSS, HTML and image files and version all static assets', function () {
return grunt.task.run([
'clean:dist',
'wiredep:app',
'copy:svg',
'useminPrepare',
'concat:generated',
'copy:dist',
@ -433,7 +437,7 @@ module.exports = function (grunt) {
]);
});
grunt.registerTask('deploy', 'Build and deploy to test server', function() {
grunt.registerTask('deploy', 'Build and deploy to test server', function () {
return grunt.task.run([
'build',
'sshexec:cleanTest',

View file

@ -23,44 +23,45 @@ angular.module('jamstash.archive.service', ['jamstash.settings.service', 'jamsta
};
var offset = 0;
var mapAlbum = function (data) {
var song = data;
var coverartthumb, coverartfull, starred, title, album, publisher, avg_rating, downloads, identifier, source, date;
var url = globals.archiveUrl + 'details/' + song.identifier;
coverartthumb = 'images/albumdefault_50.jpg';
coverartfull = 'images/albumdefault_160.jpg';
if (parseInt(song.avg_rating) == 5) { starred = true; } else { starred = false; }
if (typeof song.title == 'undefined') { title = '&nbsp;'; } else { title = song.title.toString(); }
if (typeof song.identifier == 'undefined') { identifier = '&nbsp;'; } else { identifier = song.identifier.toString(); }
if (typeof song.collection[0] == 'undefined') { album = '&nbsp;'; } else { album = song.collection[0].toString(); }
if (typeof song.source == 'undefined') { source = '&nbsp;'; } else { source = song.source.toString(); }
if (typeof song.date == 'undefined') { date = '&nbsp;'; } else { date = song.date.toString(); }
if (typeof song.publisher == 'undefined') { publisher = '&nbsp;'; } else { publisher = song.publisher.toString(); }
if (typeof song.avg_rating == 'undefined') { avg_rating = '&nbsp;'; } else { avg_rating = song.avg_rating.toString(); }
if (typeof song.downloads == 'undefined') { downloads = '&nbsp;'; } else { downloads = song.downloads.toString(); }
var archiveService = {
mapAlbum: function (data) {
var song = data;
var coverartthumb, coverartfull, starred, title, album, publisher, avg_rating, downloads, identifier, source, date;
var url = globals.archiveUrl + 'details/' + song.identifier;
coverartthumb = 'images/albumdefault_50.jpg';
coverartfull = 'images/albumdefault_160.jpg';
if (parseInt(song.avg_rating) == 5) { starred = true; } else { starred = false; }
if (typeof song.title == 'undefined') { title = '&nbsp;'; } else { title = song.title.toString(); }
if (typeof song.identifier == 'undefined') { identifier = '&nbsp;'; } else { identifier = song.identifier.toString(); }
if (typeof song.collection[0] == 'undefined') { album = '&nbsp;'; } else { album = song.collection[0].toString(); }
if (typeof song.source == 'undefined') { source = '&nbsp;'; } else { source = song.source.toString(); }
if (typeof song.date == 'undefined') { date = '&nbsp;'; } else { date = song.date.toString(); }
if (typeof song.publisher == 'undefined') { publisher = '&nbsp;'; } else { publisher = song.publisher.toString(); }
if (typeof song.avg_rating == 'undefined') { avg_rating = '&nbsp;'; } else { avg_rating = song.avg_rating.toString(); }
if (typeof song.downloads == 'undefined') { downloads = '&nbsp;'; } else { downloads = song.downloads.toString(); }
//var description = '<b>Details</b><br />';
var description = '<b>Source</b>: ' + source + '<br />';
description += '<b>Date</b>: ' + date + '<br />';
description += '<b>Transferer</b>: ' + publisher + '<br />';
description += '<b>Rating</b>: ' + avg_rating + '<br />';
description += '<b>Downloads</b>: ' + downloads + '<br />';
return new model.Album(identifier, null, title, album, '', coverartthumb, coverartfull, $.format.date(new Date(song.publicdate), "yyyy-MM-dd h:mm a"), starred, $sce.trustAsHtml(description), url);
};
var mapSong = function (key, song, server, dir, identifier, coverart) {
var url, time, track, title, rating, starred, contenttype, suffix;
var specs = '';
if (song.format == 'VBR MP3') {
url = 'http://' + server + dir + key;
if (typeof song.bitrate == 'undefined' || typeof song.format == 'undefined') { specs = '&nbsp;'; } else { specs = song.bitrate + 'kbps, ' + song.format.toLowerCase(); }
if (typeof song.track == 'undefined') { track = '&nbsp;'; } else { track = song.track; }
if (typeof song.title == 'undefined') { title = '&nbsp;'; } else { title = song.title; }
if (typeof song.length == 'undefined') { time = '&nbsp;'; } else { time = utils.timeToSeconds(song.length); }
return new model.Song(song.md5, identifier, song.track, title, song.creator, '', song.album, '', coverart, coverart, time, '', '', 'mp3', specs, url, 0, '');
}
};
//var description = '<b>Details</b><br />';
var description = '<b>Source</b>: ' + source + '<br />';
description += '<b>Date</b>: ' + date + '<br />';
description += '<b>Transferer</b>: ' + publisher + '<br />';
description += '<b>Rating</b>: ' + avg_rating + '<br />';
description += '<b>Downloads</b>: ' + downloads + '<br />';
return new model.Album(identifier, null, title, album, '', coverartthumb, coverartfull, utils.formatDate(new Date(song.publicdate), "yyyy-MM-dd h:mm a"), starred, $sce.trustAsHtml(description), url);
},
mapSong: function (key, song, server, dir, identifier, coverart) {
var url, time, track, title, rating, starred, contenttype, suffix;
var specs = '';
if (song.format == 'VBR MP3') {
url = 'http://' + server + dir + key;
if (typeof song.bitrate == 'undefined' || typeof song.format == 'undefined') { specs = '&nbsp;'; } else { specs = song.bitrate + 'kbps, ' + song.format.toLowerCase(); }
if (typeof song.track == 'undefined') { track = '&nbsp;'; } else { track = song.track; }
if (typeof song.title == 'undefined') { title = '&nbsp;'; } else { title = song.title; }
if (typeof song.length == 'undefined') { time = '&nbsp;'; } else { time = utils.timeToSeconds(song.length); }
return new model.Song(song.md5, identifier, song.track, title, song.creator, '', song.album, '', coverart, coverart, time, '', '', 'mp3', specs, url, 0, '');
}
},
return {
getArtists: function (query) {
var deferred = $q.defer();
if (globals.settings.Debug) { console.log("LOAD ARCHIVE.ORG COLLECTIONS"); }
@ -138,7 +139,7 @@ angular.module('jamstash.archive.service', ['jamstash.settings.service', 'jamsta
content.album = [];
content.song = [];
angular.forEach(items, function (item, key) {
content.album.push(mapAlbum(item));
content.album.push(archiveService.mapAlbum(item));
});
notifications.updateMessage(content.album.length + ' Items Returned', true);
} else {
@ -178,7 +179,7 @@ angular.module('jamstash.archive.service', ['jamstash.settings.service', 'jamsta
var items = data.files;
if (action == 'add') {
angular.forEach(items, function (item, key) {
var song = mapSong(key, item, server, dir, identifier, coverart);
var song = archiveService.mapSong(key, item, server, dir, identifier, coverart);
if (song) {
player.queue.push(song);
}
@ -187,7 +188,7 @@ angular.module('jamstash.archive.service', ['jamstash.settings.service', 'jamsta
} else if (action == 'play') {
player.queue = [];
angular.forEach(items, function (item, key) {
var song = mapSong(key, item, server, dir, identifier, coverart);
var song = archiveService.mapSong(key, item, server, dir, identifier, coverart);
if (song) {
player.queue.push(song);
}
@ -199,7 +200,7 @@ angular.module('jamstash.archive.service', ['jamstash.settings.service', 'jamsta
content.album = [];
content.song = [];
angular.forEach(items, function (item, key) {
var song = mapSong(key, item, server, dir, identifier, coverart);
var song = archiveService.mapSong(key, item, server, dir, identifier, coverart);
if (song) {
content.song.push(song);
}
@ -214,4 +215,5 @@ angular.module('jamstash.archive.service', ['jamstash.settings.service', 'jamsta
return deferred.promise;
}
};
return archiveService;
}]);

View file

@ -1,4 +1,52 @@
describe("archive service", function() {
'use strict';
'use strict';
var archive, mockBackend, mockGlobals, utils,
response;
beforeEach(function() {
mockGlobals = {
archiveUrl: "http://hysterotomy.com/hippolytus/quercitrin?a=chillagite&b=savour#superfecundation"
};
module('jamstash.archive.service', function ($provide) {
$provide.value('globals', mockGlobals);
$provide.decorator('player', function () {
var playerService = jasmine.createSpyObj("player", ["play"]);
playerService.queue = [];
return playerService;
});
$provide.decorator('notifications', function () {
return jasmine.createSpyObj("notifications", ["updateMessage"]);
});
$provide.decorator('utils', function () {
return jasmine.createSpyObj("utils", ["formatDate"]);
});
});
inject(function (_archive_, $httpBackend, _utils_) {
archive = _archive_;
mockBackend = $httpBackend;
utils = _utils_;
});
});
afterEach(function() {
mockBackend.verifyNoOutstandingExpectation();
mockBackend.verifyNoOutstandingRequest();
});
describe("mapAlbum() -", function() {
it("Given album data with a publicDate defined, when I map it to an Album, then utils.formatDate will be called", function() {
var albumData = {
id: 504,
publicDate: "2015-03-29T18:22:06.000Z",
collection: ['Sternal Daubreelite']
};
archive.mapAlbum(albumData);
expect(utils.formatDate).toHaveBeenCalledWith(jasmine.any(Date), "yyyy-MM-dd h:mm a");
});
});
});

View file

@ -44,7 +44,7 @@
<a class="add" href="" title="Add To Play Queue" ng-click="getSongs(o.id, 'add')" stop-event="click"></a>
<a class="play" href="" title="Play" ng-click="getSongs(o.id, 'play')" stop-event="click"></a>
<a class="download" href="" title="Download"></a>
<a href="" title="Favorite" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="updateFavorite(o)" stop-event="click"></a>
<a href="" title="Favorite" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="toggleStar(o)" stop-event="click"></a>
</div>
<div class="albumart"><img ng-src="{{o.coverart}}" src="images/albumdefault_50.jpg"></div>
<div class="title">{{o.name}}</div>

View file

@ -166,6 +166,10 @@ angular.module('jamstash.archive.controller', ['jamstash.archive.service'])
$rootScope.removeSong(item, $scope.song);
};
$scope.toggleStar = function (item) {
//Do nothing: we aren't logged in archive.org, so we can't star anything there.
};
/* Launch on Startup */
//$scope.getArtists();
$scope.getAlbums();

View file

@ -1,6 +1,6 @@
angular.module('JamStash')
.controller('AppController', ['$scope', '$rootScope', '$document', '$window', '$location', '$cookieStore', '$http', 'utils', 'globals', 'model', 'notifications', 'player', 'persistence', 'Page',
function($scope, $rootScope, $document, $window, $location, $cookieStore, $http, utils, globals, model, notifications, player, persistence, Page) {
.controller('AppController', ['$scope', '$rootScope', '$document', '$window', '$location', '$cookieStore', '$http', 'utils', 'globals', 'model', 'notifications', 'player', 'persistence', 'Page', 'subsonic',
function ($scope, $rootScope, $document, $window, $location, $cookieStore, $http, utils, globals, model, notifications, player, persistence, Page, subsonic) {
'use strict';
$rootScope.settings = globals.settings;
@ -10,7 +10,6 @@ angular.module('JamStash')
$rootScope.Genres = [];
$rootScope.Messages = [];
$rootScope.SelectedMusicFolder = "";
$rootScope.unity = null;
$scope.Page = Page;
$rootScope.loggedIn = function () {
@ -380,27 +379,13 @@ angular.module('JamStash')
});
};
$scope.updateFavorite = function (item) {
var id = item.id;
var starred = item.starred;
var url;
if (starred) {
url = globals.BaseURL() + '/unstar.view?' + globals.BaseParams() + '&id=' + id;
item.starred = undefined;
} else {
url = globals.BaseURL() + '/star.view?' + globals.BaseParams() + '&id=' + id;
item.starred = true;
}
$.ajax({
url: url,
method: 'GET',
dataType: globals.settings.Protocol,
timeout: globals.settings.Timeout,
success: function () {
notifications.updateMessage('Favorite Updated!', true);
}
$scope.toggleStar = function (item) {
subsonic.toggleStar(item).then(function (newStarred) {
item.starred = newStarred;
notifications.updateMessage('Favorite Updated!', true);
});
};
$scope.toTrusted = function (html) {
return $sce.trustAsHtml(html);
};

View file

@ -1,13 +1,14 @@
describe("Main controller", function() {
'use strict';
var controllerParams, $controller, scope, mockGlobals, player, utils, persistence;
var controllerParams, $controller, $q, scope, mockGlobals, player, utils, persistence, subsonic, notifications,
deferred;
beforeEach(function() {
mockGlobals = {
settings: {
SaveTrackPosition: false,
ShowQueue: false,
Debug: true,
Debug: false,
Jukebox: false
}
};
@ -30,9 +31,21 @@ describe("Main controller", function() {
"saveSettings"
]);
inject(function (_$controller_, $rootScope, _$document_, _$window_, _$location_, _$cookieStore_, _utils_, globals, _model_, _notifications_, _Page_) {
// Mock the subsonic service
subsonic = jasmine.createSpyObj("subsonic", [
"toggleStar"
]);
// Mock the notifications service
notifications = jasmine.createSpyObj("notifications", [
"updateMessage"
]);
inject(function (_$controller_, $rootScope, _$q_, _$document_, _$window_, _$location_, _$cookieStore_, _utils_, globals, _model_, _Page_) {
scope = $rootScope.$new();
utils = _utils_;
$q = _$q_;
deferred = $q.defer();
spyOn(utils, "switchTheme");
@ -47,37 +60,15 @@ describe("Main controller", function() {
utils: utils,
globals: globals,
model: _model_,
notifications: _notifications_,
notifications: notifications,
player: player,
persistence: persistence,
Page: _Page_
Page: _Page_,
subsonic: subsonic
};
});
});
xdescribe("updateFavorite -", function() {
xit("when starring a song, it notifies the user that the star was saved", function() {
});
xit("when starring an album, it notifies the user that the star was saved", function() {
});
xit("when starring an artist, it notifies the user that the star was saved", function() {
});
xit("given that the Subsonic server returns an error, when starring something, it notifies the user with the error message", function() {
//TODO: move to higher level
});
xit("given that the Subsonic server is unreachable, when starring something, it notifies the user with the HTTP error code", function() {
//TODO: move to higher level
});
});
xdescribe("toggleSetting -", function() {
});
@ -191,6 +182,34 @@ describe("Main controller", function() {
});
});
describe("toggleStar() -", function() {
beforeEach(function() {
subsonic.toggleStar.and.returnValue(deferred.promise);
});
it("Given an artist that was not starred, when I toggle its star, then subsonic service will be called, the artist will be starred and a notification will be displayed", function() {
var artist = { id: 4218, starred: false };
scope.toggleStar(artist);
deferred.resolve(true);
scope.$apply();
expect(subsonic.toggleStar).toHaveBeenCalledWith(artist);
expect(artist.starred).toBeTruthy();
expect(notifications.updateMessage).toHaveBeenCalledWith('Favorite Updated!', true);
});
it("Given a song that was starred, when I toggle its star, then subsonic service will be called, the song will be starred and a notification will be displayed", function() {
var song = { id: 784, starred: true };
scope.toggleStar(song);
deferred.resolve(false);
scope.$apply();
expect(subsonic.toggleStar).toHaveBeenCalledWith(song);
expect(song.starred).toBeFalsy();
expect(notifications.updateMessage).toHaveBeenCalledWith('Favorite Updated!', true);
});
});
});
describe("When starting up,", function() {

View file

@ -58,7 +58,7 @@ describe("model service", function() {
expect(result).toEqual(episodes);
});
it("Given album data without artist info, when I map it to an Album, an Album with an empty artist name will be returned", function() {
it("Given album data without artist info, when I map it to an Album, then an Album with an empty artist name will be returned", function() {
var albumData = {
id: 584,
artist: undefined,

View file

@ -77,6 +77,19 @@ angular.module('jamstash.persistence', ['angular-locker',
locker.forget('Volume');
};
/* Manage selected music folder */
this.getSelectedMusicFolder = function () {
return locker.get('MusicFolders');
};
this.saveSelectedMusicFolder = function (selectedMusicFolder) {
locker.put('MusicFolders', selectedMusicFolder);
};
this.deleteSelectedMusicFolder = function () {
locker.forget('MusicFolders');
};
/* Manage user settings */
this.getSettings = function () {
// If the latest version from changelog.json is newer than the version stored in local storage,

View file

@ -154,6 +154,48 @@ describe("Persistence service", function() {
expect(locker.forget).toHaveBeenCalledWith('Volume');
});
describe("getSelectedMusicFolder() -", function() {
it("Given a previously saved selected music folder in local storage, when I get the saved music folder, then an object containing the id and name of the selected music folder will be returned", function() {
fakeStorage = {
'MusicFolders': {
'id': 74,
'name': 'kooliman unhurled'
}
};
var selectedMusicFolder = persistence.getSelectedMusicFolder();
expect(locker.get).toHaveBeenCalledWith('MusicFolders');
expect(selectedMusicFolder).toEqual({
'id': 74,
'name': 'kooliman unhurled'
});
});
it("Given that no selected music folder was previously saved in local storage, when I get the saved music folder, then undefined will be returned", function() {
var selectedMusicFolder = persistence.getSelectedMusicFolder();
expect(locker.get).toHaveBeenCalledWith('MusicFolders');
expect(selectedMusicFolder).toBeUndefined();
});
});
it("saveSelectedMusicFolder() - given an object containing the id and name of the selected music folder, when I save the music folder, then it will be set in local storage", function() {
persistence.saveSelectedMusicFolder({
id: 41,
name: 'parlormaid carcinolytic'
});
expect(locker.put).toHaveBeenCalledWith('MusicFolders', {
id: 41,
name: 'parlormaid carcinolytic'
});
});
it("deleteSelectedMusicFolder() - when I delete the selected music folder, then it will be erased from local storage", function() {
persistence.deleteSelectedMusicFolder();
expect(locker.forget).toHaveBeenCalledWith('MusicFolders');
});
describe("getSettings() -", function() {
beforeEach(function() {
spyOn(persistence, 'upgradeVersion');

View file

@ -2,9 +2,9 @@
<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="Start Playing From This Song" ng-click="playFrom($index)" 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="Favorite" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="updateFavorite(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>
<div class="clear"></div>
</div>
<div class="track floatleft" ng-bind-html="o.track"></div>

View file

@ -1,7 +1,7 @@
<li class="row song" ng-repeat="o in song" ng-click="selectSong(o)" ng-dblclick="playSong(o)" ng-class="{'selected': o.selected, 'playing': o.playing}">
<div class="itemactions">
<a class="remove" href="" title="Remove Song" ng-click="removeSongFromQueue(o)" stop-event="click"></a>
<a href="" title="Favorite" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="updateFavorite(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>
<div class="clear"></div>
</div>
<div class="title floatleft" title="{{o.description}}" ng-bind-html="o.name"></div>

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,673 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<symbol id="account-login" viewBox="0 0 8 8">
<path d="M3 0v1h4v5h-4v1h5v-7h-5zm1 2v1h-4v1h4v1l2-1.5-2-1.5z"></path>
</symbol>
<symbol id="account-logout" viewBox="0 0 8 8">
<path d="M3 0v1h4v5h-4v1h5v-7h-5zm-1 2l-2 1.5 2 1.5v-1h4v-1h-4v-1z"></path>
</symbol>
<symbol id="action-redo" viewBox="0 0 8 8">
<path d="M3.5 0c-1.93 0-3.5 1.57-3.5 3.5 0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v.5h-1l2 2 2-2h-1v-.5c0-1.93-1.57-3.5-3.5-3.5z" transform="translate(0 1)"></path>
</symbol>
<symbol id="action-undo" viewBox="0 0 8 8">
<path d="M4.5 0c-1.93 0-3.5 1.57-3.5 3.5v.5h-1l2 2 2-2h-1v-.5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5c0-1.93-1.57-3.5-3.5-3.5z" transform="translate(0 1)"></path>
</symbol>
<symbol id="align-center" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm1 2v1h6v-1h-6zm-1 2v1h8v-1h-8zm1 2v1h6v-1h-6z"></path>
</symbol>
<symbol id="align-left" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm0 2v1h6v-1h-6zm0 2v1h8v-1h-8zm0 2v1h6v-1h-6z"></path>
</symbol>
<symbol id="align-right" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm2 2v1h6v-1h-6zm-2 2v1h8v-1h-8zm2 2v1h6v-1h-6z"></path>
</symbol>
<symbol id="aperture" viewBox="0 0 8 8">
<path d="M4 0c-.69 0-1.336.19-1.906.5l3.219 2.344.719-2.25c-.59-.36-1.281-.594-2.031-.594zm-2.75 1.125c-.76.73-1.25 1.735-1.25 2.875 0 .25.022.489.063.719l3.094-2.219-1.906-1.375zm5.625.125l-1.219 3.75h2.219c.08-.32.125-.65.125-1 0-1.07-.435-2.03-1.125-2.75zm-4.719 3.188l-1.75 1.281c.55 1.13 1.595 1.989 2.875 2.219l-1.125-3.5zm1.563 1.563l.625 1.969c1.33-.11 2.454-.879 3.094-1.969h-3.719z"></path>
</symbol>
<symbol id="arrow-bottom" viewBox="0 0 8 8">
<path d="M2 0v5h-2l2.531 3 2.469-3h-2v-5h-1z" transform="translate(1)"></path>
</symbol>
<symbol id="arrow-circle-bottom" viewBox="0 0 8 8">
<path d="M4 0c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-1 1h2v3h2l-3 3-3-3h2v-3z"></path>
</symbol>
<symbol id="arrow-circle-left" viewBox="0 0 8 8">
<path d="M4 0c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 1v2h3v2h-3v2l-3-3 3-3z"></path>
</symbol>
<symbol id="arrow-circle-right" viewBox="0 0 8 8">
<path d="M4 0c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 1l3 3-3 3v-2h-3v-2h3v-2z"></path>
</symbol>
<symbol id="arrow-circle-top" viewBox="0 0 8 8">
<path d="M4 0c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 1l3 3h-2v3h-2v-3h-2l3-3z"></path>
</symbol>
<symbol id="arrow-left" viewBox="0 0 8 8">
<path d="M3 0l-3 2.531 3 2.469v-2h5v-1h-5v-2z" transform="translate(0 1)"></path>
</symbol>
<symbol id="arrow-right" viewBox="0 0 8 8">
<path d="M5 0v2h-5v1h5v2l3-2.531-3-2.469z" transform="translate(0 1)"></path>
</symbol>
<symbol id="arrow-thick-bottom" viewBox="0 0 8 8">
<path d="M2 0v5h-2l3.031 3 2.969-3h-2v-5h-2z" transform="translate(1)"></path>
</symbol>
<symbol id="arrow-thick-left" viewBox="0 0 8 8">
<path d="M3 0l-3 3.031 3 2.969v-2h5v-2h-5v-2z" transform="translate(0 1)"></path>
</symbol>
<symbol id="arrow-thick-right" viewBox="0 0 8 8">
<path d="M5 0v2h-5v2h5v2l3-3.031-3-2.969z" transform="translate(0 1)"></path>
</symbol>
<symbol id="arrow-thick-top" viewBox="0 0 8 8">
<path d="M2.969 0l-2.969 3h2v5h2v-5h2l-3.031-3z" transform="translate(1)"></path>
</symbol>
<symbol id="arrow-top" viewBox="0 0 8 8">
<path d="M2.469 0l-2.469 3h2v5h1v-5h2l-2.531-3z" transform="translate(1)"></path>
</symbol>
<symbol id="audio-spectrum" viewBox="0 0 8 8">
<path d="M4 0v8h1v-8h-1zm-2 1v6h1v-6h-1zm4 1v4h1v-4h-1zm-6 1v2h1v-2h-1z"></path>
</symbol>
<symbol id="audio" viewBox="0 0 8 8">
<path d="M1.188 0c-.734.722-1.188 1.748-1.188 2.844 0 1.095.454 2.09 1.188 2.813l.688-.719c-.546-.538-.875-1.269-.875-2.094s.329-1.587.875-2.125l-.688-.719zm5.625 0l-.688.719c.552.552.875 1.289.875 2.125 0 .836-.327 1.554-.875 2.094l.688.719c.732-.72 1.188-1.708 1.188-2.813 0-1.104-.459-2.115-1.188-2.844zm-4.219 1.406c-.362.362-.594.889-.594 1.438 0 .548.232 1.045.594 1.406l.688-.719c-.178-.178-.281-.416-.281-.688 0-.272.103-.54.281-.719l-.688-.719zm2.813 0l-.688.719c.183.183.281.434.281.719s-.099.505-.281.688l.688.719c.357-.357.594-.851.594-1.406 0-.555-.236-1.08-.594-1.438z" transform="translate(0 1)"></path>
</symbol>
<symbol id="badge" viewBox="0 0 8 8">
<path d="M2 0c-1.105 0-2 .895-2 2s.895 2 2 2 2-.895 2-2-.895-2-2-2zm-1 4.813v3.188l1-1 1 1v-3.188c-.31.11-.65.188-1 .188s-.69-.077-1-.188z" transform="translate(2)"></path>
</symbol>
<symbol id="ban" viewBox="0 0 8 8">
<path d="M4 0c-2.203 0-4 1.797-4 4 0 2.203 1.797 4 4 4 2.203 0 4-1.797 4-4 0-2.203-1.797-4-4-4zm0 1c.655 0 1.258.209 1.75.563l-4.188 4.188c-.353-.492-.563-1.095-.563-1.75 0-1.663 1.337-3 3-3zm2.438 1.25c.353.492.563 1.095.563 1.75 0 1.663-1.337 3-3 3-.655 0-1.258-.209-1.75-.563l4.188-4.188z"></path>
</symbol>
<symbol id="bar-chart" viewBox="0 0 8 8">
<path d="M0 0v7h8v-1h-7v-6h-1zm5 0v5h2v-5h-2zm-3 2v3h2v-3h-2z"></path>
</symbol>
<symbol id="basket" viewBox="0 0 8 8">
<path d="M3.969 0c-.127.011-.259.083-.344.188l-2.344 2.813h-1.281v1h1v3.656c0 .18.164.344.344.344h5.313c.18 0 .344-.164.344-.344v-3.656h1v-1h-1.281c-.274-.329-2.387-2.866-2.406-2.875-.105-.09-.216-.136-.344-.125zm.031 1.281l1.438 1.719h-2.875l1.438-1.719zm-1.5 3.719c.28 0 .5.22.5.5v1c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5zm3 0c.28 0 .5.22.5.5v1c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5z"></path>
</symbol>
<symbol id="battery-empty" viewBox="0 0 8 8">
<path d="M.094 0c-.06 0-.094.034-.094.094v5.813c0 .06.034.094.094.094h6.813c.06 0 .094-.034.094-.094v-1.906h1v-2h-1v-1.906c0-.06-.034-.094-.094-.094h-6.813zm.906 1h5v4h-5v-4z" transform="translate(0 1)"></path>
</symbol>
<symbol id="battery-full" viewBox="0 0 8 8">
<path d="M.094 0c-.06 0-.094.034-.094.094v5.813c0 .06.034.094.094.094h6.813c.06 0 .094-.034.094-.094v-1.906h1v-2h-1v-1.906c0-.06-.034-.094-.094-.094h-6.813z" transform="translate(0 1)"></path>
</symbol>
<symbol id="beaker" viewBox="0 0 8 8">
<path d="M1.344 0a.502.502 0 0 0 .156 1h.5v1.406c-.088.172-1.194 2.313-1.656 3.094-.153.268-.344.612-.344 1.063 0 .383.139.764.406 1.031.26.26.643.406 1.031.406h5.125c.383 0 .764-.139 1.031-.406.26-.26.406-.643.406-1.031 0-.452-.194-.801-.344-1.063-.463-.78-1.568-2.922-1.656-3.094v-1.406h.5a.5.5 0 1 0 0-1h-5a.5.5 0 0 0-.094 0 .502.502 0 0 0-.063 0zm1.656 1h2v1.625l.063.094s.652 1.233 1.219 2.281h-4.563c.567-1.049 1.219-2.281 1.219-2.281l.063-.094v-1.625z"></path>
</symbol>
<symbol id="bell" viewBox="0 0 8 8">
<path d="M4 0c-1.1 0-2 .9-2 2 0 1.04-.524 1.976-1.344 2.656-.42.34-.656.824-.656 1.344h8c0-.52-.236-1.004-.656-1.344-.82-.68-1.344-1.616-1.344-2.656 0-1.1-.9-2-2-2zm-1 7c0 .55.45 1 1 1s1-.45 1-1h-2z"></path>
</symbol>
<symbol id="bluetooth" viewBox="0 0 8 8">
<path d="M1.5 0v2.5l-.75-.75-.75.75 1.5 1.5-1.5 1.5.75.75.75-.75v2.5h.5l3.5-2.5-2.25-1.531 2.25-1.469-3.5-2.5h-.5zm1 1.5l1.5 1-1.5 1v-2zm0 3l1.5 1-1.5 1v-2z" transform="translate(1)"></path>
</symbol>
<symbol id="bold" viewBox="0 0 8 8">
<path d="M0 0v1c.55 0 1 .45 1 1v4c0 .55-.45 1-1 1v1h5.5c1.38 0 2.5-1.12 2.5-2.5 0-1-.588-1.85-1.438-2.25.27-.34.438-.78.438-1.25 0-1.1-.9-2-2-2h-5zm3 1h1c.55 0 1 .45 1 1s-.45 1-1 1h-1v-2zm0 3h1.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-1.5v-3z"></path>
</symbol>
<symbol id="bolt" viewBox="0 0 8 8">
<path d="M3 0l-3 5h2v3l3-5h-2v-3z" transform="translate(1)"></path>
</symbol>
<symbol id="book" viewBox="0 0 8 8">
<path d="M1 0c-.07 0-.127.001-.188.031-.39.08-.701.391-.781.781-.03.06-.031.118-.031.188v5.5c0 .83.67 1.5 1.5 1.5h5.5v-1h-5.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5h5.5v-5.5c0-.28-.22-.5-.5-.5h-.5v3l-1-1-1 1v-3h-3z"></path>
</symbol>
<symbol id="bookmark" viewBox="0 0 8 8">
<path d="M0 0v8l2-2 2 2v-8h-4z" transform="translate(2)"></path>
</symbol>
<symbol id="box" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm0 2v5.906c0 .06.034.094.094.094h7.813c.06 0 .094-.034.094-.094v-5.906h-2.969v1.031h-2.031v-1.031h-3z"></path>
</symbol>
<symbol id="briefcase" viewBox="0 0 8 8">
<path d="M3 0c-.554 0-1 .458-1 1v1h-1.906c-.06 0-.094.034-.094.094v2.406c0 .28.22.5.5.5h7c.28 0 .5-.22.5-.5v-2.406c0-.06-.034-.094-.094-.094h-1.906v-1c0-.542-.446-1-1-1h-2zm0 1h2v1h-2v-1zm-3 4.906v2c0 .06.034.094.094.094h7.813c.06 0 .094-.034.094-.094v-2c-.16.05-.32.094-.5.094h-7c-.18 0-.34-.044-.5-.094z"></path>
</symbol>
<symbol id="british-pound" viewBox="0 0 8 8">
<path d="M3 0c-.619 0-1.159.262-1.5.688-.341.426-.5.986-.5 1.563 0 .692.165 1.245.25 1.75h-1.25v1h1.219c-.112.448-.37.964-1.063 1.656l-.156.125v1.2189999999999999h6v-1h-4.906c.641-.729.982-1.397 1.125-2h1.781v-1h-1.719c-.078-.683-.281-1.242-.281-1.75 0-.394.115-.731.281-.938.166-.207.368-.313.719-.313.394 0 .609.109.75.25.141.141.25.356.25.75h1c0-.576-.165-1.102-.531-1.469-.366-.366-.893-.531-1.469-.531z" transform="translate(1)"></path>
</symbol>
<symbol id="browser" viewBox="0 0 8 8">
<path d="M.344 0a.5.5 0 0 0-.344.5v7a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.5-.5h-7a.5.5 0 0 0-.094 0 .5.5 0 0 0-.063 0zm1.156 1c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5.22-.5.5-.5zm2 0h3c.28 0 .5.22.5.5s-.22.5-.5.5h-3c-.28 0-.5-.22-.5-.5s.22-.5.5-.5zm-2.5 2h6v4h-6v-4z"></path>
</symbol>
<symbol id="brush" viewBox="0 0 8 8">
<path d="M7.438.031c-.029-.001-.037.016-.063.031l-3.75 2.656c-.04.03-.095.106-.125.156l-.125.25c.719.229 1.271.781 1.5 1.5l.25-.125c.05-.02.126-.075.156-.125l2.656-3.75c.03-.04.04-.116 0-.156l-.406-.406c-.02-.02-.065-.03-.094-.031zm-4.781 3.969c-.73 0-1.313.614-1.313 1.344 0 .99-.544 1.821-1.344 2.281.4.23.864.375 1.344.375 1.48 0 2.656-1.176 2.656-2.656 0-.73-.604-1.344-1.344-1.344z"></path>
</symbol>
<symbol id="bug" viewBox="0 0 8 8">
<path d="M3.5 0c-1.19 0-1.978 1.69-1.188 2.5-.09.07-.196.137-.281.219l-1.313-.656a.5.5 0 0 0-.344-.063.5.5 0 0 0-.094.938l1.156.563c-.09.156-.186.328-.25.5h-.688a.5.5 0 0 0-.094 0 .502.502 0 1 0 .094 1h.5c0 .227.023.445.063.656l-.781.406a.5.5 0 1 0 .438.875l.656-.344c.245.46.59.844 1 1.094.35-.19.625-.439.625-.719v-1.438a.5.5 0 0 0 0-.094v-.813a.5.5 0 0 0 0-.219c.045-.231.254-.406.5-.406.28 0 .5.22.5.5v.875a.5.5 0 0 0 0 .094v.063a.5.5 0 0 0 0 .094v1.344c0 .27.275.497.625.688.41-.245.755-.604 1-1.063l.656.344a.5.5 0 1 0 .438-.875l-.781-.406c.04-.211.063-.429.063-.656h.5a.5.5 0 1 0 0-1h-.688c-.064-.172-.16-.344-.25-.5l1.156-.563a.5.5 0 0 0-.313-.938.5.5 0 0 0-.125.063l-1.313.656c-.086-.082-.191-.149-.281-.219.78-.83.003-2.5-1.188-2.5z"></path>
</symbol>
<symbol id="bullhorn" viewBox="0 0 8 8">
<path d="M6.094 0c-.03 0-.06.022-.094.031v5.969c.033.007.065 0 .094 0h.813c.06 0 .094-.034.094-.094v-5.813c0-.06-.034-.094-.094-.094h-.813zm-1.094.5l-2.906 1.469c-.05.02-.127.031-.188.031h-1.813c-.06 0-.094.034-.094.094v1.813c0 .06.034.094.094.094h.906l1.031 2.719c.11.25.406.36.656.25.25-.11.36-.406.25-.656l-.719-1.781c.033-.136.136-.25.281-.25v-.031l2.5 1.25v-5z"></path>
</symbol>
<symbol id="calculator" viewBox="0 0 8 8">
<path d="M.094 0c-.06 0-.094.034-.094.094v7.813c0 .06.034.094.094.094h6.813c.06 0 .094-.034.094-.094v-7.813c0-.06-.034-.094-.094-.094h-6.813zm.906 1h5v2h-5v-2zm0 3h1v1h-1v-1zm2 0h1v1h-1v-1zm2 0h1v3h-1v-3zm-4 2h1v1h-1v-1zm2 0h1v1h-1v-1z"></path>
</symbol>
<symbol id="calendar" viewBox="0 0 8 8">
<path d="M0 0v2h7v-2h-7zm0 3v4.906c0 .06.034.094.094.094h6.813c.06 0 .094-.034.094-.094v-4.906h-7zm1 1h1v1h-1v-1zm2 0h1v1h-1v-1zm2 0h1v1h-1v-1zm-4 2h1v1h-1v-1zm2 0h1v1h-1v-1z"></path>
</symbol>
<symbol id="camera-slr" viewBox="0 0 8 8">
<path d="M4.094 0c-.06 0-.105.044-.125.094l-.938 1.813c-.02.05-.065.094-.125.094h-1.406c-.83 0-1.5.67-1.5 1.5v4.406c0 .06.034.094.094.094h7.813c.06 0 .094-.034.094-.094v-5.813c0-.06-.034-.094-.094-.094h-.813c-.06 0-.105-.044-.125-.094l-.938-1.813c-.02-.05-.065-.094-.125-.094h-1.813zm-2.594 3c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5.22-.5.5-.5zm3.5 0c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm0 1c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z"></path>
</symbol>
<symbol id="caret-bottom" viewBox="0 0 8 8">
<path d="M0 0l4 4 4-4h-8z" transform="translate(0 2)"></path>
</symbol>
<symbol id="caret-left" viewBox="0 0 8 8">
<path d="M4 0l-4 4 4 4v-8z" transform="translate(2)"></path>
</symbol>
<symbol id="caret-right" viewBox="0 0 8 8">
<path d="M0 0v8l4-4-4-4z" transform="translate(2)"></path>
</symbol>
<symbol id="caret-top" viewBox="0 0 8 8">
<path d="M4 0l-4 4h8l-4-4z" transform="translate(0 2)"></path>
</symbol>
<symbol id="cart" viewBox="0 0 8 8">
<path d="M.344 0a.502.502 0 0 0 .156 1h1.5l.094.25.406 1.25.406 1.25c.04.13.204.25.344.25h3.5c.14 0 .304-.12.344-.25l.813-2.531c.04-.12-.016-.219-.156-.219h-4.438l-.375-.719a.5.5 0 0 0-.438-.281h-2a.5.5 0 0 0-.094 0 .502.502 0 0 0-.063 0zm3.156 5c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm3 0c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5z" transform="translate(0 1)"></path>
</symbol>
<symbol id="chat" viewBox="0 0 8 8">
<path d="M0 0v5l1-1h1v-3h3v-1h-5zm3 2v4h4l1 1v-5h-5z"></path>
</symbol>
<symbol id="check" viewBox="0 0 8 8">
<path d="M6.406 0l-.719.688-2.781 2.781-.781-.781-.719-.688-1.406 1.406.688.719 1.5 1.5.719.688.719-.688 3.5-3.5.688-.719-1.406-1.406z" transform="translate(0 1)"></path>
</symbol>
<symbol id="chevron-bottom" viewBox="0 0 8 8">
<path d="M1.5 0l-1.5 1.5 4 4 4-4-1.5-1.5-2.5 2.5-2.5-2.5z" transform="translate(0 1)"></path>
</symbol>
<symbol id="chevron-left" viewBox="0 0 8 8">
<path d="M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z" transform="translate(1)"></path>
</symbol>
<symbol id="chevron-right" viewBox="0 0 8 8">
<path d="M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z" transform="translate(1)"></path>
</symbol>
<symbol id="chevron-top" viewBox="0 0 8 8">
<path d="M4 0l-4 4 1.5 1.5 2.5-2.5 2.5 2.5 1.5-1.5-4-4z" transform="translate(0 1)"></path>
</symbol>
<symbol id="circle-check" viewBox="0 0 8 8">
<path d="M4 0c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm2 1.781l.719.719-3.219 3.219-1.719-1.719.719-.719 1 1 2.5-2.5z"></path>
</symbol>
<symbol id="circle-x" viewBox="0 0 8 8">
<path d="M4 0c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-1.5 1.781l1.5 1.5 1.5-1.5.719.719-1.5 1.5 1.5 1.5-.719.719-1.5-1.5-1.5 1.5-.719-.719 1.5-1.5-1.5-1.5.719-.719z"></path>
</symbol>
<symbol id="clipboard" viewBox="0 0 8 8">
<path d="M3.5 0c-.28 0-.5.22-.5.5v.5h-.75c-.14 0-.25.11-.25.25v.75h3v-.75c0-.14-.11-.25-.25-.25h-.75v-.5c0-.28-.22-.5-.5-.5zm-3.25 1c-.14 0-.25.11-.25.25v6.5c0 .14.11.25.25.25h6.5c.14 0 .25-.11.25-.25v-6.5c0-.14-.11-.25-.25-.25h-.75v2h-5v-2h-.75z"></path>
</symbol>
<symbol id="clock" viewBox="0 0 8 8">
<path d="M4 0c-2.203 0-4 1.797-4 4 0 2.203 1.797 4 4 4 2.203 0 4-1.797 4-4 0-2.203-1.797-4-4-4zm0 1c1.663 0 3 1.337 3 3s-1.337 3-3 3-3-1.337-3-3 1.337-3 3-3zm-.5 1v2.219l.156.125.5.5.344.375.719-.719-.375-.344-.344-.344v-1.813h-1z"></path>
</symbol>
<symbol id="cloud-download" viewBox="0 0 8 8">
<path d="M4.5 0c-1.21 0-2.27.86-2.5 2-1.1 0-2 .9-2 2 0 .37.111.7.281 1h2.719v-.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v.5h1.906c.05-.16.094-.32.094-.5 0-.65-.42-1.29-1-1.5v-.5c0-1.38-1.12-2.5-2.5-2.5zm-.156 4a.5.5 0 0 0-.344.5v1.5h-1.5l2 2 2-2h-1.5v-1.5a.5.5 0 0 0-.594-.5.5.5 0 0 0-.063 0z"></path>
</symbol>
<symbol id="cloud-upload" viewBox="0 0 8 8">
<path d="M4.5 0c-1.21 0-2.27.86-2.5 2-1.1 0-2 .9-2 2 0 .37.111.7.281 1h2.219l2-2 2 2h1.406c.05-.16.094-.32.094-.5 0-.65-.42-1.29-1-1.5v-.5c0-1.38-1.12-2.5-2.5-2.5zm0 4.5l-2.5 2.5h2v.5a.5.5 0 1 0 1 0v-.5h2l-2.5-2.5z"></path>
</symbol>
<symbol id="cloud" viewBox="0 0 8 8">
<path d="M4.5 0c-1.21 0-2.27.86-2.5 2-1.1 0-2 .9-2 2s.9 2 2 2h4.5c.83 0 1.5-.67 1.5-1.5 0-.65-.42-1.29-1-1.5v-.5c0-1.38-1.12-2.5-2.5-2.5z" transform="translate(0 1)"></path>
</symbol>
<symbol id="cloudy" viewBox="0 0 8 8">
<path d="M2.5 0c-1.38 0-2.5 1.12-2.5 2.5 0 .39.09.743.25 1.063.3-.21.63-.379 1-.469.55-1.25 1.82-2.084 3.25-2.094-.46-.6-1.18-1-2-1zm2 2c-1.21 0-2.27.86-2.5 2-1.1 0-2 .9-2 2s.9 2 2 2h4.5c.83 0 1.5-.67 1.5-1.5 0-.65-.42-1.29-1-1.5v-.5c0-1.38-1.12-2.5-2.5-2.5z"></path>
</symbol>
<symbol id="code" viewBox="0 0 8 8">
<path d="M5 0l-3 6h1l3-6h-1zm-4 1l-1 2 1 2h1l-1-2 1-2h-1zm5 0l1 2-1 2h1l1-2-1-2h-1z" transform="translate(0 1)"></path>
</symbol>
<symbol id="cog" viewBox="0 0 8 8">
<path d="M3.5 0l-.5 1.188c-.1.04-.191.085-.281.125l-1.188-.5-.719.719.5 1.188c-.05.1-.095.181-.125.281l-1.188.5v1l1.188.5c.03.1.075.213.125.313l-.5 1.156.719.719 1.188-.5c.1.05.181.085.281.125l.5 1.188h1l.5-1.188c.1-.03.191-.085.281-.125l1.188.5.719-.719-.5-1.188c.04-.09.085-.181.125-.281l1.188-.5v-1l-1.188-.5c-.03-.09-.075-.191-.125-.281l.469-1.188-.688-.719-1.188.5c-.09-.04-.181-.095-.281-.125l-.5-1.188h-1zm.5 2.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5-1.5-.67-1.5-1.5.67-1.5 1.5-1.5z"></path>
</symbol>
<symbol id="collapse-down" viewBox="0 0 8 8">
<path d="M0 0v2h8v-2h-8zm2 3l2 2 2-2h-4zm-2 4v1h8v-1h-8z"></path>
</symbol>
<symbol id="collapse-left" viewBox="0 0 8 8">
<path d="M0 0v8h1v-8h-1zm6 0v8h2v-8h-2zm-1 2l-2 2 2 2v-4z"></path>
</symbol>
<symbol id="collapse-right" viewBox="0 0 8 8">
<path d="M0 0v8h2v-8h-2zm7 0v8h1v-8h-1zm-4 2v4l2-2-2-2z"></path>
</symbol>
<symbol id="collapse-up" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm4 3l-2 2h4l-2-2zm-4 3v2h8v-2h-8z"></path>
</symbol>
<symbol id="command" viewBox="0 0 8 8">
<path d="M1.5 0c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5h.5v1h-.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5v-.5h1v.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5h-.5v-1h.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5-1.5.67-1.5 1.5v.5h-1v-.5c0-.83-.67-1.5-1.5-1.5zm0 1c.28 0 .5.22.5.5v.5h-.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5zm4 0c.28 0 .5.22.5.5s-.22.5-.5.5h-.5v-.5c0-.28.22-.5.5-.5zm-2.5 2h1v1h-1v-1zm-1.5 2h.5v.5c0 .28-.22.5-.5.5s-.5-.22-.5-.5.22-.5.5-.5zm3.5 0h.5c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5v-.5z"></path>
</symbol>
<symbol id="comment-square" viewBox="0 0 8 8">
<path d="M.094 0c-.06 0-.094.034-.094.094v5.813c0 .06.034.094.094.094h5.906l2 2v-7.906000000000001c0-.06-.034-.094-.094-.094h-7.813z"></path>
</symbol>
<symbol id="compass" viewBox="0 0 8 8">
<path d="M4 0c-2.203 0-4 1.797-4 4 0 2.203 1.797 4 4 4 2.203 0 4-1.797 4-4 0-2.203-1.797-4-4-4zm0 1c1.663 0 3 1.337 3 3s-1.337 3-3 3-3-1.337-3-3 1.337-3 3-3zm2 1l-3 1-1 3 3-1 1-3zm-2 1.5c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5.22-.5.5-.5z"></path>
</symbol>
<symbol id="contrast" viewBox="0 0 8 8">
<path d="M4 0c-2.203 0-4 1.797-4 4 0 2.203 1.797 4 4 4 2.203 0 4-1.797 4-4 0-2.203-1.797-4-4-4zm0 1c1.663 0 3 1.337 3 3s-1.337 3-3 3v-6z"></path>
</symbol>
<symbol id="copywriting" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm0 2v1h5v-1h-5zm0 3v1h8v-1h-8zm0 2v1h6v-1h-6zm7.5 0c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5z"></path>
</symbol>
<symbol id="credit-card" viewBox="0 0 8 8">
<path d="M.25 0c-.14 0-.25.11-.25.25v.75h8v-.75c0-.14-.11-.25-.25-.25h-7.5zm-.25 2v3.75c0 .14.11.25.25.25h7.5c.14 0 .25-.11.25-.25v-3.75h-8zm1 2h1v1h-1v-1zm2 0h1v1h-1v-1z" transform="translate(0 1)"></path>
</symbol>
<symbol id="crop" viewBox="0 0 8 8">
<path d="M1 0v1h-1v1h1v5h5v1h1v-1h1v-1h-1v-4.5l1-1-.5-.5-1 1h-4.5v-1h-1zm1 2h3.5l-3.5 3.5v-3.5zm4 .5v3.5h-3.5l3.5-3.5z"></path>
</symbol>
<symbol id="dashboard" viewBox="0 0 8 8">
<path d="M4 0c-2.203 0-4 1.797-4 4 0 2.203 1.797 4 4 4 2.203 0 4-1.797 4-4 0-2.203-1.797-4-4-4zm0 1c1.663 0 3 1.337 3 3s-1.337 3-3 3-3-1.337-3-3 1.337-3 3-3zm0 1c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm-1.656 1a.5.5 0 0 0-.188.844l.906.906c-.023.085-.063.158-.063.25 0 .552.448 1 1 1s1-.448 1-1-.448-1-1-1c-.092 0-.165.039-.25.063l-.906-.906a.5.5 0 0 0-.438-.156.5.5 0 0 0-.063 0zm3.156 0c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5z"></path>
</symbol>
<symbol id="data-transfer-download" viewBox="0 0 8 8">
<path d="M3 0v3h-2l3 3 3-3h-2v-3h-2zm-3 7v1h8v-1h-8z"></path>
</symbol>
<symbol id="data-transfer-upload" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm4 2l-3 3h2v3h2v-3h2l-3-3z"></path>
</symbol>
<symbol id="delete" viewBox="0 0 8 8">
<path d="M2 0l-2 3 2 3h6v-6h-6zm1.5.781l1.5 1.5 1.5-1.5.719.719-1.5 1.5 1.5 1.5-.719.719-1.5-1.5-1.5 1.5-.719-.719 1.5-1.5-1.5-1.5.719-.719z" transform="translate(0 1)"></path>
</symbol>
<symbol id="dial" viewBox="0 0 8 8">
<path d="M4 0c-2.201 0-4 1.799-4 4h1c0-1.659 1.341-3 3-3s3 1.341 3 3h1c0-2.201-1.799-4-4-4zm-.594 2.094c-.82.25-1.406 1.006-1.406 1.906 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.586-1.656-1.406-1.906l-.594.875-.594-.875z" transform="translate(0 1)"></path>
</symbol>
<symbol id="document" viewBox="0 0 8 8">
<path d="M0 0v8h7v-4h-4v-4h-3zm4 0v3h3l-3-3zm-3 2h1v1h-1v-1zm0 2h1v1h-1v-1zm0 2h4v1h-4v-1z"></path>
</symbol>
<symbol id="dollar" viewBox="0 0 8 8">
<path d="M2 0v1h-.75c-.686 0-1.25.564-1.25 1.25v.5c0 .678.437 1.242 1.094 1.406l2.563.656c.143.036.344.296.344.438v.5c0 .134-.116.25-.25.25h-2.5c-.116 0-.212-.037-.25-.063v-.938h-1v1c0 .342.203.627.438.781.234.155.518.219.813.219h.75v1h1v-1h.75c.686 0 1.25-.564 1.25-1.25v-.5c0-.678-.437-1.242-1.094-1.406l-2.563-.656c-.143-.036-.344-.296-.344-.438v-.5c0-.134.116-.25.25-.25h2.5c.116 0 .212.037.25.063v.938h1v-1c0-.342-.203-.627-.438-.781-.234-.155-.518-.219-.813-.219h-.75v-1h-1z" transform="translate(1)"></path>
</symbol>
<symbol id="double-quote-sans-left" viewBox="0 0 8 8">
<path d="M0 0v6l3-3v-3h-3zm5 0v6l3-3v-3h-3z" transform="translate(0 1)"></path>
</symbol>
<symbol id="double-quote-sans-right" viewBox="0 0 8 8">
<path d="M3 0l-3 3v3h3v-6zm5 0l-3 3v3h3v-6z" transform="translate(0 1)"></path>
</symbol>
<symbol id="double-quote-serif-left" viewBox="0 0 8 8">
<path d="M3 0c-1.651 0-3 1.349-3 3v3h3v-3h-2c0-1.109.891-2 2-2v-1zm5 0c-1.651 0-3 1.349-3 3v3h3v-3h-2c0-1.109.891-2 2-2v-1z" transform="translate(0 1)"></path>
</symbol>
<symbol id="double-quote-serif-right" viewBox="0 0 8 8">
<path d="M0 0v3h2c0 1.109-.891 2-2 2v1c1.651 0 3-1.349 3-3v-3h-3zm5 0v3h2c0 1.109-.891 2-2 2v1c1.651 0 3-1.349 3-3v-3h-3z" transform="translate(0 1)"></path>
</symbol>
<symbol id="droplet" viewBox="0 0 8 8">
<path d="M3 0l-.344.344c-.11.11-2.656 2.685-2.656 4.875 0 1.65 1.35 3 3 3s3-1.35 3-3c0-2.19-2.546-4.765-2.656-4.875l-.344-.344zm-1.5 4.719c.28 0 .5.22.5.5 0 .55.45 1 1 1 .28 0 .5.22.5.5s-.22.5-.5.5c-1.1 0-2-.9-2-2 0-.28.22-.5.5-.5z" transform="translate(1)"></path>
</symbol>
<symbol id="eject" viewBox="0 0 8 8">
<path d="M4 0l-4 5h8l-4-5zm-4 6v2h8v-2h-8z"></path>
</symbol>
<symbol id="elevator" viewBox="0 0 8 8">
<path d="M3 0l-3 3h6l-3-3zm-3 5l3 3 3-3h-6z" transform="translate(1)"></path>
</symbol>
<symbol id="ellipses" viewBox="0 0 8 8">
<path d="M0 0v2h2v-2h-2zm3 0v2h2v-2h-2zm3 0v2h2v-2h-2z" transform="translate(0 3)"></path>
</symbol>
<symbol id="envelope-closed" viewBox="0 0 8 8">
<path d="M0 0v1l4 2 4-2v-1h-8zm0 2v4h8v-4l-4 2-4-2z" transform="translate(0 1)"></path>
</symbol>
<symbol id="envelope-open" viewBox="0 0 8 8">
<path d="M4 0l-4 2v6h8v-6l-4-2zm0 1.125l3 1.5v1.875l-3 1.5-3-1.5v-1.875l3-1.5zm-2 1.875v1l2 1 2-1v-1h-4z"></path>
</symbol>
<symbol id="euro" viewBox="0 0 8 8">
<path d="M6 0c-1.858 0-3.398 1.278-3.844 3h-1.906l-.25 1h2c0 .345.073.68.156 1h-1.969l-.188 1h2.563c.696 1.185 1.969 2 3.438 2 .734 0 1.407-.215 2-.563v-1.219c-.531.479-1.225.781-2 .781-.888 0-1.671-.392-2.219-1h2.219l.156-1h-2.969c-.113-.317-.188-.643-.188-1h3.344l.156-1h-3.313c.414-1.16 1.507-2 2.813-2 .655 0 1.258.209 1.75.563l.156-1.063c-.57-.313-1.213-.5-1.906-.5z" transform="translate(-1)"></path>
</symbol>
<symbol id="excerpt" viewBox="0 0 8 8">
<path d="M0 0v1h7v-1h-7zm0 2v1h5v-1h-5zm0 2v1h8v-1h-8zm0 2v1h1v-1h-1zm2 0v1h1v-1h-1zm2 0v1h1v-1h-1z"></path>
</symbol>
<symbol id="expand-down" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm2 2l2 2 2-2h-4zm-2 4v2h8v-2h-8z"></path>
</symbol>
<symbol id="expand-left" viewBox="0 0 8 8">
<path d="M0 0v8h1v-8h-1zm6 0v8h2v-8h-2zm-4 2v4l2-2-2-2z"></path>
</symbol>
<symbol id="expand-right" viewBox="0 0 8 8">
<path d="M0 0v8h2v-8h-2zm7 0v8h1v-8h-1zm-1 2l-2 2 2 2v-4z"></path>
</symbol>
<symbol id="expand-up" viewBox="0 0 8 8">
<path d="M0 0v2h8v-2h-8zm4 4l-2 2h4l-2-2zm-4 3v1h8v-1h-8z"></path>
</symbol>
<symbol id="external-link" viewBox="0 0 8 8">
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
</symbol>
<symbol id="eye" viewBox="0 0 8 8">
<path d="M4.031 0c-2.53 0-4.031 3-4.031 3s1.501 3 4.031 3c2.47 0 3.969-3 3.969-3s-1.499-3-3.969-3zm-.031 1c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm0 1c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1c0-.1-.032-.191-.063-.281-.08.16-.237.281-.438.281-.28 0-.5-.22-.5-.5 0-.2.121-.357.281-.438-.09-.03-.181-.063-.281-.063z" transform="translate(0 1)"></path>
</symbol>
<symbol id="eyedropper" viewBox="0 0 8 8">
<path d="M3.313 0a.5.5 0 0 0-.188.844l.625.625-3.594 3.656-.156.156v2.7190000000000003h2.719l.125-.156 3.656-3.656.625.656a.5.5 0 1 0 .719-.688l-.938-.938.656-.656c.59-.58.59-1.545 0-2.125-.56-.57-1.555-.57-2.125 0l-.656.656-.938-.938a.5.5 0 0 0-.469-.156.5.5 0 0 0-.063 0zm1.156 2.188l1.313 1.313-3.156 3.156-1.281-1.313 3.125-3.156z"></path>
</symbol>
<symbol id="file" viewBox="0 0 8 8">
<path d="M0 0v8h7v-4h-4v-4h-3zm4 0v3h3l-3-3z"></path>
</symbol>
<symbol id="fire" viewBox="0 0 8 8">
<path d="M2 0c1 2-2 3-2 5s2 3 2 3c-.98-1.98 2-3 2-5s-2-3-2-3zm3 3c1 2-2 3-2 5h3c.4 0 1-.5 1-2 0-2-2-3-2-3z"></path>
</symbol>
<symbol id="flag" viewBox="0 0 8 8">
<path d="M0 0v8h1v-8h-1zm2 0v4h2v1h4l-2-1.969 2-2.031h-3v-1h-3z"></path>
</symbol>
<symbol id="flash" viewBox="0 0 8 8">
<path d="M1.5 0l-1.5 3h2l-.656 2h-1.344l1 3 3-3h-1.5l1.5-3h-2l1-2h-1.5z" transform="translate(2)"></path>
</symbol>
<symbol id="folder" viewBox="0 0 8 8">
<path d="M0 0v2h8v-1h-5v-1h-3zm0 3v4.5c0 .28.22.5.5.5h7c.28 0 .5-.22.5-.5v-4.5h-8z"></path>
</symbol>
<symbol id="fork" viewBox="0 0 8 8">
<path d="M1.5 0c-.828 0-1.5.672-1.5 1.5 0 .656.414 1.202 1 1.406v2.188c-.586.204-1 .75-1 1.406 0 .828.672 1.5 1.5 1.5s1.5-.672 1.5-1.5c0-.595-.341-1.101-.844-1.344.09-.09.205-.156.344-.156h2c.823 0 1.5-.677 1.5-1.5v-.594c.586-.204 1-.75 1-1.406 0-.828-.672-1.5-1.5-1.5s-1.5.672-1.5 1.5c0 .656.414 1.202 1 1.406v.594c0 .277-.223.5-.5.5h-2c-.171 0-.346.04-.5.094v-1.188c.586-.204 1-.75 1-1.406 0-.828-.672-1.5-1.5-1.5z"></path>
</symbol>
<symbol id="fullscreen-enter" viewBox="0 0 8 8">
<path d="M0 0v4l1.5-1.5 1.5 1.5 1-1-1.5-1.5 1.5-1.5h-4zm5 4l-1 1 1.5 1.5-1.5 1.5h4v-4l-1.5 1.5-1.5-1.5z"></path>
</symbol>
<symbol id="fullscreen-exit" viewBox="0 0 8 8">
<path d="M1 0l-1 1 1.5 1.5-1.5 1.5h4v-4l-1.5 1.5-1.5-1.5zm3 4v4l1.5-1.5 1.5 1.5 1-1-1.5-1.5 1.5-1.5h-4z"></path>
</symbol>
<symbol id="globe" viewBox="0 0 8 8">
<path d="M4 0c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 1c.333 0 .637.086.938.188-.214.197-.45.383-.406.563.04.18.688.13.688.5 0 .27-.425.346-.125.656.35.35-.636.978-.656 1.438-.03.83.841.969 1.531.969.424 0 .503.195.469.438-.546.758-1.438 1.25-2.438 1.25-.378 0-.729-.09-1.063-.219.224-.442-.313-1.344-.781-1.625-.226-.226-.689-.114-.969-.219-.092-.271-.178-.545-.188-.844.031-.05.081-.094.156-.094.19 0 .454.374.594.344.18-.04-.742-1.313-.313-1.563.2-.12.609.394.469-.156-.12-.51.366-.276.656-.406.26-.11.455-.414.125-.594-.057-.031-.133-.104-.219-.188.45-.27.972-.438 1.531-.438zm2.313 1.094c.184.222.323.481.438.75-.043.065-.083.114-.188.219-.29.27-.327-.212-.438-.313-.13-.11-.638.025-.688-.125-.077-.181.499-.418.875-.531z"></path>
</symbol>
<symbol id="graph" viewBox="0 0 8 8">
<path d="M7.031 0l-3.031 3-1-1-3 3.031 1 1 2-2.031 1 1 4-4-.969-1zm-7.031 7v1h8v-1h-8z"></path>
</symbol>
<symbol id="grid-four-up" viewBox="0 0 8 8">
<path d="M0 0v1h1v-1h-1zm2 0v1h1v-1h-1zm2 0v1h1v-1h-1zm2 0v1h1v-1h-1zm-6 2v1h1v-1h-1zm2 0v1h1v-1h-1zm2 0v1h1v-1h-1zm2 0v1h1v-1h-1zm-6 2v1h1v-1h-1zm2 0v1h1v-1h-1zm2 0v1h1v-1h-1zm2 0v1h1v-1h-1zm-6 2v1h1v-1h-1zm2 0v1h1v-1h-1zm2 0v1h1v-1h-1zm2 0v1h1v-1h-1z"></path>
</symbol>
<symbol id="grid-three-up" viewBox="0 0 8 8">
<path d="M0 0v2h2v-2h-2zm3 0v2h2v-2h-2zm3 0v2h2v-2h-2zm-6 3v2h2v-2h-2zm3 0v2h2v-2h-2zm3 0v2h2v-2h-2zm-6 3v2h2v-2h-2zm3 0v2h2v-2h-2zm3 0v2h2v-2h-2z"></path>
</symbol>
<symbol id="grid-two-up" viewBox="0 0 8 8">
<path d="M0 0v3h3v-3h-3zm5 0v3h3v-3h-3zm-5 5v3h3v-3h-3zm5 0v3h3v-3h-3z"></path>
</symbol>
<symbol id="hard-drive" viewBox="0 0 8 8">
<path d="M.188 0c-.11 0-.188.077-.188.188v3.313c0 .28.22.5.5.5h6c.28 0 .5-.22.5-.5v-3.313c0-.11-.077-.188-.188-.188h-6.625zm-.188 4.906v2.906c0 .11.077.188.188.188h6.625c.11 0 .188-.077.188-.188v-2.906c-.16.05-.32.094-.5.094h-6c-.18 0-.34-.044-.5-.094zm5.5 1.094c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5.22-.5.5-.5z"></path>
</symbol>
<symbol id="header" viewBox="0 0 8 8">
<path d="M0 0v1h.5c.28 0 .5.22.5.5v4c0 .28-.22.5-.5.5h-.5v1h3v-1h-.5c-.28 0-.5-.22-.5-.5v-1.5h3v1.5c0 .28-.22.5-.5.5h-.5v1h3v-1h-.5c-.28 0-.5-.22-.5-.5v-4c0-.28.22-.5.5-.5h.5v-1h-3v1h.5c.28 0 .5.22.5.5v1.5h-3v-1.5c0-.28.22-.5.5-.5h.5v-1h-3z"></path>
</symbol>
<symbol id="headphones" viewBox="0 0 8 8">
<path d="M4 0c-1.651 0-3 1.349-3 3v1h-.5a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-3.5c0-1.109.891-2 2-2s2 .891 2 2v3.5a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 0-.5-.5h-.5v-1c0-1.651-1.349-3-3-3z"></path>
</symbol>
<symbol id="heart" viewBox="0 0 8 8">
<path d="M2 0c-.55 0-1.046.224-1.406.594-.37.36-.594.856-.594 1.406 0 .55.224 1.046.594 1.406l3.406 3.438 3.406-3.438c.37-.37.594-.856.594-1.406 0-.55-.224-1.046-.594-1.406-.36-.37-.856-.594-1.406-.594-.55 0-1.046.224-1.406.594-.37.36-.594.856-.594 1.406 0-.55-.224-1.046-.594-1.406-.36-.37-.856-.594-1.406-.594z" transform="translate(0 1)"></path>
</symbol>
<symbol id="home" viewBox="0 0 8 8">
<path d="M4 0l-4 3h1v4h2v-2h2v2h2v-4.031l1 .031-4-3z"></path>
</symbol>
<symbol id="image" viewBox="0 0 8 8">
<path d="M0 0v8h8v-8h-8zm1 1h6v3l-1-1-1 1 2 2v1h-1l-4-4-1 1v-3z"></path>
</symbol>
<symbol id="inbox" viewBox="0 0 8 8">
<path d="M.188 0c-.11 0-.188.077-.188.188v7.625c0 .11.077.188.188.188h7.625c.11 0 .188-.077.188-.188v-7.625c0-.11-.077-.188-.188-.188h-7.625zm.813 2h6v3h-1l-1 1h-2l-1-1h-1v-3z"></path>
</symbol>
<symbol id="infinity" viewBox="0 0 8 8">
<path d="M2 0c-1.31 0-2 1.01-2 2s.69 2 2 2c.79 0 1.42-.559 2-1.219.58.66 1.19 1.219 2 1.219 1.31 0 2-1.01 2-2s-.69-2-2-2c-.81 0-1.42.559-2 1.219-.57-.66-1.21-1.219-2-1.219zm0 1c.42 0 .884.47 1.344 1-.46.53-.924 1-1.344 1-.74 0-1-.54-1-1 0-.46.26-1 1-1zm4 0c.74 0 1 .54 1 1 0 .46-.26 1-1 1-.43 0-.894-.47-1.344-1 .45-.53.914-1 1.344-1z" transform="translate(0 2)"></path>
</symbol>
<symbol id="info" viewBox="0 0 8 8">
<path d="M3 0c-.552 0-1 .448-1 1s.448 1 1 1 1-.448 1-1-.448-1-1-1zm-1.5 2.5c-.83 0-1.5.67-1.5 1.5h1c0-.28.22-.5.5-.5s.5.22.5.5-1 1.64-1 2.5c0 .86.67 1.5 1.5 1.5s1.5-.67 1.5-1.5h-1c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-.36 1-1.84 1-2.5 0-.81-.67-1.5-1.5-1.5z" transform="translate(2)"></path>
</symbol>
<symbol id="italic" viewBox="0 0 8 8">
<path d="M2 0v1h1.625l-.063.125-2 5-.344.875h-1.219v1h5v-1h-1.625l.063-.125 2-5 .344-.875h1.219v-1h-5z"></path>
</symbol>
<symbol id="justify-center" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm0 2v1h8v-1h-8zm0 2v1h8v-1h-8zm1 2v1h6v-1h-6z"></path>
</symbol>
<symbol id="justify-left" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm0 2v1h8v-1h-8zm0 2v1h8v-1h-8zm0 2v1h6v-1h-6z"></path>
</symbol>
<symbol id="justify-right" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm0 2v1h8v-1h-8zm0 2v1h8v-1h-8zm2 2v1h6v-1h-6z"></path>
</symbol>
<symbol id="key" viewBox="0 0 8 8">
<path d="M5.5 0c-1.38 0-2.5 1.12-2.5 2.5 0 .16.033.297.063.438l-3.063 3.063v2h3v-2h2v-1l.063-.063c.14.03.277.063.438.063 1.38 0 2.5-1.12 2.5-2.5s-1.12-2.5-2.5-2.5zm.5 1c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1z"></path>
</symbol>
<symbol id="laptop" viewBox="0 0 8 8">
<path d="M1.344 0a.5.5 0 0 0-.344.5v3.5h-1v1.5c0 .28.22.5.5.5h6.999999999999999c.28 0 .5-.22.5-.5v-1.5h-1v-3.5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0-.094 0 .5.5 0 0 0-.063 0zm.656 1h4v3h-1v1h-2v-1h-1v-3z" transform="translate(0 1)"></path>
</symbol>
<symbol id="layers" viewBox="0 0 8 8">
<path d="M0 0v4h4v-4h-4zm5 2v3h-3v1h4v-4h-1zm2 2v3h-3v1h4v-4h-1z"></path>
</symbol>
<symbol id="lightbulb" viewBox="0 0 8 8">
<path d="M3.406 0a.5.5 0 0 0-.125.063l-3 1.5a.5.5 0 1 0 .438.875l3-1.5a.5.5 0 0 0-.313-.938zm1 1.5a.5.5 0 0 0-.125.063l-4 2a.5.5 0 1 0 .438.875l4-2a.5.5 0 0 0-.313-.938zm0 2a.5.5 0 0 0-.125.063l-3 1.5a.5.5 0 0 0 .219.938h2a.502.502 0 0 0 .156-1l1.063-.563a.5.5 0 0 0-.313-.938zm-2.563 3.5a.502.502 0 0 0 .156 1h1a.5.5 0 1 0 0-1h-1a.5.5 0 0 0-.094 0 .502.502 0 0 0-.063 0z" transform="translate(1)"></path>
</symbol>
<symbol id="link-broken" viewBox="0 0 8 8">
<path d="M2 0v1h-1v1h2v-2h-1zm3.875.031c-.184.01-.354.03-.531.094-.27.095-.531.25-.75.469l-.438.438a.5.5 0 1 0 .688.688l.438-.438c.101-.101.245-.173.375-.219.352-.126.78-.064 1.063.219.395.389.4 1.037 0 1.438l-1.5 1.5a.5.5 0 1 0 .688.688l1.5-1.5c.78-.78.785-2.041 0-2.813-.279-.279-.606-.452-.969-.531-.181-.039-.379-.041-.563-.031zm-3.594 2.906a.5.5 0 0 0-.188.156l-1.5 1.5c-.78.78-.785 2.041 0 2.813.557.557 1.355.722 2.063.469.27-.095.531-.25.75-.469l.438-.438a.5.5 0 1 0-.688-.688l-.438.438c-.101.101-.245.173-.375.219-.352.126-.78.064-1.063-.219-.395-.389-.4-1.037 0-1.438l1.5-1.5a.5.5 0 0 0-.438-.844.5.5 0 0 0-.063 0zm2.719 3.063v2h1v-1h1v-1h-2z"></path>
</symbol>
<symbol id="link-intact" viewBox="0 0 8 8">
<path d="M5.875.031c-.184.01-.354.03-.531.094-.27.095-.531.25-.75.469a.5.5 0 1 0 .688.688c.101-.101.245-.173.375-.219.352-.126.78-.064 1.063.219.395.389.4 1.037 0 1.438l-1.5 1.5c-.434.434-.799.483-1.063.469-.264-.015-.406-.125-.406-.125a.504.504 0 1 0-.5.875s.34.222.844.25c.504.028 1.197-.165 1.813-.781l1.5-1.5c.78-.78.785-2.041 0-2.813-.279-.279-.606-.452-.969-.531-.181-.039-.379-.041-.563-.031zm-2 2.313c-.501-.019-1.186.155-1.781.75l-1.5 1.5c-.78.78-.785 2.041 0 2.813.557.557 1.355.722 2.063.469.27-.095.531-.25.75-.469a.5.5 0 1 0-.688-.688c-.101.101-.245.173-.375.219-.352.126-.78.064-1.063-.219-.395-.389-.4-1.037 0-1.438l1.5-1.5c.405-.405.752-.448 1.031-.438.279.011.469.094.469.094a.5.5 0 1 0 .438-.875s-.343-.199-.844-.219z"></path>
</symbol>
<symbol id="list-rich" viewBox="0 0 8 8">
<path d="M0 0v3h3v-3h-3zm4 0v1h4v-1h-4zm0 2v1h3v-1h-3zm-4 2v3h3v-3h-3zm4 0v1h4v-1h-4zm0 2v1h3v-1h-3z"></path>
</symbol>
<symbol id="list" viewBox="0 0 8 8">
<path d="M.5 0c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm1.5 0v1h6v-1h-6zm-1.5 2c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm1.5 0v1h6v-1h-6zm-1.5 2c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm1.5 0v1h6v-1h-6zm-1.5 2c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm1.5 0v1h6v-1h-6z"></path>
</symbol>
<symbol id="location" viewBox="0 0 8 8">
<path d="M8 0l-8 4 3 1 1 3 4-8z"></path>
</symbol>
<symbol id="lock-locked" viewBox="0 0 8 8">
<path d="M3 0c-1.099 0-2 .901-2 2v1h-1v4h6v-4h-1v-1c0-1.099-.901-2-2-2zm0 1c.561 0 1 .439 1 1v1h-2v-1c0-.561.439-1 1-1z" transform="translate(1)"></path>
</symbol>
<symbol id="lock-unlocked" viewBox="0 0 8 8">
<path d="M3 0c-1.099 0-2 .901-2 2h1c0-.561.439-1 1-1 .561 0 1 .439 1 1v2h-4v4h6v-4h-1v-2c0-1.099-.901-2-2-2z" transform="translate(1)"></path>
</symbol>
<symbol id="loop-circular" viewBox="0 0 8 8">
<path d="M4 0c-1.651 0-3 1.349-3 3h-1l1.5 2 1.5-2h-1c0-1.109.891-2 2-2v-1zm2.5 1l-1.5 2h1c0 1.109-.891 2-2 2v1c1.651 0 3-1.349 3-3h1l-1.5-2z" transform="translate(0 1)"></path>
</symbol>
<symbol id="loop-square" viewBox="0 0 8 8">
<path d="M1 0v2h1v-1h4v2h-1l1.5 2.5 1.5-2.5h-1v-3h-6zm.5 2.5l-1.5 2.5h1v3h6v-2h-1v1h-4v-2h1l-1.5-2.5z"></path>
</symbol>
<symbol id="loop" viewBox="0 0 8 8">
<path d="M6 0v1h-5c-.554 0-1 .446-1 1v1h1v-1h5v1l2-1.5-2-1.5zm-4 4l-2 1.5 2 1.5v-1h5c.542 0 1-.458 1-1v-1h-1v1h-5v-1z"></path>
</symbol>
<symbol id="magnifying-glass" viewBox="0 0 8 8">
<path d="M3.5 0c-1.927 0-3.5 1.573-3.5 3.5s1.573 3.5 3.5 3.5c.592 0 1.166-.145 1.656-.406a1 1 0 0 0 .125.125l1 1a1.016 1.016 0 1 0 1.438-1.438l-1-1a1 1 0 0 0-.156-.125c.266-.493.438-1.059.438-1.656 0-1.927-1.573-3.5-3.5-3.5zm0 1c1.387 0 2.5 1.113 2.5 2.5 0 .661-.241 1.273-.656 1.719-.01.011-.021.021-.031.031a1 1 0 0 0-.125.125c-.442.397-1.043.625-1.688.625-1.387 0-2.5-1.113-2.5-2.5s1.113-2.5 2.5-2.5z"></path>
</symbol>
<symbol id="map-marker" viewBox="0 0 8 8">
<path d="M3 0c-1.66 0-3 1.34-3 3 0 2 3 5 3 5s3-3 3-5c0-1.66-1.34-3-3-3zm0 1c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z" transform="translate(1)"></path>
</symbol>
<symbol id="map" viewBox="0 0 8 8">
<path d="M0 0v8h8v-2.375a.5.5 0 0 0 0-.219v-5.406h-8zm1 1h6v4h-1.5a.5.5 0 0 0-.094 0 .502.502 0 1 0 .094 1h1.5v1h-6v-6zm2.5 1c-.83 0-1.5.67-1.5 1.5 0 1 1.5 2.5 1.5 2.5s1.5-1.5 1.5-2.5c0-.83-.67-1.5-1.5-1.5zm0 1c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5.22-.5.5-.5z"></path>
</symbol>
<symbol id="media-pause" viewBox="0 0 8 8">
<path d="M0 0v6h2v-6h-2zm4 0v6h2v-6h-2z" transform="translate(1 1)"></path>
</symbol>
<symbol id="media-play" viewBox="0 0 8 8">
<path d="M0 0v6l6-3-6-3z" transform="translate(1 1)"></path>
</symbol>
<symbol id="media-record" viewBox="0 0 8 8">
<path d="M3 0c-1.657 0-3 1.343-3 3s1.343 3 3 3 3-1.343 3-3-1.343-3-3-3z" transform="translate(1 1)"></path>
</symbol>
<symbol id="media-skip-backward" viewBox="0 0 8 8">
<path d="M4 0l-4 3 4 3v-6zm0 3l4 3v-6l-4 3z" transform="translate(0 1)"></path>
</symbol>
<symbol id="media-skip-forward" viewBox="0 0 8 8">
<path d="M0 0v6l4-3-4-3zm4 3v3l4-3-4-3v3z" transform="translate(0 1)"></path>
</symbol>
<symbol id="media-step-backward" viewBox="0 0 8 8">
<path d="M0 0v6h2v-6h-2zm2 3l5 3v-6l-5 3z" transform="translate(0 1)"></path>
</symbol>
<symbol id="media-step-forward" viewBox="0 0 8 8">
<path d="M0 0v6l5-3-5-3zm5 3v3h2v-6h-2v3z" transform="translate(0 1)"></path>
</symbol>
<symbol id="media-stop" viewBox="0 0 8 8">
<path d="M0 0v6h6v-6h-6z" transform="translate(1 1)"></path>
</symbol>
<symbol id="medical-cross" viewBox="0 0 8 8">
<path d="M2 0v2h-2v4h2v2h4v-2h2v-4h-2v-2h-4z"></path>
</symbol>
<symbol id="menu" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm0 2.969v1h8v-1h-8zm0 3v1h8v-1h-8z" transform="translate(0 1)"></path>
</symbol>
<symbol id="microphone" viewBox="0 0 8 8">
<path d="M2.906-.031a1 1 0 0 0-.125.031 1 1 0 0 0-.781 1v2a1 1 0 1 0 2 0v-2a1 1 0 0 0-1.094-1.031zm-2.563 2.031a.5.5 0 0 0-.344.5v.5c0 1.476 1.091 2.693 2.5 2.938v1.063h-.5c-.55 0-1 .45-1 1h4c0-.55-.45-1-1-1h-.5v-1.063c1.409-.244 2.5-1.461 2.5-2.938v-.5a.5.5 0 1 0-1 0v.5c0 1.109-.891 2-2 2s-2-.891-2-2v-.5a.5.5 0 0 0-.594-.5.5.5 0 0 0-.063 0z" transform="translate(1)"></path>
</symbol>
<symbol id="minus" viewBox="0 0 8 8">
<path d="M0 0v2h8v-2h-8z" transform="translate(0 3)"></path>
</symbol>
<symbol id="monitor" viewBox="0 0 8 8">
<path d="M.344 0a.5.5 0 0 0-.344.5v5a.5.5 0 0 0 .5.5h2.5v1h-1c-.55 0-1 .45-1 1h6c0-.55-.45-1-1-1h-1v-1h2.5a.5.5 0 0 0 .5-.5v-5a.5.5 0 0 0-.5-.5h-7a.5.5 0 0 0-.094 0 .5.5 0 0 0-.063 0zm.656 1h6v4h-6v-4z"></path>
</symbol>
<symbol id="moon" viewBox="0 0 8 8">
<path d="M2.719 0c-1.58.53-2.719 2.021-2.719 3.781 0 2.21 1.79 4 4 4 1.76 0 3.251-1.17 3.781-2.75-.4.14-.831.25-1.281.25-2.21 0-4-1.79-4-4 0-.44.079-.881.219-1.281z"></path>
</symbol>
<symbol id="move" viewBox="0 0 8 8">
<path d="M3.5 0l-1.5 1.5h1v1.5h-1.5v-1l-1.5 1.5 1.5 1.5v-1h1.5v1.5h-1l1.5 1.5 1.5-1.5h-1v-1.5h1.5v1l1.5-1.5-1.5-1.5v1h-1.5v-1.5h1l-1.5-1.5z"></path>
</symbol>
<symbol id="musical-note" viewBox="0 0 8 8">
<path d="M8 0c-5 0-6 1-6 1v4.093999999999999c-.154-.054-.327-.094-.5-.094-.828 0-1.5.672-1.5 1.5s.672 1.5 1.5 1.5 1.5-.672 1.5-1.5v-3.969c.732-.226 1.99-.438 4-.5v2.063c-.154-.054-.327-.094-.5-.094-.828 0-1.5.672-1.5 1.5s.672 1.5 1.5 1.5 1.5-.672 1.5-1.5v-5.5z"></path>
</symbol>
<symbol id="paperclip" viewBox="0 0 8 8">
<path d="M5 0c-.514 0-1.021.201-1.406.594l-2.781 2.719c-1.07 1.07-1.07 2.805 0 3.875 1.07 1.07 2.805 1.07 3.875 0l1.25-1.25-.688-.688-.906.875-.344.375c-.69.69-1.81.69-2.5 0-.682-.682-.668-1.778 0-2.469l2.781-2.719v-.031c.389-.395 1.037-.4 1.438 0 .388.381.378 1.006 0 1.406l-2.5 2.469c-.095.095-.28.095-.375 0-.095-.095-.095-.28 0-.375l.375-.344.594-.625-.688-.688-.875.875-.094.094c-.485.485-.485 1.265 0 1.75.485.485 1.265.485 1.75 0l2.5-2.438c.78-.78.785-2.041 0-2.813-.39-.39-.893-.594-1.406-.594z"></path>
</symbol>
<symbol id="pencil" viewBox="0 0 8 8">
<path d="M6 0l-1 1 2 2 1-1-2-2zm-2 2l-4 4v2h2l4-4-2-2z"></path>
</symbol>
<symbol id="people" viewBox="0 0 8 8">
<path d="M5.5 0c-.51 0-.949.355-1.219.875.45.54.719 1.275.719 2.125 0 .29-.034.574-.094.844.18.11.374.156.594.156.83 0 1.5-.9 1.5-2s-.67-2-1.5-2zm-3 1c-.828 0-1.5.895-1.5 2s.672 2 1.5 2 1.5-.895 1.5-2-.672-2-1.5-2zm4.75 3.156c-.43.51-1.018.824-1.688.844.27.38.438.844.438 1.344v.656h2v-1.656c0-.52-.31-.968-.75-1.188zm-6.5 1c-.44.22-.75.668-.75 1.188v1.656h5v-1.656c0-.52-.31-.968-.75-1.188-.44.53-1.06.844-1.75.844s-1.31-.314-1.75-.844z"></path>
</symbol>
<symbol id="person" viewBox="0 0 8 8">
<path d="M4 0c-1.105 0-2 1.119-2 2.5s.895 2.5 2 2.5 2-1.119 2-2.5-.895-2.5-2-2.5zm-2.094 5c-1.07.04-1.906.92-1.906 2v1h8v-1c0-1.08-.836-1.96-1.906-2-.54.61-1.284 1-2.094 1-.81 0-1.554-.39-2.094-1z"></path>
</symbol>
<symbol id="phone" viewBox="0 0 8 8">
<path d="M.188 0c-.11 0-.188.077-.188.188v7.625c0 .11.077.188.188.188h4.625c.11 0 .188-.077.188-.188v-7.625c0-.11-.077-.188-.188-.188h-4.625zm.813 1h3v5h-3v-5zm1.5 5.5c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5.22-.5.5-.5z" transform="translate(1)"></path>
</symbol>
<symbol id="pie-chart" viewBox="0 0 8 8">
<path d="M3.5 0c-.97 0-1.839.391-2.469 1.031l2.969 2.969v-3.969c-.16-.03-.33-.031-.5-.031zm1.5 1.063v3.406l-2.719 2.719c.6.5 1.369.813 2.219.813 1.93 0 3.5-1.57 3.5-3.5 0-1.76-1.31-3.197-3-3.438zm-4.094 1.313c-.55.54-.906 1.285-.906 2.125 0 .95.435 1.804 1.125 2.344l2.156-2.125-2.375-2.344z"></path>
</symbol>
<symbol id="pin" viewBox="0 0 8 8">
<path d="M1.344 0a.502.502 0 0 0 .156 1h.5v2h-1c-.55 0-1 .45-1 1h3v3l.438 1 .563-1v-3h3c0-.55-.45-1-1-1h-1v-2h.5a.5.5 0 1 0 0-1h-4a.5.5 0 0 0-.094 0 .502.502 0 0 0-.063 0z"></path>
</symbol>
<symbol id="play-circle" viewBox="0 0 8 8">
<path d="M4 0c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-1 2l3 2-3 2v-4z"></path>
</symbol>
<symbol id="plus" viewBox="0 0 8 8">
<path d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"></path>
</symbol>
<symbol id="power-standby" viewBox="0 0 8 8">
<path d="M3 0v4h1v-4h-1zm-1.281 1.438l-.375.313c-.803.64-1.344 1.634-1.344 2.75 0 1.929 1.571 3.5 3.5 3.5s3.5-1.571 3.5-3.5c0-1.116-.529-2.11-1.344-2.75l-.375-.313-.625.781.375.313c.585.46.969 1.165.969 1.969 0 1.391-1.109 2.5-2.5 2.5s-2.5-1.109-2.5-2.5c0-.804.361-1.509.938-1.969l.406-.313-.625-.781z"></path>
</symbol>
<symbol id="print" viewBox="0 0 8 8">
<path d="M2 0v2h4v-2h-4zm-1.906 3c-.06 0-.094.034-.094.094v2.813c0 .06.034.094.094.094h.906v-2h6v2h.906c.06 0 .094-.034.094-.094v-2.813c0-.06-.034-.094-.094-.094h-7.813zm1.906 2v3h4v-3h-4z"></path>
</symbol>
<symbol id="project" viewBox="0 0 8 8">
<path d="M0 0v7h1v-7h-1zm7 0v7h1v-7h-1zm-5 1v1h2v-1h-2zm1 2v1h2v-1h-2zm1 2v1h2v-1h-2z"></path>
</symbol>
<symbol id="pulse" viewBox="0 0 8 8">
<path d="M3.25 0l-.469 1.531-.781 2.563-.031-.063-.094-.344h-1.875v1h1.1560000000000001l.375 1.156.469 1.469.469-1.469.781-2.5.781 2.5.406 1.313.531-1.281.594-1.469.125.281h2.3129999999999997v-1h-1.688l-.375-.719-.5-1-.406 1.031-.469 1.188-.844-2.656-.469-1.531z"></path>
</symbol>
<symbol id="puzzle-piece" viewBox="0 0 8 8">
<path d="M3 0c-.28 0-.539.101-.719.281-.18.18-.281.439-.281.719 0 .28.181.479.281.719.03.06.063.161.063.281h-2.344v6h2.344c0-.12-.011-.221-.031-.281-.11-.24-.313-.439-.313-.719 0-.28.101-.539.281-.719.18-.18.439-.281.719-.281.28 0 .539.101.719.281.18.18.281.439.281.719 0 .28-.181.479-.281.719-.03.06-.063.161-.063.281h2.344v-2.344c.12 0 .221.011.281.031.24.11.439.313.719.313.28 0 .539-.101.719-.281.18-.18.281-.439.281-.719 0-.28-.101-.539-.281-.719-.18-.18-.439-.281-.719-.281-.28 0-.479.181-.719.281-.06.03-.161.063-.281.063v-2.344h-2.344c0-.12.011-.221.031-.281.11-.24.313-.439.313-.719 0-.28-.101-.539-.281-.719-.18-.18-.439-.281-.719-.281z"></path>
</symbol>
<symbol id="question-mark" viewBox="0 0 8 8">
<path d="M2.469 0c-.854 0-1.48.256-1.875.656s-.54.901-.594 1.281l1 .125c.036-.26.125-.497.313-.688.188-.19.491-.375 1.156-.375.664 0 1.019.163 1.219.344.199.181.281.405.281.656 0 .833-.313 1.063-.813 1.5-.5.438-1.188 1.083-1.188 2.25v.25h1v-.25c0-.833.344-1.063.844-1.5.5-.438 1.156-1.083 1.156-2.25 0-.479-.168-1.02-.594-1.406-.426-.387-1.071-.594-1.906-.594zm-.5 7v1h1v-1h-1z" transform="translate(2)"></path>
</symbol>
<symbol id="rain" viewBox="0 0 8 8">
<path d="M4.5 0c-1.21 0-2.27.86-2.5 2-1.1 0-2 .9-2 2 0 .52.201 1.015.531 1.375.26-.22.599-.375.969-.375.2 0 .393.055.563.125.17-.64.748-1.125 1.438-1.125s1.268.485 1.438 1.125c.17-.07.362-.125.563-.125.63 0 1.155.388 1.375.938.64-.17 1.125-.747 1.125-1.438 0-.65-.42-1.29-1-1.5v-.5c0-1.38-1.12-2.5-2.5-2.5zm-1.156 5a.5.5 0 0 0-.344.5v2a.5.5 0 1 0 1 0v-2a.5.5 0 0 0-.594-.5.5.5 0 0 0-.063 0zm-2 1a.5.5 0 0 0-.344.5v1a.5.5 0 1 0 1 0v-1a.5.5 0 0 0-.594-.5.5.5 0 0 0-.063 0zm4 0a.5.5 0 0 0-.344.5v1a.5.5 0 1 0 1 0v-1a.5.5 0 0 0-.594-.5.5.5 0 0 0-.063 0z"></path>
</symbol>
<symbol id="random" viewBox="0 0 8 8">
<path d="M6 0v1h-.5c-.354 0-.6.116-.813.375l-1.406 1.75-1.5-1.75v-.031c-.212-.236-.427-.344-.781-.344h-1v1h1c-.037 0 .008-.011.031 0v.031l1.625 1.906-1.625 2.031c.016-.02.019.022 0 .031-.019.009-.068 0-.031 0h-1v1h1c.354 0 .6-.116.813-.375l1.531-1.906 1.625 1.906v.031c.212.236.427.344.781.344h.25v1l2-1.5-2-1.5v1h-.25c.037 0-.008.011-.031 0v-.031l-1.75-2.063 1.5-1.875v-.031c.019-.009.068 0 .031 0h.5v1l2-1.5-2-1.5z"></path>
</symbol>
<symbol id="reload" viewBox="0 0 8 8">
<path d="M4 0c-2.201 0-4 1.799-4 4s1.799 4 4 4c1.104 0 2.092-.456 2.813-1.188l-.688-.688c-.54.548-1.289.875-2.125.875-1.659 0-3-1.341-3-3s1.341-3 3-3c.834 0 1.545.354 2.094.906l-1.094 1.094h3v-3l-1.188 1.188c-.731-.72-1.719-1.188-2.813-1.188z"></path>
</symbol>
<symbol id="resize-both" viewBox="0 0 8 8">
<path d="M4 0l1.656 1.656-4 4-1.656-1.656v4h4l-1.656-1.656 4-4 1.656 1.656v-4h-4z"></path>
</symbol>
<symbol id="resize-height" viewBox="0 0 8 8">
<path d="M2.5 0l-2.5 3h2v2h-2l2.5 3 2.5-3h-2v-2h2l-2.5-3z" transform="translate(1)"></path>
</symbol>
<symbol id="resize-width" viewBox="0 0 8 8">
<path d="M3 0l-3 2.5 3 2.5v-2h2v2l3-2.5-3-2.5v2h-2v-2z" transform="translate(0 1)"></path>
</symbol>
<symbol id="rss-alt" viewBox="0 0 8 8">
<path d="M0 0v2c3.331 0 6 2.669 6 6h2c0-4.409-3.591-8-8-8zm0 3v2c1.67 0 3 1.33 3 3h2c0-2.75-2.25-5-5-5zm0 3v2h2c0-1.11-.89-2-2-2z"></path>
</symbol>
<symbol id="rss" viewBox="0 0 8 8">
<path d="M1 0v1c3.32 0 6 2.68 6 6h1c0-3.86-3.14-7-7-7zm0 2v1c2.221 0 4 1.779 4 4h1c0-2.759-2.241-5-5-5zm0 2v1c1.109 0 2 .891 2 2h1c0-1.651-1.349-3-3-3zm0 2c-.552 0-1 .448-1 1s.448 1 1 1 1-.448 1-1-.448-1-1-1z"></path>
</symbol>
<symbol id="script" viewBox="0 0 8 8">
<path d="M3 0c-.55 0-1 .45-1 1v5.5c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-1.5h-1v2c0 .55.45 1 1 1h5c.55 0 1-.45 1-1v-3h-4v-2.5c0-.28.22-.5.5-.5s.5.22.5.5v1.5h4v-2c0-.55-.45-1-1-1h-4z"></path>
</symbol>
<symbol id="share-boxed" viewBox="0 0 8 8">
<path d="M.75 0c-.402 0-.75.348-.75.75v5.5c0 .402.348.75.75.75h4.5c.402 0 .75-.348.75-.75v-1.25h-1v1h-4v-5h2v-1h-2.25zm5.25 0v1c-2.05 0-3.704 1.544-3.938 3.531.213-.875.999-1.531 1.938-1.531h2v1l2-2-2-2z"></path>
</symbol>
<symbol id="share" viewBox="0 0 8 8">
<path d="M5 0v2c-4 0-5 2.05-5 5 .52-1.98 2-3 4-3h1v2l3-3.156-3-2.844z"></path>
</symbol>
<symbol id="shield" viewBox="0 0 8 8">
<path d="M4 0l-.188.094-3.5 1.469-.313.125v.313c0 1.657.666 3.122 1.469 4.188.401.533.828.969 1.25 1.281.422.313.826.531 1.281.531.455 0 .86-.219 1.281-.531.422-.313.849-.749 1.25-1.281.803-1.065 1.469-2.53 1.469-4.188v-.313l-.313-.125-3.5-1.469-.188-.094zm0 1.094v5.906c-.045 0-.328-.069-.656-.313s-.714-.631-1.063-1.094c-.642-.851-1.137-2.025-1.219-3.281l2.938-1.219z"></path>
</symbol>
<symbol id="signal" viewBox="0 0 8 8">
<path d="M6 0v8h1v-8h-1zm-2 1v7h1v-7h-1zm-2 2v5h1v-5h-1zm-2 2v3h1v-3h-1z"></path>
</symbol>
<symbol id="signpost" viewBox="0 0 8 8">
<path d="M3 0v1h-2l-1 1 1 1h2v5h1v-4h2l1-1-1-1h-2v-2h-1z"></path>
</symbol>
<symbol id="sort-ascending" viewBox="0 0 8 8">
<path d="M2 0v6h-2l2.5 2 2.5-2h-2v-6h-1zm2 0v1h2v-1h-2zm0 2v1h3v-1h-3zm0 2v1h4v-1h-4z"></path>
</symbol>
<symbol id="sort-descending" viewBox="0 0 8 8">
<path d="M2 0v6h-2l2.5 2 2.5-2h-2v-6h-1zm2 0v1h4v-1h-4zm0 2v1h3v-1h-3zm0 2v1h2v-1h-2z"></path>
</symbol>
<symbol id="spreadsheet" viewBox="0 0 8 8">
<path d="M.75 0c-.402 0-.75.348-.75.75v5.5c0 .402.348.75.75.75h6.5c.402 0 .75-.348.75-.75v-5.5c0-.402-.348-.75-.75-.75h-6.5zm.25 1h1v1h-1v-1zm2 0h4v1h-4v-1zm-2 2h1v1h-1v-1zm2 0h4v1h-4v-1zm-2 2h1v1h-1v-1zm2 0h4v1h-4v-1z"></path>
</symbol>
<symbol id="star" viewBox="0 0 8 8">
<path d="M4 0l-1 3h-3l2.5 2-1 3 2.5-2 2.5 2-1-3 2.5-2h-3l-1-3z"></path>
</symbol>
<symbol id="sun" viewBox="0 0 8 8">
<path d="M4 0c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm-2.5 1c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm5 0c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm-2.5 1c-1.105 0-2 .895-2 2s.895 2 2 2 2-.895 2-2-.895-2-2-2zm-3.5 1.5c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm7 0c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm-6 2.5c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm5 0c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5zm-2.5 1c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5z"></path>
</symbol>
<symbol id="tablet" viewBox="0 0 8 8">
<path d="M.344 0c-.18 0-.344.164-.344.344v7.313c0 .18.164.344.344.344h6.313c.18 0 .344-.164.344-.344v-7.313c0-.18-.164-.344-.344-.344h-6.313zm.656 1h5v5h-5v-5zm2.5 5.5c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5.22-.5.5-.5z"></path>
</symbol>
<symbol id="tag" viewBox="0 0 8 8">
<path d="M0 0v3l5 5 3-3-5-5h-3zm2 1c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1z"></path>
</symbol>
<symbol id="tags" viewBox="0 0 8 8">
<path d="M0 0v2l3 3 1.5-1.5.5-.5-2-2-1-1h-2zm3.406 0l3 3-1.188 1.219.781.781 2-2-3-3h-1.594zm-1.906 1c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5.22-.5.5-.5z" transform="translate(0 1)"></path>
</symbol>
<symbol id="target" viewBox="0 0 8 8">
<path d="M4 0c-2.203 0-4 1.797-4 4 0 2.203 1.797 4 4 4 2.203 0 4-1.797 4-4 0-2.203-1.797-4-4-4zm0 1c1.663 0 3 1.337 3 3s-1.337 3-3 3-3-1.337-3-3 1.337-3 3-3zm0 1c-1.099 0-2 .901-2 2s.901 2 2 2 2-.901 2-2-.901-2-2-2zm0 1c.558 0 1 .442 1 1s-.442 1-1 1-1-.442-1-1 .442-1 1-1z"></path>
</symbol>
<symbol id="task" viewBox="0 0 8 8">
<path d="M0 0v7h7v-3.594l-1 1v1.594h-5v-5h3.594l1-1h-5.594zm7 0l-3 3-1-1-1 1 2 2 4-4-1-1z"></path>
</symbol>
<symbol id="terminal" viewBox="0 0 8 8">
<path d="M.094 0c-.06 0-.094.034-.094.094v7.813c0 .06.034.094.094.094h7.813c.06 0 .094-.034.094-.094v-7.813c0-.06-.034-.094-.094-.094h-7.813zm1.406.781l1.719 1.719-1.719 1.719-.719-.719 1-1-1-1 .719-.719zm2.5 2.219h3v1h-3v-1z"></path>
</symbol>
<symbol id="text" viewBox="0 0 8 8">
<path d="M0 0v2h.5c0-.55.45-1 1-1h1.5v5.5c0 .28-.22.5-.5.5h-.5v1h4v-1h-.5c-.28 0-.5-.22-.5-.5v-5.5h1.5c.55 0 1 .45 1 1h.5v-2h-8z"></path>
</symbol>
<symbol id="thumb-down" viewBox="0 0 8 8">
<path d="M0 0v4h1v-4h-1zm2 0v4.001c.28 0 .529.101.719.281.18.19 1.151 2.115 1.281 2.375.13.26.386.393.656.313.26-.08.393-.355.313-.625-.08-.26-.469-1.594-.469-1.844s.22-.5.5-.5h1.5c.28 0 .5-.22.5-.5s-1.031-3.188-1.031-3.188c-.08-.18-.259-.313-.469-.313h-3.5z"></path>
</symbol>
<symbol id="thumb-up" viewBox="0 0 8 8">
<path d="M4.438 0c-.19.021-.34.149-.438.344-.13.26-1.101 2.185-1.281 2.375-.19.18-.439.281-.719.281v4.001h3.5c.21 0 .389-.133.469-.313 0 0 1.031-2.908 1.031-3.188 0-.28-.22-.5-.5-.5h-1.5c-.28 0-.5-.25-.5-.5s.389-1.574.469-1.844c.08-.27-.053-.545-.313-.625-.067-.02-.155-.038-.219-.031zm-4.438 3v4h1v-4h-1z"></path>
</symbol>
<symbol id="timer" viewBox="0 0 8 8">
<path d="M2 0v1h1v.031c-1.697.241-3 1.707-3 3.469 0 1.929 1.571 3.5 3.5 3.5s3.5-1.571 3.5-3.5c0-.45-.086-.874-.219-1.25l-.938.344c.107.304.156.596.156.906 0 1.391-1.109 2.5-2.5 2.5s-2.5-1.109-2.5-2.5 1.109-2.5 2.5-2.5c.298 0 .585.051.875.156l.344-.938c-.221-.081-.471-.119-.719-.156v-.063h1v-1h-3zm5 1.125s-3.675 2.8-3.875 3c-.2.2-.2.519 0 .719.2.2.519.2.719 0 .2-.19 3.156-3.719 3.156-3.719z"></path>
</symbol>
<symbol id="transfer" viewBox="0 0 8 8">
<path d="M6 0v1h-6v1h6v1l2-1.5-2-1.5zm-4 4l-2 1.5 2 1.5v-1h6v-1h-6v-1z"></path>
</symbol>
<symbol id="trash" viewBox="0 0 8 8">
<path d="M3 0c-.55 0-1 .45-1 1h-1c-.55 0-1 .45-1 1h7c0-.55-.45-1-1-1h-1c0-.55-.45-1-1-1h-1zm-2 3v4.813c0 .11.077.188.188.188h4.625c.11 0 .188-.077.188-.188v-4.813h-1v3.5c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-3.5h-1v3.5c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-3.5h-1z"></path>
</symbol>
<symbol id="underline" viewBox="0 0 8 8">
<path d="M1 0v4c0 1.1 1.12 2 2.5 2h.5c1.1 0 2-.9 2-2v-4h-1v4c0 .55-.45 1-1 1s-1-.45-1-1v-4h-2zm-1 7v1h7v-1h-7z"></path>
</symbol>
<symbol id="vertical-align-bottom" viewBox="0 0 8 8">
<path d="M.094 0c-.06 0-.094.034-.094.094v4.813c0 .06.034.094.094.094h1.813c.06 0 .094-.034.094-.094v-4.813c0-.06-.034-.094-.094-.094h-1.813zm6 0c-.06 0-.094.034-.094.094v4.813c0 .06.034.094.094.094h1.813c.06 0 .094-.034.094-.094v-4.813c0-.06-.034-.094-.094-.094h-1.813zm-3 2c-.06 0-.094.034-.094.094v2.813c0 .06.034.094.094.094h1.813c.06 0 .094-.034.094-.094v-2.813c0-.06-.034-.094-.094-.094h-1.813zm-3.094 4v1h8v-1h-8z"></path>
</symbol>
<symbol id="vertical-align-center" viewBox="0 0 8 8">
<path d="M.094 0c-.06 0-.094.034-.094.094v1.906h2v-1.906c0-.06-.034-.094-.094-.094h-1.813zm6 0c-.06 0-.094.034-.094.094v1.906h2v-1.906c0-.06-.034-.094-.094-.094h-1.813zm-3 1c-.06 0-.094.034-.094.094v.906h2v-.906c0-.06-.034-.094-.094-.094h-1.813zm-3.094 2v1h8v-1h-8zm0 2v1.906c0 .06.034.094.094.094h1.813c.06 0 .094-.034.094-.094v-1.906h-2zm3 0v.906c0 .06.034.094.094.094h1.813c.06 0 .094-.034.094-.094v-.906h-2zm3 0v1.906c0 .06.034.094.094.094h1.813c.06 0 .094-.034.094-.094v-1.906h-2z"></path>
</symbol>
<symbol id="vertical-align-top" viewBox="0 0 8 8">
<path d="M0 0v1h8v-1h-8zm.094 2c-.06 0-.094.034-.094.094v4.813c0 .06.034.094.094.094h1.813c.06 0 .094-.034.094-.094v-4.813c0-.06-.034-.094-.094-.094h-1.813zm3 0c-.06 0-.094.034-.094.094v2.813c0 .06.034.094.094.094h1.813c.06 0 .094-.034.094-.094v-2.813c0-.06-.034-.094-.094-.094h-1.813zm3 0c-.06 0-.094.034-.094.094v4.813c0 .06.034.094.094.094h1.813c.06 0 .094-.034.094-.094v-4.813c0-.06-.034-.094-.094-.094h-1.813z"></path>
</symbol>
<symbol id="video" viewBox="0 0 8 8">
<path d="M.5 0c-.28 0-.5.22-.5.5v4c0 .28.22.5.5.5h5c.28 0 .5-.22.5-.5v-1.5l1 1h1v-3h-1l-1 1v-1.5c0-.28-.22-.5-.5-.5h-5z" transform="translate(0 1)"></path>
</symbol>
<symbol id="volume-high" viewBox="0 0 8 8">
<path d="M3.344 0l-1.344 2h-2v4h2l1.344 2h.656v-8h-.656zm1.656 1v1c.152 0 .313.026.469.063h.031c.86.215 1.5.995 1.5 1.938 0 .942-.64 1.722-1.5 1.938-.166.041-.338.063-.5.063v1c.258 0 .516-.035.75-.094 1.3-.325 2.25-1.508 2.25-2.906 0-1.398-.95-2.581-2.25-2.906-.234-.059-.492-.094-.75-.094zm0 2v2c.04 0 .134-.002.25-.031.433-.118.75-.507.75-.969 0-.446-.325-.819-.75-.938v-.031c-.005-.001-.025.002-.031 0-.043-.011-.111-.031-.219-.031z"></path>
</symbol>
<symbol id="volume-low" viewBox="0 0 8 8">
<path d="M3.344 0l-1.344 2h-2v4h2l1.344 2h.656v-8h-.656zm1.656 3v2c.04 0 .134-.002.25-.031.433-.118.75-.507.75-.969 0-.446-.325-.819-.75-.938v-.031c-.005-.001-.025.002-.031 0-.043-.011-.111-.031-.219-.031z" transform="translate(1)"></path>
</symbol>
<symbol id="volume-off" viewBox="0 0 8 8">
<path d="M3.344 0l-1.344 2h-2v4h2l1.344 2h.656v-8h-.656z" transform="translate(2)"></path>
</symbol>
<symbol id="warning" viewBox="0 0 8 8">
<path d="M3.094 0c-.06 0-.105.044-.125.094l-2.938 6.813c-.02.05-.031.128-.031.188v.813c0 .06.034.094.094.094h6.813c.06 0 .094-.034.094-.094v-.813c0-.06-.011-.128-.031-.188l-2.938-6.813c-.02-.05-.065-.094-.125-.094h-.813zm-.094 3h1v2h-1v-2zm0 3h1v1h-1v-1z"></path>
</symbol>
<symbol id="wifi" viewBox="0 0 8 8">
<path d="M3.75 0c-1.374 0-2.66.372-3.75 1.063l.531.875c.93-.59 2.033-.938 3.219-.938 1.2 0 2.323.31 3.25.906l.531-.813c-1.093-.703-2.401-1.094-3.781-1.094zm.031 3c-.795 0-1.531.227-2.156.625l.531.844c.475-.302 1.02-.469 1.625-.469.593 0 1.13.177 1.594.469l.531-.844c-.616-.388-1.338-.625-2.125-.625zm-.031 3c-.552 0-1 .448-1 1s.448 1 1 1 1-.448 1-1-.448-1-1-1z"></path>
</symbol>
<symbol id="wrench" viewBox="0 0 8 8">
<path d="M5.5 0c-1.38 0-2.5 1.12-2.5 2.5 0 .32.078.626.188.906l-2.906 2.875c-.39.39-.39 1.016 0 1.406.2.2.459.313.719.313.26 0 .519-.091.719-.281l2.875-2.875c.28.1.586.156.906.156 1.38 0 2.5-1.12 2.5-2.5 0-.16-.032-.297-.063-.438l-.938.938h-2v-2l.938-.938c-.14-.03-.277-.062-.438-.063zm-4.5 6.5c.28 0 .5.22.5.5s-.22.5-.5.5-.5-.22-.5-.5.22-.5.5-.5z"></path>
</symbol>
<symbol id="x" viewBox="0 0 8 8">
<path d="M1.406 0l-1.406 1.406.688.719 1.781 1.781-1.781 1.781-.688.719 1.406 1.406.719-.688 1.781-1.781 1.781 1.781.719.688 1.406-1.406-.688-.719-1.781-1.781 1.781-1.781.688-.719-1.406-1.406-.719.688-1.781 1.781-1.781-1.781-.719-.688z"></path>
</symbol>
<symbol id="yen" viewBox="0 0 8 8">
<path d="M0 0l2.25 3h-2.25v1h3v1h-3v1h3v2h1v-2h3v-1h-3v-1h3v-1h-2.25l2.25-3h-1l-2.313 3h-.375l-2.313-3h-1z"></path>
</symbol>
<symbol id="zoom-in" viewBox="0 0 8 8">
<path d="M3.5 0c-1.927 0-3.5 1.573-3.5 3.5s1.573 3.5 3.5 3.5c.592 0 1.166-.145 1.656-.406a1 1 0 0 0 .094.094l1.031 1.031a1.016 1.016 0 1 0 1.438-1.438l-1.031-1.031a1 1 0 0 0-.125-.094c.266-.493.438-1.059.438-1.656 0-1.927-1.573-3.5-3.5-3.5zm0 1c1.387 0 2.5 1.113 2.5 2.5 0 .587-.196 1.137-.531 1.563-.009.012-.022.02-.031.031a1 1 0 0 0-.063.031 1 1 0 0 0-.281.281 1 1 0 0 0-.063.063c-.422.326-.953.531-1.531.531-1.387 0-2.5-1.113-2.5-2.5s1.113-2.5 2.5-2.5zm-.5 1v1h-1v1h1v1h1v-1h1v-1h-1v-1h-1z"></path>
</symbol>
<symbol id="zoom-out" viewBox="0 0 8 8">
<path d="M3.5 0c-1.927 0-3.5 1.573-3.5 3.5s1.573 3.5 3.5 3.5c.592 0 1.166-.145 1.656-.406a1 1 0 0 0 .094.094l1.031 1.031a1.016 1.016 0 1 0 1.438-1.438l-1.031-1.031a1 1 0 0 0-.125-.094c.266-.493.438-1.059.438-1.656 0-1.927-1.573-3.5-3.5-3.5zm0 1c1.387 0 2.5 1.113 2.5 2.5 0 .587-.196 1.137-.531 1.563-.009.012-.022.02-.031.031a1 1 0 0 0-.063.031 1 1 0 0 0-.281.281 1 1 0 0 0-.063.063c-.422.326-.953.531-1.531.531-1.387 0-2.5-1.113-2.5-2.5s1.113-2.5 2.5-2.5zm-1.5 2v1h3v-1h-3z"></path>
</symbol>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 57 KiB

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/Mobile.css" rel="stylesheet" type="text/css" data-name="main" />
<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>
<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">
@ -87,13 +91,12 @@
<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/underscore/underscore.js"></script>
<script src="bower_components/angular-underscore/angular-underscore.js"></script>
<script src="bower_components/angular-locker/dist/angular-locker.min.js"></script>
<script src="bower_components/angular-ui-utils/keypress.js"></script>
<!-- endbower -->
<script src="vendor/jquery.base64.js"></script>
<script src="vendor/jquery.dateFormat-1.0.js"></script>
<!-- endbuild -->
<!-- our scripts -->
<!-- build:js(app) scripts/scripts.min.js -->
@ -113,6 +116,7 @@
<script src="player/player.js"></script>
<script src="player/player-directive.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="common/filters.js"></script>
<script src="common/directives.js"></script>

View file

@ -32,7 +32,6 @@ describe("jplayer directive", function() {
return $delegate;
});
$provide.value('globals', mockGlobals);
$provide.constant('jamstashVersion', '1.0.0');
});
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.
*/
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';
var playerVolume = 1.0;
@ -18,6 +18,10 @@ angular.module('jamstash.player.service', ['jamstash.settings.service', 'angular
pauseSong: false,
restartSong: false,
loadSong: false,
settings: {
repeat: "none",
repeatValues: ["queue", "song", "none"]
},
play: function (song) {
// 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
songEnded: function () {
if (globals.settings.Repeat) {
if (player.settings.repeat === "song") {
// repeat current track
player.restart();
} else if (player.isLastSongPlaying() === true) {
if (globals.settings.LoopQueue) {
if (player.settings.repeat === "queue") {
// Loop to first track in queue
player.playFirstSong();
}
@ -101,7 +105,13 @@ angular.module('jamstash.player.service', ['jamstash.settings.service', 'angular
},
shuffleQueue: function () {
player.queue = _(player.queue).shuffle();
var shuffled = _(player.queue).without(player._playingSong);
shuffled = _(shuffled).shuffle();
if(player._playingSong !== undefined) {
shuffled.unshift(player._playingSong);
player._playingIndex = 0;
}
player.queue = shuffled;
return player;
},

View file

@ -1,21 +1,13 @@
describe("Player service -", function() {
'use strict';
var player, mockGlobals, firstSong, secondSong, thirdSong, newSong;
var player, firstSong, secondSong, thirdSong, newSong;
beforeEach(function() {
// We redefine globals because in some tests we need to alter the settings
mockGlobals = {
settings: {
Repeat: false,
LoopQueue: false
}
};
module('jamstash.player.service', function ($provide) {
$provide.value('globals', mockGlobals);
});
module('jamstash.player.service');
inject(function (_player_) {
player = _player_;
});
player.settings.repeat = "none";
});
describe("Given that I have 3 songs in my playing queue,", function() {
@ -138,6 +130,25 @@ describe("Player service -", function() {
expect(player.queue).toEqual([]);
});
it("and given the third song was playing, when I shuffle the playing queue, then the third song will be at the first position and the rest of the queue will be shuffled", function() {
player._playingSong = thirdSong;
player.shuffleQueue();
expect(player.queue[0]).toBe(thirdSong);
expect(player.queue).toContain(firstSong);
expect(player.queue).toContain(secondSong);
});
it("and given no song was playing, when I shuffle the playing queue, then the whole queue will be shuffled", function() {
player.shuffleQueue();
expect(player.queue).toContain(firstSong);
expect(player.queue).toContain(secondSong);
expect(player.queue).toContain(thirdSong);
expect(player.queue).not.toContain(undefined);
});
it("when I get the index of the first song, it returns 0", function() {
expect(player.indexOfSong(firstSong)).toBe(0);
});
@ -187,9 +198,9 @@ describe("Player service -", function() {
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");
mockGlobals.settings.Repeat = true;
player.settings.repeat = "song";
player.songEnded();
@ -201,9 +212,9 @@ describe("Player service -", function() {
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");
mockGlobals.settings.LoopQueue = true;
player.settings.repeat = "queue";
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 id="songdetails">
<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" />
</a>
</div>
@ -27,9 +27,9 @@
</ul>
<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="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 title="Favorite" href="" ng-class="{'favorite': getPlayingSong().starred, 'rate': !getPlayingSong().starred}" ng-click="updateFavorite(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_UnMute" class="unmute" title="Unmute" style="display: none;"></a>
<!--<div class="jp-volume-bar"><div class="jp-volume-bar-value"></div></div><a href="" id="action_VolumeMax" class="volume" title="Max Volume"></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
* 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',
function($scope, player, globals){
function ($scope, player, globals) {
'use strict';
$scope.getPlayingSong = player.getPlayingSong;
$scope.settings = globals.settings;
$scope.playerSettings = player.settings;
$scope.play = function () {
if (globals.settings.Jukebox) {
@ -31,6 +33,4 @@ angular.module('jamstash.player.controller', ['jamstash.player.service', 'jamsta
$scope.previousTrack = player.previousTrack;
$scope.nextTrack = player.nextTrack;
//TODO: Hyz: updateFavorite - leave in rootScope ?
}]);

View file

@ -1,40 +1,64 @@
describe("Player controller", function() {
'use strict';
var player, scope;
var player, scope, mockGlobals;
beforeEach(function() {
// We redefine globals because in some tests we need to alter the settings
mockGlobals = {
settings: {
Jukebox: false
}
};
module('jamstash.player.controller');
inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
player = jasmine.createSpyObj("player", ["getPlayingSong", "previousTrack", "nextTrack"]);
player = jasmine.createSpyObj("player", [
"getPlayingSong",
"previousTrack",
"nextTrack",
"getRepeatValues",
"togglePause"
]);
$controller('PlayerController', {
$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();
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();
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();
expect(player.nextTrack).toHaveBeenCalled();
});
// TODO: updateFavorite
});

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

@ -10,7 +10,7 @@
<li class="row song id{{o.id}}" ng-repeat="o in song" ng-click="selectSong(o)" ng-dblclick="playSong(o)" ng-class="{'selected': o.selected, 'playing': isPlayingSong(o)}">
<div class="itemactions">
<a class="remove" href="" title="Remove Song" ng-click="removeSongFromQueue(o)" stop-event="click"></a>
<a href="" title="Favorite" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="updateFavorite(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>
<div class="clear"></div>
</div>
<div class="title floatleft" title="{{o.description}}" ng-bind-html="o.name"></div>

View file

@ -4,7 +4,7 @@
* 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'])
angular.module('jamstash.queue.controller', ['jamstash.player.service', 'jamstash.settings.service'])
.controller('QueueController', ['$scope', 'globals', 'player',
function ($scope, globals, player) {
@ -16,21 +16,23 @@ angular.module('jamstash.queue.controller', ['jamstash.player.service'])
player.play(song);
};
$scope.emptyQueue = function() {
$scope.emptyQueue = function () {
player.emptyQueue();
//TODO: Hyz: Shouldn't it be in a directive ?
$.fancybox.close();
};
$scope.shuffleQueue = function() {
$scope.shuffleQueue = function () {
player.shuffleQueue();
//TODO: Hyz: Shouldn't it be in a directive ?
$('#SideBar').stop().scrollTo('.header', 400);
};
$scope.addSongToQueue = function(song) {
$scope.addSongToQueue = function (song) {
player.addSong(song);
};
$scope.removeSongFromQueue = function(song) {
$scope.removeSongFromQueue = function (song) {
player.removeSong(song);
};
@ -63,6 +65,4 @@ angular.module('jamstash.queue.controller', ['jamstash.player.service'])
end = ui.item.index();
player.queue.splice(end, 0, player.queue.splice(start, 1)[0]);
};
//TODO: Hyz: updateFavorite - leave in rootScope ?
}]);

View file

@ -36,10 +36,14 @@ describe("Queue controller", function() {
expect($.fancybox.close).toHaveBeenCalled();
});
it("When I shuffle the queue, it calls shuffleQueue in the player service", function() {
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");
spyOn($.fn, 'scrollTo');
scope.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() {

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>
<label for="AutoPlay">Auto Play</label>
<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>
<label for="HideAZ">Hide A-Z</label>
<div class="clear"></div>

View file

@ -71,6 +71,7 @@ angular.module('jamstash.settings.controller', ['jamstash.settings.service', 'ja
$rootScope.showIndex = true;
}, function (error) {
//TODO: Hyz: Duplicate from subsonic.js - requestSongs. Find a way to handle this only once.
globals.settings.ApiVersion = error.version;
var errorNotif;
if (error.subsonicError !== undefined) {
errorNotif = error.reason + ' ' + error.subsonicError.message;

View file

@ -90,6 +90,24 @@ describe("Settings controller", function() {
expect(subsonic.ping).toHaveBeenCalled();
});
it("Given the server and Jamstash had different api versions, when I save the settings and the server responds an error, then the ApiVersion setting will be updated with the one sent from the server", function() {
scope.settings.Server = 'http://gallotannate.com/tetranychus/puzzlement?a=stoically&b=mantuamaker#marianolatrist';
scope.settings.Username = 'Vandervelden';
scope.settings.Password = 'PA3DhdfAu0dy';
scope.ApiVersion = '1.10.2';
subsonic.ping.and.returnValue(deferred.promise);
scope.save();
deferred.reject({
reason: 'Error when contacting the Subsonic server.',
subsonicError: {code: 30, message: 'Incompatible Subsonic REST protocol version. Server must upgrade.'},
version: '1.8.0'
});
scope.$apply();
expect(mockGlobals.settings.ApiVersion).toEqual('1.8.0');
});
});
it("reset() - When I reset the settings, they will be deleted from the persistence service and will be reloaded with default values", function() {

View file

@ -1474,15 +1474,6 @@ ul.songlist li:hover
display: block;
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
{
float: left;
@ -1833,5 +1824,3 @@ legend
font-variant: small-caps;
font-weight: bold;
}

View file

@ -5,10 +5,10 @@
* Also offers more fine-grained functionality that is not part of Subsonic's API.
*/
angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
'jamstash.settings.service', 'jamstash.utils', 'jamstash.model', 'jamstash.notifications', 'jamstash.player.service'])
'jamstash.settings.service', 'jamstash.utils', 'jamstash.model'])
.factory('subsonic', ['$rootScope', '$http', '$q', 'globals', 'utils', 'map', 'notifications', 'player',
function ($rootScope, $http, $q, globals, utils, map, notifications, player) {
.factory('subsonic', ['$rootScope', '$http', '$q', 'globals', 'utils', 'map',
function ($rootScope, $http, $q, globals, utils, map) {
'use strict';
//TODO: Hyz: Remove when refactored
@ -31,9 +31,10 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
var offset = 0;
var showPlaylist = false;
return {
var subsonicService = {
showIndex: $rootScope.showIndex,
showPlaylist: showPlaylist,
//TODO: Hyz: Do we still need this ? it's only used in the songpreview directive
getSongTemplate: function (callback) {
var id = '16608';
var url = globals.BaseURL() + '/getMusicDirectory.view?' + globals.BaseParams() + '&id=' + id;
@ -106,6 +107,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
} else {
if(subsonicResponse.status === 'failed' && subsonicResponse.error !== undefined) {
exception.subsonicError = subsonicResponse.error;
exception.version = subsonicResponse.version;
}
deferred.reject(exception);
}
@ -117,28 +119,36 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
},
ping: function () {
return this.subsonicRequest('ping.view');
return subsonicService.subsonicRequest('ping.view');
},
getMusicFolders: function () {
var exception = {reason: 'No music folder found on the Subsonic server.'};
var promise = subsonicService.subsonicRequest('getMusicFolders.view')
.then(function (subsonicResponse) {
if (subsonicResponse.musicFolders !== undefined && subsonicResponse.musicFolders.musicFolder !== undefined) {
return [].concat(subsonicResponse.musicFolders.musicFolder);
} else {
return $q.reject(exception);
}
});
return promise;
},
getArtists: function (folder) {
var exception = {reason: 'No artist found on the Subsonic server.'};
// TODO: Hyz: Move loading / saving the music folder to persistence-service
if (isNaN(folder) && utils.getValue('MusicFolders')) {
var musicFolder = angular.fromJson(utils.getValue('MusicFolders'));
folder = musicFolder.id;
}
var params;
if (!isNaN(folder)) {
params = {
musicFolderId: folder
};
}
var promise = this.subsonicRequest('getIndexes.view', {
var promise = subsonicService.subsonicRequest('getIndexes.view', {
params: params
}).then(function (subsonicResponse) {
if(subsonicResponse.indexes !== undefined && (subsonicResponse.indexes.index !== undefined || subsonicResponse.indexes.shortcut !== undefined)) {
// Make sure shortcut, index and each index's artist are arrays
// because Madsonic will return objects and not arrays if there is only 1 artist
// because Madsonic will return an object when there's only one element
var formattedResponse = {};
formattedResponse.shortcut = [].concat(subsonicResponse.indexes.shortcut);
formattedResponse.index = [].concat(subsonicResponse.indexes.index);
@ -160,10 +170,11 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
var params = {
id: id
};
var promise = this.subsonicRequest('getMusicDirectory.view', {
var promise = subsonicService.subsonicRequest('getMusicDirectory.view', {
params: params
}).then(function (subsonicResponse) {
if(subsonicResponse.directory.child !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var childArray = [].concat(subsonicResponse.directory.child);
if (childArray.length > 0) {
content.song = [];
@ -199,10 +210,11 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
type: id,
offset: offset
};
var promise = this.subsonicRequest('getAlbumList.view', {
var promise = subsonicService.subsonicRequest('getAlbumList.view', {
params: params
}).then(function (subsonicResponse) {
if(subsonicResponse.albumList.album !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var albumArray = [].concat(subsonicResponse.albumList.album);
if (albumArray.length > 0) {
content.song = [];
@ -253,52 +265,25 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
});
return deferred.promise;
},
getSongs: function (id, action) {
var exception = {reason: 'No songs found on the Subsonic server.'};
var promise = this.subsonicRequest('getMusicDirectory.view', {
getSongs: function (id) {
var exception = {reason: 'This directory is empty.'};
var promise = subsonicService.subsonicRequest('getMusicDirectory.view', {
params: {
id: id
}
}).then(function (subsonicResponse) {
if(subsonicResponse.directory.child !== undefined) {
var items = [].concat(subsonicResponse.directory.child);
if (items.length > 0) {
content.selectedAlbum = id;
if (action == 'add') {
angular.forEach(items, function (item, key) {
player.queue.push(map.mapSong(item));
});
notifications.updateMessage(items.length + ' Song(s) Added to Queue', true);
} else if (action == 'play') {
player.queue = [];
angular.forEach(items, function (item, key) {
player.queue.push(map.mapSong(item));
});
var next = player.queue[0];
player.play(next);
notifications.updateMessage(items.length + ' Song(s) Added to Queue', true);
} else {
if (subsonicResponse.directory.id != 'undefined') {
var albumId = subsonicResponse.directory.id;
var albumName = subsonicResponse.directory.name;
if (content.breadcrumb.length > 0) { content.breadcrumb.splice(1, (content.breadcrumb.length - 1)); }
content.breadcrumb.push({ 'type': 'album', 'id': albumId, 'name': albumName });
}
content.song = [];
content.album = [];
var albums = [];
angular.forEach(items, function (item, key) {
if (item.isDir) {
albums.push(map.mapAlbum(item));
} else {
content.song.push(map.mapSong(item));
}
});
if (albums.length > 0) {
content.album = albums;
}
}
return content;
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var children = [].concat(subsonicResponse.directory.child);
if (children.length > 0) {
var allChildren = _(children).partition(function (item) {
return item.isDir;
});
return {
directories: map.mapAlbums(allChildren[0]),
songs: map.mapSongs(allChildren[1])
};
}
}
// We end up here for every else
@ -307,28 +292,73 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
return promise;
},
// This is used when we add or play a directory, so we recursively get all its contents
recursiveGetSongs: function (id) {
var deferred = $q.defer();
// We first use getSongs() to get the contents of the root directory
subsonicService.getSongs(id).then(function (data) {
var directories = data.directories;
var songs = data.songs;
// If there are only songs, we return them immediately: this is a leaf directory and the end of the recursion
if (directories.length === 0) {
deferred.resolve(songs);
} else {
// otherwise, for each directory, we call ourselves
var promises = [];
angular.forEach(directories, function (dir) {
var subdirectoryRequest = subsonicService.recursiveGetSongs(dir.id).then(function (data) {
// This is where we join all the songs together in a single array
return songs.concat(data);
});
promises.push(subdirectoryRequest);
});
// since all of this is asynchronous, we need to wait for all the requests to finish by using $q.all()
var allRequestsFinished = $q.all(promises).then(function (data) {
// and since $q.all() wraps everything in another array, we use flatten() to end up with only one array of songs
return _(data).flatten();
});
deferred.resolve(allRequestsFinished);
}
}, function () {
// Even if getSongs returns an error, we resolve with an empty array. Otherwise one empty directory somewhere
// would keep us from playing all the songs of a directory recursively
deferred.resolve([]);
});
return deferred.promise;
},
search: function (query, type) {
if(_([0, 1, 2]).contains(type)) {
var promise = this.subsonicRequest('search2.view', {
var promise = subsonicService.subsonicRequest('search2.view', {
params: {
query: query
}
}).then(function (subsonicResponse) {
var searchResult;
if (!_.isEmpty(subsonicResponse.searchResult2)) {
searchResult = subsonicResponse.searchResult2;
} else if (!_.isEmpty(subsonicResponse.search2)) {
// We also check search2 because Music Cabinet doesn't respond the same thing
// as everyone else...
searchResult = subsonicResponse.search2;
}
if (!_.isEmpty(searchResult)) {
// Make sure that song, album and artist are arrays using concat
// because Madsonic will return an object when there's only one element
switch (type) {
case 0:
if (subsonicResponse.searchResult2.song !== undefined) {
return map.mapSongs([].concat(subsonicResponse.searchResult2.song));
if (searchResult.song !== undefined) {
return map.mapSongs([].concat(searchResult.song));
}
break;
case 1:
if (subsonicResponse.searchResult2.album !== undefined) {
return map.mapAlbums([].concat(subsonicResponse.searchResult2.album));
if (searchResult.album !== undefined) {
return map.mapAlbums([].concat(searchResult.album));
}
break;
case 2:
if (subsonicResponse.searchResult2.artist !== undefined) {
return [].concat(subsonicResponse.searchResult2.artist);
if (searchResult.artist !== undefined) {
return [].concat(searchResult.artist);
}
break;
}
@ -353,10 +383,11 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
if (!isNaN(folder)) {
params.musicFolderId = folder;
}
var promise = this.subsonicRequest('getRandomSongs.view', {
var promise = subsonicService.subsonicRequest('getRandomSongs.view', {
params: params
}).then(function (subsonicResponse) {
if(subsonicResponse.randomSongs !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var songArray = [].concat(subsonicResponse.randomSongs.song);
if (songArray.length > 0) {
return map.mapSongs(songArray);
@ -369,7 +400,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
},
getStarred: function () {
var promise = this.subsonicRequest('getStarred.view', { cache: true })
var promise = subsonicService.subsonicRequest('getStarred.view', { cache: true })
.then(function (subsonicResponse) {
if(angular.equals(subsonicResponse.starred, {})) {
return $q.reject({reason: 'Nothing is starred on the Subsonic server.'});
@ -381,9 +412,10 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
},
getRandomStarredSongs: function () {
var promise = this.getStarred()
var promise = subsonicService.getStarred()
.then(function (starred) {
if(starred.song !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var songArray = [].concat(starred.song);
if (songArray.length > 0) {
// Return random subarray of songs
@ -399,9 +431,10 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
getPlaylists: function () {
var exception = {reason: 'No playlist found on the Subsonic server.'};
var promise = this.subsonicRequest('getPlaylists.view')
var promise = subsonicService.subsonicRequest('getPlaylists.view')
.then(function (subsonicResponse) {
if(subsonicResponse.playlists.playlist !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var playlistArray = [].concat(subsonicResponse.playlists.playlist);
if (playlistArray.length > 0) {
var allPlaylists = _(playlistArray).partition(function (item) {
@ -418,12 +451,13 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
getPlaylist: function (id) {
var exception = {reason: 'This playlist is empty.'};
var promise = this.subsonicRequest('getPlaylist.view', {
var promise = subsonicService.subsonicRequest('getPlaylist.view', {
params: {
id: id
}
}).then(function (subsonicResponse) {
if (subsonicResponse.playlist.entry !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var entryArray = [].concat(subsonicResponse.playlist.entry);
if (entryArray.length > 0) {
return map.mapSongs(entryArray);
@ -436,7 +470,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
},
newPlaylist: function (name) {
var promise = this.subsonicRequest('createPlaylist.view', {
var promise = subsonicService.subsonicRequest('createPlaylist.view', {
params: {
name: name
}
@ -445,7 +479,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
},
deletePlaylist: function (id) {
var promise = this.subsonicRequest('deletePlaylist.view', {
var promise = subsonicService.subsonicRequest('deletePlaylist.view', {
params: {
id: id
}
@ -463,7 +497,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
for (var i = 0; i < songs.length; i++) {
params.params.songId.push(songs[i].id);
}
return this.subsonicRequest('createPlaylist.view', params);
return subsonicService.subsonicRequest('createPlaylist.view', params);
},
//TODO: Hyz: move to controller
@ -507,13 +541,14 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
getPodcasts: function () {
var exception = {reason: 'No podcast found on the Subsonic server.'};
var promise = this.subsonicRequest('getPodcasts.view', {
var promise = subsonicService.subsonicRequest('getPodcasts.view', {
params: {
includeEpisodes: false
}
})
.then(function (subsonicResponse) {
if (subsonicResponse.podcasts !== undefined && subsonicResponse.podcasts.channel !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var channelArray = [].concat(subsonicResponse.podcasts.channel);
if (channelArray.length > 0) {
return channelArray;
@ -527,7 +562,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
getPodcast: function (id) {
var exception = {reason: 'This podcast was not found on the Subsonic server.'};
var promise = this.subsonicRequest('getPodcasts.view', {
var promise = subsonicService.subsonicRequest('getPodcasts.view', {
params: {
id: id,
includeEpisodes: true
@ -535,10 +570,12 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
}).then(function (subsonicResponse) {
var episodes = [];
if (subsonicResponse.podcasts.channel !== undefined) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var channelArray = [].concat(subsonicResponse.podcasts.channel);
if (channelArray.length > 0) {
var channel = channelArray[0];
if (channel !== null && channel.id === id) {
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
var episodesArray = [].concat(channel.episode);
episodes = _(episodesArray).filter(function (episode) {
return episode.status === "completed";
@ -558,7 +595,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
},
scrobble: function (song) {
var promise = this.subsonicRequest('scrobble.view', {
var promise = subsonicService.subsonicRequest('scrobble.view', {
params: {
id: song.id,
submisssion: true
@ -568,7 +605,20 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
return true;
});
return promise;
},
toggleStar: function (item) {
var partialUrl = (item.starred) ? 'unstar.view' : 'star.view';
var promise = subsonicService.subsonicRequest(partialUrl, {
params: {
id: item.id
}
}).then(function () {
return !item.starred;
});
return promise;
}
// End subsonic
};
return subsonicService;
}]);

View file

@ -1,8 +1,8 @@
describe("Subsonic service -", function() {
'use strict';
var subsonic, mockBackend, mockGlobals;
var response;
var $q, mockBackend, subsonic, mockGlobals,
response, url;
beforeEach(function() {
// We redefine it because in some tests we need to alter the settings
mockGlobals = {
@ -46,9 +46,12 @@ describe("Subsonic service -", function() {
});
});
inject(function (_subsonic_, $httpBackend) {
installPromiseMatchers();
inject(function (_subsonic_, $httpBackend, _$q_) {
subsonic = _subsonic_;
mockBackend = $httpBackend;
$q = _$q_;
});
response = {"subsonic-response": {status: "ok", version: "1.10.2"}};
});
@ -59,40 +62,65 @@ describe("Subsonic service -", function() {
});
describe("subsonicRequest() -", function() {
var partialUrl, url;
var partialUrl;
beforeEach(function() {
partialUrl = '/getStarred.view';
url ='http://demo.subsonic.com/rest/getStarred.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
});
it("Given that the Subsonic server is not responding, when I make a request to Subsonic it returns an error object with a message", function() {
it("Given that the Subsonic server was not responding, when I make a request to Subsonic, then an error object with a message will be returned", function() {
mockBackend.expectJSONP(url).respond(503, 'Service Unavailable');
var promise = subsonic.subsonicRequest(partialUrl);
mockBackend.flush();
expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', httpError: 503});
expect(promise).toBeRejectedWith({
reason: 'Error when contacting the Subsonic server.',
httpError: 503
});
});
it("Given a missing parameter, when I make a request to Subsonic it returns an error object with a message", function() {
it("Given a missing parameter, when I make a request to Subsonic, then an error object with a message will be returned", function() {
delete mockGlobals.settings.Password;
var missingPasswordUrl = 'http://demo.subsonic.com/rest/getStarred.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&u=Hyzual&v=1.10.2';
var errorResponse = {"subsonic-response" : {
"status" : "failed",
"version" : "1.10.2",
"error" : {"code" : 10,"message" : "Required parameter is missing."}
var errorResponse = {'subsonic-response' : {
status : 'failed',
version : '1.10.2',
error : {code : 10, message : 'Required parameter is missing.'}
}};
mockBackend.expectJSONP(missingPasswordUrl).respond(JSON.stringify(errorResponse));
var promise = subsonic.subsonicRequest(partialUrl);
mockBackend.flush();
expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', subsonicError: {code: 10, message:'Required parameter is missing.'}});
expect(promise).toBeRejectedWith({
reason: 'Error when contacting the Subsonic server.',
subsonicError: {code: 10, message:'Required parameter is missing.'},
version: '1.10.2'
});
});
it("Given a partialUrl that does not start with '/', it adds '/' before it and makes a correct request", function() {
it("Given that server and Jamstash had different api versions, when I make a request to Subsonic and the server responds an error, then it will return the server's api version in the error object", function() {
var errorResponse = {'subsonic-response': {
status: 'failed',
version: '1.8.0',
error: {code: 30, message: 'Incompatible Subsonic REST protocol version. Server must upgrade.'}
}};
mockBackend.expectJSONP(url).respond(JSON.stringify(errorResponse));
var promise = subsonic.subsonicRequest(partialUrl);
mockBackend.flush();
expect(promise).toBeRejectedWith({
reason: 'Error when contacting the Subsonic server.',
subsonicError: {code: 30, message: 'Incompatible Subsonic REST protocol version. Server must upgrade.'},
version: '1.8.0'
});
});
it("Given a partialUrl that did not start with '/', when I make a request to Subsonic, then a '/' will be added before the partial url", function() {
partialUrl = 'getStarred.view';
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
@ -100,7 +128,7 @@ describe("Subsonic service -", function() {
mockBackend.flush();
});
it("Given $http config params, it does not overwrite them", function() {
it("Given $http config params, when I make a request to Subsonic, then the params won't be overwritten", function() {
partialUrl = 'scrobble.view';
url ='http://demo.subsonic.com/rest/scrobble.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&id=75&p=enc:cGFzc3dvcmQ%3D&submission=false&u=Hyzual&v=1.10.2';
@ -115,7 +143,7 @@ describe("Subsonic service -", function() {
mockBackend.flush();
});
it("Given that the global protocol setting is 'json', when I make a request to Subsonic it uses GET and does not use the JSON_CALLBACK parameter", function() {
it("Given that the global protocol setting was 'json', when I make a request to Subsonic, then it will use GET and won't use the JSON_CALLBACK parameter", function() {
mockGlobals.settings.Protocol = 'json';
var getUrl = 'http://demo.subsonic.com/rest/getStarred.view?'+
'c=Jamstash&f=json&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
@ -129,7 +157,6 @@ describe("Subsonic service -", function() {
});
describe("getAlbums() -", function() {
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=21'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
@ -182,62 +209,127 @@ describe("Subsonic service -", function() {
});
});
//TODO: Hyz: Rename into getDirectory(), because we don't know if there will be songs or other directories in it
describe("getSongs() -", function() {
var url;
beforeEach(function() {
var url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+
url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=209'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
});
it("Given that there were 2 songs in the given directory id in my library, when I get the songs from that directory, then a promise will be resolved with an array of 2 songs", function() {
it("Given a directory containing 2 songs and 1 subdirectory and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 2 songs and an array of 1 subdirectory", function() {
response["subsonic-response"].directory = {
child: [
{ id: 778 },
{ id: 614 }
{ id: 614 },
{ id: 205, isDir: true}
]
};
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
var promise = subsonic.getSongs(209);
//TODO: Hyz: Replace with toBeResolvedWith() when getSongs() is refactored
var success = function (data) {
expect(data.album).toEqual([]);
expect(data.song).toEqual([
{ id: 778 },
{ id: 614 }
]);
};
promise.then(success);
mockBackend.flush();
expect(promise).toBeResolved();
expect(promise).toBeResolvedWith({
directories: [{ id: 205, isDir: true}],
songs: [{id: 778}, {id: 614}]
});
});
it("Given that there was only 1 song in the given directory id in my Madsonic library, when I get the songs from that directory, then a promise will be resolved with an array of 1 song", function() {
it("Given a directory containing 1 song in my Madsonic library and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 1 song and an empty array of subdirectories", function() {
response["subsonic-response"].directory = {
child: { id: 402 }
};
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
var promise = subsonic.getSongs(209);
//TODO: Hyz: Replace with toBeResolvedWith() when getSongs() is refactored
var success = function (data) {
expect(data.album).toEqual([]);
expect(data.song).toEqual([
{ id: 402 }
]);
};
promise.then(success);
mockBackend.flush();
expect(promise).toBeResolved();
expect(promise).toBeResolvedWith({
directories: [],
songs: [{id: 402}]
});
});
it("Given a directory that didn't contain anything and given its id, when I get the songs from that directory, then a promise will be rejected with an error message", function() {
response["subsonic-response"].directory = {};
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
var promise = subsonic.getSongs(209);
mockBackend.flush();
expect(promise).toBeRejectedWith({reason: 'This directory is empty.'});
});
});
describe("recursiveGetSongs() -", function() {
var deferred;
beforeEach(function() {
deferred = $q.defer();
spyOn(subsonic, 'getSongs').and.returnValue(deferred.promise);
});
it("Given a directory containing 2 songs and a subdirectory itself containing 2 songs and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 4 songs", function() {
// Mock getSongs so we are only testing the recursivity
var firstDeferred = $q.defer();
var secondDeferred = $q.defer();
subsonic.getSongs.and.callFake(function (id) {
// First call to getSongs
if (id === 499) {
return firstDeferred.promise;
// Second call to getSongs
} else if (id === 553) {
return secondDeferred.promise;
}
});
var promise = subsonic.recursiveGetSongs(499);
// On the first call to getSongs, we expect 2 songs and a subdirectory
firstDeferred.resolve({
directories: [{ id: 553, type: 'byfolder' }],
songs: [
{ id: 695 },
{ id: 227 }
]
});
// On the second call, we expect 2 songs
secondDeferred.resolve({
directories: [],
songs: [
{ id: 422 },
{ id: 171 }
]
});
expect(promise).toBeResolvedWith([
{ id: 695 },
{ id: 227 },
{ id: 422 },
{ id: 171 },
]);
});
it("Given a directory containing only 2 songs and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 2 songs", function() {
var promise = subsonic.recursiveGetSongs(14);
deferred.resolve({
directories: [],
songs: [
{ id: 33 }, { id: 595 }
]
});
expect(promise).toBeResolvedWith([
{ id: 33 }, { id: 595 }
]);
});
it("Given a directory that didn't contain anything and given its id, when I get the songs from that directory, then a promise will be resolved with an empty array", function() {
var promise = subsonic.recursiveGetSongs(710);
deferred.reject({reason: 'This directory is empty.'});
expect(promise).toBeResolvedWith([]);
});
});
describe("getAlbumListBy() -", function() {
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getAlbumList.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&offset=0'+'&p=enc:cGFzc3dvcmQ%3D'+'&size=3&type=newest'+'&u=Hyzual&v=1.10.2';
@ -295,7 +387,7 @@ describe("Subsonic service -", function() {
});
describe("getStarred() -", function() {
var url
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getStarred.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
@ -331,7 +423,6 @@ describe("Subsonic service -", function() {
});
describe("getRandomStarredSongs() -", function() {
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getStarred.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
@ -393,7 +484,6 @@ describe("Subsonic service -", function() {
});
describe("getRandomSongs() -", function() {
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getRandomSongs.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D'+'&size=3'+'&u=Hyzual&v=1.10.2';
@ -507,8 +597,83 @@ describe("Subsonic service -", function() {
expect(promise).toBeResolvedWith(true);
});
describe("toggleStar() -", function() {
it("Given an item (can be an artist, an album or a song) that wasn't starred, when I toggle its star, then a promise will be resolved with true", function() {
var song = { id: 7748, starred: false };
var url = 'http://demo.subsonic.com/rest/star.view?' +
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=7748'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
var promise = subsonic.toggleStar(song);
mockBackend.flush();
expect(promise).toBeResolvedWith(true);
});
it("Given an item (can be an artist, an album or a song) that was starred, when I toggle its star, then a promise will be resolved with false", function() {
var album = { id: 6631, starred: true };
var url = 'http://demo.subsonic.com/rest/unstar.view?' +
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=6631'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
var promise = subsonic.toggleStar(album);
mockBackend.flush();
expect(promise).toBeResolvedWith(false);
});
});
describe("getMusicFolders() -", function() {
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getMusicFolders.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
});
it("Given that there were 2 music folders in my library, when I get the music folders, then a promise will be resolved with an array of 2 music folders", function() {
response["subsonic-response"].musicFolders = {
musicFolder: [
{ id: 80, name: "languageless" },
{ id: 38, name: "mala" }
]
};
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
var promise = subsonic.getMusicFolders();
mockBackend.flush();
expect(promise).toBeResolvedWith([
{ id: 80, name: "languageless" },
{ id: 38, name: "mala" }
]);
});
it("Given that there was 1 music folder in my Madsonic library, when I get the music folders, then a promise will be resolved with an array of 1 music folder", function() {
response["subsonic-response"].musicFolders = {
musicFolder: {id: 56, name: "dite"}
};
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
var promise = subsonic.getMusicFolders();
mockBackend.flush();
expect(promise).toBeResolvedWith([
{ id: 56, name: "dite"}
]);
});
it("Given that there wasn't any music folder in my library, when I get the music folders, then a promise will be rejected with an error message", function() {
response["subsonic-response"].musicFolders = {};
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
var promise = subsonic.getMusicFolders();
mockBackend.flush();
expect(promise).toBeRejectedWith({reason: 'No music folder found on the Subsonic server.'});
});
});
describe("getArtists() -", function() {
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getIndexes.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
@ -542,6 +707,15 @@ describe("Subsonic service -", function() {
expect(promise).toBeResolvedWith(response["subsonic-response"].indexes);
});
it("Given a folder id, when I get the artists, then it will be used as parameter in the request", function() {
url = 'http://demo.subsonic.com/rest/getIndexes.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&musicFolderId=54'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
subsonic.getArtists(54);
mockBackend.flush();
});
it("Given that there were 2 artist at the top level of my Madsonic library, when I get the artists, then a promise will be resolved with an array of two artist", function() {
response["subsonic-response"].indexes = {
shortcut: { id: 433, name: "Podcast" },
@ -689,7 +863,6 @@ describe("Subsonic service -", function() {
});
describe("getPlaylist() -", function() {
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getPlaylist.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=9123'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
@ -781,7 +954,6 @@ describe("Subsonic service -", function() {
});
describe("getPodcasts() -", function() {
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getPodcasts.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&includeEpisodes=false'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
@ -833,7 +1005,6 @@ describe("Subsonic service -", function() {
});
describe("getPodcast() -", function() {
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/getPodcasts.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=2695&includeEpisodes=true'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
@ -933,7 +1104,6 @@ describe("Subsonic service -", function() {
});
describe("search() -", function() {
var url;
beforeEach(function() {
url = 'http://demo.subsonic.com/rest/search2.view?'+
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D'+'&query=unintersetingly'+'&u=Hyzual&v=1.10.2';
@ -967,6 +1137,34 @@ describe("Subsonic service -", function() {
]);
});
it("Given that songs containing 'unintersetingly' existed in my Music Cabinet library, when I search for a song that contains 'unintersetingly', then a promise will be resolved with an array of songs", function() {
response["subsonic-response"].search2 = {
song: [
{
id: 7907,
name: "unintersetingly Caragana"
}, {
id: 4089,
name: "attacolite unintersetingly"
}
]
};
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
var promise = subsonic.search("unintersetingly", 0);
mockBackend.flush();
expect(promise).toBeResolvedWith([
{
id: 7907,
name: "unintersetingly Caragana"
}, {
id: 4089,
name: "attacolite unintersetingly"
}
]);
});
it("Given that only one song containing 'unintersetingly' existed in my Madsonic library, when I search for a song that contains 'unintersetingly', then a promise will be resolved with an array of one song", function() {
response["subsonic-response"].searchResult2 = {
song: { id: 142, name: "unintersetingly rescue" }

View file

@ -41,7 +41,7 @@
<div id="BreadCrumbs" class="floatleft">
<div class="crumb"><a ng-click="toggleArtists()" title="Toggle Artists">Artists</a> &gt;</div>
<div class="crumb" ng-repeat="o in BreadCrumbs | filter:{type:'artist'}"><a ng-click="getAlbums(o.id, o.name)">{{o.name}}</a> &gt;</div>
<div class="crumb" ng-repeat="o in BreadCrumbs | filter:{type:'album'}"><a ng-click="getSongs(o.id, '')">{{o.name}}</a> &gt;</div>
<div class="crumb" ng-repeat="o in BreadCrumbs | filter:{type:'album'}"><a ng-click="getSongs('display', o.id, o.name)">{{o.name}}</a> &gt;</div>
</div>
</div>
</li>
@ -49,12 +49,12 @@
<div class="clear"></div>
<ul class="simplelist songlist noselect">
<div class="" ng-repeat="o in album" ng-switch on="o.type">
<li class="album" ng-switch-when="byfolder" id="{{o.id}}" ng-class="{'selected': selectedAlbum == o.id, 'albumgrid': settings.DefaultLibraryLayout.id == 'grid'}" ng-click="getSongs(o.id, '')" parentid="{{o.parentid}}">
<li class="album" ng-switch-when="byfolder" id="{{o.id}}" ng-class="{'selected': selectedAlbum == o.id, 'albumgrid': settings.DefaultLibraryLayout.id == 'grid'}" ng-click="getSongs('display', o.id, o.name)" parentid="{{o.parentid}}">
<div class="itemactions">
<a class="add hover" href="" title="Add To Play Queue" ng-click="getSongs(o.id, 'add')" stop-event="click"></a>
<a class="play hover" href="" title="Play" ng-click="getSongs(o.id, 'play')" stop-event="click"></a>
<a class="add hover" href="" title="Add To Play Queue" ng-click="getSongs('add', o.id, o.name)" stop-event="click"></a>
<a class="play hover" href="" title="Play" ng-click="getSongs('play', o.id, o.name)" stop-event="click"></a>
<a class="download hover" href="" ng-click="download(o.id)" title="Download" stop-event="click"></a>
<a title="Favorite hover" href="" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="updateFavorite(o)" stop-event="click"></a>
<a class="hover" href="" title="Star" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="toggleStar(o)" stop-event="click"></a>
<a class="info hover" href="" title="{{'Created: ' + o.date}}"></a>
</div>
<div class="albumart"><img ng-src="{{o.coverartthumb}}" src="images/albumdefault_160.jpg"></div>
@ -86,7 +86,9 @@
</ul>
<div class="clear"></div>
<div id="IndexContainer" class="leftsubsection" ng-show="showIndex">
<select id="MusicFolders" class="folders" ng-model="$root.SelectedMusicFolder" ng-options="o.name for o in MusicFolders"></select>
<select id="MusicFolders" class="folders" ng-model="SelectedMusicFolder" ng-options="o.name for o in MusicFolders track by o.id">
<option value="">All Folders</option>
</select>
<div id="AZIndex" ng-show="!settings.HideAZ" class="subactionsfixed">
<a href="" ng-click="toggleAZ()" stop-event="click">A-Z</a>
</div>

View file

@ -4,10 +4,10 @@
* Access and use the Subsonic Server. The Controller is in charge of relaying the Service's messages to the user through the
* notifications.
*/
angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'jamstash.player.service'])
angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'jamstash.player.service', 'jamstash.persistence'])
.controller('SubsonicController', ['$scope', '$rootScope', '$routeParams', '$window', 'utils', 'globals', 'map', 'subsonic', 'notifications', 'player',
function ($scope, $rootScope, $routeParams, $window, utils, globals, map, subsonic, notifications, player) {
.controller('SubsonicController', ['$scope', '$rootScope', '$routeParams', '$window', 'utils', 'globals', 'map', 'subsonic', 'notifications', 'player', 'persistence',
function ($scope, $rootScope, $routeParams, $window, utils, globals, map, subsonic, notifications, player, persistence) {
'use strict';
$scope.settings = globals.settings;
@ -141,13 +141,20 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
}
}
});
$rootScope.$watch("SelectedMusicFolder", function (newValue, oldValue) {
$scope.$watch("SelectedMusicFolder", function (newValue, oldValue) {
if (newValue !== oldValue) {
// TODO: Hyz: Move loading / saving the music folder to persistence-service
utils.setValue('MusicFolders', angular.toJson(newValue), true);
$scope.getArtists(newValue.id);
var folderId;
if (newValue) {
folderId = newValue.id;
persistence.saveSelectedMusicFolder(newValue);
} else {
persistence.deleteSelectedMusicFolder();
}
$scope.getArtists(folderId);
}
});
$scope.searching = {
query: "",
typeId: globals.settings.DefaultSearchType,
@ -201,7 +208,12 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
$scope.song = data.song;
});
};
$scope.getArtists = function (folder) {
var savedFolder = $scope.SelectedMusicFolder;
if (isNaN(folder) && savedFolder) {
folder = savedFolder.id;
}
var promise = subsonic.getArtists(folder);
$scope.handleErrors(promise).then(function (data) {
$scope.index = data.index;
@ -214,8 +226,9 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
}
});
};
$scope.refreshArtists = function () {
utils.setValue('MusicFolders', null, true);
$scope.SelectedMusicFolder = undefined;
$scope.getArtists();
$scope.getPlaylists();
};
@ -279,10 +292,11 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
* @param {String} action the action to be taken with the songs. Must be 'add', 'play' or 'display'
* @return {Promise} the original promise passed in first param. That way we can chain it further !
*/
//TODO: Hyz: Maybe we should move this to a service
$scope.requestSongs = function (promise, action) {
$scope.handleErrors(promise)
.then(function (songs) {
if(action === 'play') {
if (action === 'play') {
player.emptyQueue().addSongs(songs).playFirstSong();
notifications.updateMessage(songs.length + ' Song(s) Added to Queue', true);
} else if (action === 'add') {
@ -300,18 +314,33 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
return promise;
};
$scope.getSongs = function (id, action) {
subsonic.getSongs(id, action).then(function (data) {
$scope.album = data.album;
$scope.song = data.song;
$scope.BreadCrumbs = data.breadcrumb;
$scope.selectedAutoAlbum = data.selectedAutoAlbum;
$scope.selectedArtist = data.selectedArtist;
$scope.selectedPlaylist = data.selectedPlaylist;
if ($scope.SelectedAlbumSort.id != "default") {
sortSubsonicAlbums($scope.SelectedAlbumSort.id);
}
});
$scope.getSongs = function (action, id, name) {
var promise;
if (action === 'play' || action === 'add') {
promise = subsonic.recursiveGetSongs(id);
$scope.requestSongs(promise, action);
} else if (action === 'display') {
promise = subsonic.getSongs(id);
$scope.handleErrors(promise).then(function (data) {
$scope.album = data.directories;
$scope.song = data.songs;
if ($scope.BreadCrumbs.length > 0) {
$scope.BreadCrumbs.splice(1, ($scope.BreadCrumbs.length - 1));
}
$scope.BreadCrumbs.push({'type': 'album', 'id': id, 'name': name});
$scope.selectedAutoAlbum = null;
$scope.selectedArtist = null;
$scope.selectedAlbum = id;
$scope.selectedAutoPlaylist = null;
$scope.selectedPlaylist = null;
$scope.selectedPodcast = null;
if ($scope.SelectedAlbumSort.id !== "default") {
sortSubsonicAlbums($scope.SelectedAlbumSort.id);
}
}, function (error) {
notifications.updateMessage(error.reason, true);
});
}
};
$scope.getRandomStarredSongs = function (action) {
@ -578,43 +607,19 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
};
$scope.getMusicFolders = function () {
$.ajax({
url: globals.BaseURL() + '/getMusicFolders.view?' + globals.BaseParams(),
method: 'GET',
dataType: globals.settings.Protocol,
timeout: globals.settings.Timeout,
success: function (data) {
if (data["subsonic-response"].musicFolders.musicFolder !== undefined) {
var folders = [];
if (data["subsonic-response"].musicFolders.musicFolder.length > 0) {
folders = data["subsonic-response"].musicFolders.musicFolder;
} else {
folders[0] = data["subsonic-response"].musicFolders.musicFolder;
}
folders.unshift({
"id": -1,
"name": "All Folders"
});
$rootScope.MusicFolders = folders;
if (utils.getValue('MusicFolders')) {
var folder = angular.fromJson(utils.getValue('MusicFolders'));
var i = 0, index = "";
angular.forEach($rootScope.MusicFolders, function (item, key) {
if (item.id == folder.id) {
index = i;
}
i++;
});
$rootScope.SelectedMusicFolder = $rootScope.MusicFolders[index];
} else {
$rootScope.SelectedMusicFolder = $rootScope.MusicFolders[0];
}
$scope.$apply();
var promise = subsonic.getMusicFolders();
$scope.handleErrors(promise).then(function (musicFolders) {
var folders = musicFolders;
$scope.MusicFolders = folders;
var savedFolder = persistence.getSelectedMusicFolder();
if (savedFolder) {
if (_.findIndex(folders, {id: savedFolder.id}) !== -1) {
$scope.SelectedMusicFolder = savedFolder;
}
}
});
};
/**
* Change the order of playlists through jQuery UI's sortable
*/
@ -627,19 +632,19 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
$scope.song.splice(end, 0, $scope.song.splice(start, 1)[0]);
};
$scope.playSong = function (song) {
player.play(song);
player.emptyQueue().addSong(song).playFirstSong();
};
$scope.addSongToQueue = function (song) {
player.addSong(song);
};
/* Launch on Startup */
$scope.getMusicFolders();
$scope.getArtists();
$scope.getPlaylists();
$scope.getGenres();
$scope.getPodcasts();
$scope.openDefaultSection();
$scope.getMusicFolders();
if ($routeParams.artistId && $routeParams.albumId) {
$scope.getAlbumByTag($routeParams.albumId);
} else if ($routeParams.artistId) {

View file

@ -2,59 +2,74 @@ describe("Subsonic controller", function() {
'use strict';
var scope, $rootScope, $controller, $window,
subsonic, notifications, player, controllerParams, deferred;
subsonic, notifications, player, utils, persistence, controllerParams, deferred;
beforeEach(function() {
jasmine.addCustomEqualityTester(angular.equals);
module('jamstash.subsonic.controller', function ($provide) {
// Mock the player service
$provide.decorator('player', function($delegate) {
module('jamstash.subsonic.controller');
$delegate.queue = [];
$delegate.play = jasmine.createSpy("play");
$delegate.playFirstSong = jasmine.createSpy("playFirstSong");
return $delegate;
});
});
inject(function (_$controller_, _$rootScope_, utils, globals, map, $q, _player_) {
inject(function (_$controller_, _$rootScope_, globals, map, $q) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
deferred = $q.defer();
player = _player_;
$window = jasmine.createSpyObj("$window", [
"prompt",
"confirm"
]);
notifications = jasmine.createSpyObj("notifications", ["updateMessage"]);
utils = jasmine.createSpyObj("utils", ["getValue"]);
persistence = jasmine.createSpyObj("persistence", [
"getSelectedMusicFolder",
"saveSelectedMusicFolder",
"deleteSelectedMusicFolder"
]);
// Mock the subsonic service
subsonic = jasmine.createSpyObj("subsonic", [
"deletePlaylist",
"getAlbums",
"getArtists",
"getGenres",
"getPlaylists",
"getPodcasts",
"getRandomStarredSongs",
"getRandomSongs",
"getMusicFolders",
"getPlaylist",
"newPlaylist",
"deletePlaylist",
"savePlaylist",
"getPlaylists",
"getPodcast",
"search"
"getPodcasts",
"getRandomSongs",
"getRandomStarredSongs",
"getSongs",
"newPlaylist",
"recursiveGetSongs",
"savePlaylist",
"search",
]);
// We make them return different promises and use our deferred variable only when testing
// a particular function, so that they stay isolated
subsonic.getAlbums.and.returnValue($q.defer().promise);
subsonic.getArtists.and.returnValue($q.defer().promise);
subsonic.getGenres.and.returnValue($q.defer().promise);
subsonic.getMusicFolders.and.returnValue($q.defer().promise);
subsonic.getPlaylists.and.returnValue($q.defer().promise);
subsonic.getPodcasts.and.returnValue($q.defer().promise);
subsonic.getSongs.and.returnValue($q.defer().promise);
subsonic.recursiveGetSongs.and.returnValue($q.defer().promise);
subsonic.showIndex = false;
// Mock the player service
player = jasmine.createSpyObj("player", [
"emptyQueue",
"addSong",
"addSongs",
"play",
"playFirstSong"
]);
player.emptyQueue.and.returnValue(player);
player.addSong.and.returnValue(player);
player.addSongs.and.returnValue(player);
player.queue = [];
$controller = _$controller_;
controllerParams = {
$scope: scope,
@ -65,7 +80,9 @@ describe("Subsonic controller", function() {
globals: globals,
map: map,
subsonic: subsonic,
notifications: notifications
notifications: notifications,
player: player,
persistence: persistence
};
});
});
@ -74,6 +91,139 @@ describe("Subsonic controller", function() {
beforeEach(function() {
$controller('SubsonicController', controllerParams);
scope.selectedPlaylist = null;
scope.$apply();
});
describe("getSongs -", function() {
beforeEach(function() {
scope.BreadCrumbs = [];
scope.SelectedAlbumSort= {
id: "default"
};
subsonic.getSongs.and.returnValue(deferred.promise);
subsonic.recursiveGetSongs.and.returnValue(deferred.promise);
spyOn(scope, "requestSongs").and.returnValue(deferred.promise);
});
it("Given a music directory that contained 3 songs and given its id and name, when I display it, then subsonic-service will be called, the breadcrumbs will be updated and the songs will be published to the scope", function() {
scope.getSongs('display', 87, 'Covetous Dadayag');
deferred.resolve({
directories: [],
songs: [
{ id: 660 },
{ id: 859 },
{ id: 545 }
]
});
scope.$apply();
expect(subsonic.getSongs).toHaveBeenCalledWith(87);
expect(scope.album).toEqual([]);
expect(scope.song).toEqual([
{ id: 660 },
{ id: 859 },
{ id: 545 }
]);
expect(scope.BreadCrumbs).toEqual([{
type: 'album',
id: 87,
name: 'Covetous Dadayag'
}]);
expect(scope.selectedAutoAlbum).toBeNull();
expect(scope.selectedArtist).toBeNull();
expect(scope.selectedAlbum).toBe(87);
expect(scope.selectedAutoPlaylist).toBeNull();
expect(scope.selectedPlaylist).toBeNull();
expect(scope.selectedPodcast).toBeNull();
});
it("Given that there was a previous level in the breadcrumbs, when I display a music directory, then the album breadcrumb will be added", function() {
scope.BreadCrumbs = [
{
type: 'artist',
id: 73,
name: 'Evan Mestanza'
}
];
scope.getSongs('display', 883, 'Pitiedly preutilizable');
deferred.resolve({
directories: [],
songs: []
});
scope.$apply();
expect(scope.BreadCrumbs).toEqual([
{
type: 'artist',
id: 73,
name: 'Evan Mestanza'
}, {
type: 'album',
id: 883,
name: 'Pitiedly preutilizable'
}
]);
});
it("Given a music directory that contained 2 songs and 1 subdirectory and given its id and name, when I display it, then subsonic-service will be called, the songs and directory will be published to the scope", function() {
scope.getSongs('display', 6, 'Potsander dormilona');
deferred.resolve({
directories: [{id: 387, type: 'byfolder'}],
songs: [
{ id: 102 },
{ id: 340 }
]
});
scope.$apply();
expect(scope.album).toEqual([
{id: 387, type: 'byfolder'}
]);
expect(scope.song).toEqual([
{ id: 102 },
{ id: 340 }
]);
});
it("Given a music directory, when I display it, then handleErrors will handle HTTP and Subsonic errors", function() {
spyOn(scope, 'handleErrors').and.returnValue(deferred.promise);
scope.getSongs('display', 88);
expect(scope.handleErrors).toHaveBeenCalledWith(deferred.promise);
});
it("Given a music directory that didn't contain anything, when I display it, then an error notification will be displayed", function() {
scope.getSongs('display', 214, 'Upside bunemost');
deferred.reject({reason: 'This directory is empty.'});
scope.$apply();
expect(notifications.updateMessage).toHaveBeenCalledWith('This directory is empty.', true);
});
it("Given a music directory that contained 3 songs and given its id, when I add it to the playing queue, then requestSongs() will be called", function() {
scope.getSongs('add', 720);
deferred.resolve([
{ id: 927 },
{ id: 598 },
{ id: 632 }
]);
scope.$apply();
expect(subsonic.recursiveGetSongs).toHaveBeenCalledWith(720);
expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'add');
});
it("Given a music directory that contained 3 songs and given its id, when I play it, then requestSongs() will be called", function() {
scope.getSongs('play', 542);
deferred.resolve([
{ id: 905 },
{ id: 181 },
{ id: 880 }
]);
scope.$apply();
expect(subsonic.recursiveGetSongs).toHaveBeenCalledWith(542);
expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'play');
});
});
describe("Given that my library contained 3 songs, ", function() {
@ -234,23 +384,22 @@ describe("Subsonic controller", function() {
deferred.resolve(response);
scope.$apply();
expect(player.queue).toEqual([
expect(player.addSongs).toHaveBeenCalledWith([
{id: "2548"}, {id: "8986"}, {id: "2986"}
]);
expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true);
});
it("when I play songs, it plays the first selected song, empties the queue and fills it with the selected songs and it notifies the user", function() {
player.queue = [{id: "7666"}];
scope.requestSongs(deferred.promise, 'play');
deferred.resolve(response);
scope.$apply();
expect(player.playFirstSong).toHaveBeenCalled();
expect(player.queue).toEqual([
expect(player.emptyQueue).toHaveBeenCalled();
expect(player.addSongs).toHaveBeenCalledWith([
{id: "2548"}, {id: "8986"}, {id: "2986"}
]);
expect(player.playFirstSong).toHaveBeenCalled();
expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true);
});
@ -362,22 +511,48 @@ describe("Subsonic controller", function() {
});
});
it("When I call playSong, it calls play in the player service", function() {
it("Given a song, when I call playSong, then the player service's queue will be emptied, the song will be added to the queue and played", function() {
var fakeSong = {"id": 3572};
scope.playSong(fakeSong);
expect(player.play).toHaveBeenCalledWith(fakeSong);
expect(player.emptyQueue).toHaveBeenCalled();
expect(player.addSong).toHaveBeenCalledWith({"id": 3572});
expect(player.playFirstSong).toHaveBeenCalled();
});
//TODO: Hyz: all starred
describe("When I load the artists,", function() {
describe("", function() {
beforeEach(function() {
spyOn(scope, "getArtists");
});
it("Given no previously selected music folder, when I select a music folder, then it will be stored in persistence and the artists will be loaded from subsonic", function() {
scope.SelectedMusicFolder = { id: 22, name: "Cascadia" };
scope.$apply();
expect(persistence.saveSelectedMusicFolder).toHaveBeenCalledWith({ id: 22, name: "Cascadia" });
expect(scope.getArtists).toHaveBeenCalledWith(22);
});
it("Given a previously selected music folder, when I select the 'All Folders' (undefined) music folder, then the stored value will be deleted from persistence and all the artists will be loaded from subsonic", function() {
scope.SelectedMusicFolder = { id: 23, name: "grantable" };
scope.$apply();
scope.SelectedMusicFolder = undefined;
scope.$apply();
expect(persistence.deleteSelectedMusicFolder).toHaveBeenCalled();
expect(scope.getArtists).not.toHaveBeenCalledWith(jasmine.any(Number));
});
});
describe("getArtists() -", function() {
beforeEach(function() {
subsonic.getArtists.and.returnValue(deferred.promise);
});
it("Given that there are songs in the library, it loads the artists and publishes them to the scope", function() {
it("Given that there were artists in the library, when I load the artists, then subsonic will be queried an index array containing the artists and a shortcut array containing the shortcuts (such as Podcasts) will be publisehd to the scope", function() {
scope.getArtists();
deferred.resolve({
index: [
@ -398,7 +573,15 @@ describe("Subsonic controller", function() {
]);
});
it("Given that there aren't any songs in the library, when loading indexes, it notifies the user with an error message", function() {
it("Given no folder id and given a selected music folder had been set in the scope, when I get the artists, then the selected music folder's id will be used as parameter to subsonic service", function() {
scope.SelectedMusicFolder = { id: 62, name: "dollardee" };
scope.getArtists();
expect(subsonic.getArtists).toHaveBeenCalledWith(62);
});
it("Given that there weren't any artist in the library, when I load the artists, then a notification with an error message will be displayed", function() {
scope.getArtists();
deferred.reject({reason: 'No artist found on the Subsonic server.'});
scope.$apply();
@ -408,13 +591,24 @@ describe("Subsonic controller", function() {
expect(notifications.updateMessage).toHaveBeenCalledWith('No artist found on the Subsonic server.', true);
});
it("it lets handleErrors handle HTTP and Subsonic errors", function() {
it("Given that the server was unreachable, when I get the music folders, then handleErrors() will deal with the error", function() {
spyOn(scope, 'handleErrors').and.returnValue(deferred.promise);
scope.getArtists();
expect(scope.handleErrors).toHaveBeenCalledWith(deferred.promise);
});
});
it("refreshArtists() - When I refresh the artists, then the selected music folder will be reset to undefined and the artists and playlists will be loaded", function() {
spyOn(scope, "getArtists");
spyOn(scope, "getPlaylists");
scope.refreshArtists();
expect(scope.SelectedMusicFolder).toBeUndefined();
expect(scope.getArtists).toHaveBeenCalled();
expect(scope.getPlaylists).toHaveBeenCalled();
});
describe("When I load the playlists,", function() {
beforeEach(function() {
subsonic.getPlaylists.and.returnValue(deferred.promise);
@ -490,7 +684,7 @@ describe("Subsonic controller", function() {
expect(scope.getPlaylists).toHaveBeenCalled();
});
it("Given no selected playlist, when I try to delete a playlist, an error message will be notified", function() {
it("Given no selected playlist, when I try to delete a playlist, an error notification will be displayed", function() {
scope.selectedPlaylist = null;
scope.deletePlaylist();
@ -522,7 +716,7 @@ describe("Subsonic controller", function() {
expect(notifications.updateMessage).toHaveBeenCalledWith('Playlist Updated!', true);
});
it("Given no selected playlist, when I try to save a playlist, an error message will be notified", function() {
it("Given no selected playlist, when I try to save a playlist, an error notification will be displayed", function() {
scope.selectedPlaylist = null;
scope.savePlaylist();
@ -564,6 +758,64 @@ describe("Subsonic controller", function() {
});
});
describe("getMusicFolders", function() {
beforeEach(function() {
subsonic.getMusicFolders.and.returnValue(deferred.promise);
});
it("Given that there were music folders in the library, when I get the music folders, then the folders will be published to the scope", function() {
scope.getMusicFolders();
deferred.resolve([
{ id: 74, name: "scirrhosis"},
{ id: 81, name: "drooper"}
]);
scope.$apply();
expect(subsonic.getMusicFolders).toHaveBeenCalled();
expect(scope.MusicFolders).toEqual([
{ id: 74, name: "scirrhosis"},
{ id: 81, name: "drooper"}
]);
});
describe("Given that there was a selected music folder previously saved in persistence", function() {
it("and that it matched one of the music folders returned by subsonic, when I get the music folders, then the scope's selected music folder will be set", function() {
persistence.getSelectedMusicFolder.and.returnValue({ id: 79, name: "dismember" });
scope.getMusicFolders();
deferred.resolve([
{ id: 93, name: "illish" },
{ id: 79, name: "dismember" }
]);
scope.$apply();
expect(scope.SelectedMusicFolder).toEqual({ id: 79, name: "dismember" });
});
it("and that it didn't match one of the music folders returned by subsonic, when I get the music folders, then the scope's selected music folder will be undefined", function() {
persistence.getSelectedMusicFolder.and.returnValue({ id: 49, name: "metafulminuric" });
scope.getMusicFolders();
deferred.resolve([
{ id: 94, name: "dorsiflexor" }
]);
scope.$apply();
expect(scope.SelectedMusicFolder).toBeUndefined();
});
});
it("Given that there weren't any music folder in the library, when I get the music folders, then handleErrors() will deal with the error", function() {
spyOn(scope, 'handleErrors').and.returnValue(deferred.promise);
scope.getMusicFolders();
deferred.reject({reason: 'No music folder found on the Subsonic server.'});
scope.$apply();
expect(scope.handleErrors).toHaveBeenCalledWith(deferred.promise);
});
});
describe("search() -", function() {
beforeEach(function() {
subsonic.search.and.returnValue(deferred.promise);

View file

@ -1,142 +0,0 @@
/**
* jQuery BASE64 functions
*
* <code>
* Encodes the given data with base64.
* String $.base64Encode ( String str )
* <br />
* Decodes a base64 encoded data.
* String $.base64Decode ( String str )
* </code>
*
* Encodes and Decodes the given data in base64.
* This encoding is designed to make binary data survive transport through transport layers that are not 8-bit clean, such as mail bodies.
* Base64-encoded data takes about 33% more space than the original data.
* This javascript code is used to encode / decode data using base64 (this encoding is designed to make binary data survive transport through transport layers that are not 8-bit clean). Script is fully compatible with UTF-8 encoding. You can use base64 encoded data as simple encryption mechanism.
* If you plan using UTF-8 encoding in your project don't forget to set the page encoding to UTF-8 (Content-Type meta tag).
* This function orginally get from the WebToolkit and rewrite for using as the jQuery plugin.
*
* Example
* Code
* <code>
* $.base64Encode("I'm Persian.");
* </code>
* Result
* <code>
* "SSdtIFBlcnNpYW4u"
* </code>
* Code
* <code>
* $.base64Decode("SSdtIFBlcnNpYW4u");
* </code>
* Result
* <code>
* "I'm Persian."
* </code>
*
* @alias Muhammad Hussein Fattahizadeh < muhammad [AT] semnanweb [DOT] com >
* @link http://www.semnanweb.com/jquery-plugin/base64.html
* @see http://www.webtoolkit.info/
* @license http://www.gnu.org/licenses/gpl.html [GNU General Public License]
* @param {jQuery} {base64Encode:function(input))
* @param {jQuery} {base64Decode:function(input))
* @return string
*/
(function($){
var keyString = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var uTF8Encode = function(string) {
string = string.replace(/\x0d\x0a/g, "\x0a");
var output = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
output += String.fromCharCode(c);
} else if ((c > 127) && (c < 2048)) {
output += String.fromCharCode((c >> 6) | 192);
output += String.fromCharCode((c & 63) | 128);
} else {
output += String.fromCharCode((c >> 12) | 224);
output += String.fromCharCode(((c >> 6) & 63) | 128);
output += String.fromCharCode((c & 63) | 128);
}
}
return output;
};
var uTF8Decode = function(input) {
var string = "";
var i = 0;
var c = c1 = c2 = 0;
while ( i < input.length ) {
c = input.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
} else if ((c > 191) && (c < 224)) {
c2 = input.charCodeAt(i+1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
} else {
c2 = input.charCodeAt(i+1);
c3 = input.charCodeAt(i+2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
}
$.extend({
base64Encode: function(input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = uTF8Encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output + keyString.charAt(enc1) + keyString.charAt(enc2) + keyString.charAt(enc3) + keyString.charAt(enc4);
}
return output;
},
base64Decode: function(input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = keyString.indexOf(input.charAt(i++));
enc2 = keyString.indexOf(input.charAt(i++));
enc3 = keyString.indexOf(input.charAt(i++));
enc4 = keyString.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
output = uTF8Decode(output);
return output;
}
});
})(jQuery);

View file

@ -1,317 +0,0 @@
(function (jQuery) {
var daysInWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
var shortMonthsInYear = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var longMonthsInYear = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];
var shortMonthsToNumber = [];
shortMonthsToNumber["Jan"] = "01";
shortMonthsToNumber["Feb"] = "02";
shortMonthsToNumber["Mar"] = "03";
shortMonthsToNumber["Apr"] = "04";
shortMonthsToNumber["May"] = "05";
shortMonthsToNumber["Jun"] = "06";
shortMonthsToNumber["Jul"] = "07";
shortMonthsToNumber["Aug"] = "08";
shortMonthsToNumber["Sep"] = "09";
shortMonthsToNumber["Oct"] = "10";
shortMonthsToNumber["Nov"] = "11";
shortMonthsToNumber["Dec"] = "12";
jQuery.format = (function () {
function strDay(value) {
return daysInWeek[parseInt(value, 10)] || value;
}
function strMonth(value) {
var monthArrayIndex = parseInt(value, 10) - 1;
return shortMonthsInYear[monthArrayIndex] || value;
}
function strLongMonth(value) {
var monthArrayIndex = parseInt(value, 10) - 1;
return longMonthsInYear[monthArrayIndex] || value;
}
var parseMonth = function (value) {
return shortMonthsToNumber[value] || value;
};
var parseTime = function (value) {
var retValue = value;
var millis = "";
if (retValue.indexOf(".") !== -1) {
var delimited = retValue.split('.');
retValue = delimited[0];
millis = delimited[1];
}
var values3 = retValue.split(":");
if (values3.length === 3) {
hour = values3[0];
minute = values3[1];
second = values3[2];
return {
time: retValue,
hour: hour,
minute: minute,
second: second,
millis: millis
};
} else {
return {
time: "",
hour: "",
minute: "",
second: "",
millis: ""
};
}
};
return {
date: function (value, format) {
/*
value = new java.util.Date()
2009-12-18 10:54:50.546
*/
try {
var date = null;
var year = null;
var month = null;
var dayOfMonth = null;
var dayOfWeek = null;
var time = null;
if (typeof value == "number"){
return this.date(new Date(value), format);
} else if (typeof value.getFullYear == "function") {
year = value.getFullYear();
month = value.getMonth() + 1;
dayOfMonth = value.getDate();
dayOfWeek = value.getDay();
time = parseTime(value.toTimeString());
} else if (value.search(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.?\d{0,3}[Z\-+]?(\d{2}:?\d{2})?/) != -1) {
/* 2009-04-19T16:11:05+02:00 || 2009-04-19T16:11:05Z */
var values = value.split(/[T\+-]/);
year = values[0];
month = values[1];
dayOfMonth = values[2];
time = parseTime(values[3].split(".")[0]);
date = new Date(year, month - 1, dayOfMonth);
dayOfWeek = date.getDay();
} else {
var values = value.split(" ");
switch (values.length) {
case 6:
/* Wed Jan 13 10:43:41 CET 2010 */
year = values[5];
month = parseMonth(values[1]);
dayOfMonth = values[2];
time = parseTime(values[3]);
date = new Date(year, month - 1, dayOfMonth);
dayOfWeek = date.getDay();
break;
case 2:
/* 2009-12-18 10:54:50.546 */
var values2 = values[0].split("-");
year = values2[0];
month = values2[1];
dayOfMonth = values2[2];
time = parseTime(values[1]);
date = new Date(year, month - 1, dayOfMonth);
dayOfWeek = date.getDay();
break;
case 7:
/* Tue Mar 01 2011 12:01:42 GMT-0800 (PST) */
case 9:
/*added by Larry, for Fri Apr 08 2011 00:00:00 GMT+0800 (China Standard Time) */
case 10:
/* added by Larry, for Fri Apr 08 2011 00:00:00 GMT+0200 (W. Europe Daylight Time) */
year = values[3];
month = parseMonth(values[1]);
dayOfMonth = values[2];
time = parseTime(values[4]);
date = new Date(year, month - 1, dayOfMonth);
dayOfWeek = date.getDay();
break;
case 1:
/* added by Jonny, for 2012-02-07CET00:00:00 (Doctrine Entity -> Json Serializer) */
var values2 = values[0].split("");
year=values2[0]+values2[1]+values2[2]+values2[3];
month= values2[5]+values2[6];
dayOfMonth = values2[8]+values2[9];
time = parseTime(values2[13]+values2[14]+values2[15]+values2[16]+values2[17]+values2[18]+values2[19]+values2[20])
date = new Date(year, month - 1, dayOfMonth);
dayOfWeek = date.getDay();
break;
default:
return value;
}
}
var pattern = "";
var retValue = "";
var unparsedRest = "";
/*
Issue 1 - variable scope issue in format.date
Thanks jakemonO
*/
for (var i = 0; i < format.length; i++) {
var currentPattern = format.charAt(i);
pattern += currentPattern;
unparsedRest = "";
switch (pattern) {
case "ddd":
retValue += strDay(dayOfWeek);
pattern = "";
break;
case "dd":
if (format.charAt(i + 1) == "d") {
break;
}
if (String(dayOfMonth).length === 1) {
dayOfMonth = '0' + dayOfMonth;
}
retValue += dayOfMonth;
pattern = "";
break;
case "d":
if (format.charAt(i + 1) == "d") {
break;
}
retValue += parseInt(dayOfMonth, 10);
pattern = "";
break;
case "MMMM":
retValue += strLongMonth(month);
pattern = "";
break;
case "MMM":
if (format.charAt(i + 1) === "M") {
break;
}
retValue += strMonth(month);
pattern = "";
break;
case "MM":
if (format.charAt(i + 1) == "M") {
break;
}
if (String(month).length === 1) {
month = '0' + month;
}
retValue += month;
pattern = "";
break;
case "M":
if (format.charAt(i + 1) == "M") {
break;
}
retValue += parseInt(month, 10);
pattern = "";
break;
case "yyyy":
retValue += year;
pattern = "";
break;
case "yy":
if (format.charAt(i + 1) == "y" &&
format.charAt(i + 2) == "y") {
break;
}
retValue += String(year).slice(-2);
pattern = "";
break;
case "HH":
retValue += time.hour;
pattern = "";
break;
case "hh":
/* time.hour is "00" as string == is used instead of === */
var hour = (time.hour == 0 ? 12 : time.hour < 13 ? time.hour : time.hour - 12);
hour = String(hour).length == 1 ? '0' + hour : hour;
retValue += hour;
pattern = "";
break;
case "h":
if (format.charAt(i + 1) == "h") {
break;
}
var hour = (time.hour == 0 ? 12 : time.hour < 13 ? time.hour : time.hour - 12);
retValue += parseInt(hour, 10);
// Fixing issue https://github.com/phstc/jquery-dateFormat/issues/21
// retValue = parseInt(retValue, 10);
pattern = "";
break;
case "mm":
retValue += time.minute;
pattern = "";
break;
case "ss":
/* ensure only seconds are added to the return string */
retValue += time.second.substring(0, 2);
pattern = "";
break;
case "SSS":
retValue += time.millis.substring(0, 3);
pattern = "";
break;
case "a":
retValue += time.hour >= 12 ? "PM" : "AM";
pattern = "";
break;
case " ":
retValue += currentPattern;
pattern = "";
break;
case "/":
retValue += currentPattern;
pattern = "";
break;
case ":":
retValue += currentPattern;
pattern = "";
break;
default:
if (pattern.length === 2 && pattern.indexOf("y") !== 0 && pattern != "SS") {
retValue += pattern.substring(0, 1);
pattern = pattern.substring(1, 2);
} else if ((pattern.length === 3 && pattern.indexOf("yyy") === -1)) {
pattern = "";
} else {
unparsedRest = pattern;
}
}
}
retValue += unparsedRest;
return retValue;
} catch (e) {
console.log(e);
return value;
}
}
};
}());
}(jQuery));
jQuery.format.date.defaultShortDateFormat = "dd/MM/yyyy";
jQuery.format.date.defaultLongDateFormat = "dd/MM/yyyy hh:mm:ss";
jQuery(document).ready(function () {
jQuery(".shortDateFormat").each(function (idx, elem) {
if (jQuery(elem).is(":input")) {
jQuery(elem).val(jQuery.format.date(jQuery(elem).val(), jQuery.format.date.defaultShortDateFormat));
} else {
jQuery(elem).text(jQuery.format.date(jQuery(elem).text(), jQuery.format.date.defaultShortDateFormat));
}
});
jQuery(".longDateFormat").each(function (idx, elem) {
if (jQuery(elem).is(":input")) {
jQuery(elem).val(jQuery.format.date(jQuery(elem).val(), jQuery.format.date.defaultLongDateFormat));
} else {
jQuery(elem).text(jQuery.format.date(jQuery(elem).text(), jQuery.format.date.defaultLongDateFormat));
}
});
});

View file

@ -19,28 +19,30 @@
"archive.org",
"music"
],
"license": "MIT",
"license": "GPL-2.0",
"repository": {
"type": "git",
"url": "https://github.com/tsquillario/Jamstash.git"
},
"main": "app/index.html",
"dependencies": {
"angular": "~1.2.0",
"angular-route": "~1.2.0",
"angular-sanitize": "~1.2.0",
"angular-cookies": "~1.2.0",
"angular-resource": "~1.2.0",
"angular": "~1.3.15",
"angular-route": "~1.3.15",
"angular-sanitize": "~1.3.15",
"angular-cookies": "~1.3.15",
"angular-resource": "~1.3.15",
"jquery": "~2.0.0",
"jquery-ui": "~1.10.0",
"jplayer": "~2.9.0",
"fancybox": "~2.1.4",
"notify.js": "<=1.2.2",
"jquery.scrollTo": "~1.4.5",
"underscore": "~1.7.0",
"jquery-dateFormat": "~1.0.2",
"underscore": "~1.8.3",
"angular-underscore": "~0.5.0",
"angular-locker": "~1.2.0",
"angular-ui-utils": "bower-keypress"
"angular-ui-utils": "bower-keypress",
"open-iconic": "~1.1.1"
},
"overrides": {
"fancybox": {
@ -51,8 +53,8 @@
}
},
"devDependencies": {
"angular-mocks": "~1.2.0",
"jasmine-promise-matchers": "~0.0.3",
"angular-mocks": "~1.3.15",
"jasmine-promise-matchers": "~1.1.1",
"jasmine-fixture": "~1.2.2"
},
"ignore": [

View file

@ -30,6 +30,7 @@ module.exports = function (config) {
'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/underscore/underscore.js',
'bower_components/angular-underscore/angular-underscore.js',
'bower_components/angular-locker/dist/angular-locker.min.js',
@ -39,14 +40,21 @@ module.exports = function (config) {
'bower_components/jasmine-fixture/dist/jasmine-fixture.js',
// endbower
'app/**/*.js',
'app/**/*_test.js'
'app/**/*_test.js',
'app/**/*.html'
],
// list of files / patterns to exclude
// exclude: ['app/vendor/**/*.js'],
preprocessors: {
'app/**/!(*_test).js': ['coverage']
'app/**/!(*_test).js': ['coverage'],
'app/**/*.html': ['ng-html2js']
},
ngHtml2JsPreprocessor: {
stripPrefix: 'app/',
moduleName: 'templates'
},
// web server port
@ -60,18 +68,7 @@ module.exports = function (config) {
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: [
'PhantomJS'
],
// Which plugins to enable
plugins: [
'karma-chrome-launcher',
'karma-phantomjs-launcher',
'karma-jasmine',
'karma-coverage',
'karma-notify-reporter'
],
browsers: [],
// Continuous Integration mode
// if true, it capture browsers, run tests and exit

View file

@ -14,7 +14,7 @@
"x37v (https://github.com/x37v)",
"Hyzual (https://github.com/Hyzual)"
],
"license": "MIT",
"license": "GPL-2.0",
"homepage": "http://jamstash.com",
"keywords": [
"subsonic",
@ -31,30 +31,29 @@
"grunt": "^0.4.5",
"grunt-bump": "^0.3.0",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-concat": "^0.5.0",
"grunt-contrib-connect": "^0.8.0",
"grunt-contrib-copy": "^0.7.0",
"grunt-contrib-cssmin": "^0.10.0",
"grunt-contrib-htmlmin": "^0.3.0",
"grunt-contrib-imagemin": "^0.9.1",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-uglify": "^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.2",
"grunt-contrib-htmlmin": "^0.4.0",
"grunt-contrib-imagemin": "^0.9.4",
"grunt-contrib-uglify": "^0.9.1",
"grunt-contrib-watch": "^0.6.1",
"grunt-filerev": "^2.1.1",
"grunt-karma": "^0.9.0",
"grunt-filerev": "^2.2.0",
"grunt-karma": "^0.10.1",
"grunt-notify": "^0.4.1",
"grunt-ssh": "^0.12.0",
"grunt-usemin": "^2.6.0",
"grunt-wiredep": "^1.9.0",
"jit-grunt": "^0.9.0",
"jshint-stylish": "^1.0.0",
"karma": "^0.12.32",
"karma-chrome-launcher": "^0.1.5",
"karma-coverage": "^0.2.6",
"karma-jasmine": "^0.3.0",
"grunt-ssh": "^0.12.2",
"grunt-svg-sprite": "^1.1.0",
"grunt-usemin": "^3.0.0",
"grunt-wiredep": "^2.0.0",
"jit-grunt": "^0.9.1",
"karma": "^0.12.31",
"karma-chrome-launcher": "^0.1.7",
"karma-coverage": "^0.2.7",
"karma-jasmine": "^0.3.5",
"karma-ng-html2js-preprocessor": "^0.1.2",
"karma-notify-reporter": "^0.1.1",
"karma-phantomjs-launcher": "^0.1.4",
"time-grunt": "^1.0.0"
"time-grunt": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"