├── .gitignore
├── README.md
├── app.js
├── assets
└── buttons.psd
├── bower.json
├── bower_components
├── angular-route
│ ├── .bower.json
│ ├── README.md
│ ├── angular-route.js
│ ├── angular-route.min.js
│ ├── angular-route.min.js.map
│ └── bower.json
└── angular
│ ├── .bower.json
│ ├── README.md
│ ├── angular-csp.css
│ ├── angular.js
│ ├── angular.min.js
│ ├── angular.min.js.gzip
│ ├── angular.min.js.map
│ └── bower.json
├── callback.html
├── controllers
├── album.js
├── artist.js
├── browse.js
├── browsecategory.js
├── home.js
├── login.js
├── player.js
├── playlist.js
├── playqueue.js
├── searchresults.js
├── user.js
└── usertracks.js
├── directives
├── contextmenu.js
├── focusme.js
├── playlistcover.js
└── responsivecover.js
├── filters
├── displaytime.js
└── timeago.js
├── images
├── btn-next.png
├── btn-pause.png
├── btn-play.png
├── btn-prev.png
└── placeholder-playlist.png
├── index.html
├── partials
├── album.html
├── artist.html
├── browse.html
├── browsecategory.html
├── home.html
├── playlist.html
├── playqueue.html
├── searchresults.html
├── user.html
└── usertracks.html
├── readme-img
└── webapi-player-example.jpg
├── services
├── api.js
├── auth.js
├── playback.js
└── playqueue.js
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Spotify Web API Player Example
2 | ==================
3 |
4 | This is a Web Player built using the [Spotify Web API](https://developer.spotify.com/web-api/). For
5 | trying it out, you can navigate to [http://lab.possan.se/thirtify/](http://lab.possan.se/thirtify/)
6 | or clone the project and run it locally.
7 |
8 | 
9 |
10 | Note that you will need a Spotify account (either free or premium) to log in to the site.
11 |
12 | ## How to Run
13 | You will need to run a server. The example is ready to work in the port 8000, so you can do:
14 |
15 | $ python -m SimpleHTTPServer 8000
16 |
17 | and open `http://localhost:8000` in a browser. (This requires python to be installed on your machine.)
18 |
19 | ## Features
20 |
21 | Most of the functionality offered through the Spotify Web API endpoints is implemented in this player:
22 |
23 | - Play 30 second audio previews
24 | - Render track, album and artist information
25 | - Render new releases in Spotify and featured playlists
26 | - Search for tracks
27 | - Fetch user's playlists, rename then and change their visibility
28 | - Delete track from playlist
29 | - Fetch user's saved tracks and save a tracks
30 | - Follow and unfollow artists or users
31 | - Check if the user is following an artist or user
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var app = angular.module('PlayerApp', ['ngRoute']);
4 |
5 | app.config(function($routeProvider) {
6 | $routeProvider.
7 | when('/', {
8 | templateUrl: 'partials/browse.html',
9 | controller: 'BrowseController'
10 | }).
11 | when('/playqueue', {
12 | templateUrl: 'partials/playqueue.html',
13 | controller: 'PlayQueueController'
14 | }).
15 | when('/users/:username', {
16 | templateUrl: 'partials/user.html',
17 | controller: 'UserController'
18 | }).
19 | when('/users/:username/tracks', {
20 | templateUrl: 'partials/usertracks.html',
21 | controller: 'UserTracksController'
22 | }).
23 | when('/users/:username/playlists/:playlist', {
24 | templateUrl: 'partials/playlist.html',
25 | controller: 'PlaylistController'
26 | }).
27 | when('/artists/:artist', {
28 | templateUrl: 'partials/artist.html',
29 | controller: 'ArtistController'
30 | }).
31 | when('/albums/:album', {
32 | templateUrl: 'partials/album.html',
33 | controller: 'AlbumController'
34 | }).
35 | when('/search', {
36 | templateUrl: 'partials/searchresults.html',
37 | controller: 'SearchResultsController'
38 | }).
39 | when('/category/:categoryid', {
40 | templateUrl: 'partials/browsecategory.html',
41 | controller: 'BrowseCategoryController'
42 | }).
43 | otherwise({
44 | redirectTo: '/'
45 | });
46 | });
47 |
48 | app.controller('AppController', function($scope, Auth, API, $location) {
49 | console.log('in AppController');
50 |
51 | console.log(location);
52 |
53 | function checkUser(redirectToLogin) {
54 | API.getMe().then(function(userInfo) {
55 | Auth.setUsername(userInfo.id);
56 | Auth.setUserCountry(userInfo.country);
57 | if (redirectToLogin) {
58 | $scope.$emit('login');
59 | $location.replace();
60 | }
61 | }, function(err) {
62 | $scope.showplayer = false;
63 | $scope.showlogin = true;
64 | $location.replace();
65 | });
66 | }
67 |
68 | window.addEventListener("message", function(event) {
69 | console.log('got postmessage', event);
70 | var hash = JSON.parse(event.data);
71 | if (hash.type == 'access_token') {
72 | Auth.setAccessToken(hash.access_token, hash.expires_in || 60);
73 | checkUser(true);
74 | }
75 | }, false);
76 |
77 | $scope.isLoggedIn = (Auth.getAccessToken() != '');
78 | $scope.showplayer = $scope.isLoggedIn;
79 | $scope.showlogin = !$scope.isLoggedIn;
80 |
81 | $scope.$on('login', function() {
82 | $scope.showplayer = true;
83 | $scope.showlogin = false;
84 | $location.path('/').replace().reload();
85 | });
86 |
87 | $scope.$on('logout', function() {
88 | $scope.showplayer = false;
89 | $scope.showlogin = true;
90 | });
91 |
92 | $scope.getClass = function(path) {
93 | if ($location.path().substr(0, path.length) == path) {
94 | return 'active';
95 | } else {
96 | return '';
97 | }
98 | };
99 |
100 | $scope.focusInput = false;
101 | $scope.menuOptions = function(playlist) {
102 |
103 | var visibilityEntry = [playlist.public ? 'Make secret' : 'Make public', function ($itemScope) {
104 | API.changePlaylistDetails(playlist.username, playlist.id, {public: !playlist.public})
105 | .then(function() {
106 | playlist.public = !playlist.public;
107 | });
108 | }];
109 |
110 | var own = playlist.username === Auth.getUsername();
111 | if (own) {
112 | return [
113 | visibilityEntry,
114 | null,
115 | ['Rename', function ($itemScope) {
116 | playlist.editing = true;
117 | $scope.focusInput = true;
118 | }]
119 | ];
120 | } else {
121 | return [ visibilityEntry ];
122 | }
123 | };
124 |
125 | $scope.playlistNameKeyUp = function(event, playlist) {
126 | if (event.which === 13) {
127 | // enter
128 | var newName = event.target.value;
129 | API.changePlaylistDetails(playlist.username, playlist.id, {name: newName})
130 | .then(function() {
131 | playlist.name = newName;
132 | playlist.editing = false;
133 | $scope.focusInput = false;
134 | });
135 | }
136 |
137 | if (event.which === 27) {
138 | // escape
139 | playlist.editing = false;
140 | $scope.focusInput = false;
141 | }
142 | };
143 |
144 | $scope.playlistNameBlur = function(playlist) {
145 | playlist.editing = false;
146 | $scope.focusInput = false;
147 | };
148 |
149 | checkUser();
150 | });
151 |
152 | })();
153 |
--------------------------------------------------------------------------------
/assets/buttons.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/possan/webapi-player-example/47d3e78fcdd8baa8c89b0e760e0d72fabf9b1386/assets/buttons.psd
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "player",
3 | "version": "0.0.0",
4 | "homepage": "https://github.com/possan/webapi-player-example",
5 | "license": "MIT",
6 | "ignore": [
7 | "**/.*",
8 | "node_modules",
9 | "bower_components",
10 | "test",
11 | "tests"
12 | ],
13 | "dependencies": {
14 | "angular-route": "~1.2.16"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/bower_components/angular-route/.bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-route",
3 | "version": "1.2.16",
4 | "main": "./angular-route.js",
5 | "dependencies": {
6 | "angular": "1.2.16"
7 | },
8 | "homepage": "https://github.com/angular/bower-angular-route",
9 | "_release": "1.2.16",
10 | "_resolution": {
11 | "type": "version",
12 | "tag": "v1.2.16",
13 | "commit": "ed0e2b796077d953f518cb81cc7af981cf695a45"
14 | },
15 | "_source": "git://github.com/angular/bower-angular-route.git",
16 | "_target": "~1.2.16",
17 | "_originalSource": "angular-route"
18 | }
--------------------------------------------------------------------------------
/bower_components/angular-route/README.md:
--------------------------------------------------------------------------------
1 | # bower-angular-route
2 |
3 | This repo is for distribution on `bower`. The source for this module is in the
4 | [main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngRoute).
5 | Please file issues and pull requests against that repo.
6 |
7 | ## Install
8 |
9 | Install with `bower`:
10 |
11 | ```shell
12 | bower install angular-route
13 | ```
14 |
15 | Add a `
19 | ```
20 |
21 | And add `ngRoute` as a dependency for your app:
22 |
23 | ```javascript
24 | angular.module('myApp', ['ngRoute']);
25 | ```
26 |
27 | ## Documentation
28 |
29 | Documentation is available on the
30 | [AngularJS docs site](http://docs.angularjs.org/api/ngRoute).
31 |
32 | ## License
33 |
34 | The MIT License
35 |
36 | Copyright (c) 2010-2012 Google, Inc. http://angularjs.org
37 |
38 | Permission is hereby granted, free of charge, to any person obtaining a copy
39 | of this software and associated documentation files (the "Software"), to deal
40 | in the Software without restriction, including without limitation the rights
41 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
42 | copies of the Software, and to permit persons to whom the Software is
43 | furnished to do so, subject to the following conditions:
44 |
45 | The above copyright notice and this permission notice shall be included in
46 | all copies or substantial portions of the Software.
47 |
48 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
49 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
50 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
51 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
52 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
53 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
54 | THE SOFTWARE.
55 |
--------------------------------------------------------------------------------
/bower_components/angular-route/angular-route.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license AngularJS v1.2.16
3 | * (c) 2010-2014 Google, Inc. http://angularjs.org
4 | * License: MIT
5 | */
6 | (function(window, angular, undefined) {'use strict';
7 |
8 | /**
9 | * @ngdoc module
10 | * @name ngRoute
11 | * @description
12 | *
13 | * # ngRoute
14 | *
15 | * The `ngRoute` module provides routing and deeplinking services and directives for angular apps.
16 | *
17 | * ## Example
18 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
19 | *
20 | *
21 | *
22 | */
23 | /* global -ngRouteModule */
24 | var ngRouteModule = angular.module('ngRoute', ['ng']).
25 | provider('$route', $RouteProvider);
26 |
27 | /**
28 | * @ngdoc provider
29 | * @name $routeProvider
30 | * @function
31 | *
32 | * @description
33 | *
34 | * Used for configuring routes.
35 | *
36 | * ## Example
37 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
38 | *
39 | * ## Dependencies
40 | * Requires the {@link ngRoute `ngRoute`} module to be installed.
41 | */
42 | function $RouteProvider(){
43 | function inherit(parent, extra) {
44 | return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra);
45 | }
46 |
47 | var routes = {};
48 |
49 | /**
50 | * @ngdoc method
51 | * @name $routeProvider#when
52 | *
53 | * @param {string} path Route path (matched against `$location.path`). If `$location.path`
54 | * contains redundant trailing slash or is missing one, the route will still match and the
55 | * `$location.path` will be updated to add or drop the trailing slash to exactly match the
56 | * route definition.
57 | *
58 | * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up
59 | * to the next slash are matched and stored in `$routeParams` under the given `name`
60 | * when the route matches.
61 | * * `path` can contain named groups starting with a colon and ending with a star:
62 | * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name`
63 | * when the route matches.
64 | * * `path` can contain optional named groups with a question mark: e.g.`:name?`.
65 | *
66 | * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match
67 | * `/color/brown/largecode/code/with/slashes/edit` and extract:
68 | *
69 | * * `color: brown`
70 | * * `largecode: code/with/slashes`.
71 | *
72 | *
73 | * @param {Object} route Mapping information to be assigned to `$route.current` on route
74 | * match.
75 | *
76 | * Object properties:
77 | *
78 | * - `controller` – `{(string|function()=}` – Controller fn that should be associated with
79 | * newly created scope or the name of a {@link angular.Module#controller registered
80 | * controller} if passed as a string.
81 | * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be
82 | * published to scope under the `controllerAs` name.
83 | * - `template` – `{string=|function()=}` – html template as a string or a function that
84 | * returns an html template as a string which should be used by {@link
85 | * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives.
86 | * This property takes precedence over `templateUrl`.
87 | *
88 | * If `template` is a function, it will be called with the following parameters:
89 | *
90 | * - `{Array.}` - route parameters extracted from the current
91 | * `$location.path()` by applying the current route
92 | *
93 | * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
94 | * template that should be used by {@link ngRoute.directive:ngView ngView}.
95 | *
96 | * If `templateUrl` is a function, it will be called with the following parameters:
97 | *
98 | * - `{Array.}` - route parameters extracted from the current
99 | * `$location.path()` by applying the current route
100 | *
101 | * - `resolve` - `{Object.=}` - An optional map of dependencies which should
102 | * be injected into the controller. If any of these dependencies are promises, the router
103 | * will wait for them all to be resolved or one to be rejected before the controller is
104 | * instantiated.
105 | * If all the promises are resolved successfully, the values of the resolved promises are
106 | * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is
107 | * fired. If any of the promises are rejected the
108 | * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object
109 | * is:
110 | *
111 | * - `key` – `{string}`: a name of a dependency to be injected into the controller.
112 | * - `factory` - `{string|function}`: If `string` then it is an alias for a service.
113 | * Otherwise if function, then it is {@link auto.$injector#invoke injected}
114 | * and the return value is treated as the dependency. If the result is a promise, it is
115 | * resolved before its value is injected into the controller. Be aware that
116 | * `ngRoute.$routeParams` will still refer to the previous route within these resolve
117 | * functions. Use `$route.current.params` to access the new route parameters, instead.
118 | *
119 | * - `redirectTo` – {(string|function())=} – value to update
120 | * {@link ng.$location $location} path with and trigger route redirection.
121 | *
122 | * If `redirectTo` is a function, it will be called with the following parameters:
123 | *
124 | * - `{Object.}` - route parameters extracted from the current
125 | * `$location.path()` by applying the current route templateUrl.
126 | * - `{string}` - current `$location.path()`
127 | * - `{Object}` - current `$location.search()`
128 | *
129 | * The custom `redirectTo` function is expected to return a string which will be used
130 | * to update `$location.path()` and `$location.search()`.
131 | *
132 | * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()`
133 | * or `$location.hash()` changes.
134 | *
135 | * If the option is set to `false` and url in the browser changes, then
136 | * `$routeUpdate` event is broadcasted on the root scope.
137 | *
138 | * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive
139 | *
140 | * If the option is set to `true`, then the particular route can be matched without being
141 | * case sensitive
142 | *
143 | * @returns {Object} self
144 | *
145 | * @description
146 | * Adds a new route definition to the `$route` service.
147 | */
148 | this.when = function(path, route) {
149 | routes[path] = angular.extend(
150 | {reloadOnSearch: true},
151 | route,
152 | path && pathRegExp(path, route)
153 | );
154 |
155 | // create redirection for trailing slashes
156 | if (path) {
157 | var redirectPath = (path[path.length-1] == '/')
158 | ? path.substr(0, path.length-1)
159 | : path +'/';
160 |
161 | routes[redirectPath] = angular.extend(
162 | {redirectTo: path},
163 | pathRegExp(redirectPath, route)
164 | );
165 | }
166 |
167 | return this;
168 | };
169 |
170 | /**
171 | * @param path {string} path
172 | * @param opts {Object} options
173 | * @return {?Object}
174 | *
175 | * @description
176 | * Normalizes the given path, returning a regular expression
177 | * and the original path.
178 | *
179 | * Inspired by pathRexp in visionmedia/express/lib/utils.js.
180 | */
181 | function pathRegExp(path, opts) {
182 | var insensitive = opts.caseInsensitiveMatch,
183 | ret = {
184 | originalPath: path,
185 | regexp: path
186 | },
187 | keys = ret.keys = [];
188 |
189 | path = path
190 | .replace(/([().])/g, '\\$1')
191 | .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){
192 | var optional = option === '?' ? option : null;
193 | var star = option === '*' ? option : null;
194 | keys.push({ name: key, optional: !!optional });
195 | slash = slash || '';
196 | return ''
197 | + (optional ? '' : slash)
198 | + '(?:'
199 | + (optional ? slash : '')
200 | + (star && '(.+?)' || '([^/]+)')
201 | + (optional || '')
202 | + ')'
203 | + (optional || '');
204 | })
205 | .replace(/([\/$\*])/g, '\\$1');
206 |
207 | ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
208 | return ret;
209 | }
210 |
211 | /**
212 | * @ngdoc method
213 | * @name $routeProvider#otherwise
214 | *
215 | * @description
216 | * Sets route definition that will be used on route change when no other route definition
217 | * is matched.
218 | *
219 | * @param {Object} params Mapping information to be assigned to `$route.current`.
220 | * @returns {Object} self
221 | */
222 | this.otherwise = function(params) {
223 | this.when(null, params);
224 | return this;
225 | };
226 |
227 |
228 | this.$get = ['$rootScope',
229 | '$location',
230 | '$routeParams',
231 | '$q',
232 | '$injector',
233 | '$http',
234 | '$templateCache',
235 | '$sce',
236 | function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) {
237 |
238 | /**
239 | * @ngdoc service
240 | * @name $route
241 | * @requires $location
242 | * @requires $routeParams
243 | *
244 | * @property {Object} current Reference to the current route definition.
245 | * The route definition contains:
246 | *
247 | * - `controller`: The controller constructor as define in route definition.
248 | * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for
249 | * controller instantiation. The `locals` contain
250 | * the resolved values of the `resolve` map. Additionally the `locals` also contain:
251 | *
252 | * - `$scope` - The current route scope.
253 | * - `$template` - The current route template HTML.
254 | *
255 | * @property {Object} routes Object with all route configuration Objects as its properties.
256 | *
257 | * @description
258 | * `$route` is used for deep-linking URLs to controllers and views (HTML partials).
259 | * It watches `$location.url()` and tries to map the path to an existing route definition.
260 | *
261 | * Requires the {@link ngRoute `ngRoute`} module to be installed.
262 | *
263 | * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API.
264 | *
265 | * The `$route` service is typically used in conjunction with the
266 | * {@link ngRoute.directive:ngView `ngView`} directive and the
267 | * {@link ngRoute.$routeParams `$routeParams`} service.
268 | *
269 | * @example
270 | * This example shows how changing the URL hash causes the `$route` to match a route against the
271 | * URL, and the `ngView` pulls in the partial.
272 | *
273 | * Note that this example is using {@link ng.directive:script inlined templates}
274 | * to get it working on jsfiddle as well.
275 | *
276 | *
278 | *
279 | *
280 | * Choose:
281 | *
Moby |
282 | *
Moby: Ch1 |
283 | *
Gatsby |
284 | *
Gatsby: Ch4 |
285 | *
Scarlet Letter
286 | *
287 | *
288 | *
289 | *
290 | *
291 | *
$location.path() = {{$location.path()}}
292 | *
$route.current.templateUrl = {{$route.current.templateUrl}}
293 | *
$route.current.params = {{$route.current.params}}
294 | *
$route.current.scope.name = {{$route.current.scope.name}}
295 | *
$routeParams = {{$routeParams}}
296 | *
297 | *
298 | *
299 | *
300 | * controller: {{name}}
301 | * Book Id: {{params.bookId}}
302 | *
303 | *
304 | *
305 | * controller: {{name}}
306 | * Book Id: {{params.bookId}}
307 | * Chapter Id: {{params.chapterId}}
308 | *
309 | *
310 | *
311 | * angular.module('ngRouteExample', ['ngRoute'])
312 | *
313 | * .controller('MainController', function($scope, $route, $routeParams, $location) {
314 | * $scope.$route = $route;
315 | * $scope.$location = $location;
316 | * $scope.$routeParams = $routeParams;
317 | * })
318 | *
319 | * .controller('BookController', function($scope, $routeParams) {
320 | * $scope.name = "BookController";
321 | * $scope.params = $routeParams;
322 | * })
323 | *
324 | * .controller('ChapterController', function($scope, $routeParams) {
325 | * $scope.name = "ChapterController";
326 | * $scope.params = $routeParams;
327 | * })
328 | *
329 | * .config(function($routeProvider, $locationProvider) {
330 | * $routeProvider
331 | * .when('/Book/:bookId', {
332 | * templateUrl: 'book.html',
333 | * controller: 'BookController',
334 | * resolve: {
335 | * // I will cause a 1 second delay
336 | * delay: function($q, $timeout) {
337 | * var delay = $q.defer();
338 | * $timeout(delay.resolve, 1000);
339 | * return delay.promise;
340 | * }
341 | * }
342 | * })
343 | * .when('/Book/:bookId/ch/:chapterId', {
344 | * templateUrl: 'chapter.html',
345 | * controller: 'ChapterController'
346 | * });
347 | *
348 | * // configure html5 to get links working on jsfiddle
349 | * $locationProvider.html5Mode(true);
350 | * });
351 | *
352 | *
353 | *
354 | *
355 | * it('should load and compile correct template', function() {
356 | * element(by.linkText('Moby: Ch1')).click();
357 | * var content = element(by.css('[ng-view]')).getText();
358 | * expect(content).toMatch(/controller\: ChapterController/);
359 | * expect(content).toMatch(/Book Id\: Moby/);
360 | * expect(content).toMatch(/Chapter Id\: 1/);
361 | *
362 | * element(by.partialLinkText('Scarlet')).click();
363 | *
364 | * content = element(by.css('[ng-view]')).getText();
365 | * expect(content).toMatch(/controller\: BookController/);
366 | * expect(content).toMatch(/Book Id\: Scarlet/);
367 | * });
368 | *
369 | *
370 | */
371 |
372 | /**
373 | * @ngdoc event
374 | * @name $route#$routeChangeStart
375 | * @eventType broadcast on root scope
376 | * @description
377 | * Broadcasted before a route change. At this point the route services starts
378 | * resolving all of the dependencies needed for the route change to occur.
379 | * Typically this involves fetching the view template as well as any dependencies
380 | * defined in `resolve` route property. Once all of the dependencies are resolved
381 | * `$routeChangeSuccess` is fired.
382 | *
383 | * @param {Object} angularEvent Synthetic event object.
384 | * @param {Route} next Future route information.
385 | * @param {Route} current Current route information.
386 | */
387 |
388 | /**
389 | * @ngdoc event
390 | * @name $route#$routeChangeSuccess
391 | * @eventType broadcast on root scope
392 | * @description
393 | * Broadcasted after a route dependencies are resolved.
394 | * {@link ngRoute.directive:ngView ngView} listens for the directive
395 | * to instantiate the controller and render the view.
396 | *
397 | * @param {Object} angularEvent Synthetic event object.
398 | * @param {Route} current Current route information.
399 | * @param {Route|Undefined} previous Previous route information, or undefined if current is
400 | * first route entered.
401 | */
402 |
403 | /**
404 | * @ngdoc event
405 | * @name $route#$routeChangeError
406 | * @eventType broadcast on root scope
407 | * @description
408 | * Broadcasted if any of the resolve promises are rejected.
409 | *
410 | * @param {Object} angularEvent Synthetic event object
411 | * @param {Route} current Current route information.
412 | * @param {Route} previous Previous route information.
413 | * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise.
414 | */
415 |
416 | /**
417 | * @ngdoc event
418 | * @name $route#$routeUpdate
419 | * @eventType broadcast on root scope
420 | * @description
421 | *
422 | * The `reloadOnSearch` property has been set to false, and we are reusing the same
423 | * instance of the Controller.
424 | */
425 |
426 | var forceReload = false,
427 | $route = {
428 | routes: routes,
429 |
430 | /**
431 | * @ngdoc method
432 | * @name $route#reload
433 | *
434 | * @description
435 | * Causes `$route` service to reload the current route even if
436 | * {@link ng.$location $location} hasn't changed.
437 | *
438 | * As a result of that, {@link ngRoute.directive:ngView ngView}
439 | * creates new scope, reinstantiates the controller.
440 | */
441 | reload: function() {
442 | forceReload = true;
443 | $rootScope.$evalAsync(updateRoute);
444 | }
445 | };
446 |
447 | $rootScope.$on('$locationChangeSuccess', updateRoute);
448 |
449 | return $route;
450 |
451 | /////////////////////////////////////////////////////
452 |
453 | /**
454 | * @param on {string} current url
455 | * @param route {Object} route regexp to match the url against
456 | * @return {?Object}
457 | *
458 | * @description
459 | * Check if the route matches the current url.
460 | *
461 | * Inspired by match in
462 | * visionmedia/express/lib/router/router.js.
463 | */
464 | function switchRouteMatcher(on, route) {
465 | var keys = route.keys,
466 | params = {};
467 |
468 | if (!route.regexp) return null;
469 |
470 | var m = route.regexp.exec(on);
471 | if (!m) return null;
472 |
473 | for (var i = 1, len = m.length; i < len; ++i) {
474 | var key = keys[i - 1];
475 |
476 | var val = 'string' == typeof m[i]
477 | ? decodeURIComponent(m[i])
478 | : m[i];
479 |
480 | if (key && val) {
481 | params[key.name] = val;
482 | }
483 | }
484 | return params;
485 | }
486 |
487 | function updateRoute() {
488 | var next = parseRoute(),
489 | last = $route.current;
490 |
491 | if (next && last && next.$$route === last.$$route
492 | && angular.equals(next.pathParams, last.pathParams)
493 | && !next.reloadOnSearch && !forceReload) {
494 | last.params = next.params;
495 | angular.copy(last.params, $routeParams);
496 | $rootScope.$broadcast('$routeUpdate', last);
497 | } else if (next || last) {
498 | forceReload = false;
499 | $rootScope.$broadcast('$routeChangeStart', next, last);
500 | $route.current = next;
501 | if (next) {
502 | if (next.redirectTo) {
503 | if (angular.isString(next.redirectTo)) {
504 | $location.path(interpolate(next.redirectTo, next.params)).search(next.params)
505 | .replace();
506 | } else {
507 | $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
508 | .replace();
509 | }
510 | }
511 | }
512 |
513 | $q.when(next).
514 | then(function() {
515 | if (next) {
516 | var locals = angular.extend({}, next.resolve),
517 | template, templateUrl;
518 |
519 | angular.forEach(locals, function(value, key) {
520 | locals[key] = angular.isString(value) ?
521 | $injector.get(value) : $injector.invoke(value);
522 | });
523 |
524 | if (angular.isDefined(template = next.template)) {
525 | if (angular.isFunction(template)) {
526 | template = template(next.params);
527 | }
528 | } else if (angular.isDefined(templateUrl = next.templateUrl)) {
529 | if (angular.isFunction(templateUrl)) {
530 | templateUrl = templateUrl(next.params);
531 | }
532 | templateUrl = $sce.getTrustedResourceUrl(templateUrl);
533 | if (angular.isDefined(templateUrl)) {
534 | next.loadedTemplateUrl = templateUrl;
535 | template = $http.get(templateUrl, {cache: $templateCache}).
536 | then(function(response) { return response.data; });
537 | }
538 | }
539 | if (angular.isDefined(template)) {
540 | locals['$template'] = template;
541 | }
542 | return $q.all(locals);
543 | }
544 | }).
545 | // after route change
546 | then(function(locals) {
547 | if (next == $route.current) {
548 | if (next) {
549 | next.locals = locals;
550 | angular.copy(next.params, $routeParams);
551 | }
552 | $rootScope.$broadcast('$routeChangeSuccess', next, last);
553 | }
554 | }, function(error) {
555 | if (next == $route.current) {
556 | $rootScope.$broadcast('$routeChangeError', next, last, error);
557 | }
558 | });
559 | }
560 | }
561 |
562 |
563 | /**
564 | * @returns {Object} the current active route, by matching it against the URL
565 | */
566 | function parseRoute() {
567 | // Match a route
568 | var params, match;
569 | angular.forEach(routes, function(route, path) {
570 | if (!match && (params = switchRouteMatcher($location.path(), route))) {
571 | match = inherit(route, {
572 | params: angular.extend({}, $location.search(), params),
573 | pathParams: params});
574 | match.$$route = route;
575 | }
576 | });
577 | // No route matched; fallback to "otherwise" route
578 | return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
579 | }
580 |
581 | /**
582 | * @returns {string} interpolation of the redirect path with the parameters
583 | */
584 | function interpolate(string, params) {
585 | var result = [];
586 | angular.forEach((string||'').split(':'), function(segment, i) {
587 | if (i === 0) {
588 | result.push(segment);
589 | } else {
590 | var segmentMatch = segment.match(/(\w+)(.*)/);
591 | var key = segmentMatch[1];
592 | result.push(params[key]);
593 | result.push(segmentMatch[2] || '');
594 | delete params[key];
595 | }
596 | });
597 | return result.join('');
598 | }
599 | }];
600 | }
601 |
602 | ngRouteModule.provider('$routeParams', $RouteParamsProvider);
603 |
604 |
605 | /**
606 | * @ngdoc service
607 | * @name $routeParams
608 | * @requires $route
609 | *
610 | * @description
611 | * The `$routeParams` service allows you to retrieve the current set of route parameters.
612 | *
613 | * Requires the {@link ngRoute `ngRoute`} module to be installed.
614 | *
615 | * The route parameters are a combination of {@link ng.$location `$location`}'s
616 | * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}.
617 | * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched.
618 | *
619 | * In case of parameter name collision, `path` params take precedence over `search` params.
620 | *
621 | * The service guarantees that the identity of the `$routeParams` object will remain unchanged
622 | * (but its properties will likely change) even when a route change occurs.
623 | *
624 | * Note that the `$routeParams` are only updated *after* a route change completes successfully.
625 | * This means that you cannot rely on `$routeParams` being correct in route resolve functions.
626 | * Instead you can use `$route.current.params` to access the new route's parameters.
627 | *
628 | * @example
629 | * ```js
630 | * // Given:
631 | * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
632 | * // Route: /Chapter/:chapterId/Section/:sectionId
633 | * //
634 | * // Then
635 | * $routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
636 | * ```
637 | */
638 | function $RouteParamsProvider() {
639 | this.$get = function() { return {}; };
640 | }
641 |
642 | ngRouteModule.directive('ngView', ngViewFactory);
643 | ngRouteModule.directive('ngView', ngViewFillContentFactory);
644 |
645 |
646 | /**
647 | * @ngdoc directive
648 | * @name ngView
649 | * @restrict ECA
650 | *
651 | * @description
652 | * # Overview
653 | * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by
654 | * including the rendered template of the current route into the main layout (`index.html`) file.
655 | * Every time the current route changes, the included view changes with it according to the
656 | * configuration of the `$route` service.
657 | *
658 | * Requires the {@link ngRoute `ngRoute`} module to be installed.
659 | *
660 | * @animations
661 | * enter - animation is used to bring new content into the browser.
662 | * leave - animation is used to animate existing content away.
663 | *
664 | * The enter and leave animation occur concurrently.
665 | *
666 | * @scope
667 | * @priority 400
668 | * @param {string=} onload Expression to evaluate whenever the view updates.
669 | *
670 | * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll
671 | * $anchorScroll} to scroll the viewport after the view is updated.
672 | *
673 | * - If the attribute is not set, disable scrolling.
674 | * - If the attribute is set without value, enable scrolling.
675 | * - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated
676 | * as an expression yields a truthy value.
677 | * @example
678 |
681 |
682 |
683 | Choose:
684 |
Moby |
685 |
Moby: Ch1 |
686 |
Gatsby |
687 |
Gatsby: Ch4 |
688 |
Scarlet Letter
689 |
690 |
693 |
694 |
695 |
$location.path() = {{main.$location.path()}}
696 |
$route.current.templateUrl = {{main.$route.current.templateUrl}}
697 |
$route.current.params = {{main.$route.current.params}}
698 |
$route.current.scope.name = {{main.$route.current.scope.name}}
699 |
$routeParams = {{main.$routeParams}}
700 |
701 |
702 |
703 |
704 |
705 | controller: {{book.name}}
706 | Book Id: {{book.params.bookId}}
707 |
708 |
709 |
710 |
711 |
712 | controller: {{chapter.name}}
713 | Book Id: {{chapter.params.bookId}}
714 | Chapter Id: {{chapter.params.chapterId}}
715 |
716 |
717 |
718 |
719 | .view-animate-container {
720 | position:relative;
721 | height:100px!important;
722 | position:relative;
723 | background:white;
724 | border:1px solid black;
725 | height:40px;
726 | overflow:hidden;
727 | }
728 |
729 | .view-animate {
730 | padding:10px;
731 | }
732 |
733 | .view-animate.ng-enter, .view-animate.ng-leave {
734 | -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
735 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
736 |
737 | display:block;
738 | width:100%;
739 | border-left:1px solid black;
740 |
741 | position:absolute;
742 | top:0;
743 | left:0;
744 | right:0;
745 | bottom:0;
746 | padding:10px;
747 | }
748 |
749 | .view-animate.ng-enter {
750 | left:100%;
751 | }
752 | .view-animate.ng-enter.ng-enter-active {
753 | left:0;
754 | }
755 | .view-animate.ng-leave.ng-leave-active {
756 | left:-100%;
757 | }
758 |
759 |
760 |
761 | angular.module('ngViewExample', ['ngRoute', 'ngAnimate'])
762 | .config(['$routeProvider', '$locationProvider',
763 | function($routeProvider, $locationProvider) {
764 | $routeProvider
765 | .when('/Book/:bookId', {
766 | templateUrl: 'book.html',
767 | controller: 'BookCtrl',
768 | controllerAs: 'book'
769 | })
770 | .when('/Book/:bookId/ch/:chapterId', {
771 | templateUrl: 'chapter.html',
772 | controller: 'ChapterCtrl',
773 | controllerAs: 'chapter'
774 | });
775 |
776 | // configure html5 to get links working on jsfiddle
777 | $locationProvider.html5Mode(true);
778 | }])
779 | .controller('MainCtrl', ['$route', '$routeParams', '$location',
780 | function($route, $routeParams, $location) {
781 | this.$route = $route;
782 | this.$location = $location;
783 | this.$routeParams = $routeParams;
784 | }])
785 | .controller('BookCtrl', ['$routeParams', function($routeParams) {
786 | this.name = "BookCtrl";
787 | this.params = $routeParams;
788 | }])
789 | .controller('ChapterCtrl', ['$routeParams', function($routeParams) {
790 | this.name = "ChapterCtrl";
791 | this.params = $routeParams;
792 | }]);
793 |
794 |
795 |
796 |
797 | it('should load and compile correct template', function() {
798 | element(by.linkText('Moby: Ch1')).click();
799 | var content = element(by.css('[ng-view]')).getText();
800 | expect(content).toMatch(/controller\: ChapterCtrl/);
801 | expect(content).toMatch(/Book Id\: Moby/);
802 | expect(content).toMatch(/Chapter Id\: 1/);
803 |
804 | element(by.partialLinkText('Scarlet')).click();
805 |
806 | content = element(by.css('[ng-view]')).getText();
807 | expect(content).toMatch(/controller\: BookCtrl/);
808 | expect(content).toMatch(/Book Id\: Scarlet/);
809 | });
810 |
811 |
812 | */
813 |
814 |
815 | /**
816 | * @ngdoc event
817 | * @name ngView#$viewContentLoaded
818 | * @eventType emit on the current ngView scope
819 | * @description
820 | * Emitted every time the ngView content is reloaded.
821 | */
822 | ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate'];
823 | function ngViewFactory( $route, $anchorScroll, $animate) {
824 | return {
825 | restrict: 'ECA',
826 | terminal: true,
827 | priority: 400,
828 | transclude: 'element',
829 | link: function(scope, $element, attr, ctrl, $transclude) {
830 | var currentScope,
831 | currentElement,
832 | previousElement,
833 | autoScrollExp = attr.autoscroll,
834 | onloadExp = attr.onload || '';
835 |
836 | scope.$on('$routeChangeSuccess', update);
837 | update();
838 |
839 | function cleanupLastView() {
840 | if(previousElement) {
841 | previousElement.remove();
842 | previousElement = null;
843 | }
844 | if(currentScope) {
845 | currentScope.$destroy();
846 | currentScope = null;
847 | }
848 | if(currentElement) {
849 | $animate.leave(currentElement, function() {
850 | previousElement = null;
851 | });
852 | previousElement = currentElement;
853 | currentElement = null;
854 | }
855 | }
856 |
857 | function update() {
858 | var locals = $route.current && $route.current.locals,
859 | template = locals && locals.$template;
860 |
861 | if (angular.isDefined(template)) {
862 | var newScope = scope.$new();
863 | var current = $route.current;
864 |
865 | // Note: This will also link all children of ng-view that were contained in the original
866 | // html. If that content contains controllers, ... they could pollute/change the scope.
867 | // However, using ng-view on an element with additional content does not make sense...
868 | // Note: We can't remove them in the cloneAttchFn of $transclude as that
869 | // function is called before linking the content, which would apply child
870 | // directives to non existing elements.
871 | var clone = $transclude(newScope, function(clone) {
872 | $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () {
873 | if (angular.isDefined(autoScrollExp)
874 | && (!autoScrollExp || scope.$eval(autoScrollExp))) {
875 | $anchorScroll();
876 | }
877 | });
878 | cleanupLastView();
879 | });
880 |
881 | currentElement = clone;
882 | currentScope = current.scope = newScope;
883 | currentScope.$emit('$viewContentLoaded');
884 | currentScope.$eval(onloadExp);
885 | } else {
886 | cleanupLastView();
887 | }
888 | }
889 | }
890 | };
891 | }
892 |
893 | // This directive is called during the $transclude call of the first `ngView` directive.
894 | // It will replace and compile the content of the element with the loaded template.
895 | // We need this directive so that the element content is already filled when
896 | // the link function of another directive on the same element as ngView
897 | // is called.
898 | ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route'];
899 | function ngViewFillContentFactory($compile, $controller, $route) {
900 | return {
901 | restrict: 'ECA',
902 | priority: -400,
903 | link: function(scope, $element) {
904 | var current = $route.current,
905 | locals = current.locals;
906 |
907 | $element.html(locals.$template);
908 |
909 | var link = $compile($element.contents());
910 |
911 | if (current.controller) {
912 | locals.$scope = scope;
913 | var controller = $controller(current.controller, locals);
914 | if (current.controllerAs) {
915 | scope[current.controllerAs] = controller;
916 | }
917 | $element.data('$ngControllerController', controller);
918 | $element.children().data('$ngControllerController', controller);
919 | }
920 |
921 | link(scope);
922 | }
923 | };
924 | }
925 |
926 |
927 | })(window, window.angular);
928 |
--------------------------------------------------------------------------------
/bower_components/angular-route/angular-route.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | AngularJS v1.2.16
3 | (c) 2010-2014 Google, Inc. http://angularjs.org
4 | License: MIT
5 | */
6 | (function(n,e,A){'use strict';function x(s,g,k){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,c,b,f,w){function y(){p&&(p.remove(),p=null);h&&(h.$destroy(),h=null);l&&(k.leave(l,function(){p=null}),p=l,l=null)}function v(){var b=s.current&&s.current.locals;if(e.isDefined(b&&b.$template)){var b=a.$new(),d=s.current;l=w(b,function(d){k.enter(d,null,l||c,function(){!e.isDefined(t)||t&&!a.$eval(t)||g()});y()});h=d.scope=b;h.$emit("$viewContentLoaded");h.$eval(u)}else y()}
7 | var h,l,p,t=b.autoscroll,u=b.onload||"";a.$on("$routeChangeSuccess",v);v()}}}function z(e,g,k){return{restrict:"ECA",priority:-400,link:function(a,c){var b=k.current,f=b.locals;c.html(f.$template);var w=e(c.contents());b.controller&&(f.$scope=a,f=g(b.controller,f),b.controllerAs&&(a[b.controllerAs]=f),c.data("$ngControllerController",f),c.children().data("$ngControllerController",f));w(a)}}}n=e.module("ngRoute",["ng"]).provider("$route",function(){function s(a,c){return e.extend(new (e.extend(function(){},
8 | {prototype:a})),c)}function g(a,e){var b=e.caseInsensitiveMatch,f={originalPath:a,regexp:a},k=f.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,e,b,c){a="?"===c?c:null;c="*"===c?c:null;k.push({name:b,optional:!!a});e=e||"";return""+(a?"":e)+"(?:"+(a?e:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");f.regexp=RegExp("^"+a+"$",b?"i":"");return f}var k={};this.when=function(a,c){k[a]=e.extend({reloadOnSearch:!0},c,a&&g(a,c));if(a){var b=
9 | "/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";k[b]=e.extend({redirectTo:a},g(b,c))}return this};this.otherwise=function(a){this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$http","$templateCache","$sce",function(a,c,b,f,g,n,v,h){function l(){var d=p(),m=r.current;if(d&&m&&d.$$route===m.$$route&&e.equals(d.pathParams,m.pathParams)&&!d.reloadOnSearch&&!u)m.params=d.params,e.copy(m.params,b),a.$broadcast("$routeUpdate",m);else if(d||m)u=!1,a.$broadcast("$routeChangeStart",
10 | d,m),(r.current=d)&&d.redirectTo&&(e.isString(d.redirectTo)?c.path(t(d.redirectTo,d.params)).search(d.params).replace():c.url(d.redirectTo(d.pathParams,c.path(),c.search())).replace()),f.when(d).then(function(){if(d){var a=e.extend({},d.resolve),c,b;e.forEach(a,function(d,c){a[c]=e.isString(d)?g.get(d):g.invoke(d)});e.isDefined(c=d.template)?e.isFunction(c)&&(c=c(d.params)):e.isDefined(b=d.templateUrl)&&(e.isFunction(b)&&(b=b(d.params)),b=h.getTrustedResourceUrl(b),e.isDefined(b)&&(d.loadedTemplateUrl=
11 | b,c=n.get(b,{cache:v}).then(function(a){return a.data})));e.isDefined(c)&&(a.$template=c);return f.all(a)}}).then(function(c){d==r.current&&(d&&(d.locals=c,e.copy(d.params,b)),a.$broadcast("$routeChangeSuccess",d,m))},function(c){d==r.current&&a.$broadcast("$routeChangeError",d,m,c)})}function p(){var a,b;e.forEach(k,function(f,k){var q;if(q=!b){var g=c.path();q=f.keys;var l={};if(f.regexp)if(g=f.regexp.exec(g)){for(var h=1,p=g.length;h` to your `index.html`:
16 |
17 | ```html
18 |
19 | ```
20 |
21 | ## Documentation
22 |
23 | Documentation is available on the
24 | [AngularJS docs site](http://docs.angularjs.org/).
25 |
26 | ## License
27 |
28 | The MIT License
29 |
30 | Copyright (c) 2010-2012 Google, Inc. http://angularjs.org
31 |
32 | Permission is hereby granted, free of charge, to any person obtaining a copy
33 | of this software and associated documentation files (the "Software"), to deal
34 | in the Software without restriction, including without limitation the rights
35 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
36 | copies of the Software, and to permit persons to whom the Software is
37 | furnished to do so, subject to the following conditions:
38 |
39 | The above copyright notice and this permission notice shall be included in
40 | all copies or substantial portions of the Software.
41 |
42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
44 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
45 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
46 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
47 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
48 | THE SOFTWARE.
49 |
--------------------------------------------------------------------------------
/bower_components/angular/angular-csp.css:
--------------------------------------------------------------------------------
1 | /* Include this file in your html if you are using the CSP mode. */
2 |
3 | @charset "UTF-8";
4 |
5 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],
6 | .ng-cloak, .x-ng-cloak,
7 | .ng-hide {
8 | display: none !important;
9 | }
10 |
11 | ng\:form {
12 | display: block;
13 | }
14 |
15 | .ng-animate-block-transitions {
16 | transition:0s all!important;
17 | -webkit-transition:0s all!important;
18 | }
19 |
--------------------------------------------------------------------------------
/bower_components/angular/angular.min.js.gzip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/possan/webapi-player-example/47d3e78fcdd8baa8c89b0e760e0d72fabf9b1386/bower_components/angular/angular.min.js.gzip
--------------------------------------------------------------------------------
/bower_components/angular/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular",
3 | "version": "1.2.16",
4 | "main": "./angular.js",
5 | "dependencies": {
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/callback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/controllers/album.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('AlbumController', function($scope, $rootScope, API, PlayQueue, $routeParams) {
6 | $scope.album = $routeParams.album;
7 | $scope.data = null;
8 | $scope.release_year = '';
9 | $scope.total_duration = 0;
10 | $scope.num_discs = 0;
11 | $scope.tracks = [];
12 |
13 | API.getAlbum($scope.album).then(function(album) {
14 | console.log('got album', album);
15 | $scope.data = album;
16 |
17 |
18 | $scope.release_year = '';
19 |
20 | if (album.release_date) {
21 | $scope.release_year = album.release_date.substring(0, 4); // så fult!
22 | }
23 |
24 | });
25 |
26 | API.getAlbumTracks($scope.album).then(function(tracks) {
27 | console.log('got album tracks', tracks);
28 |
29 | // split into discs
30 | var discs = [];
31 | var disc = { disc_number: 1, tracks: [] };
32 | var tot = 0;
33 | tracks.items.forEach(function(track) {
34 | tot += track.duration_ms;
35 | if (track.disc_number != disc.disc_number) {
36 | discs.push(disc);
37 | disc = { disc_number: track.disc_number, tracks: [] };
38 | }
39 | disc.tracks.push(track);
40 |
41 | track.popularity = 0;
42 | });
43 | discs.push(disc);
44 | console.log('discs', discs);
45 | $scope.discs = discs;
46 | $scope.tracks = tracks.items;
47 | $scope.num_discs = discs.length;
48 | $scope.total_duration = tot;
49 |
50 | // find out if they are in the user's collection
51 | var ids = $scope.tracks.map(function(track) {
52 | return track.id;
53 | });
54 |
55 |
56 | API.getTracks(ids).then(function(results) {
57 | results.tracks.forEach(function(result, index) {
58 | $scope.tracks[index].popularity = result.popularity;
59 | });
60 | });
61 |
62 | API.containsUserTracks(ids).then(function(results) {
63 | results.forEach(function(result, index) {
64 | $scope.tracks[index].inYourMusic = result;
65 | });
66 | });
67 |
68 | });
69 |
70 | $scope.currenttrack = PlayQueue.getCurrent();
71 | $rootScope.$on('playqueuechanged', function() {
72 | $scope.currenttrack = PlayQueue.getCurrent();
73 | });
74 |
75 | $scope.play = function(trackuri) {
76 | var trackuris = $scope.tracks.map(function(track) {
77 | return track.uri;
78 | });
79 | PlayQueue.clear();
80 | PlayQueue.enqueueList(trackuris);
81 | PlayQueue.playFrom(trackuris.indexOf(trackuri));
82 | };
83 |
84 | $scope.playall = function() {
85 | var trackuris = $scope.tracks.map(function(track) {
86 | return track.uri;
87 | });
88 | PlayQueue.clear();
89 | PlayQueue.enqueueList(trackuris);
90 | PlayQueue.playFrom(0);
91 | };
92 |
93 | $scope.toggleFromYourMusic = function(index) {
94 | if ($scope.tracks[index].inYourMusic) {
95 | API.removeFromMyTracks([$scope.tracks[index].id]).then(function(response) {
96 | $scope.tracks[index].inYourMusic = false;
97 | });
98 | } else {
99 | API.addToMyTracks([$scope.tracks[index].id]).then(function(response) {
100 | $scope.tracks[index].inYourMusic = true;
101 | });
102 | }
103 | };
104 |
105 | });
106 |
107 | })();
108 |
--------------------------------------------------------------------------------
/controllers/artist.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('ArtistController', function($scope, $rootScope, API, PlayQueue, $routeParams, Auth) {
6 | $scope.artist = $routeParams.artist;
7 | $scope.data = null;
8 | $scope.discog = [];
9 | $scope.albums = [];
10 | $scope.singles = [];
11 | $scope.appearson = [];
12 |
13 | $scope.currenttrack = PlayQueue.getCurrent();
14 | $scope.isFollowing = false;
15 | $scope.isFollowHovered = false;
16 | $rootScope.$on('playqueuechanged', function() {
17 | $scope.currenttrack = PlayQueue.getCurrent();
18 | });
19 |
20 | API.getArtist($scope.artist).then(function(artist) {
21 | console.log('got artist', artist);
22 | $scope.data = artist;
23 | });
24 |
25 | API.getArtistTopTracks($scope.artist, Auth.getUserCountry()).then(function(toptracks) {
26 | console.log('got artist', toptracks);
27 | $scope.toptracks = toptracks.tracks;
28 |
29 | var ids = $scope.toptracks.map(function(track) {
30 | return track.id;
31 | });
32 |
33 | API.containsUserTracks(ids).then(function(results) {
34 | results.forEach(function(result, index) {
35 | $scope.toptracks[index].inYourMusic = result;
36 | });
37 | });
38 | });
39 |
40 | API.getArtistAlbums($scope.artist, Auth.getUserCountry()).then(function(albums) {
41 | console.log('got artist albums', albums);
42 | $scope.albums = [];
43 | $scope.singles = [];
44 | $scope.appearson = [];
45 | albums.items.forEach(function(album) {
46 | console.log(album);
47 | if (album.album_type == 'album') {
48 | $scope.albums.push(album);
49 | }
50 | if (album.album_type == 'single') {
51 | $scope.singles.push(album);
52 | }
53 | if (album.album_type == 'appears-on') {
54 | $scope.appearson.push(album);
55 | }
56 | })
57 | });
58 |
59 | API.isFollowing($scope.artist, "artist").then(function(booleans) {
60 | console.log("Got following status for artist: " + booleans[0]);
61 | $scope.isFollowing = booleans[0];
62 | });
63 |
64 | $scope.playtoptrack = function(trackuri) {
65 | var trackuris = $scope.toptracks.map(function(track) {
66 | return track.uri;
67 | });
68 | PlayQueue.clear();
69 | PlayQueue.enqueueList(trackuris);
70 | PlayQueue.playFrom(trackuris.indexOf(trackuri));
71 | };
72 |
73 | $scope.playall = function(trackuri) {
74 | var trackuris = $scope.toptracks.map(function(track) {
75 | return track.uri;
76 | });
77 | PlayQueue.clear();
78 | PlayQueue.enqueueList(trackuris);
79 | PlayQueue.playFrom(0);
80 | };
81 |
82 | $scope.follow = function(isFollowing) {
83 | if (isFollowing) {
84 | API.unfollow($scope.artist, "artist").then(function() {
85 | $scope.isFollowing = false;
86 | $scope.data.followers.total--;
87 | });
88 | } else {
89 | API.follow($scope.artist, "artist").then(function() {
90 | $scope.isFollowing = true;
91 | $scope.data.followers.total++;
92 | });
93 | }
94 | };
95 |
96 | $scope.toggleFromYourMusic = function(index) {
97 | if ($scope.toptracks[index].inYourMusic) {
98 | API.removeFromMyTracks([$scope.toptracks[index].id]).then(function(response) {
99 | $scope.toptracks[index].inYourMusic = false;
100 | });
101 | } else {
102 | API.addToMyTracks([$scope.toptracks[index].id]).then(function(response) {
103 | $scope.toptracks[index].inYourMusic = true;
104 | });
105 | }
106 | };
107 |
108 | });
109 |
110 | })();
111 |
--------------------------------------------------------------------------------
/controllers/browse.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('BrowseController', function($scope, API, Auth, $routeParams) {
6 |
7 | function pad(number) {
8 | if ( number < 10 ) {
9 | return '0' + number;
10 | }
11 | return number;
12 | }
13 |
14 | /**
15 | * Returns an ISO string containing the local time for the user,
16 | * clearing minutes and seconds to improve caching
17 | * @param Date date The date to format
18 | * @return string The formatted date
19 | */
20 | function isoString(date) {
21 | return date.getUTCFullYear() +
22 | '-' + pad( date.getUTCMonth() + 1 ) +
23 | '-' + pad( date.getUTCDate() ) +
24 | 'T' + pad( date.getHours() ) +
25 | ':' + pad( 0 ) +
26 | ':' + pad( 0 )
27 | }
28 |
29 | API.getFeaturedPlaylists(Auth.getUserCountry(), isoString(new Date())).then(function(results) {
30 | $scope.featuredPlaylists = results.playlists.items;
31 | $scope.message = results.message;
32 | });
33 |
34 | API.getNewReleases(Auth.getUserCountry()).then(function(results) {
35 | // @todo: description, follower count
36 | $scope.newReleases = results.albums.items;
37 | });
38 |
39 | API.getBrowseCategories().then(function(results) {
40 | $scope.genresMoods = results.categories.items;
41 | });
42 | });
43 |
44 | })();
45 |
--------------------------------------------------------------------------------
/controllers/browsecategory.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('BrowseCategoryController', function($scope, API, $routeParams, Auth) {
6 | $scope.categoryname = '';
7 |
8 | API.getBrowseCategory($routeParams.categoryid).then(function(result) {
9 | $scope.categoryname = result.name;
10 | });
11 | API.getBrowseCategoryPlaylists($routeParams.categoryid, Auth.getUserCountry()).then(function(results) {
12 | $scope.playlists = results.playlists.items;
13 | });
14 | });
15 |
16 | })();
17 |
--------------------------------------------------------------------------------
/controllers/home.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('HomeController', function($scope, $routeParams) {
6 | $scope.id = $routeParams.id;
7 | });
8 |
9 | })();
10 |
--------------------------------------------------------------------------------
/controllers/login.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('LoginController', function($scope, Auth) {
6 | $scope.isLoggedIn = false;
7 |
8 | $scope.login = function() {
9 | // do login!
10 | console.log('do login...');
11 |
12 | Auth.openLogin();
13 | // $scope.$emit('login');
14 | }
15 | });
16 |
17 | })();
18 |
--------------------------------------------------------------------------------
/controllers/player.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('PlayerController', function($scope, $rootScope, Auth, API, PlayQueue, Playback, $location) {
6 | $scope.view = 'welcome';
7 | $scope.profileUsername = Auth.getUsername();
8 | $scope.playlists = [];
9 | $scope.playing = false;
10 | $scope.progress = 0;
11 | $scope.duration = 4000;
12 | $scope.trackdata = null;
13 | $scope.currenttrack = '';
14 |
15 | function updatePlaylists() {
16 | if ($scope.profileUsername != '') {
17 | API.getPlaylists(Auth.getUsername()).then(function(items) {
18 | $scope.playlists = items.map(function(pl) {
19 | return {
20 | id: pl.id,
21 | name: pl.name,
22 | uri: pl.uri,
23 | username: pl.owner.id,
24 | collaborative: pl.collaborative,
25 | 'public': pl['public']
26 | };
27 | });
28 | });
29 | }
30 | }
31 |
32 | updatePlaylists();
33 |
34 | // subscribe to an event
35 | $rootScope.$on('playlistsubscriptionchange', function() {
36 | updatePlaylists();
37 | });
38 |
39 | $scope.logout = function() {
40 | // do login!
41 | console.log('do logout...');
42 | Auth.setUsername('');
43 | Auth.setAccessToken('', 0);
44 | $scope.$emit('logout');
45 | };
46 |
47 | $scope.resume = function() {
48 | Playback.resume();
49 | };
50 |
51 | $scope.pause = function() {
52 | Playback.pause();
53 | };
54 |
55 | $scope.next = function() {
56 | PlayQueue.next();
57 | Playback.startPlaying(PlayQueue.getCurrent());
58 | };
59 |
60 | $scope.prev = function() {
61 | PlayQueue.prev();
62 | Playback.startPlaying(PlayQueue.getCurrent());
63 | };
64 |
65 | $scope.queue = function(trackuri) {
66 | PlayQueue.enqueue(trackuri);
67 | };
68 |
69 | $scope.showhome = function() {
70 | console.log('load home view');
71 | };
72 |
73 | $scope.showplayqueue = function() {
74 | console.log('load playqueue view');
75 | };
76 |
77 | $scope.showplaylist = function(playlisturi) {
78 | console.log('load playlist view', playlisturi);
79 | };
80 |
81 | $scope.query = '';
82 |
83 | $scope.loadsearch = function() {
84 | console.log('search for', $scope.query);
85 | $location.path('/search').search({ q: $scope.query }).replace();
86 | };
87 |
88 |
89 | $scope.volume = Playback.getVolume();
90 |
91 | $scope.changevolume = function() {
92 | Playback.setVolume($scope.volume);
93 | };
94 |
95 | $scope.changeprogress = function() {
96 | Playback.setProgress($scope.progress);
97 | };
98 |
99 | $rootScope.$on('login', function() {
100 | $scope.profileUsername = Auth.getUsername();
101 | updatePlaylists();
102 | });
103 |
104 | $rootScope.$on('playqueuechanged', function() {
105 | console.log('PlayerController: play queue changed.');
106 | // $scope.duration = Playback.getDuration();
107 | });
108 |
109 | $rootScope.$on('playerchanged', function() {
110 | console.log('PlayerController: player changed.');
111 | $scope.currenttrack = Playback.getTrack();
112 | $scope.playing = Playback.isPlaying();
113 | $scope.trackdata = Playback.getTrackData();
114 | });
115 |
116 | $rootScope.$on('endtrack', function() {
117 | console.log('PlayerController: end track.');
118 | $scope.currenttrack = Playback.getTrack();
119 | $scope.trackdata = Playback.getTrackData();
120 | $scope.playing = Playback.isPlaying();
121 | PlayQueue.next();
122 | Playback.startPlaying(PlayQueue.getCurrent());
123 | $scope.duration = Playback.getDuration();
124 | });
125 |
126 | $rootScope.$on('trackprogress', function() {
127 | console.log('PlayerController: trackprogress.');
128 | $scope.progress = Playback.getProgress();
129 | $scope.duration = Playback.getDuration();
130 | });
131 | });
132 |
133 | })();
134 |
--------------------------------------------------------------------------------
/controllers/playlist.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('PlaylistController', function($scope, $rootScope, API, PlayQueue, $routeParams, Auth, $sce) {
6 | $scope.playlist = $routeParams.playlist;
7 | $scope.username = $routeParams.username;
8 | $scope.name = '';
9 | $scope.tracks = [];
10 | $scope.data = null;
11 | $scope.total_duration = 0;
12 |
13 | $scope.currenttrack = PlayQueue.getCurrent();
14 | $scope.isFollowing = false;
15 | $scope.isFollowHovered = false;
16 |
17 | $rootScope.$on('playqueuechanged', function() {
18 | $scope.currenttrack = PlayQueue.getCurrent();
19 | });
20 |
21 | API.getPlaylist($scope.username, $scope.playlist).then(function(list) {
22 | console.log('got playlist', list);
23 | $scope.name = list.name;
24 | $scope.data = list;
25 | $scope.playlistDescription = $sce.trustAsHtml(list.description);
26 | });
27 |
28 | API.getPlaylistTracks($scope.username, $scope.playlist).then(function(list) {
29 | console.log('got playlist tracks', list);
30 | var tot = 0;
31 | list.items.forEach(function(track) {
32 | tot += track.track.duration_ms;
33 | });
34 | $scope.tracks = list.items;
35 | console.log('tot', tot);
36 | $scope.total_duration = tot;
37 |
38 | // find out if they are in the user's collection
39 | var ids = $scope.tracks.map(function(track) {
40 | return track.track.id;
41 | });
42 |
43 | var i, j, temparray, chunk = 20;
44 | for (i = 0, j = ids.length; i < j; i += chunk) {
45 | temparray = ids.slice(i, i + chunk);
46 | var firstIndex = i;
47 | (function(firstIndex){
48 | API.containsUserTracks(temparray).then(function(results) {
49 | results.forEach(function(result, index) {
50 | $scope.tracks[firstIndex + index].track.inYourMusic = result;
51 | });
52 | });
53 | })(firstIndex);
54 | }
55 | });
56 |
57 | API.isFollowingPlaylist($scope.username, $scope.playlist).then(function(booleans) {
58 | console.log("Got following status for playlist: " + booleans[0]);
59 | $scope.isFollowing = booleans[0];
60 | });
61 |
62 | $scope.follow = function(isFollowing) {
63 | if (isFollowing) {
64 | API.unfollowPlaylist($scope.username, $scope.playlist).then(function() {
65 | $scope.isFollowing = false;
66 | $rootScope.$emit('playlistsubscriptionchange');
67 | });
68 | } else {
69 | API.followPlaylist($scope.username, $scope.playlist).then(function() {
70 | $scope.isFollowing = true;
71 | $rootScope.$emit('playlistsubscriptionchange');
72 | });
73 | }
74 | };
75 |
76 | $scope.play = function(trackuri) {
77 | var trackuris = $scope.tracks.map(function(track) {
78 | return track.track.uri;
79 | });
80 | PlayQueue.clear();
81 | PlayQueue.enqueueList(trackuris);
82 | PlayQueue.playFrom(trackuris.indexOf(trackuri));
83 | };
84 |
85 | $scope.playall = function() {
86 | var trackuris = $scope.tracks.map(function(track) {
87 | return track.track.uri;
88 | });
89 | PlayQueue.clear();
90 | PlayQueue.enqueueList(trackuris);
91 | PlayQueue.playFrom(0);
92 | };
93 |
94 | $scope.toggleFromYourMusic = function(index) {
95 | if ($scope.tracks[index].track.inYourMusic) {
96 | API.removeFromMyTracks([$scope.tracks[index].track.id]).then(function(response) {
97 | $scope.tracks[index].track.inYourMusic = false;
98 | });
99 | } else {
100 | API.addToMyTracks([$scope.tracks[index].track.id]).then(function(response) {
101 | $scope.tracks[index].track.inYourMusic = true;
102 | });
103 | }
104 | };
105 |
106 | $scope.menuOptionsPlaylistTrack = function() {
107 | if ($scope.username === Auth.getUsername()) {
108 | return [[
109 | 'Delete',
110 | function ($itemScope) {
111 | var position = $itemScope.$index;
112 | API.removeTrackFromPlaylist(
113 | $scope.username,
114 | $scope.playlist,
115 | $itemScope.t.track, position).then(function() {
116 | $scope.tracks.splice(position, 1);
117 | });
118 | }]]
119 | } else {
120 | return null;
121 | }
122 | };
123 |
124 | });
125 |
126 | })();
127 |
--------------------------------------------------------------------------------
/controllers/playqueue.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('PlayQueueController', function($scope, $rootScope, $routeParams, PlayQueue) {
6 |
7 | function refresh() {
8 | $scope.current = PlayQueue.getCurrent();
9 | $scope.position = PlayQueue.getPosition();
10 | $scope.queue = PlayQueue.getQueue();
11 | }
12 |
13 | $rootScope.$on('playqueuechanged', function() {
14 | refresh();
15 | });
16 |
17 | refresh();
18 | });
19 |
20 | })();
21 |
--------------------------------------------------------------------------------
/controllers/searchresults.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('SearchResultsController', function($scope, API, $location, PlayQueue, $routeParams) {
6 | $scope.query = $location.search().q || '';
7 | $scope.tracks = [];
8 |
9 | API.getSearchResults($scope.query).then(function(results) {
10 | console.log('got search results', results);
11 | $scope.tracks = results.tracks.items;
12 | $scope.playlists = results.playlists.items;
13 |
14 | // find out if they are in the user's collection
15 | var ids = $scope.tracks.map(function(track) {
16 | return track.id;
17 | });
18 |
19 | API.containsUserTracks(ids).then(function(results) {
20 | results.forEach(function(result, index) {
21 | $scope.tracks[index].inYourMusic = result;
22 | });
23 | });
24 |
25 | });
26 |
27 | $scope.play = function(trackuri) {
28 | var trackuris = $scope.tracks.map(function(track) {
29 | return track.uri;
30 | });
31 | PlayQueue.clear();
32 | PlayQueue.enqueueList(trackuris);
33 | PlayQueue.playFrom(trackuris.indexOf(trackuri));
34 | };
35 |
36 | $scope.toggleFromYourMusic = function(index) {
37 | if ($scope.tracks[index].inYourMusic) {
38 | API.removeFromMyTracks([$scope.tracks[index].id]).then(function(response) {
39 | $scope.tracks[index].inYourMusic = false;
40 | });
41 | } else {
42 | API.addToMyTracks([$scope.tracks[index].id]).then(function(response) {
43 | $scope.tracks[index].inYourMusic = true;
44 | });
45 | }
46 | };
47 | });
48 |
49 | })();
50 |
--------------------------------------------------------------------------------
/controllers/user.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('UserController', function($scope, $routeParams, API) {
6 | $scope.profileUsername = $routeParams.username;
7 | $scope.data = null;
8 | $scope.playlistError = null;
9 | $scope.isFollowing = false;
10 | $scope.isFollowHovered = false;
11 |
12 | API.getUser($scope.profileUsername).then(function(user) {
13 | console.log('got user', user);
14 | $scope.data = user;
15 | });
16 |
17 | API.getPlaylists($scope.profileUsername).then(function(userplaylists){
18 | console.log('got user playlists', userplaylists);
19 | $scope.userplaylists = userplaylists;
20 | }, function(reason){
21 | console.log("got error", reason);
22 | $scope.playlistError = true;
23 | });
24 |
25 | API.isFollowing($scope.profileUsername, "user").then(function(booleans) {
26 | console.log("Got following status for user: " + booleans[0]);
27 | $scope.isFollowing = booleans[0];
28 | });
29 |
30 | $scope.follow = function(isFollowing) {
31 | if (isFollowing) {
32 | API.unfollow($scope.profileUsername, "user").then(function() {
33 | $scope.isFollowing = false;
34 | $scope.data.followers.total--;
35 | });
36 | } else {
37 | API.follow($scope.profileUsername, "user").then(function() {
38 | $scope.isFollowing = true;
39 | $scope.data.followers.total++;
40 | });
41 | }
42 | };
43 |
44 | });
45 |
46 | })();
47 |
--------------------------------------------------------------------------------
/controllers/usertracks.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 |
5 | module.controller('UserTracksController', function($scope, $routeParams, API, PlayQueue) {
6 | $scope.username = $routeParams.username;
7 | $scope.tracks = [];
8 |
9 | API.getMyTracks().then(function(tracks) {
10 | console.log('got user tracks', tracks);
11 | $scope.tracks = tracks.items;
12 | });
13 |
14 | $scope.play = function(trackuri) {
15 | var trackuris = $scope.tracks.map(function(track) {
16 | return track.track.uri;
17 | });
18 | PlayQueue.clear();
19 | PlayQueue.enqueueList(trackuris);
20 | PlayQueue.playFrom(trackuris.indexOf(trackuri));
21 | };
22 |
23 | $scope.removeFromYourMusic = function(index) {
24 | API.removeFromMyTracks([$scope.tracks[index].track.id]).then(function(response) {
25 | $scope.tracks.splice(index, 1);
26 | });
27 | };
28 |
29 | });
30 |
31 | })();
32 |
--------------------------------------------------------------------------------
/directives/contextmenu.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var module = angular.module('PlayerApp');
4 | module.directive('ngContextMenu', function ($parse) {
5 | var renderContextMenu = function ($scope, event, options) {
6 | if (!$) { var $ = angular.element; }
7 | $(event.target).addClass('context');
8 | var $contextMenu = $('