├── .bowerrc
├── .gitattributes
├── .gitignore
├── CHANGELOG.md
├── Gruntfile.js
├── LICENSE
├── MANIFEST.in
├── README.rst
├── __init__.py.tpl
├── bower.json
├── build.config.js
├── changelog.tpl
├── dist
├── assets
│ ├── css
│ │ └── moped-0.7.1.css
│ ├── fonts
│ │ ├── glyphicons-halflings-regular.eot
│ │ ├── glyphicons-halflings-regular.svg
│ │ ├── glyphicons-halflings-regular.ttf
│ │ ├── glyphicons-halflings-regular.woff
│ │ └── glyphicons-halflings-regular.woff2
│ ├── images
│ │ ├── ajax-loader.gif
│ │ ├── android-chrome-144x144.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-36x36.png
│ │ ├── android-chrome-48x48.png
│ │ ├── android-chrome-72x72.png
│ │ ├── android-chrome-96x96.png
│ │ ├── apple-touch-icon-114x114.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-144x144.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── apple-touch-icon-57x57.png
│ │ ├── apple-touch-icon-60x60.png
│ │ ├── apple-touch-icon-72x72.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── apple-touch-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-96x96.png
│ │ ├── favicon.ico
│ │ ├── library-icon.png
│ │ ├── manifest.json
│ │ ├── mstile-144x144.png
│ │ ├── mstile-150x150.png
│ │ ├── mstile-310x150.png
│ │ ├── mstile-310x310.png
│ │ ├── mstile-70x70.png
│ │ ├── noalbum.png
│ │ ├── playlists-icon.png
│ │ ├── radio-icon.png
│ │ ├── search-icon.png
│ │ ├── settings-icon.png
│ │ ├── touch-icon.png
│ │ └── vinyl-icon.png
│ └── moped-0.7.1.js
└── index.html
├── ext.conf
├── karma
└── karma-unit.tpl.js
├── mopidy_moped
├── __init__.py
├── ext.conf
└── static
│ ├── assets
│ ├── css
│ │ └── moped-0.7.1.css
│ ├── fonts
│ │ ├── glyphicons-halflings-regular.eot
│ │ ├── glyphicons-halflings-regular.svg
│ │ ├── glyphicons-halflings-regular.ttf
│ │ ├── glyphicons-halflings-regular.woff
│ │ └── glyphicons-halflings-regular.woff2
│ ├── images
│ │ ├── ajax-loader.gif
│ │ ├── android-chrome-144x144.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-36x36.png
│ │ ├── android-chrome-48x48.png
│ │ ├── android-chrome-72x72.png
│ │ ├── android-chrome-96x96.png
│ │ ├── apple-touch-icon-114x114.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-144x144.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── apple-touch-icon-57x57.png
│ │ ├── apple-touch-icon-60x60.png
│ │ ├── apple-touch-icon-72x72.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── apple-touch-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-96x96.png
│ │ ├── favicon.ico
│ │ ├── library-icon.png
│ │ ├── manifest.json
│ │ ├── mstile-144x144.png
│ │ ├── mstile-150x150.png
│ │ ├── mstile-310x150.png
│ │ ├── mstile-310x310.png
│ │ ├── mstile-70x70.png
│ │ ├── noalbum.png
│ │ ├── playlists-icon.png
│ │ ├── radio-icon.png
│ │ ├── search-icon.png
│ │ ├── settings-icon.png
│ │ ├── touch-icon.png
│ │ └── vinyl-icon.png
│ └── moped-0.7.1.js
│ └── index.html
├── package.json
├── screenshots
├── moped-all-720.png
├── moped-all.png
├── moped-desktop-artist.png
├── moped-desktop-playlist.png
├── moped-ipad-album.png
├── moped-ipad-artist.png
├── moped-ipad-playlist.png
├── moped-ipad-radio.png
├── moped-ipad-search.png
├── moped-iphone-menu.png
└── moped-iphone-playlist.png
├── setup.py
├── src
├── app
│ ├── README.md
│ ├── app.js
│ ├── app.spec.js
│ ├── browse
│ │ ├── album.tpl.html
│ │ ├── artist.tpl.html
│ │ └── browse.js
│ ├── filters.js
│ ├── home
│ │ ├── home.js
│ │ ├── home.spec.js
│ │ └── home.tpl.html
│ ├── library
│ │ ├── container.tpl.html
│ │ ├── directory.tpl.html
│ │ ├── library.js
│ │ ├── menu.tpl.html
│ │ └── playlist.tpl.html
│ ├── nowplaying
│ │ ├── nowplaying.js
│ │ └── nowplaying.tpl.html
│ ├── playercontrols
│ │ ├── playercontrols.js
│ │ └── playercontrols.tpl.html
│ ├── playlists
│ │ ├── list.tpl.html
│ │ ├── menu.tpl.html
│ │ ├── playlistfolder.tpl.html
│ │ └── playlists.js
│ ├── radio
│ │ ├── menu.tpl.html
│ │ ├── radio.js
│ │ └── radio.tpl.html
│ ├── search
│ │ ├── results.tpl.html
│ │ ├── search.js
│ │ └── search.tpl.html
│ ├── services
│ │ ├── lastfmservice.js
│ │ ├── mopidyservice.js
│ │ └── util.js
│ ├── settings
│ │ ├── settings.js
│ │ └── settings.tpl.html
│ └── widgets
│ │ ├── _module.js
│ │ ├── album.js
│ │ ├── album.tpl.html
│ │ ├── albumimage.js
│ │ ├── albumimage.tpl.html
│ │ ├── artistimage.js
│ │ ├── playlist.js
│ │ ├── playlist.tpl.html
│ │ ├── slider.js
│ │ ├── track-medium.tpl.html
│ │ ├── track-short.tpl.html
│ │ ├── track.js
│ │ └── track.tpl.html
├── assets
│ └── images
│ │ ├── ajax-loader.gif
│ │ ├── android-chrome-144x144.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-36x36.png
│ │ ├── android-chrome-48x48.png
│ │ ├── android-chrome-72x72.png
│ │ ├── android-chrome-96x96.png
│ │ ├── apple-touch-icon-114x114.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-144x144.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── apple-touch-icon-57x57.png
│ │ ├── apple-touch-icon-60x60.png
│ │ ├── apple-touch-icon-72x72.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── apple-touch-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-96x96.png
│ │ ├── favicon.ico
│ │ ├── library-icon.png
│ │ ├── manifest.json
│ │ ├── mstile-144x144.png
│ │ ├── mstile-150x150.png
│ │ ├── mstile-310x150.png
│ │ ├── mstile-310x310.png
│ │ ├── mstile-70x70.png
│ │ ├── noalbum.png
│ │ ├── playlists-icon.png
│ │ ├── radio-icon.png
│ │ ├── search-icon.png
│ │ ├── settings-icon.png
│ │ ├── touch-icon.png
│ │ └── vinyl-icon.png
├── index.html
└── less
│ ├── README.md
│ ├── main.less
│ ├── moped.less
│ └── variables.less
└── vendor
├── bootstrap-slider
├── bootstrap-slider.js
└── slider.css
└── mopidy
└── mopidy.js
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "vendor",
3 | "json": "bower.json"
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | dist/* linguist-vendored
2 | mopidy_moped/* linguist-vendored
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.pyc
3 | dist/*.tar.gz
4 | dist/*.whl
5 | *.sw*
6 | *~
7 | build/
8 | bin/
9 | node_modules/
10 | vendor/*
11 | !vendor/mopidy
12 | !vendor/bootstrap-slider
13 | .DS_Store
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.7.1 (2017-05-21)
2 |
3 | - Fixed search by sending individual arguments as array, except when surrounded with double quotes (#69)
4 | - Fixed issue where mopidy server url was set to undefined in LocalStorage when leaving the field empty (#53)
5 |
6 | # 0.7.0 (2016-10-21)
7 |
8 | - Added stop button in player controls (#45)
9 | - Removed (experimental) radio station search
10 |
11 | # 0.6.4 (2015-10-30)
12 |
13 | - Fixed navigation issue on iOS 9 when running from start screen (using UIWebView)
14 |
15 | # 0.6.3 (2015-09-25)
16 |
17 | - Fixed rescaling issue on iOS 9 when using left menu
18 | - Improved experience on iOS by removing hover effect on the playback buttons
19 |
20 | # 0.6.2 (2015-09-11)
21 |
22 | - Fixed seek issue with Mopidy (#55)
23 | - Browsing now supports all ref types (#54, #56)
24 |
25 | # 0.6.1 (2015-06-10)
26 |
27 | - Optimized loading of playlists
28 |
29 | # 0.6.0 (2015-05-12)
30 |
31 | - Added list of current tracks to home screen
32 | - Added Moped version to browser title bar
33 | - New icon and support for favicon in windows phone
34 | - Fixed back button behaviour in standalone mode
35 |
36 | # 0.5.0 (2015-04-05)
37 |
38 | - Updated mopidy.js to 0.5.0
39 | - Mopidy 1.0.0 compatibility
40 | - Updated player controls active and hover styles (Sebastian)
41 |
42 | # 0.4.4 (2015-03-14)
43 |
44 | - Fixed search
45 |
46 | # 0.4.3 (2015-03-14)
47 |
48 | - Min. characters for search is now 2 instead of 3
49 | - Use protocol relative urls for fonts (André Gaul)
50 | - Updated Angular to 1.3.x
51 | - Updated various other js libs to latest version
52 | - Try to display Mopidy album images before requesting album images from LastFM
53 | - Removed clear_current_track parameter from mopidy.stop() method for Mopidy 0.20 compatibility
54 |
55 | # 0.4.2 (2014-11-17)
56 |
57 | - Fixed accidentally disabled error logger
58 |
59 | # 0.4.1 (2014-11-16)
60 |
61 | - Added random toggle switch
62 | - Fixed browsing of playlists (David Tischler)
63 | - Reverted interpolation of track position due to instability
64 | - Search query is passed to mopidy as an array to support new Spotify backend
65 |
66 | # 0.4.0 (2014-10-10)
67 |
68 | - Support for Mopidy browsing (David Tischler, https://github.com/tischlda)
69 | - Fix for search queries (David Tischler)
70 | - Backend provider is displayed in track list (Julien Bordellier)
71 | - Allow special characters in search
72 | - Interpolation of track position and checking every 10 seconds
73 |
74 | # 0.3.3 (2014-08-03)
75 |
76 | - Reduced default amount of logging
77 |
78 | # 0.3.2 (2014-08-03)
79 |
80 | - Fixed volume slider
81 |
82 | # 0.3.1 (2014-07-23)
83 |
84 | - Fixed PyPI package manifest
85 | - Support for playlist folders in PyPI package
86 |
87 | # 0.3.0 (2014-06-24)
88 |
89 | - Moped as installable Mopidy extension
90 |
91 | # 0.2.0 (2013-12-18)
92 |
93 | - Angular version added.
94 |
95 | # 0.1.0 (2013-12-04)
96 |
97 | - Initial Durandal version.
98 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function ( grunt ) {
2 |
3 | /**
4 | * Load required Grunt tasks. These are installed based on the versions listed
5 | * in `package.json` when you do `npm install` in this directory.
6 | */
7 | grunt.loadNpmTasks('grunt-contrib-clean');
8 | grunt.loadNpmTasks('grunt-contrib-copy');
9 | grunt.loadNpmTasks('grunt-contrib-jshint');
10 | grunt.loadNpmTasks('grunt-contrib-concat');
11 | grunt.loadNpmTasks('grunt-contrib-watch');
12 | grunt.loadNpmTasks('grunt-contrib-uglify');
13 | grunt.loadNpmTasks('grunt-contrib-coffee');
14 | grunt.loadNpmTasks('grunt-conventional-changelog');
15 | grunt.loadNpmTasks('grunt-bump');
16 | grunt.loadNpmTasks('grunt-coffeelint');
17 | grunt.loadNpmTasks('grunt-contrib-less');
18 | grunt.loadNpmTasks('grunt-karma');
19 | grunt.loadNpmTasks('grunt-ng-annotate');
20 | grunt.loadNpmTasks('grunt-html2js');
21 | grunt.loadNpmTasks('grunt-contrib-connect');
22 |
23 | /**
24 | * Load in our build configuration file.
25 | */
26 | var userConfig = require( './build.config.js' );
27 |
28 | /**
29 | * This is the configuration object Grunt uses to give each plugin its
30 | * instructions.
31 | */
32 | var taskConfig = {
33 | /**
34 | * We read in our `package.json` file so we can access the package name and
35 | * version. It's already there, so we don't repeat ourselves here.
36 | */
37 | pkg: grunt.file.readJSON("package.json"),
38 |
39 | /**
40 | * The banner is the comment that is placed at the top of our compiled
41 | * source files. It is first processed as a Grunt template, where the `<%=`
42 | * pairs are evaluated based on this very configuration object.
43 | */
44 | meta: {
45 | banner:
46 | '/**\n' +
47 | ' * <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +
48 | ' * <%= pkg.homepage %>\n' +
49 | ' *\n' +
50 | ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
51 | ' * Licensed <%= pkg.licenses.type %> <<%= pkg.licenses.url %>>\n' +
52 | ' */\n'
53 | },
54 |
55 | /**
56 | * Creates a changelog on a new version.
57 | */
58 | changelog: {
59 | options: {
60 | dest: 'CHANGELOG.md',
61 | template: 'changelog.tpl'
62 | }
63 | },
64 |
65 | /**
66 | * Increments the version number, etc.
67 | */
68 | bump: {
69 | options: {
70 | files: [
71 | "package.json",
72 | "bower.json"
73 | ],
74 | commit: false,
75 | commitMessage: 'chore(release): v%VERSION%',
76 | commitFiles: [
77 | "package.json",
78 | "client/bower.json"
79 | ],
80 | createTag: false,
81 | tagName: 'v%VERSION%',
82 | tagMessage: 'Version %VERSION%',
83 | push: false,
84 | pushTo: 'origin'
85 | }
86 | },
87 |
88 | /**
89 | * The directories to delete when `grunt clean` is executed.
90 | */
91 | clean: {
92 | options: {
93 | force: true
94 | },
95 | build: [ '<%= build_dir %>' ],
96 | compile: [ '<%= compile_dir %>' ],
97 | mopidy: [ '<%= mopidy_package_dir %>' ]
98 | },
99 |
100 | /**
101 | * The `copy` task just copies files from A to B. We use it here to copy
102 | * our project assets (images, fonts, etc.) and javascripts into
103 | * `build_dir`, and then to copy the assets to `compile_dir`.
104 | */
105 | copy: {
106 | build_app_assets: {
107 | files: [
108 | {
109 | src: [ '**' ],
110 | dest: '<%= build_dir %>/assets/',
111 | cwd: 'src/assets',
112 | expand: true
113 | }
114 | ]
115 | },
116 | build_vendor_assets: {
117 | files: [
118 | {
119 | src: [ '<%= vendor_files.assets %>' ],
120 | dest: '<%= build_dir %>/assets/',
121 | cwd: '.',
122 | expand: true,
123 | flatten: true
124 | }
125 | ]
126 | },
127 | build_vendor_fonts: {
128 | files: [
129 | {
130 | src: [ '<%= vendor_files.fonts %>' ],
131 | dest: '<%= build_dir %>/assets/fonts/',
132 | cwd: '.',
133 | expand: true,
134 | flatten: true
135 | }
136 | ]
137 | },
138 | build_appjs: {
139 | files: [
140 | {
141 | src: [ '<%= app_files.js %>' ],
142 | dest: '<%= build_dir %>/',
143 | cwd: '.',
144 | expand: true
145 | }
146 | ]
147 | },
148 | build_vendorjs: {
149 | files: [
150 | {
151 | src: [ '<%= vendor_files.js %>' ],
152 | dest: '<%= build_dir %>/',
153 | cwd: '.',
154 | expand: true
155 | }
156 | ]
157 | },
158 | compile_assets: {
159 | files: [
160 | {
161 | src: [ '**' ],
162 | dest: '<%= compile_dir %>/assets',
163 | cwd: '<%= build_dir %>/assets',
164 | expand: true
165 | }
166 | ]
167 | },
168 | build_for_mopidy: {
169 | files: [
170 | {
171 | src: [ '**' ],
172 | dest: '<%= mopidy_package_dir %>/static',
173 | cwd: '<%= compile_dir %>',
174 | expand: true
175 | }
176 | ]
177 | }
178 | },
179 |
180 | /**
181 | * `grunt concat` concatenates multiple source files into a single file.
182 | */
183 | concat: {
184 | /**
185 | * The `build_css` target concatenates compiled CSS and vendor CSS
186 | * together.
187 | */
188 | build_css: {
189 | src: [
190 | '<%= vendor_files.css %>',
191 | '<%= build_dir %>/assets/css/<%= pkg.name %>-<%= pkg.version %>.css'
192 | ],
193 | dest: '<%= build_dir %>/assets/css/<%= pkg.name %>-<%= pkg.version %>.css'
194 | },
195 | /**
196 | * The `compile_js` target is the concatenation of our application source
197 | * code and all specified vendor source code into a single file.
198 | */
199 | compile_js: {
200 | options: {
201 | banner: '<%= meta.banner %>'
202 | },
203 | src: [
204 | '<%= vendor_files.js %>',
205 | '<%= build_dir %>/src/**/*.js',
206 | '<%= html2js.app.dest %>',
207 | '<%= html2js.common.dest %>'
208 | ],
209 | dest: '<%= compile_dir %>/assets/<%= pkg.name %>-<%= pkg.version %>.js'
210 | }
211 | },
212 |
213 | /**
214 | * `grunt coffee` compiles the CoffeeScript sources. To work well with the
215 | * rest of the build, we have a separate compilation task for sources and
216 | * specs so they can go to different places. For example, we need the
217 | * sources to live with the rest of the copied JavaScript so we can include
218 | * it in the final build, but we don't want to include our specs there.
219 | */
220 | coffee: {
221 | source: {
222 | options: {
223 | bare: true
224 | },
225 | expand: true,
226 | cwd: '.',
227 | src: [ '<%= app_files.coffee %>' ],
228 | dest: '<%= build_dir %>',
229 | ext: '.js'
230 | }
231 | },
232 |
233 | /**
234 | * `ngAnnotate` annotates the sources before minifying. That is, it allows us
235 | * to code without the array syntax.
236 | */
237 | ngAnnotate: {
238 | compile: {
239 | files: [
240 | {
241 | src: [ '<%= app_files.js %>' ],
242 | cwd: '<%= build_dir %>',
243 | dest: '<%= build_dir %>',
244 | expand: true
245 | }
246 | ]
247 | }
248 | },
249 |
250 | /**
251 | * Minify the sources!
252 | */
253 | uglify: {
254 | compile: {
255 | options: {
256 | banner: '<%= meta.banner %>'
257 | },
258 | files: {
259 | '<%= concat.compile_js.dest %>': '<%= concat.compile_js.dest %>'
260 | }
261 | }
262 | },
263 |
264 | /**
265 | * `grunt-contrib-less` handles our LESS compilation and uglification automatically.
266 | * Only our `main.less` file is included in compilation; all other files
267 | * must be imported from this file.
268 | */
269 | less: {
270 | build: {
271 | files: {
272 | '<%= build_dir %>/assets/css/<%= pkg.name %>-<%= pkg.version %>.css': '<%= app_files.less %>'
273 | }
274 | },
275 | compile: {
276 | files: {
277 | '<%= build_dir %>/assets/css/<%= pkg.name %>-<%= pkg.version %>.css': '<%= app_files.less %>'
278 | },
279 | options: {
280 | cleancss: true,
281 | compress: true
282 | }
283 | }
284 | },
285 |
286 | /**
287 | * `jshint` defines the rules of our linter as well as which files we
288 | * should check. This file, all javascript sources, and all our unit tests
289 | * are linted based on the policies listed in `options`. But we can also
290 | * specify exclusionary patterns by prefixing them with an exclamation
291 | * point (!); this is useful when code comes from a third party but is
292 | * nonetheless inside `src/`.
293 | */
294 | jshint: {
295 | src: [
296 | '<%= app_files.js %>'
297 | ],
298 | test: [
299 | '<%= app_files.jsunit %>'
300 | ],
301 | gruntfile: [
302 | 'Gruntfile.js'
303 | ],
304 | options: {
305 | curly: true,
306 | immed: true,
307 | newcap: true,
308 | noarg: true,
309 | sub: true,
310 | boss: true,
311 | eqnull: true
312 | },
313 | globals: {}
314 | },
315 |
316 | /**
317 | * `coffeelint` does the same as `jshint`, but for CoffeeScript.
318 | * CoffeeScript is not the default in ngBoilerplate, so we're just using
319 | * the defaults here.
320 | */
321 | coffeelint: {
322 | src: {
323 | files: {
324 | src: [ '<%= app_files.coffee %>' ]
325 | }
326 | },
327 | test: {
328 | files: {
329 | src: [ '<%= app_files.coffeeunit %>' ]
330 | }
331 | }
332 | },
333 |
334 | /**
335 | * HTML2JS is a Grunt plugin that takes all of your template files and
336 | * places them into JavaScript files as strings that are added to
337 | * AngularJS's template cache. This means that the templates too become
338 | * part of the initial payload as one JavaScript file. Neat!
339 | */
340 | html2js: {
341 | /**
342 | * These are the templates from `src/app`.
343 | */
344 | app: {
345 | options: {
346 | base: 'src/app'
347 | },
348 | src: [ '<%= app_files.atpl %>' ],
349 | dest: '<%= build_dir %>/templates-app.js'
350 | },
351 |
352 | /**
353 | * These are the templates from `src/common`.
354 | */
355 | common: {
356 | options: {
357 | base: 'src/common'
358 | },
359 | src: [ '<%= app_files.ctpl %>' ],
360 | dest: '<%= build_dir %>/templates-common.js'
361 | }
362 | },
363 |
364 | /**
365 | * The Karma configurations.
366 | */
367 | karma: {
368 | options: {
369 | configFile: '<%= build_dir %>/karma-unit.js'
370 | },
371 | unit: {
372 | runnerPort: 9101,
373 | background: true,
374 | port: 9877
375 | },
376 | continuous: {
377 | singleRun: true
378 | }
379 | },
380 |
381 | connect: {
382 | server: {
383 | options: {
384 | port: 3001,
385 | base: 'build',
386 | hostname: '*'
387 | }
388 | }
389 | },
390 |
391 | /**
392 | * The `index` task compiles the `index.html` file as a Grunt template. CSS
393 | * and JS files co-exist here but they get split apart later.
394 | */
395 | index: {
396 |
397 | /**
398 | * During development, we don't want to have wait for compilation,
399 | * concatenation, minification, etc. So to avoid these steps, we simply
400 | * add all script files directly to the `
` of `index.html`. The
401 | * `src` property contains the list of included files.
402 | */
403 | build: {
404 | dir: '<%= build_dir %>',
405 | src: [
406 | '<%= vendor_files.js %>',
407 | '<%= build_dir %>/src/**/*.js',
408 | '<%= html2js.common.dest %>',
409 | '<%= html2js.app.dest %>',
410 | '<%= vendor_files.css %>',
411 | '<%= build_dir %>/assets/css/<%= pkg.name %>-<%= pkg.version %>.css'
412 | ]
413 | },
414 |
415 | /**
416 | * When it is time to have a completely compiled application, we can
417 | * alter the above to include only a single JavaScript and a single CSS
418 | * file. Now we're back!
419 | */
420 | compile: {
421 | dir: '<%= compile_dir %>',
422 | src: [
423 | '<%= concat.compile_js.dest %>',
424 | '<%= vendor_files.css %>',
425 | '<%= build_dir %>/assets/css/<%= pkg.name %>-<%= pkg.version %>.css'
426 | ]
427 | }
428 | },
429 |
430 | /**
431 | * This task compiles the karma template so that changes to its file array
432 | * don't have to be managed manually.
433 | */
434 | karmaconfig: {
435 | unit: {
436 | dir: '<%= build_dir %>',
437 | src: [
438 | '<%= vendor_files.js %>',
439 | '<%= html2js.app.dest %>',
440 | '<%= html2js.common.dest %>',
441 | '<%= test_files.js %>'
442 | ]
443 | }
444 | },
445 |
446 | /**
447 | * And for rapid development, we have a watch set up that checks to see if
448 | * any of the files listed below change, and then to execute the listed
449 | * tasks when they do. This just saves us from having to type "grunt" into
450 | * the command-line every time we want to see what we're working on; we can
451 | * instead just leave "grunt watch" running in a background terminal. Set it
452 | * and forget it, as Ron Popeil used to tell us.
453 | *
454 | * But we don't need the same thing to happen for all the files.
455 | */
456 | delta: {
457 | /**
458 | * By default, we want the Live Reload to work for all tasks; this is
459 | * overridden in some tasks (like this file) where browser resources are
460 | * unaffected. It runs by default on port 35729, which your browser
461 | * plugin should auto-detect.
462 | */
463 | options: {
464 | livereload: true
465 | },
466 |
467 | /**
468 | * When the Gruntfile changes, we just want to lint it. In fact, when
469 | * your Gruntfile changes, it will automatically be reloaded!
470 | */
471 | gruntfile: {
472 | files: 'Gruntfile.js',
473 | tasks: [ 'jshint:gruntfile' ],
474 | options: {
475 | livereload: false
476 | }
477 | },
478 |
479 | /**
480 | * When our JavaScript source files change, we want to run lint them and
481 | * run our unit tests.
482 | */
483 | jssrc: {
484 | files: [
485 | '<%= app_files.js %>'
486 | ],
487 | tasks: [ 'jshint:src', 'karma:unit:run', 'copy:build_appjs' ]
488 | },
489 |
490 | /**
491 | * When our CoffeeScript source files change, we want to run lint them and
492 | * run our unit tests.
493 | */
494 | coffeesrc: {
495 | files: [
496 | '<%= app_files.coffee %>'
497 | ],
498 | tasks: [ 'coffeelint:src', 'coffee:source', 'karma:unit:run', 'copy:build_appjs' ]
499 | },
500 |
501 | /**
502 | * When assets are changed, copy them. Note that this will *not* copy new
503 | * files, so this is probably not very useful.
504 | */
505 | assets: {
506 | files: [
507 | 'src/assets/**/*'
508 | ],
509 | tasks: [ 'copy:build_app_assets' ]
510 | },
511 |
512 | /**
513 | * When index.html changes, we need to compile it.
514 | */
515 | html: {
516 | files: [ '<%= app_files.html %>' ],
517 | tasks: [ 'index:build' ]
518 | },
519 |
520 | /**
521 | * When our templates change, we only rewrite the template cache.
522 | */
523 | tpls: {
524 | files: [
525 | '<%= app_files.atpl %>',
526 | '<%= app_files.ctpl %>'
527 | ],
528 | tasks: [ 'html2js' ]
529 | },
530 |
531 | /**
532 | * When the CSS files change, we need to compile and minify them.
533 | */
534 | less: {
535 | files: [ 'src/**/*.less' ],
536 | tasks: [ 'less:build' ]
537 | },
538 |
539 | /**
540 | * When a JavaScript unit test file changes, we only want to lint it and
541 | * run the unit tests. We don't want to do any live reloading.
542 | */
543 | jsunit: {
544 | files: [
545 | '<%= app_files.jsunit %>'
546 | ],
547 | tasks: [ 'jshint:test', 'karma:unit:run' ],
548 | options: {
549 | livereload: false
550 | }
551 | },
552 |
553 | /**
554 | * When a CoffeeScript unit test file changes, we only want to lint it and
555 | * run the unit tests. We don't want to do any live reloading.
556 | */
557 | coffeeunit: {
558 | files: [
559 | '<%= app_files.coffeeunit %>'
560 | ],
561 | tasks: [ 'coffeelint:test', 'karma:unit:run' ],
562 | options: {
563 | livereload: false
564 | }
565 | }
566 | }
567 | };
568 |
569 | grunt.initConfig( grunt.util._.extend( taskConfig, userConfig ) );
570 |
571 | /**
572 | * In order to make it safe to just compile or copy *only* what was changed,
573 | * we need to ensure we are starting from a clean, fresh build. So we rename
574 | * the `watch` task to `delta` (that's why the configuration var above is
575 | * `delta`) and then add a new task called `watch` that does a clean build
576 | * before watching for changes.
577 | */
578 | grunt.renameTask( 'watch', 'delta' );
579 | grunt.registerTask( 'watch', [ 'build', 'karma:unit', 'delta' ] );
580 |
581 | /**
582 | * The default task is to build and compile.
583 | */
584 | grunt.registerTask( 'default', [ 'build', 'compile' ] );
585 |
586 | /**
587 | * The `build` task gets your app ready to run for development and testing.
588 | */
589 | grunt.registerTask( 'build', [
590 | 'clean:build', 'html2js', 'jshint', 'coffeelint', 'coffee', 'less:build',
591 | 'concat:build_css', 'copy:build_app_assets', 'copy:build_vendor_assets',
592 | 'copy:build_vendor_fonts',
593 | 'copy:build_appjs', 'copy:build_vendorjs', 'index:build', 'karmaconfig',
594 | 'karma:continuous', 'connect'
595 | ]);
596 |
597 | /**
598 | * The `compile` task gets your app ready for deployment by concatenating and
599 | * minifying your code.
600 | */
601 | grunt.registerTask( 'compile', [
602 | 'clean:compile', 'copy:compile_assets', 'ngAnnotate', 'concat:compile_js', 'uglify', 'index:compile'
603 | ]);
604 |
605 | /**
606 | * The `build-mopidy-extension` task builds Mopidy extension package
607 | */
608 | grunt.registerTask( 'build-mopidy-extension', [ 'clean:mopidy', 'build', 'compile', 'copy:build_for_mopidy', 'mopidypackageversion' ] );
609 |
610 | /**
611 | * A utility function to get all app JavaScript sources.
612 | */
613 | function filterForJS ( files ) {
614 | return files.filter( function ( file ) {
615 | return file.match( /\.js$/ );
616 | });
617 | }
618 |
619 | /**
620 | * A utility function to get all app CSS sources.
621 | */
622 | function filterForCSS ( files ) {
623 | return files.filter( function ( file ) {
624 | return file.match( /\.css$/ ) && ! file.match( /vendor/ ); // vendor css files are concatenated
625 | });
626 | }
627 |
628 | /**
629 | * The index.html template includes the stylesheet and javascript sources
630 | * based on dynamic names calculated in this Gruntfile. This task assembles
631 | * the list into variables for the template to use and then runs the
632 | * compilation.
633 | */
634 | grunt.registerMultiTask( 'index', 'Process index.html template', function () {
635 | var dirRE = new RegExp( '^('+grunt.config('build_dir')+'|'+grunt.config('compile_dir')+')\/', 'g' );
636 | var jsFiles = filterForJS( this.filesSrc ).map( function ( file ) {
637 | return file.replace( dirRE, '' );
638 | });
639 | var cssFiles = filterForCSS( this.filesSrc ).map( function ( file ) {
640 | return file.replace( dirRE, '' );
641 | });
642 |
643 | grunt.file.copy('src/index.html', this.data.dir + '/index.html', {
644 | process: function ( contents, path ) {
645 | return grunt.template.process( contents, {
646 | data: {
647 | scripts: jsFiles,
648 | styles: cssFiles,
649 | version: grunt.config( 'pkg.version' )
650 | }
651 | });
652 | }
653 | });
654 | });
655 |
656 | /**
657 | * In order to avoid having to specify manually the files needed for karma to
658 | * run, we use grunt to manage the list for us. The `karma/*` files are
659 | * compiled as grunt templates for use by Karma. Yay!
660 | */
661 | grunt.registerMultiTask( 'karmaconfig', 'Process karma config templates', function () {
662 | var jsFiles = filterForJS( this.filesSrc );
663 |
664 | grunt.file.copy( 'karma/karma-unit.tpl.js', grunt.config( 'build_dir' ) + '/karma-unit.js', {
665 | process: function ( contents, path ) {
666 | return grunt.template.process( contents, {
667 | data: {
668 | scripts: jsFiles
669 | }
670 | });
671 | }
672 | });
673 | });
674 |
675 | grunt.registerTask( 'mopidypackageversion', 'Create a mopidy package', function () {
676 | grunt.file.copy('__init__.py.tpl', grunt.config( 'mopidy_package_dir' ) + '/__init__.py', {
677 | process: function ( contents, path ) {
678 | return grunt.template.process( contents, {
679 | data: {
680 | version: grunt.config( 'pkg.version' )
681 | }
682 | });
683 | }
684 | });
685 | grunt.file.copy('ext.conf', grunt.config( 'mopidy_package_dir' ) + '/ext.conf');
686 | });
687 |
688 | };
689 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013-2014 Martijn Boland
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include MANIFEST.in
3 | include README.rst
4 | include CHANGELOG.md
5 |
6 | recursive-include mopidy_moped ext.conf
7 | recursive-include mopidy_moped/static *
8 |
9 | exclude .bowerrc
10 | exclude *.tpl
11 | exclude *.json
12 | exclude build.config.js
13 | exclude Gruntfile.js
14 | exclude ext.conf
15 |
16 | recursive-exclude build *
17 | recursive-exclude dist *
18 | recursive-exclude karma *
19 | recursive-exclude node_modules *
20 | recursive-exclude screenshots *
21 | recursive-exclude src *
22 | recursive-exclude vendor *
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ************
2 | Mopidy-Moped
3 | ************
4 |
5 | Moped is a responsive HTML5 + JavaScript client for the `Mopidy `_ music server.
6 |
7 | .. image:: https://github.com/martijnboland/moped/raw/master/screenshots/moped-all-720.png?raw=true
8 |
9 | Installation
10 | ============
11 |
12 | Make sure to have Mopidy 1.0.0 or higher `installed `_ on your music server. Also make sure that the `HTTP extension `_ is enabled.
13 |
14 | Install from PyPI on your music server::
15 |
16 | sudo pip install Mopidy-Moped
17 |
18 | Alternatively, clone the `GitHub repository `_ and copy all files from the /dist/ directory to the webclient directory on your server.
19 |
20 | Usage
21 | =====
22 |
23 | Browse to the Moped app on your Mopidy server (e.g. http://localhost:6680/moped).
24 |
25 | Known issues
26 | ============
27 |
28 | - The Mopidy HTTP frontend uses Web Sockets. Most modern browsers support this but not the default Android browser (4.3 and older). To use Moped on Android you have to use a different browser like Firefox or Chrome;
29 | - Searching radio streams is still experimental.
30 |
31 | Security warning
32 | ================
33 |
34 | (from the Mopidy web site)
35 |
36 | As a simple security measure, the web server is by default only available from localhost. To make it available from other computers, change the http/hostname config value. Before you do so, note that the HTTP extension does not feature any form of user authentication or authorization. Anyone able to access the web server can use the full core API of Mopidy. Thus, you probably only want to make the web server available from your local network or place it behind a web proxy which takes care or user authentication. You have been warned.
37 |
38 | Development
39 | ===========
40 |
41 | 1. Install `Nodejs `_
42 | 2. Install grunt-cli, karma and bower::
43 |
44 | npm install -g grunt-cli karma bower
45 |
46 | 3. Clone the repository to your local machine::
47 |
48 | git clone https://github.com/martijnboland/moped.git
49 |
50 | 4. Install dependencies::
51 |
52 | npm install
53 | bower install
54 |
55 | 5. Start the build and watch process::
56 |
57 | grunt watch
58 |
59 | This will start a local web server on port 3001.
60 |
61 |
62 | To build the compiled distribution, just enter::
63 |
64 | grunt
65 |
66 | and to build the Mopidy extension::
67 |
68 | grunt build-mopidy-extension
69 |
70 | Project resources
71 | =================
72 |
73 | - `Source code `_
74 | - `Issue tracker `_
75 | - `Download development snapshot `_
76 |
77 | Changelog
78 | =========
79 |
80 | 0.7.1 (2017-05-21)
81 | ------------------
82 |
83 | - Fixed search by sending individual arguments as array, except when surrounded with double quotes (#69)
84 | - Fixed issue where mopidy server url was set to undefined in LocalStorage when leaving the field empty (#53)
85 |
86 | 0.7.0 (2016-10-21)
87 | ------------------
88 |
89 | - Added stop button in player controls (#45)
90 | - Removed (experimental) radio station search
91 |
92 | 0.6.4 (2015-10-28)
93 | ------------------
94 |
95 | - Fixed navigation issue on iOS 9 when running from start screen (using UIWebView)
96 |
97 | 0.6.3 (2015-09-25)
98 | ------------------
99 |
100 | - Fixed rescaling issue on iOS 9 when using left menu
101 | - Improved experience on iOS by removing hover effect on the playback buttons.
102 |
103 | 0.6.2 (2015-09-11)
104 | ------------------
105 |
106 | - Fixed seek issue with Mopidy (#55)
107 | - Browsing now supports all ref types (#54, #56)
108 |
109 | 0.6.1 (2015-06-10)
110 | ------------------
111 |
112 | - Optimized loading of playlists
113 |
114 | 0.6.0 (2015-05-12)
115 | ------------------
116 |
117 | - Added list of current tracks to home screen
118 | - Added Moped version to browser title bar
119 | - New icon and support for favicon in windows phone
120 | - Fixed back button behaviour in standalone mode
121 |
122 | 0.5.0 (2015-04-05)
123 | ------------------
124 |
125 | - Updated mopidy.js to 0.5.0
126 | - Mopidy 1.0.0 compatibility
127 | - Updated player controls active and hover styles (Sebastian)
128 |
129 | 0.4.4 (2015-03-14)
130 | ------------------
131 |
132 | Fixed search
133 |
134 | 0.4.3 (2015-03-14)
135 | ------------------
136 |
137 | - Min. characters for search is now 2 instead of 3
138 | - Use protocol relative urls for fonts (André Gaul)
139 | - Updated Angular to 1.3.x
140 | - Updated various other js libs to latest version
141 | - Try to display Mopidy album images before requesting album images from LastFM
142 | - Removed clear_current_track parameter from mopidy.stop() method for Mopidy 0.20 compatibility
143 |
144 | 0.4.2 (2014-11-17)
145 | ------------------
146 |
147 | - Fixed accidentally disabled error logger
148 |
149 | 0.4.1 (2014-11-16)
150 | ------------------
151 |
152 | - Added random toggle switch
153 | - Fixed browsing of playlists (David Tischler)
154 | - Reverted interpolation of track position due to instability
155 | - Search query is passed to mopidy as an array to support new Spotify backend
156 |
157 | 0.4.0 (2014-10-10)
158 | ------------------
159 |
160 | - Support for Mopidy browsing (David Tischler, https://github.com/tischlda)
161 | - Fix for search queries (David Tischler)
162 | - Backend provider is displayed in track list (Julien Bordellier)
163 | - Allow special characters in search
164 | - Interpolation of track position and checking every 10 seconds
165 |
166 | 0.3.3 (2014-08-03)
167 | ------------------
168 |
169 | - Reduced default amount of logging
170 |
171 | 0.3.2 (2014-08-03)
172 | ------------------
173 |
174 | - Fixed volume slider
175 |
176 | 0.3.1 (2014-07-23)
177 | ------------------
178 |
179 | - Fixed PyPI package manifest
180 | - Support for playlist folders in PyPI package
181 |
182 | 0.3.0 (2014-06-24)
183 | ------------------
184 |
185 | - Moped as installable Mopidy extension
186 |
187 | 0.2.0 (2013-12-18)
188 | ------------------
189 |
190 | - Angular version added.
191 |
192 | 0.1.0 (2013-12-04)
193 | ------------------
194 |
195 | - Initial Durandal version.
196 |
--------------------------------------------------------------------------------
/__init__.py.tpl:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import os
4 |
5 | from mopidy import config, ext
6 |
7 | __version__ = '<%= version %>'
8 |
9 | class MopedExtension(ext.Extension):
10 | dist_name = 'Mopidy-Moped'
11 | ext_name = 'moped'
12 | version = __version__
13 |
14 | def get_default_config(self):
15 | conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
16 | return config.read(conf_file)
17 |
18 | def setup(self, registry):
19 | registry.add('http:static', {
20 | 'name': self.ext_name,
21 | 'path': os.path.join(os.path.dirname(__file__), 'static'),
22 | })
23 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "moped",
3 | "version": "0.7.1",
4 | "devDependencies": {
5 | "angular": "~1.6.0",
6 | "angular-mocks": "~1.6.0",
7 | "angular-route": "~1.6.0",
8 | "angular-sanitize": "~1.6.0",
9 | "angular-touch": "~1.6.0",
10 | "bootstrap": "~3.3.2",
11 | "lodash": "~3.0.0",
12 | "jquery": "~2.1.0",
13 | "lastfm-api": "~0.0.1",
14 | "modernizr": "~2.8.3",
15 | "fastclick": "~1.0.6"
16 | },
17 | "dependencies": {},
18 | "resolutions": {
19 | "angular": "~1.6.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/build.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file/module contains all configuration for the build process.
3 | */
4 | module.exports = {
5 | /**
6 | * The `build_dir` folder is where our projects are compiled during
7 | * development and the `compile_dir` folder is where our app resides once it's
8 | * completely built.
9 | */
10 | build_dir: 'build',
11 | compile_dir: 'dist',
12 | mopidy_package_dir: 'mopidy_moped',
13 |
14 | /**
15 | * This is a collection of file patterns that refer to our app code (the
16 | * stuff in `src/`). These file paths are used in the configuration of
17 | * build tasks. `js` is all project javascript, less tests. `ctpl` contains
18 | * our reusable components' (`src/common`) template HTML files, while
19 | * `atpl` contains the same, but for our app's code. `html` is just our
20 | * main HTML file, `less` is our main stylesheet, and `unit` contains our
21 | * app's unit tests.
22 | */
23 | app_files: {
24 | js: [ 'src/**/*.js', '!src/**/*.spec.js', '!src/assets/**/*.js' ],
25 | jsunit: [ 'src/**/*.spec.js' ],
26 |
27 | coffee: [ 'src/**/*.coffee', '!src/**/*.spec.coffee' ],
28 | coffeeunit: [ 'src/**/*.spec.coffee' ],
29 |
30 | atpl: [ 'src/app/**/*.tpl.html' ],
31 | ctpl: [ 'src/common/**/*.tpl.html' ],
32 |
33 | html: [ 'src/index.html' ],
34 | less: 'src/less/main.less'
35 | },
36 |
37 | /**
38 | * This is a collection of files used during testing only.
39 | */
40 | test_files: {
41 | js: [
42 | 'vendor/angular-mocks/angular-mocks.js'
43 | ]
44 | },
45 |
46 | /**
47 | * This is the same as `app_files`, except it contains patterns that
48 | * reference vendor code (`vendor/`) that we need to place into the build
49 | * process somewhere. While the `app_files` property ensures all
50 | * standardized files are collected for compilation, it is the user's job
51 | * to ensure non-standardized (i.e. vendor-related) files are handled
52 | * appropriately in `vendor_files.js`.
53 | *
54 | * The `vendor_files.js` property holds files to be automatically
55 | * concatenated and minified with our project source files.
56 | *
57 | * The `vendor_files.css` property holds any CSS files to be automatically
58 | * included in our app.
59 | *
60 | * The `vendor_files.assets` property holds any assets to be copied along
61 | * with our app's assets. This structure is flattened, so it is not
62 | * recommended that you use wildcards.
63 | */
64 | vendor_files: {
65 | js: [
66 | 'vendor/modernizr/modernizr.js',
67 | 'vendor/mopidy/mopidy.js',
68 | 'vendor/fastclick/lib/fastclick.js',
69 | 'vendor/jquery/dist/jquery.js',
70 | 'vendor/angular/angular.js',
71 | 'vendor/angular-route/angular-route.js',
72 | 'vendor/angular-sanitize/angular-sanitize.js',
73 | 'vendor/angular-touch/angular-touch.js',
74 | 'vendor/placeholders/angular-placeholders-0.0.1-SNAPSHOT.min.js',
75 | 'vendor/lodash/lodash.js',
76 | 'vendor/bootstrap-slider/bootstrap-slider.js',
77 | 'vendor/lastfm-api/lastfm-api.js'
78 | ],
79 | css: [
80 | 'vendor/bootstrap-slider/slider.css'
81 | ],
82 | assets: [
83 | ],
84 | fonts: [
85 | 'vendor/bootstrap/dist/fonts/glyphicons-halflings-regular.*'
86 | ]
87 | },
88 | };
89 |
--------------------------------------------------------------------------------
/changelog.tpl:
--------------------------------------------------------------------------------
1 |
2 | # <%= version%> (<%= today%>)
3 |
4 | <% if (_(changelog.feat).size() > 0) { %> ## Features
5 | <% _(changelog.feat).forEach(function(changes, scope) { %>
6 | - **<%= scope%>:**
7 | <% changes.forEach(function(change) { %> - <%= change.msg%> (<%= helpers.commitLink(change.sha1) %>)
8 | <% }); %>
9 | <% }); %> <% } %>
10 |
11 | <% if (_(changelog.fix).size() > 0) { %> ## Fixes
12 | <% _(changelog.fix).forEach(function(changes, scope) { %>
13 | - **<%= scope%>:**
14 | <% changes.forEach(function(change) { %> - <%= change.msg%> (<%= helpers.commitLink(change.sha1) %>)
15 | <% }); %>
16 | <% }); %> <% } %>
17 |
18 | <% if (_(changelog.breaking).size() > 0) { %> ## Breaking Changes
19 | <% _(changelog.breaking).forEach(function(changes, scope) { %>
20 | - **<%= scope%>:**
21 | <% changes.forEach(function(change) { %> <%= change.msg%>
22 | <% }); %>
23 | <% }); %> <% } %>
24 |
--------------------------------------------------------------------------------
/dist/assets/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/dist/assets/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/dist/assets/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/dist/assets/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/dist/assets/images/ajax-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/ajax-loader.gif
--------------------------------------------------------------------------------
/dist/assets/images/android-chrome-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/android-chrome-144x144.png
--------------------------------------------------------------------------------
/dist/assets/images/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/android-chrome-192x192.png
--------------------------------------------------------------------------------
/dist/assets/images/android-chrome-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/android-chrome-36x36.png
--------------------------------------------------------------------------------
/dist/assets/images/android-chrome-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/android-chrome-48x48.png
--------------------------------------------------------------------------------
/dist/assets/images/android-chrome-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/android-chrome-72x72.png
--------------------------------------------------------------------------------
/dist/assets/images/android-chrome-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/android-chrome-96x96.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/dist/assets/images/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/apple-touch-icon.png
--------------------------------------------------------------------------------
/dist/assets/images/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #bb3333
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/dist/assets/images/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/favicon-16x16.png
--------------------------------------------------------------------------------
/dist/assets/images/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/favicon-32x32.png
--------------------------------------------------------------------------------
/dist/assets/images/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/favicon-96x96.png
--------------------------------------------------------------------------------
/dist/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/favicon.ico
--------------------------------------------------------------------------------
/dist/assets/images/library-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/library-icon.png
--------------------------------------------------------------------------------
/dist/assets/images/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "My app",
3 | "icons": [
4 | {
5 | "src": "\/assets\/images\/android-chrome-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "\/assets\/images\/android-chrome-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image\/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "\/assets\/images\/android-chrome-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image\/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "\/assets\/images\/android-chrome-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image\/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "\/assets\/images\/android-chrome-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image\/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "\/assets\/images\/android-chrome-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image\/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/dist/assets/images/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/mstile-144x144.png
--------------------------------------------------------------------------------
/dist/assets/images/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/mstile-150x150.png
--------------------------------------------------------------------------------
/dist/assets/images/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/mstile-310x150.png
--------------------------------------------------------------------------------
/dist/assets/images/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/mstile-310x310.png
--------------------------------------------------------------------------------
/dist/assets/images/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/mstile-70x70.png
--------------------------------------------------------------------------------
/dist/assets/images/noalbum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/noalbum.png
--------------------------------------------------------------------------------
/dist/assets/images/playlists-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/playlists-icon.png
--------------------------------------------------------------------------------
/dist/assets/images/radio-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/radio-icon.png
--------------------------------------------------------------------------------
/dist/assets/images/search-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/search-icon.png
--------------------------------------------------------------------------------
/dist/assets/images/settings-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/settings-icon.png
--------------------------------------------------------------------------------
/dist/assets/images/touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/touch-icon.png
--------------------------------------------------------------------------------
/dist/assets/images/vinyl-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/dist/assets/images/vinyl-icon.png
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
36 |
37 |
38 |
39 |
56 |
57 |
62 |
63 |
66 |
67 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/ext.conf:
--------------------------------------------------------------------------------
1 | [moped]
2 | enabled = true
--------------------------------------------------------------------------------
/karma/karma-unit.tpl.js:
--------------------------------------------------------------------------------
1 | module.exports = function ( karma ) {
2 | karma.set({
3 | /**
4 | * From where to look for files, starting with the location of this file.
5 | */
6 | basePath: '../',
7 |
8 | /**
9 | * This is the list of file patterns to load into the browser during testing.
10 | */
11 | files: [
12 | <% scripts.forEach( function ( file ) { %>'<%= file %>',
13 | <% }); %>
14 | 'src/**/*.js',
15 | 'src/**/*.coffee',
16 | ],
17 | exclude: [
18 | 'src/assets/**/*.js'
19 | ],
20 | frameworks: [ 'jasmine' ],
21 | plugins: [ 'karma-jasmine', 'karma-firefox-launcher', 'karma-coffee-preprocessor' ],
22 | preprocessors: {
23 | '**/*.coffee': 'coffee',
24 | },
25 |
26 | /**
27 | * How to report, by default.
28 | */
29 | reporters: 'dots',
30 |
31 | /**
32 | * On which port should the browser connect, on which port is the test runner
33 | * operating, and what is the URL path for the browser to use.
34 | */
35 | port: 9018,
36 | runnerPort: 9100,
37 | urlRoot: '/',
38 |
39 | /**
40 | * Disable file watching by default.
41 | */
42 | autoWatch: false,
43 |
44 | /**
45 | * The list of browsers to launch to test on. This includes only "Firefox" by
46 | * default, but other browser names include:
47 | * Chrome, ChromeCanary, Firefox, Opera, Safari, PhantomJS
48 | *
49 | * Note that you can also use the executable name of the browser, like "chromium"
50 | * or "firefox", but that these vary based on your operating system.
51 | *
52 | * You may also leave this blank and manually navigate your browser to
53 | * http://localhost:9018/ when you're running tests. The window/tab can be left
54 | * open and the tests will automatically occur there during the build. This has
55 | * the aesthetic advantage of not launching a browser every time you save.
56 | */
57 | browsers: [
58 | 'Firefox'
59 | ]
60 | });
61 | };
--------------------------------------------------------------------------------
/mopidy_moped/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import os
4 |
5 | from mopidy import config, ext
6 |
7 | __version__ = '0.7.1'
8 |
9 | class MopedExtension(ext.Extension):
10 | dist_name = 'Mopidy-Moped'
11 | ext_name = 'moped'
12 | version = __version__
13 |
14 | def get_default_config(self):
15 | conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
16 | return config.read(conf_file)
17 |
18 | def setup(self, registry):
19 | registry.add('http:static', {
20 | 'name': self.ext_name,
21 | 'path': os.path.join(os.path.dirname(__file__), 'static'),
22 | })
23 |
--------------------------------------------------------------------------------
/mopidy_moped/ext.conf:
--------------------------------------------------------------------------------
1 | [moped]
2 | enabled = true
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/ajax-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/ajax-loader.gif
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/android-chrome-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/android-chrome-144x144.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/android-chrome-192x192.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/android-chrome-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/android-chrome-36x36.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/android-chrome-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/android-chrome-48x48.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/android-chrome-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/android-chrome-72x72.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/android-chrome-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/android-chrome-96x96.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/apple-touch-icon.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #bb3333
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/favicon-16x16.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/favicon-32x32.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/favicon-96x96.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/favicon.ico
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/library-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/library-icon.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "My app",
3 | "icons": [
4 | {
5 | "src": "\/assets\/images\/android-chrome-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "\/assets\/images\/android-chrome-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image\/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "\/assets\/images\/android-chrome-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image\/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "\/assets\/images\/android-chrome-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image\/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "\/assets\/images\/android-chrome-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image\/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "\/assets\/images\/android-chrome-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image\/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/mstile-144x144.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/mstile-150x150.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/mstile-310x150.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/mstile-310x310.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/mstile-70x70.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/noalbum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/noalbum.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/playlists-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/playlists-icon.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/radio-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/radio-icon.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/search-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/search-icon.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/settings-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/settings-icon.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/touch-icon.png
--------------------------------------------------------------------------------
/mopidy_moped/static/assets/images/vinyl-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/mopidy_moped/static/assets/images/vinyl-icon.png
--------------------------------------------------------------------------------
/mopidy_moped/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
36 |
37 |
38 |
39 |
56 |
57 |
62 |
63 |
66 |
67 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Martijn Boland",
3 | "name": "moped",
4 | "version": "0.7.1",
5 | "homepage": "https://github.com/martijnboland/moped",
6 | "licenses": [
7 | {
8 | "type": "MIT",
9 | "url": "https://raw.github.com/martijnboland/moped/master/LICENSE"
10 | }
11 | ],
12 | "bugs": "https://github.com/martijnboland/moped",
13 | "repository": {
14 | "type": "git",
15 | "url": "git@github.com:martijnboland/moped.git"
16 | },
17 | "dependencies": {},
18 | "devDependencies": {
19 | "grunt": "~0.4.1",
20 | "grunt-bump": "0.0.6",
21 | "grunt-coffeelint": "0.0.6",
22 | "grunt-contrib-clean": "^0.4.1",
23 | "grunt-contrib-coffee": "^0.7.0",
24 | "grunt-contrib-concat": "^0.3.0",
25 | "grunt-contrib-connect": "~0.5.0",
26 | "grunt-contrib-copy": "^0.4.1",
27 | "grunt-contrib-jshint": "^0.4.3",
28 | "grunt-contrib-less": "~0.11.0",
29 | "grunt-contrib-uglify": "^0.2.7",
30 | "grunt-contrib-watch": "^0.5.3",
31 | "grunt-conventional-changelog": "^0.1.2",
32 | "grunt-html2js": "^0.1.9",
33 | "grunt-karma": "^2.0.0",
34 | "grunt-ng-annotate": "^1.0.1",
35 | "jasmine-core": "^2.6.1",
36 | "karma": "^1.6.0",
37 | "karma-coffee-preprocessor": "^1.0.1",
38 | "karma-firefox-launcher": "^1.0.1",
39 | "karma-jasmine": "^1.1.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/screenshots/moped-all-720.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-all-720.png
--------------------------------------------------------------------------------
/screenshots/moped-all.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-all.png
--------------------------------------------------------------------------------
/screenshots/moped-desktop-artist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-desktop-artist.png
--------------------------------------------------------------------------------
/screenshots/moped-desktop-playlist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-desktop-playlist.png
--------------------------------------------------------------------------------
/screenshots/moped-ipad-album.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-ipad-album.png
--------------------------------------------------------------------------------
/screenshots/moped-ipad-artist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-ipad-artist.png
--------------------------------------------------------------------------------
/screenshots/moped-ipad-playlist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-ipad-playlist.png
--------------------------------------------------------------------------------
/screenshots/moped-ipad-radio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-ipad-radio.png
--------------------------------------------------------------------------------
/screenshots/moped-ipad-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-ipad-search.png
--------------------------------------------------------------------------------
/screenshots/moped-iphone-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-iphone-menu.png
--------------------------------------------------------------------------------
/screenshots/moped-iphone-playlist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/screenshots/moped-iphone-playlist.png
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import re
4 |
5 | from setuptools import find_packages, setup
6 |
7 |
8 | def get_version(filename):
9 | content = open(filename).read()
10 | metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", content))
11 | return metadata['version']
12 |
13 |
14 | setup(
15 | name='Mopidy-Moped',
16 | version=get_version('mopidy_moped/__init__.py'),
17 | url='https://github.com/martijnboland/moped',
18 | license='MIT License',
19 | author='Martijn Boland',
20 | author_email='martijn@boland.org',
21 | description='Responsive Web client for Mopidy',
22 | long_description=open('README.rst').read(),
23 | packages=find_packages(),
24 | zip_safe=False,
25 | include_package_data=True,
26 | install_requires=[
27 | 'setuptools',
28 | 'Mopidy >= 1.0.0'
29 | ],
30 | entry_points={
31 | 'mopidy.ext': [
32 | 'moped = mopidy_moped:MopedExtension',
33 | ],
34 | },
35 | classifiers=[
36 | 'Development Status :: 4 - Beta',
37 | 'Environment :: No Input/Output (Daemon)',
38 | 'Intended Audience :: End Users/Desktop',
39 | 'License :: OSI Approved :: MIT License',
40 | 'Operating System :: OS Independent',
41 | 'Programming Language :: Python :: 2',
42 | 'Topic :: Multimedia :: Sound/Audio :: Players',
43 | ],
44 | )
45 |
--------------------------------------------------------------------------------
/src/app/README.md:
--------------------------------------------------------------------------------
1 | # The `src/app` Directory
2 |
3 | ## Overview
4 |
5 | ```
6 | src/
7 | |- app/
8 | | |- home/
9 | | |- about/
10 | | |- app.js
11 | | |- app.spec.js
12 | ```
13 |
14 | The `src/app` directory contains all code specific to this application. Apart
15 | from `app.js` and its accompanying tests (discussed below), this directory is
16 | filled with subdirectories corresponding to high-level sections of the
17 | application, often corresponding to top-level routes. Each directory can have as
18 | many subdirectories as it needs, and the build system will understand what to
19 | do. For example, a top-level route might be "products", which would be a folder
20 | within the `src/app` directory that conceptually corresponds to the top-level
21 | route `/products`, though this is in no way enforced. Products may then have
22 | subdirectories for "create", "view", "search", etc. The "view" submodule may
23 | then define a route of `/products/:id`, ad infinitum.
24 |
25 | As `ngBoilerplate` is quite minimal, take a look at the two provided submodules
26 | to gain a better understanding of how these are used as well as to get a
27 | glimpse of how powerful this simple construct can be.
28 |
29 | ## `app.js`
30 |
31 | This is our main app configuration file. It kickstarts the whole process by
32 | requiring all the modules from `src/app` that we need. We must load these now to
33 | ensure the routes are loaded. If as in our "products" example there are
34 | subroutes, we only require the top-level module, and allow the submodules to
35 | require their own submodules.
36 |
37 | As a matter of course, we also require the template modules that are generated
38 | during the build.
39 |
40 | However, the modules from `src/common` should be required by the app
41 | submodules that need them to ensure proper dependency handling. These are
42 | app-wide dependencies that are required to assemble your app.
43 |
44 | ```js
45 | angular.module( 'ngBoilerplate', [
46 | 'templates-app',
47 | 'templates-common',
48 | 'ngBoilerplate.home',
49 | 'ngBoilerplate.about'
50 | 'ui.state',
51 | 'ui.route'
52 | ])
53 | ```
54 |
55 | With app modules broken down in this way, all routing is performed by the
56 | submodules we include, as that is where our app's functionality is really
57 | defined. So all we need to do in `app.js` is specify a default route to follow,
58 | which route of course is defined in a submodule. In this case, our `home` module
59 | is where we want to start, which has a defined route for `/home` in
60 | `src/app/home/home.js`.
61 |
62 | ```js
63 | .config( function myAppConfig ( $stateProvider, $urlRouterProvider ) {
64 | $urlRouterProvider.otherwise( '/home' );
65 | })
66 | ```
67 |
68 | Use the main applications run method to execute any code after services
69 | have been instantiated.
70 |
71 | ```js
72 | .run( function run () {
73 | })
74 | ```
75 |
76 | And then we define our main application controller. This is a good place for logic
77 | not specific to the template or route, such as menu logic or page title wiring.
78 |
79 | ```js
80 | .controller( 'AppCtrl', function AppCtrl ( $scope, $location ) {
81 | $scope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){
82 | if ( angular.isDefined( toState.data.pageTitle ) ) {
83 | $scope.pageTitle = toState.data.pageTitle + ' | ngBoilerplate' ;
84 | }
85 | });
86 | })
87 | ```
88 |
89 | ### Testing
90 |
91 | One of the design philosophies of `ngBoilerplate` is that tests should exist
92 | alongside the code they test and that the build system should be smart enough to
93 | know the difference and react accordingly. As such, the unit test for `app.js`
94 | is `app.spec.js`, though it is quite minimal.
95 |
--------------------------------------------------------------------------------
/src/app/app.js:
--------------------------------------------------------------------------------
1 | angular.module('moped', [
2 | 'ngTouch',
3 | 'moped.mopidy',
4 | 'moped.search',
5 | 'moped.library',
6 | 'moped.playlists',
7 | 'moped.radio',
8 | 'moped.settings',
9 | 'moped.home',
10 | 'moped.browse',
11 | 'moped.nowplaying',
12 | 'moped.playercontrols',
13 | 'moped.widgets',
14 | 'moped.filters',
15 | 'templates-app',
16 | 'templates-common'
17 | ])
18 |
19 | .config(function ($provide, $locationProvider) {
20 |
21 | // Decorator for promises so the ui knows when one or more promises are pending.
22 | $provide.decorator('$q', ['$delegate', '$rootScope', function($delegate, $rootScope) {
23 | var pendingPromises = 0;
24 | $rootScope.$watch(function() {
25 | return pendingPromises > 0;
26 | }, function(working) {
27 | $rootScope.working = working;
28 | });
29 | var $q = $delegate;
30 | var origDefer = $q.defer;
31 | $q.defer = function() {
32 | var defer = origDefer();
33 | pendingPromises++;
34 | defer.promise['finally'](function() {
35 | pendingPromises--;
36 | });
37 | return defer;
38 | };
39 | return $q;
40 | }]);
41 |
42 | $locationProvider.hashPrefix('');
43 | })
44 |
45 | .run(function run () {
46 |
47 | })
48 |
49 | .controller('AppCtrl', function AppController ($scope, $location, $window, mopidyservice) {
50 | var connectionStates = {
51 | online: 'Online',
52 | offline: 'Offline'
53 | };
54 | var defaultPageTitle = 'Moped';
55 |
56 | $scope.isSidebarVisibleForMobile = false;
57 | $scope.isBackVisible = false;
58 | $scope.connectionState = connectionStates.offline;
59 | $scope.pageTitle = defaultPageTitle;
60 |
61 | $scope.$on('mopidy:state:online', function() {
62 | $scope.connectionState = connectionStates.online;
63 | $scope.$apply();
64 | });
65 |
66 | $scope.$on('mopidy:state:offline', function() {
67 | $scope.connectionState = connectionStates.offline;
68 | $scope.$apply();
69 | });
70 |
71 | $scope.$on('settings:saved', function() {
72 | mopidyservice.restart();
73 | });
74 |
75 | $scope.$on('$locationChangeSuccess', function(ev, newUrl, currentUrl) {
76 | var path = $location.path();
77 | $scope.isSidebarVisibleForMobile = false;
78 | $scope.isBackVisible = $window.navigator.standalone && path !== '/';
79 | });
80 |
81 | $scope.$on("$routeChangeSuccess", function(ev, currentRoute, previousRoute) {
82 | $scope.pageTitle = currentRoute.title || defaultPageTitle;
83 | });
84 |
85 | $scope.toggleSidebar = function() {
86 | $scope.isSidebarVisibleForMobile = ! $scope.isSidebarVisibleForMobile;
87 | };
88 |
89 | $scope.goBack = function() {
90 | $window.history.back();
91 | return false;
92 | };
93 |
94 | mopidyservice.start();
95 |
96 | window.addEventListener('load', function() {
97 | FastClick.attach(document.body);
98 | }, false);
99 |
100 | });
101 |
--------------------------------------------------------------------------------
/src/app/app.spec.js:
--------------------------------------------------------------------------------
1 | describe( 'Test app module', function() {
2 | describe( 'Test AppCtrl controller', function() {
3 | var ctrl, $location, $scope;
4 |
5 | var windowMock = {
6 | navigator: {
7 | standalone: false
8 | },
9 | history: {
10 | back: jasmine.createSpy()
11 | }
12 | };
13 |
14 | beforeEach(module(function($provide) {
15 | $provide.value('$window', windowMock);
16 | }));
17 |
18 | beforeEach(module('moped'));
19 |
20 | beforeEach(inject(function($controller, _$location_, $rootScope) {
21 | $location = _$location_;
22 | $scope = $rootScope.$new();
23 | ctrl = $controller('AppCtrl', { $location: $location, $scope: $scope, $window: windowMock });
24 | }));
25 |
26 | it('should pass a dummy test', inject( function() {
27 | expect(ctrl).toBeTruthy();
28 | }));
29 |
30 | it('should make the backbutton visible when location is not home and device is standalone', function() {
31 | $location.path('/');
32 | expect($scope.isBackVisible).toBe(false);
33 |
34 | windowMock.navigator.standalone = true;
35 | $scope.$apply();
36 |
37 | expect($scope.isBackVisible).toBe(false);
38 |
39 | $location.path('/test');
40 | $scope.$apply();
41 |
42 | expect($scope.isBackVisible).toBe(true);
43 | });
44 |
45 | it('should call history.back when back is clicked.', function() {
46 | $scope.goBack();
47 | expect(windowMock.history.back).toHaveBeenCalled();
48 | });
49 |
50 | it('should toggle the sidebar visibility when toggle button is clicked', function() {
51 | var currentVisibility = $scope.isSidebarVisibleForMobile;
52 | $scope.toggleSidebar();
53 |
54 | expect($scope.isSidebarVisibleForMobile).not.toBe(currentVisibility);
55 | });
56 |
57 | it('should hide the sidebar on location change', function() {
58 | $scope.isSidebarVisibleForMobile = true;
59 |
60 | $location.path('/whatever');
61 | $scope.$apply();
62 |
63 | expect($scope.isSidebarVisibleForMobile).toBe(false);
64 | });
65 |
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/src/app/browse/album.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/browse/artist.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
{{artist.name}}
3 |
4 |
5 |
6 |
7 |
8 | Albums
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Singles
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Appears on
25 |
26 |
27 |
30 |
--------------------------------------------------------------------------------
/src/app/browse/browse.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.browse', [
2 | 'moped.mopidy',
3 | 'moped.lastfm',
4 | 'ngRoute',
5 | 'ngSanitize'
6 | ])
7 |
8 | .config(function config($routeProvider) {
9 | $routeProvider
10 | .when('/album/:uri', {
11 | templateUrl: 'browse/album.tpl.html',
12 | controller: 'AlbumCtrl',
13 | title: 'Album'
14 | })
15 | .when('/artist/:uri/:name', {
16 | templateUrl: 'browse/artist.tpl.html',
17 | controller: 'ArtistCtrl',
18 | title: 'Artist'
19 | });
20 | })
21 |
22 | .controller('AlbumCtrl', function AlbumController($scope, $timeout, $routeParams, mopidyservice) {
23 | var defaultAlbumImageUrl = 'assets/images/noalbum.png';
24 |
25 | $scope.album = {};
26 | $scope.tracks = [];
27 |
28 | mopidyservice.getAlbum($routeParams.uri).then(function(data) {
29 |
30 | // data comes as a list of tracks.
31 | if (data.length > 0) {
32 |
33 | _.forEach(data, function(track) {
34 | // don't add unplayable tracks
35 | if (track.name.indexOf('[unplayable]') === -1) {
36 | $scope.tracks.push(track);
37 | }
38 | });
39 |
40 | // Extract album and artist(s) from first track.
41 | var firstTrack = $scope.tracks[0];
42 | $scope.album = firstTrack.album;
43 | }
44 | }, console.error.bind(console));
45 |
46 | $timeout(function() {
47 | mopidyservice.getCurrentTrack().then(function(track) {
48 | if (track) {
49 | $scope.$broadcast('moped:currenttrackrequested', track);
50 | }
51 | });
52 | }, 500);
53 |
54 | $scope.$on('moped:playtrackrequest', function(event, track) {
55 | mopidyservice.playTrack(track, $scope.tracks);
56 | });
57 | })
58 |
59 | .controller('ArtistCtrl', function ArtistController($scope, $timeout, $routeParams, util, mopidyservice, lastfmservice) {
60 | var defaultAlbumImageUrl = 'assets/images/noalbum.png';
61 |
62 | var uri = $routeParams.uri;
63 | var name = util.urlDecode($routeParams.name);
64 |
65 | $scope.artistSummary = '';
66 | $scope.albums = [];
67 | $scope.singles = [];
68 | $scope.appearsOn = [];
69 |
70 | $scope.artist = { name: name };
71 |
72 | lastfmservice.getArtistInfo(name, function(err, artistInfo) {
73 | if (! err) {
74 | $scope.artistSummary = artistInfo.artist.bio.summary;
75 | }
76 | });
77 |
78 | mopidyservice.getArtist(uri).then(function(data) {
79 |
80 | // data comes as a list of tracks.
81 | if (data.length > 0) {
82 | // First filter unplayable tracks
83 | _.remove(data, function(track) {
84 | return track.name.indexOf('[unplayable]') > -1;
85 | });
86 |
87 | // Get artist object from list of tracks
88 | $scope.artist = _.chain(data)
89 | .map(function(track) {
90 | return track.artists[0];
91 | })
92 | .find({ uri: uri })
93 | .value();
94 |
95 | // Extract albums from list of tracks
96 | var allAlbums = _.chain(data)
97 | .map(function(track) {
98 | return track.album;
99 | })
100 | .uniq(function(album) {
101 | return album.uri;
102 | })
103 | .sortBy('date')
104 | .value();
105 |
106 | var tracksByAlbum = _.groupBy(data, function(track) {
107 | return track.album.uri;
108 | });
109 |
110 | _.forEachRight(allAlbums, function(album) {
111 | var tracks = tracksByAlbum[album.uri];
112 | var albumObject = { album: album, tracks: tracks };
113 | if (album.artists[0].uri === uri) { // Main artist, otherwise appears on.
114 | if (tracks.length > 4) { // If an album has 4 tracks or less, we're categorizing it as single.
115 | $scope.albums.push(albumObject);
116 | }
117 | else {
118 | $scope.singles.push(albumObject);
119 | }
120 | }
121 | else {
122 | $scope.appearsOn.push(albumObject);
123 | }
124 | });
125 | }
126 | }, console.error.bind(console));
127 |
128 | $timeout(function() {
129 | mopidyservice.getCurrentTrack().then(function(track) {
130 | if (track) {
131 | $scope.$broadcast('moped:currenttrackrequested', track);
132 | }
133 | });
134 | }, 500);
135 |
136 | $scope.$on('moped:playtrackalbumrequest', function(event, album) {
137 | mopidyservice.playTrack(album.currenttrack, album.tracks);
138 | });
139 |
140 | });
141 |
--------------------------------------------------------------------------------
/src/app/filters.js:
--------------------------------------------------------------------------------
1 | angular.module(['moped.filters'], [
2 | 'moped.util'
3 | ])
4 | .filter('urlEncode', function (util) {
5 | return util.urlEncode;
6 | })
7 | .filter('urlDecode', function (util) {
8 | return util.urlDecode;
9 | })
10 | .filter('doubleUrlEncode', function (util) {
11 | return util.doubleUrlEncode;
12 | })
13 | .filter('directoryUrlEncode', function (util) {
14 | return util.directoryUrlEncode;
15 | });
16 |
--------------------------------------------------------------------------------
/src/app/home/home.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Each section of the site has its own module. It probably also has
3 | * submodules, though this boilerplate is too simple to demonstrate it. Within
4 | * `src/app/home`, however, could exist several additional folders representing
5 | * additional modules that would then be listed as dependencies of this one.
6 | * For example, a `note` section could have the submodules `note.create`,
7 | * `note.delete`, `note.edit`, etc.
8 | *
9 | * Regardless, so long as dependencies are managed correctly, the build process
10 | * will automatically take take of the rest.
11 | *
12 | * The dependencies block here is also where component dependencies should be
13 | * specified, as shown below.
14 | */
15 | angular.module('moped.home', [
16 | 'ngRoute'
17 | ])
18 |
19 | /**
20 | * Each section or module of the site can also have its own routes. AngularJS
21 | * will handle ensuring they are all available at run-time, but splitting it
22 | * this way makes each module more "self-contained".
23 | */
24 | .config(function config($routeProvider) {
25 | $routeProvider
26 | .when('/', {
27 | templateUrl: 'home/home.tpl.html',
28 | controller: 'HomeCtrl'
29 | });
30 | })
31 |
32 | /**
33 | * And of course we define a controller for our route.
34 | */
35 | .controller('HomeCtrl', function HomeController($scope, $timeout, mopidyservice) {
36 | $scope.hello = "Hello Moped";
37 | $scope.currentTracks = [];
38 |
39 | mopidyservice.getCurrentTrackList()
40 | .then(function(tracks) {
41 | $scope.currentTracks = tracks;
42 | });
43 |
44 | $timeout(function() {
45 | mopidyservice.getCurrentTrack().then(function(track) {
46 | if (track) {
47 | $scope.$broadcast('moped:currenttrackrequested', track);
48 | }
49 | });
50 | }, 1000);
51 |
52 | $scope.$on('moped:playtrackrequest', function(event, track) {
53 | mopidyservice.playTrack(track, $scope.currentTracks);
54 | });
55 |
56 | })
57 |
58 | ;
59 |
60 |
--------------------------------------------------------------------------------
/src/app/home/home.spec.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Tests sit right alongside the file they are testing, which is more intuitive
3 | * and portable than separating `src` and `test` directories. Additionally, the
4 | * build process will exclude all `.spec.js` files from the build
5 | * automatically.
6 | */
7 | describe( 'home section', function() {
8 | beforeEach( module( 'moped.home' ) );
9 |
10 | it( 'should have a dummy test', inject( function() {
11 | expect( true ).toBeTruthy();
12 | }));
13 | });
14 |
15 |
--------------------------------------------------------------------------------
/src/app/home/home.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Current track list
4 |
5 | No tracks in the current track list
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Moped
13 |
Moped is an HTML frontend for the Mopidy music server .
14 |
Search
15 |
Find artists, albums or tracks.
16 |
Browse
17 |
Browse and play categorized music.
18 |
Playlists
19 |
All playlists appear in the left menu. Select and play.
20 |
Radio
21 |
22 | Moped can play radio streams. Select 'Radio', enter the stream address and play. It's possible to store radio stations locally by starring. These stations will appear in the left menu.
23 | Alternatively, you can also search for radio streams.
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/app/library/container.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
{{container.name}}
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/app/library/directory.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{directory.name}}
4 |
--------------------------------------------------------------------------------
/src/app/library/library.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.library', [
2 | 'moped.mopidy',
3 | 'moped.util',
4 | 'ngRoute'
5 | ])
6 |
7 | .config(function config($routeProvider) {
8 | $routeProvider
9 | .when('/library/:uri/:name?', {
10 | templateUrl: 'library/container.tpl.html',
11 | controller: 'ContainerCtrl',
12 | title: 'Directories'
13 | });
14 | })
15 |
16 | .controller('LibraryMenuCtrl', function LibraryMenuController($scope, mopidyservice) {
17 | $scope.libraryDirectories = [];
18 |
19 | function loadLibraryDirectories() {
20 | mopidyservice.getLibrary().then(function(data) {
21 | $scope.libraryDirectories = data;
22 | }, function(data) {
23 | throw data;
24 | });
25 | }
26 |
27 | $scope.$on('mopidy:state:online', function() {
28 | loadLibraryDirectories();
29 | });
30 | })
31 |
32 | .controller('ContainerCtrl', function ContainerController($scope, $routeParams, mopidyservice, util) {
33 | $scope.container = { name: util.urlDecode($routeParams.name), uri: util.urlDecode($routeParams.uri) };
34 | $scope.directories = [];
35 | $scope.playlists = [];
36 | $scope.tracks = [];
37 |
38 | mopidyservice.getLibraryItems($scope.container.uri).then(function(data) {
39 | var currentTrackIndex = 0;
40 | _.forEach(data, function (item) {
41 | if (item.type === 'directory') {
42 | item.fullName = $scope.container.name + '/' + item.name;
43 | $scope.directories.push(item);
44 | }
45 | else if (item.type === 'track') {
46 | (
47 | function(trackIndex) {
48 | mopidyservice.getTrack(item.uri).then(function (data) {
49 | if (data.length == 1) {
50 | $scope.tracks[trackIndex] = data[0];
51 | }
52 | else {
53 | throw new Error('Expected exactly one track in result.');
54 | }
55 | });
56 | }(currentTrackIndex++)
57 | );
58 | }
59 | else { // container
60 | item.fullName = $scope.container.name + '/' + item.name;
61 | $scope.playlists.push(item);
62 | }
63 | });
64 | }, console.error.bind(console));
65 |
66 | $scope.$on('moped:playtrackrequest', function(event, track) {
67 | mopidyservice.playTrack(track, $scope.tracks);
68 | });
69 | })
70 |
71 | .directive("directory", function () {
72 | return {
73 | restrict: "E",
74 | scope: { directory: '=' },
75 | templateUrl: 'library/directory.tpl.html',
76 | replace:true
77 | };
78 | })
79 |
80 | .directive("playlist", function () {
81 | return {
82 | restrict: "E",
83 | scope: { playlist: '=' },
84 | templateUrl: 'library/playlist.tpl.html',
85 | replace:true
86 | };
87 | });
--------------------------------------------------------------------------------
/src/app/library/menu.tpl.html:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/app/library/playlist.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{playlist.name}}
4 |
--------------------------------------------------------------------------------
/src/app/nowplaying/nowplaying.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.nowplaying', [
2 | 'moped.mopidy',
3 | 'moped.lastfm',
4 | 'moped.util'
5 | ])
6 |
7 | .controller('NowPlayingCtrl', function NowPlayingController($scope, mopidyservice, lastfmservice, util) {
8 | var checkPositionTimer;
9 | var isSeeking = false;
10 | var defaultTrackImageUrl = 'assets/images/vinyl-icon.png';
11 |
12 | resetCurrentTrack();
13 |
14 | $scope.$on('moped:slidervaluechanging', function(event, value) {
15 | isSeeking = true;
16 | });
17 |
18 | $scope.$on('moped:slidervaluechanged', function(event, value) {
19 | seek(value);
20 | isSeeking = false;
21 | });
22 |
23 | $scope.$on('mopidy:state:online', function(event, data) {
24 | mopidyservice.getCurrentTrack().then(function(track) {
25 | mopidyservice.getTimePosition().then(function(timePosition) {
26 | updateCurrentTrack(track, timePosition);
27 | });
28 | });
29 | mopidyservice.getState().then(function (state) {
30 | if (state === 'playing') {
31 | checkPositionTimer = setInterval(function() {
32 | checkTimePosition();
33 | }, 1000);
34 | }
35 | });
36 | });
37 |
38 | $scope.$on('mopidy:state:offline', function() {
39 | clearInterval(checkPositionTimer);
40 | resetCurrentTrack();
41 | });
42 |
43 | $scope.$on('mopidy:event:playbackStateChanged', function(event, data) {
44 | if (data.new_state === 'playing') {
45 | checkPositionTimer = setInterval(function() {
46 | checkTimePosition();
47 | }, 1000);
48 | }
49 | else {
50 | clearInterval(checkPositionTimer);
51 | }
52 | });
53 |
54 | $scope.$on('mopidy:event:trackPlaybackStarted', function(event, data) {
55 | updateCurrentTrack(data.tl_track.track, data.time_position);
56 | });
57 |
58 | $scope.$on('mopidy:event:trackPlaybackPaused', function(event, data) {
59 | updateCurrentTrack(data.tl_track.track, data.time_position);
60 | });
61 |
62 | function updateCurrentTrack(track, timePosition) {
63 | if (track) {
64 | $scope.currentTrack = track.name;
65 | $scope.currentArtists = track.artists;
66 | $scope.currentTrackLength = track.length;
67 | $scope.currentTrackLengthString = util.timeFromMilliSeconds(track.length);
68 |
69 | if (track !== null && timePosition !== null && track.length > 0) {
70 | $scope.currentTimePosition = (timePosition / track.length) * 100;
71 | $scope.currentTrackPosition = util.timeFromMilliSeconds(timePosition);
72 | }
73 | else
74 | {
75 | $scope.currentTimePosition = 0;
76 | $scope.currentTrackPosition = util.timeFromMilliSeconds(0);
77 | }
78 |
79 | if (track.album !== undefined) {
80 | $scope.currentAlbumUri = track.album.uri;
81 |
82 | if (track.album.images && track.album.images.length > 0) {
83 | $scope.currentTrackImageUrl = track.album.images[0];
84 | } else {
85 | lastfmservice.getTrackImage(track, 'medium', function(err, trackImageUrl) {
86 | if (! err && trackImageUrl !== undefined && trackImageUrl !== '') {
87 | $scope.currentTrackImageUrl = trackImageUrl;
88 | }
89 | else
90 | {
91 | $scope.currentTrackImageUrl = defaultTrackImageUrl;
92 | }
93 | $scope.$apply();
94 | });
95 | }
96 | }
97 | else {
98 | $scope.currentAlbumUri = null;
99 | }
100 | }
101 | }
102 |
103 | function resetCurrentTrack() {
104 | $scope.currentTrack = '';
105 | $scope.currentAlbumUri = '';
106 | $scope.currentArtists = [];
107 | $scope.currentTrackLength = 0;
108 | $scope.currentTrackLengthString = '';
109 | $scope.currentTimePosition = 0; // 0-100
110 | $scope.currentTrackPosition = util.timeFromMilliSeconds(0);
111 | $scope.currentTrackImageUrl = defaultTrackImageUrl;
112 | }
113 |
114 | function checkTimePosition() {
115 | if (! isSeeking) {
116 | mopidyservice.getTimePosition().then(function(timePosition) {
117 | if ($scope.currentTrackLength > 0 && timePosition > 0) {
118 | $scope.currentTimePosition = (timePosition / $scope.currentTrackLength) * 100;
119 | $scope.currentTrackPosition = util.timeFromMilliSeconds(timePosition);
120 | }
121 | else {
122 | $scope.currentTimePosition = 0;
123 | $scope.currentTrackPosition = util.timeFromMilliSeconds(0);
124 | }
125 | });
126 | }
127 | }
128 |
129 | function seek(sliderValue) {
130 | if ($scope.currentTrackLength > 0) {
131 | var milliSeconds = ($scope.currentTrackLength / 100) * sliderValue;
132 | mopidyservice.seek(Math.round(milliSeconds));
133 | }
134 | }
135 |
136 | });
137 |
--------------------------------------------------------------------------------
/src/app/nowplaying/nowplaying.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
{{currentTrackPosition}}
13 |
{{currentTrackLengthString}}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/app/playercontrols/playercontrols.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.playercontrols', [
2 | 'moped.mopidy',
3 | 'moped.util',
4 | 'moped.widgets'
5 | ])
6 |
7 | .controller('PlayerControlsCtrl', function PlayerControlsController($scope, mopidyservice) {
8 |
9 | $scope.volume = 0;
10 | $scope.isPlaying = false;
11 | $scope.isRandom = false;
12 |
13 | $scope.$on('moped:slidervaluechanged', function(event, value) {
14 | mopidyservice.setVolume(value);
15 | });
16 |
17 | $scope.$on('mopidy:event:playbackStateChanged', function(event, data) {
18 | $scope.isPlaying = data.new_state === 'playing';
19 | $scope.$apply();
20 | });
21 |
22 | $scope.$on('mopidy:state:online', function() {
23 | mopidyservice.getVolume().then(function(volume) {
24 | $scope.volume = volume;
25 | });
26 | mopidyservice.getState().then(function(state) {
27 | $scope.isPlaying = state === 'playing';
28 | });
29 | mopidyservice.getRandom().then(function (isRandom) {
30 | $scope.isRandom = isRandom === true;
31 | });
32 | });
33 |
34 | $scope.$on('mopidy:event:volumeChanged', function(event, data) {
35 | $scope.volume = data.volume;
36 | $scope.$apply();
37 | });
38 |
39 | $scope.play = function() {
40 | if ($scope.isPlaying) {
41 | // pause
42 | mopidyservice.pause();
43 | }
44 | else {
45 | // play
46 | mopidyservice.play();
47 | }
48 | };
49 |
50 | $scope.previous = function() {
51 | mopidyservice.previous();
52 | };
53 |
54 | $scope.next = function() {
55 | mopidyservice.next();
56 | };
57 |
58 | $scope.stop = function() {
59 | mopidyservice.stopPlayback();
60 | };
61 |
62 | $scope.toggleRandom = function () {
63 | if ($scope.isRandom) {
64 | mopidyservice.setRandom(false).then(function () {
65 | $scope.isRandom = false;
66 | });
67 | } else {
68 | mopidyservice.setRandom(true).then(function () {
69 | $scope.isRandom = true;
70 | });
71 | }
72 | };
73 | });
74 |
--------------------------------------------------------------------------------
/src/app/playercontrols/playercontrols.tpl.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/playlists/list.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
{{playlist.name}}
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/app/playlists/menu.tpl.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/playlists/playlistfolder.tpl.html:
--------------------------------------------------------------------------------
1 | {{folder.name}}
2 |
--------------------------------------------------------------------------------
/src/app/playlists/playlists.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.playlists', [
2 | 'moped.mopidy',
3 | 'moped.util',
4 | 'ngRoute'
5 | ])
6 |
7 | .config(function config($routeProvider) {
8 | $routeProvider
9 | .when('/playlist/:uri', {
10 | templateUrl: 'playlists/list.tpl.html',
11 | controller: 'PlaylistCtrl',
12 | title: 'Playlist'
13 | });
14 | })
15 |
16 | .controller('PlaylistMenuCtrl', function PlaylistMenuController($scope, mopidyservice) {
17 | function ensureFolderExists(folderPaths, processedPlaylists) {
18 | // Check if a compatible folder exists in processedPlaylists. If not, create and return it.
19 | var currentFolder = null;
20 | _.forEach(folderPaths, function (folderPath) {
21 | if (currentFolder === null) {
22 | currentFolder = _.find(processedPlaylists, function (playlistItem) {
23 | return playlistItem.hasOwnProperty('items') && playlistItem.name === folderPath;
24 | });
25 | if (! currentFolder) {
26 | currentFolder = { name: folderPath, items: [], expanded: false };
27 | processedPlaylists.push(currentFolder);
28 | }
29 | }
30 | else {
31 | var previousFolder = currentFolder;
32 | currentFolder = _.find(previousFolder.items, function (playlistItem) {
33 | return playlistItem.hasOwnProperty('items') && playlistItem.name === folderPath;
34 | });
35 | if (! currentFolder) {
36 | currentFolder = { name: folderPath, items: [], expanded: false };
37 | previousFolder.items.push(currentFolder);
38 | }
39 | }
40 | });
41 | return currentFolder;
42 | }
43 |
44 | function processPlaylists(playlists) {
45 | var processedPlaylists = [];
46 | // Extract playlist folders from playlist names ('/' is the separator) and shove the playlist into
47 | // the right folders.
48 | _.forEach(playlists, function (playlist) {
49 | var paths = playlist.name.split('/');
50 | if (paths.length > 1) {
51 | // Folders, last item in array is the playlist name
52 | playlist.name = paths.pop();
53 | var folder = ensureFolderExists(paths, processedPlaylists);
54 | folder.items.push(playlist);
55 | }
56 | else {
57 | processedPlaylists.push(playlist);
58 | }
59 |
60 | });
61 |
62 | return processedPlaylists;
63 | }
64 |
65 | function loadPlaylists() {
66 | mopidyservice.getPlaylists().then(function(data) {
67 | $scope.playlists = processPlaylists(data);
68 | }, console.error.bind(console));
69 | }
70 |
71 | $scope.playlists = [];
72 |
73 | $scope.$on('mopidy:state:online', function() {
74 | loadPlaylists();
75 | });
76 |
77 | $scope.$on('mopidy:event:playlistsLoaded', function() {
78 | loadPlaylists();
79 | });
80 | })
81 |
82 | .controller('PlaylistCtrl', function PlaylistController($scope, $routeParams, mopidyservice, util) {
83 | function loadPlaylist() {
84 | mopidyservice.getPlaylist($routeParams.uri).then(function(data) {
85 | $scope.playlist = data;
86 | }, console.error.bind(console));
87 | }
88 |
89 | $scope.playlist = {};
90 |
91 | loadPlaylist();
92 |
93 | $scope.$on('moped:playtrackrequest', function(event, track) {
94 | mopidyservice.playTrack(track, $scope.playlist.tracks);
95 | });
96 | })
97 |
98 | .directive("playlistFolder", function ($compile) {
99 | return {
100 | restrict: "E",
101 | scope: { folder: '=' },
102 | templateUrl: 'playlists/playlistfolder.tpl.html',
103 | controller: function ($scope) {
104 | $scope.toggle = function (folder) {
105 | folder.expanded = ! folder.expanded;
106 | };
107 | },
108 | compile: function (el, attr) {
109 | var contents = el.contents().remove();
110 | var compiledContents;
111 | return function(scope, el, attr) {
112 | if(! compiledContents) {
113 | compiledContents = $compile(contents);
114 | }
115 | compiledContents(scope, function (clone, scope) {
116 | el.append(clone);
117 | });
118 | };
119 | }
120 | };
121 | });
122 |
--------------------------------------------------------------------------------
/src/app/radio/menu.tpl.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/app/radio/radio.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.radio', [
2 | 'moped.mopidy',
3 | 'ngRoute'
4 | ])
5 |
6 | .factory('radioservice', function($http, $window) {
7 |
8 | function getStations() {
9 | var stationsFromStorage = $window.localStorage['stations'];
10 | if (stationsFromStorage && stationsFromStorage !== '') {
11 | return JSON.parse(stationsFromStorage);
12 | }
13 | else {
14 | return [];
15 | }
16 | }
17 |
18 | function storeStations(stations) {
19 | $window.localStorage['stations'] = JSON.stringify(stations);
20 | }
21 |
22 | return {
23 | getRadioStations: function(callback) {
24 | var stations = getStations();
25 | callback(null, stations);
26 | },
27 | getRadioStation: function(stationName, callback) {
28 | var stations = getStations();
29 | var station = _.find(stations, { name: stationName });
30 | if (station) {
31 | callback(null, station);
32 | }
33 | else {
34 | callback({ message: 'Radio station ' + stationName + ' could not be found.' }, null);
35 | }
36 | },
37 | saveRadioStation: function(stationName, stationUri, callback) {
38 | this.getRadioStation(stationName, function(err, station) {
39 | var stations = getStations();
40 | if (station) {
41 | callback({ message: 'Radio station ' + stationName + ' already exists.'}, null);
42 | }
43 | else {
44 | var newStation = { name: stationName, uri: stationUri };
45 | stations.push(newStation);
46 | storeStations(stations);
47 | callback(null, newStation);
48 | }
49 | });
50 | },
51 | removeRadioStation: function(stationName, callback) {
52 | var stations = getStations();
53 | _.remove(stations, function(station) {
54 | return station.name === stationName;
55 | });
56 | storeStations(stations);
57 | callback(null, stationName);
58 | }
59 |
60 | };
61 | })
62 |
63 | .config(function config($routeProvider) {
64 | $routeProvider
65 | .when('/radio/:stationname?', {
66 | templateUrl: 'radio/radio.tpl.html',
67 | controller: 'RadioCtrl',
68 | title: 'Radio'
69 | });
70 | })
71 |
72 | .controller('RadioMenuCtrl', function RadioMenuController($scope, radioservice) {
73 | $scope.radiostations = [];
74 |
75 | radioservice.getRadioStations(function(err, stations) {
76 | if (! err) {
77 | _.forEach(stations, function(station) {
78 | $scope.radiostations.push(station);
79 | });
80 | }
81 | });
82 | $scope.$on('moped:radiostationadded', function(event, station) {
83 | $scope.radiostations.push(station);
84 | });
85 | $scope.$on('moped:radiostationremoved', function(event, stationName) {
86 | _.remove($scope.radiostations, function(station) {
87 | return station.name === stationName;
88 | });
89 | });
90 | })
91 |
92 | .controller('RadioCtrl', function RadioController($scope, $rootScope, $routeParams, $window, mopidyservice, radioservice, util) {
93 | var stationName = $routeParams.stationname;
94 |
95 | $scope.currentStreamName = '';
96 | $scope.currentStreamUri = '';
97 | $scope.currentStation = null;
98 | $scope.searchQuery = '';
99 | $scope.searchResults = [];
100 |
101 | $scope.canAddToFavourites = function() {
102 | return $window.localStorage && $scope.currentStreamUri !== '' && $scope.currentStation === null;
103 | };
104 |
105 | $scope.canRemoveFromFavourites = function() {
106 | return $window.localStorage && $scope.currentStreamName !== '' && $scope.currentStation !== null;
107 | };
108 |
109 | $scope.play = function() {
110 | if (util.isValidStreamUri($scope.currentStreamUri)) {
111 | mopidyservice.playStream($scope.currentStreamUri);
112 | }
113 | else {
114 | alert('Invalid stream address');
115 | }
116 | };
117 |
118 | $scope.addToFavourites = function() {
119 | if ($scope.currentStreamName === '') {
120 | alert('Please enter a name for the radio station.');
121 | return;
122 | }
123 | radioservice.saveRadioStation($scope.currentStreamName, $scope.currentStreamUri, function(err, station) {
124 | if (err) {
125 | alert(err.message);
126 | }
127 | else {
128 | $scope.currentStation = station;
129 | $scope.currentStreamName = station.name;
130 | $scope.currentStreamUri = station.uri;
131 | $rootScope.$broadcast('moped:radiostationadded', station);
132 | }
133 | });
134 | };
135 |
136 | $scope.removeFromFavourites = function() {
137 | if (confirm('Are you sure?')) {
138 | radioservice.removeRadioStation($scope.currentStreamName, function(err, stationName) {
139 | if (err) {
140 | alert(err.message);
141 | }
142 | else {
143 | $rootScope.$broadcast('moped:radiostationremoved', $scope.currentStreamName);
144 | $scope.currentStation = null;
145 | alert('Station ' + stationName + ' is removed from the favourites.');
146 | }
147 | });
148 | }
149 | };
150 |
151 | $scope.playStream = function(name, streamUri) {
152 | $scope.currentStreamUri = streamUri;
153 | $scope.currentStreamName = name;
154 | $scope.play();
155 | };
156 |
157 | // Play stream immediately when station is given.
158 | if (stationName) {
159 | radioservice.getRadioStation(stationName, function(err, station) {
160 | if (err) {
161 | console.log(err.message);
162 | }
163 | else if (station) {
164 | $scope.currentStreamUri = station.uri;
165 | $scope.currentStreamName = station.name;
166 | $scope.currentStation = station;
167 | $scope.play();
168 | $scope.isInitializedFromUrl = true;
169 | }
170 | });
171 | }
172 |
173 | $scope.$watch('currentStreamUri', function(value) {
174 | if (! $scope.isInitializedFromUrl && $scope.currentStation !== null && value !== $scope.currentStation.name) {
175 | $scope.currentStreamName = '';
176 | $scope.currentStation = null;
177 | }
178 | else if ($scope.isInitializedFromUrl) {
179 | $scope.isInitializedFromUrl = false;
180 | }
181 | });
182 |
183 |
184 | });
185 |
186 |
--------------------------------------------------------------------------------
/src/app/radio/radio.tpl.html:
--------------------------------------------------------------------------------
1 | Radio
2 |
24 |
25 |
26 | Alternatively, you can use the tunein mopidy plugin to listen to radio stations, see
27 | https://github.com/kingosticks/mopidy-tunein .
28 |
--------------------------------------------------------------------------------
/src/app/search/results.tpl.html:
--------------------------------------------------------------------------------
1 |
2 | Showing results for {{query}}
3 |
23 |
24 |
25 |
Tracks
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/app/search/search.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.search', [
2 | 'moped.util',
3 | 'moped.mopidy',
4 | 'ngRoute'
5 | ])
6 |
7 | .config(function config($routeProvider) {
8 | $routeProvider
9 | .when('/search/:query', {
10 | templateUrl: 'search/results.tpl.html',
11 | controller: 'SearchResultsCtrl',
12 | title: 'Search results'
13 | });
14 | })
15 |
16 | .controller('SearchCtrl', function SearchController($scope, $location, util) {
17 |
18 | $scope.query = '';
19 |
20 | $scope.find = function() {
21 | if ($scope.query !== '' && $scope.query.length > 1) {
22 | document.activeElement.blur();
23 | $location.path('/search/' + util.urlEncode($scope.query));
24 | }
25 | else {
26 | alert('Enter at least 2 characters');
27 | }
28 | };
29 | })
30 |
31 | .controller('SearchResultsCtrl', function SearchResultsController($scope, $routeParams, util, mopidyservice) {
32 | $scope.artists = [];
33 | $scope.albums = [];
34 | $scope.tracks = [];
35 |
36 | $scope.query = util.urlDecode($routeParams.query);
37 | if ($scope.query.length > 1) {
38 | mopidyservice.search($scope.query).then(function(results) {
39 | _.forEach(results, function(result) {
40 | var artists = _.take(result.artists, 6);
41 | _.forEach(artists, function(artist) {
42 | $scope.artists.push(artist);
43 | });
44 | var albums = _.take(result.albums, 6);
45 | _.forEach(albums, function(album) {
46 | $scope.albums.push(album);
47 | });
48 | var tracks = _.take(result.tracks, 20);
49 | _.forEach(tracks, function(track) {
50 | $scope.tracks.push(track);
51 | });
52 | });
53 | });
54 | }
55 |
56 | $scope.$on('moped:playtrackrequest', function(event, track) {
57 | mopidyservice.playTrack(track, $scope.tracks);
58 | });
59 | });
60 |
61 |
--------------------------------------------------------------------------------
/src/app/search/search.tpl.html:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/app/services/lastfmservice.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.lastfm', [])
2 | .factory('lastfmservice', function() {
3 |
4 | var API_KEY= '077c9fa281240d1c38b24d48bc234940';
5 | var API_SECRET = '';
6 |
7 | var fmcache = new LastFMCache();
8 | var lastfm = new LastFM({
9 | apiKey : API_KEY,
10 | apiSecret : API_SECRET,
11 | cache : fmcache
12 | });
13 |
14 | return {
15 | getTrackImage: function(track, size, callback) {
16 | var artistName = track.artists[0].name;
17 | var albumName = track.album !== null ? track.album.name : '';
18 | lastfm.album.getInfo({artist: artistName, album: albumName}, {
19 | success: function(data){
20 | var img = _.find(data.album.image, { size: size });
21 | if (img !== undefined) {
22 | callback(null, img['#text']);
23 | }
24 | }, error: function(code, message){
25 | console.log('Error #'+code+': '+message);
26 | callback({ code: code, message: message}, null);
27 | }
28 | });
29 | },
30 | getAlbumImage: function(album, size, callback) {
31 | if (album.artists && album.artists.length > 0) {
32 | lastfm.album.getInfo({artist: album.artists[0].name, album: album.name}, {
33 | success: function(data){
34 | var img = _.find(data.album.image, { size: size });
35 | if (img !== undefined) {
36 | callback(null, img['#text']);
37 | }
38 | }, error: function(code, message){
39 | console.log('Error #'+code+': '+message);
40 | callback({ code: code, message: message}, null);
41 | }
42 | });
43 | }
44 | },
45 | getArtistInfo: function(artistName, callback) {
46 | lastfm.artist.getInfo({artist: artistName}, {
47 | success: function(data){
48 | callback(null, data);
49 | }, error: function(code, message){
50 | console.log('Error #'+code+': '+message);
51 | callback({ code: code, message: message}, null);
52 | }
53 | });
54 | }
55 | };
56 | });
--------------------------------------------------------------------------------
/src/app/services/mopidyservice.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.mopidy', [])
2 | .factory('mopidyservice', function($q, $rootScope) {
3 |
4 | //var consoleLog = console.log.bind(console);
5 | var consoleLog = function () {};
6 | var consoleError = console.error.bind(console);
7 |
8 | // Wraps calls to mopidy api and converts mopidy's promise to Angular $q promise.
9 | // Mopidy method calls are passed as a string because some methods are not
10 | // available yet when this method is called, due to the introspection.
11 | // See also http://blog.mbfisher.com/2013/06/mopidy-websockets-and-introspective-apis.html
12 | function wrapMopidyFunc(functionNameToWrap, thisObj) {
13 | return function() {
14 | var deferred = $q.defer();
15 | var args = Array.prototype.slice.call(arguments);
16 | var self = thisObj || this;
17 |
18 | $rootScope.$broadcast('moped:mopidycalling', { name: functionNameToWrap, args: args });
19 |
20 | if (self.isConnected) {
21 | executeFunctionByName(functionNameToWrap, self, args).then(function(data) {
22 | deferred.resolve(data);
23 | $rootScope.$broadcast('moped:mopidycalled', { name: functionNameToWrap, args: args });
24 | }, function(err) {
25 | deferred.reject(err);
26 | $rootScope.$broadcast('moped:mopidyerror', { name: functionNameToWrap, args: args, err: err });
27 | });
28 | }
29 | else
30 | {
31 | self.mopidy.on("state:online", function() {
32 | executeFunctionByName(functionNameToWrap, self, args).then(function(data) {
33 | deferred.resolve(data);
34 | $rootScope.$broadcast('moped:mopidycalled', { name: functionNameToWrap, args: args });
35 | }, function(err) {
36 | deferred.reject(err);
37 | $rootScope.$broadcast('moped:mopidyerror', { name: functionNameToWrap, args: args, err: err });
38 | });
39 | });
40 | }
41 | return deferred.promise;
42 | };
43 | }
44 |
45 | function executeFunctionByName(functionName, context, args) {
46 | var namespaces = functionName.split(".");
47 | var func = namespaces.pop();
48 | for(var i = 0; i < namespaces.length; i++) {
49 | context = context[namespaces[i]];
50 | }
51 | return context[func].apply(context, args);
52 | }
53 |
54 | return {
55 | mopidy: {},
56 | isConnected: false,
57 | currentTlTracks: [],
58 | start: function() {
59 | var self = this;
60 | $rootScope.$broadcast('moped:mopidystarting');
61 |
62 | if (window.localStorage && localStorage['moped.mopidyUrl']) {
63 | this.mopidy = new Mopidy({
64 | webSocketUrl: localStorage['moped.mopidyUrl'],
65 | callingConvention: 'by-position-or-by-name'
66 | });
67 | }
68 | else {
69 | this.mopidy = new Mopidy({
70 | callingConvention: 'by-position-or-by-name'
71 | });
72 | }
73 | this.mopidy.on(consoleLog);
74 | // Convert Mopidy events to Angular events
75 | this.mopidy.on(function(ev, args) {
76 | $rootScope.$broadcast('mopidy:' + ev, args);
77 | if (ev === 'state:online') {
78 | self.isConnected = true;
79 | }
80 | if (ev === 'state:offline') {
81 | self.isConnected = false;
82 | }
83 | });
84 |
85 | $rootScope.$broadcast('moped:mopidystarted');
86 | },
87 | stop: function() {
88 | $rootScope.$broadcast('moped:mopidystopping');
89 | this.mopidy.close();
90 | this.mopidy.off();
91 | this.mopidy = null;
92 | $rootScope.$broadcast('moped:mopidystopped');
93 | },
94 | restart: function() {
95 | this.stop();
96 | this.start();
97 | },
98 | getPlaylists: function() {
99 | return wrapMopidyFunc("mopidy.playlists.asList", this)();
100 | },
101 | getPlaylist: function(uri) {
102 | return wrapMopidyFunc("mopidy.playlists.lookup", this)({ uri: uri });
103 | },
104 | getLibrary: function() {
105 | return wrapMopidyFunc("mopidy.library.browse", this)({ uri: null });
106 | },
107 | getLibraryItems: function(uri) {
108 | return wrapMopidyFunc("mopidy.library.browse", this)({ uri: uri });
109 | },
110 | refresh: function(uri) {
111 | return wrapMopidyFunc("mopidy.library.refresh", this)({ uri: uri });
112 | },
113 | getDirectory: function(uri) {
114 | return wrapMopidyFunc("mopidy.library.lookup", this)({ uri: uri });
115 | },
116 | getTrack: function(uri) {
117 | return wrapMopidyFunc("mopidy.library.lookup", this)({ uri: uri });
118 | },
119 | getAlbum: function(uri) {
120 | return wrapMopidyFunc("mopidy.library.lookup", this)({ uri: uri });
121 | },
122 | getArtist: function(uri) {
123 | return wrapMopidyFunc("mopidy.library.lookup", this)({ uri: uri });
124 | },
125 | search: function(query) {
126 | var queryElements = query.match(/(".*?"|[^"\s]+)+(?=\s*|\s*$)/g);
127 | queryElements = queryElements.map(function (el) {
128 | return el.replace(/"/g, '');
129 | });
130 | return wrapMopidyFunc("mopidy.library.search", this)({ any : queryElements });
131 | },
132 | getCurrentTrack: function() {
133 | return wrapMopidyFunc("mopidy.playback.getCurrentTrack", this)();
134 | },
135 | getTimePosition: function() {
136 | return wrapMopidyFunc("mopidy.playback.getTimePosition", this)();
137 | },
138 | seek: function(timePosition) {
139 | return wrapMopidyFunc("mopidy.playback.seek", this)({ time_position: timePosition });
140 | },
141 | getVolume: function() {
142 | return wrapMopidyFunc("mopidy.mixer.getVolume", this)();
143 | },
144 | setVolume: function(volume) {
145 | return wrapMopidyFunc("mopidy.mixer.setVolume", this)({ volume: volume });
146 | },
147 | getState: function() {
148 | return wrapMopidyFunc("mopidy.playback.getState", this)();
149 | },
150 | playTrack: function(track, surroundingTracks) {
151 | var self = this;
152 |
153 | // Check if a playlist change is required. If not just change the track.
154 | if (self.currentTlTracks.length > 0) {
155 | var trackUris = _.pluck(surroundingTracks, 'uri');
156 | var currentTrackUris = _.map(self.currentTlTracks, function(tlTrack) {
157 | return tlTrack.track.uri;
158 | });
159 | if (_.difference(trackUris, currentTrackUris).length === 0) {
160 | // no playlist change required, just play a different track.
161 | self.mopidy.playback.stop()
162 | .then(function () {
163 | var tlTrackToPlay = _.find(self.currentTlTracks, function(tlTrack) {
164 | return tlTrack.track.uri === track.uri;
165 | });
166 | self.mopidy.playback.play({ tl_track: tlTrackToPlay });
167 | });
168 | return;
169 | }
170 | }
171 |
172 | self.mopidy.playback.stop()
173 | .then(function() {
174 | self.mopidy.tracklist.clear();
175 | }, consoleError)
176 | .then(function() {
177 | self.mopidy.tracklist.add({ tracks: surroundingTracks });
178 | }, consoleError)
179 | .then(function() {
180 | self.mopidy.tracklist.getTlTracks()
181 | .then(function(tlTracks) {
182 | self.currentTlTracks = tlTracks;
183 | var tlTrackToPlay = _.find(tlTracks, function(tlTrack) {
184 | return tlTrack.track.uri === track.uri;
185 | });
186 | self.mopidy.playback.play({ tl_track: tlTrackToPlay });
187 | }, consoleError);
188 | } , consoleError);
189 | },
190 | playStream: function(streamUri) {
191 | var self = this;
192 |
193 | self.stopPlayback(true)
194 | .then(function() {
195 | self.mopidy.tracklist.clear();
196 | }, consoleError)
197 | .then(function() {
198 | self.mopidy.tracklist.add({ at_position: 0, uri: streamUri });
199 | }, consoleError)
200 | .then(function() {
201 | self.mopidy.playback.play();
202 | }, consoleError);
203 | },
204 | play: function() {
205 | return wrapMopidyFunc("mopidy.playback.play", this)();
206 | },
207 | pause: function() {
208 | return wrapMopidyFunc("mopidy.playback.pause", this)();
209 | },
210 | stopPlayback: function(clearCurrentTrack) {
211 | return wrapMopidyFunc("mopidy.playback.stop", this)();
212 | },
213 | previous: function() {
214 | return wrapMopidyFunc("mopidy.playback.previous", this)();
215 | },
216 | next: function() {
217 | return wrapMopidyFunc("mopidy.playback.next", this)();
218 | },
219 | getRandom: function () {
220 | return wrapMopidyFunc("mopidy.tracklist.getRandom", this)();
221 | },
222 | setRandom: function (isRandom) {
223 | return wrapMopidyFunc("mopidy.tracklist.setRandom", this)([ isRandom ]);
224 | },
225 | getCurrentTrackList: function () {
226 | return wrapMopidyFunc("mopidy.tracklist.getTracks", this)();
227 | }
228 | };
229 | });
230 |
--------------------------------------------------------------------------------
/src/app/services/util.js:
--------------------------------------------------------------------------------
1 | angular.module(['moped.util'], [])
2 | .factory('util', function($window) {
3 | return {
4 | timeFromMilliSeconds: function(length) {
5 | if (length === undefined) {
6 | return '';
7 | }
8 | var d = Number(length/1000);
9 | var h = Math.floor(d / 3600);
10 | var m = Math.floor(d % 3600 / 60);
11 | var s = Math.floor(d % 3600 % 60);
12 | return ((h > 0 ? h + ":" : "") + (m > 0 ? (h > 0 && m < 10 ? "0" : "") + m + ":" : "0:") + (s < 10 ? "0" : "") + s);
13 | },
14 | getTrackArtistsAsString: function(track) {
15 | return this.getArtistsAsString(track.artists);
16 | },
17 | getArtistsAsString: function(artists) {
18 | return _.map(artists, 'name').join(',');
19 | },
20 | getTrackDuration: function(track) {
21 | return this.timeFromMilliSeconds(track.length);
22 | },
23 | isValidStreamUri: function(uri) {
24 | var regexp = /(mms|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
25 | return regexp.test(uri);
26 | },
27 | safeApply: function($scope, fn) {
28 | var phase = $scope.$root.$$phase;
29 | if(phase == '$apply' || phase == '$digest') {
30 | if (fn) {
31 | $scope.$eval(fn);
32 | }
33 | }
34 | else {
35 | if (fn) {
36 | $scope.$apply(fn);
37 | } else {
38 | $scope.$apply();
39 | }
40 | }
41 | },
42 | urlEncode: function(textToEncode) {
43 | return $window.encodeURIComponent(textToEncode);
44 | },
45 | doubleUrlEncode: function(textToEncode) {
46 | return $window.encodeURIComponent($window.encodeURIComponent(textToEncode));
47 | },
48 | directoryUrlEncode: function(textToEncode) {
49 | return $window.encodeURIComponent($window.encodeURIComponent(textToEncode));
50 | },
51 | urlDecode: function(textToDecode) {
52 | return $window.decodeURIComponent(textToDecode);
53 | },
54 | doubleUrlDecode: function(textToDecode) {
55 | return $window.decodeURIComponent($window.decodeURIComponent(textToDecode));
56 | }
57 | };
58 | });
--------------------------------------------------------------------------------
/src/app/settings/settings.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.settings', [
2 | 'ngRoute'
3 | ])
4 | .config(function config($routeProvider) {
5 | $routeProvider
6 | .when('/settings', {
7 | templateUrl: 'settings/settings.tpl.html',
8 | controller: 'SettingsCtrl',
9 | title: 'Settings'
10 | });
11 | })
12 |
13 | .controller('SettingsCtrl', function SettingsController($scope, $rootScope, $window) {
14 | $scope.settings = {
15 | mopidyUrl: ''
16 | };
17 |
18 | if (window.localStorage && localStorage['moped.mopidyUrl'] !== null) {
19 | $scope.settings.mopidyUrl = localStorage['moped.mopidyUrl'];
20 | }
21 |
22 | $scope.saveSettings = function() {
23 | if (window.localStorage) {
24 | if ($scope.settings.mopidyUrl) {
25 | localStorage['moped.mopidyUrl'] = $scope.settings.mopidyUrl;
26 | }
27 | else {
28 | localStorage['moped.mopidyUrl'] = '';
29 | }
30 |
31 | $window.alert('Settings are saved.');
32 | $rootScope.$broadcast('settings:saved');
33 | }
34 | };
35 |
36 | $scope.verifyConnection = function(e) {
37 | e.preventDefault();
38 |
39 | var mopidy = new Mopidy({
40 | autoConnect: false,
41 | webSocketUrl: $scope.settings.mopidyUrl
42 | });
43 | mopidy.on(console.log.bind(console));
44 | mopidy.on('state:online', function() {
45 | $window.alert('Connection successful.');
46 | });
47 | mopidy.on('websocket:error', function(error) {
48 | $window.alert('Unable to connect to Mopidy server. Check if the url is correct.');
49 | });
50 |
51 | mopidy.connect();
52 |
53 | setTimeout(function() {
54 | mopidy.close();
55 | mopidy.off();
56 | mopidy = null;
57 | console.log('Mopidy closed.');
58 | }, 1000);
59 | };
60 | });
--------------------------------------------------------------------------------
/src/app/settings/settings.tpl.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/widgets/_module.js:
--------------------------------------------------------------------------------
1 | var widgetModule = angular.module('moped.widgets', [
2 | 'moped.util',
3 | 'moped.lastfm'
4 | ]);
--------------------------------------------------------------------------------
/src/app/widgets/album.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.widgets')
2 | .directive('mopedAlbum', function(util, lastfmservice) {
3 |
4 | var defaultAlbumImageUrl = 'assets/images/noalbum.png';
5 | var defaultAlbumImageSize = 'large';
6 |
7 | return {
8 | restrict: 'E',
9 | scope: {
10 | album: '=',
11 | tracks: '=',
12 | hideArtist: '=',
13 | linkAlbumTitle: '=',
14 | imageSize: '@'
15 | },
16 | replace: true,
17 | templateUrl: 'widgets/album.tpl.html',
18 | link: function(scope, element, attrs) {
19 |
20 | scope.$watch('album', function(newAlbum, oldAlbum) {
21 | if (newAlbum.name) {
22 | scope.discs = [];
23 | scope.album = scope.album || (scope.tracks.length > 0 ? scope.tracks[0].album : null);
24 | scope.artist = util.getArtistsAsString(scope.album.artists);
25 | scope.albumImageUrl = defaultAlbumImageUrl;
26 | scope.albumImageSize = scope.imageSize || defaultAlbumImageSize;
27 |
28 | // Group album into discs
29 | if (scope.tracks.length > 0) {
30 | var discNo = 1;
31 | var currentTrackNo = 1;
32 | var groupedTracks = _.groupBy(scope.tracks, function(track) {
33 | if (track.track_no < currentTrackNo) {
34 | discNo++;
35 | }
36 | currentTrackNo = track.track_no;
37 | return discNo;
38 | });
39 |
40 | _.forEach(groupedTracks, function(tracksOnDisc, index) {
41 | scope.discs.push({ disc: index, tracksOnDisc: tracksOnDisc });
42 | });
43 | }
44 |
45 | // Album image
46 | lastfmservice.getAlbumImage(scope.album, scope.albumImageSize, function(err, albumImageUrl) {
47 | if (! err && albumImageUrl !== undefined && albumImageUrl !== '') {
48 | scope.albumImageUrl = albumImageUrl;
49 | }
50 | else
51 | {
52 | scope.albumImageUrl = defaultAlbumImageUrl;
53 | }
54 | scope.$apply();
55 | });
56 |
57 | var cleanUpPlayTrackRequest = scope.$on('moped:playtrackrequest', function(event, track) {
58 | var surroundingTracks = [];
59 | // Broadcast album.
60 | scope.$emit('moped:playtrackalbumrequest', { tracks: scope.tracks, currenttrack: track });
61 | });
62 |
63 | scope.$on('$destroy', function() {
64 | cleanUpPlayTrackRequest();
65 | });
66 | }
67 | });
68 | }
69 | };
70 | });
--------------------------------------------------------------------------------
/src/app/widgets/album.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
{{album.date}}
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Disc {{disc.disc}}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/app/widgets/albumimage.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.widgets')
2 | .directive('mopedAlbumImage', function(util, lastfmservice) {
3 |
4 | var defaultAlbumImageUrl = 'assets/images/noalbum.png';
5 | var defaultAlbumImageSize = 'large';
6 |
7 | return {
8 | restrict: 'A',
9 | scope: {
10 | album: '=',
11 | imageSize: '@'
12 | },
13 | templateUrl: 'widgets/albumimage.tpl.html',
14 | link: function(scope, element, attrs) {
15 | scope.albumImageUrl = defaultAlbumImageUrl;
16 | scope.albumImageSize = scope.imageSize || defaultAlbumImageSize;
17 |
18 | // First check if an album image is set in the album itself
19 | if (scope.album.images && scope.album.images.length > 0) {
20 | scope.albumImageUrl = scope.album.images[0];
21 | } else {
22 | // Album image via LastFM
23 | lastfmservice.getAlbumImage(scope.album, scope.albumImageSize, function(err, albumImageUrl) {
24 | if (! err && albumImageUrl !== undefined && albumImageUrl !== '') {
25 | scope.albumImageUrl = albumImageUrl;
26 | scope.$digest();
27 | }
28 | else
29 | {
30 | scope.albumImageUrl = defaultAlbumImageUrl;
31 | }
32 | });
33 | }
34 | }
35 | };
36 |
37 | });
--------------------------------------------------------------------------------
/src/app/widgets/albumimage.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{album.name}}
4 |
--------------------------------------------------------------------------------
/src/app/widgets/artistimage.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.widgets')
2 | .directive('mopedArtistImage', function(util, lastfmservice) {
3 | var defaultArtistImageUrl = 'assets/images/noalbum.png';
4 | var defaultArtistImageSize = 'large';
5 |
6 | return {
7 | restrict: 'A',
8 | transclude: true,
9 | scope: {
10 | artistName: '=',
11 | imageSize: '@'
12 | },
13 | template: '
',
14 | link: function(scope, element, attrs) {
15 | scope.artistImageUrl = defaultArtistImageUrl;
16 | scope.artistImageSize = scope.imageSize || defaultArtistImageSize;
17 |
18 | // Get artist image
19 | lastfmservice.getArtistInfo(scope.artistName, function(err, artistInfo) {
20 | if (! err) {
21 | var img = _.find(artistInfo.artist.image, { size: scope.artistImageSize });
22 | if (img !== undefined) {
23 | scope.artistImageUrl = img['#text'];
24 | scope.$apply();
25 | }
26 | }
27 | });
28 | }
29 | };
30 |
31 | });
--------------------------------------------------------------------------------
/src/app/widgets/playlist.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.widgets')
2 | .directive('mopedPlaylist', function() {
3 | return {
4 | restrict: "E",
5 | scope: { playlist: '=' },
6 | templateUrl: 'widgets/playlist.tpl.html',
7 | replace:true
8 | };
9 | });
--------------------------------------------------------------------------------
/src/app/widgets/playlist.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{playlist.name}}
4 |
--------------------------------------------------------------------------------
/src/app/widgets/slider.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.widgets')
2 | .directive('mopedSlider', function(util) {
3 | return {
4 | restrict: 'A',
5 | scope: {
6 | sliderValue: '='
7 | },
8 | link: function(scope, element, attrs) {
9 | var $slider = $(element).slider(scope.$eval(attrs.mopedSlider));
10 |
11 | $slider.on('slideStart', function(ev) {
12 | scope.$emit('moped:slidervaluechanging', ev.value);
13 | });
14 |
15 | $slider.on('slideStop', function(ev) {
16 | scope.$emit('moped:slidervaluechanged', ev.value);
17 | });
18 |
19 | scope.$watch('sliderValue', function(val) {
20 | $slider.slider('setValue', val);
21 | });
22 | }
23 | };
24 | });
--------------------------------------------------------------------------------
/src/app/widgets/track-medium.tpl.html:
--------------------------------------------------------------------------------
1 |
2 | {{trackNo}}
3 | {{track.name}}
4 | {{trackDuration()}}
5 |
6 | {{artistsAsString()}}
7 | {{track.album.name}}
8 |
--------------------------------------------------------------------------------
/src/app/widgets/track-short.tpl.html:
--------------------------------------------------------------------------------
1 |
2 | {{trackNo}}
3 | {{track.name}}
4 | {{trackDuration()}}
5 |
--------------------------------------------------------------------------------
/src/app/widgets/track.js:
--------------------------------------------------------------------------------
1 | angular.module('moped.widgets')
2 | .directive('mopedTrack', function(util) {
3 | return {
4 | restrict: 'E',
5 | scope: {
6 | trackNo: '=',
7 | track: '='
8 | },
9 | replace: true,
10 | templateUrl: function(element, attrs) {
11 | var display = attrs.display || 'default';
12 | switch (display) {
13 | case 'short':
14 | return 'widgets/track-short.tpl.html';
15 | case 'medium':
16 | return 'widgets/track-medium.tpl.html';
17 | case 'default':
18 | return 'widgets/track.tpl.html';
19 | }
20 | },
21 | link: function(scope, element, attrs) {
22 | scope.artistsAsString = function() {
23 | return util.getTrackArtistsAsString(scope.track);
24 | };
25 | scope.trackDuration = function() {
26 | return util.getTrackDuration(scope.track);
27 | };
28 | scope.playTrack = function() {
29 | scope.$emit('moped:playtrackrequest', scope.track);
30 | return false;
31 | };
32 | scope.trackProvider = function() {
33 | var provider = scope.track.uri.split(':')[0];
34 | return (provider.charAt(0).toUpperCase() + provider.slice(1));
35 | };
36 |
37 | var cleanUpTrackPlaybackStarted = scope.$on('mopidy:event:trackPlaybackStarted', function(event, data) {
38 | scope.isPlaying = data.tl_track.track.uri === scope.track.uri;
39 | });
40 |
41 | var cleanUpTrackPlaybackPaused = scope.$on('mopidy:event:trackPlaybackPaused', function(event, data) {
42 | scope.isPlaying = data.tl_track.track.uri === scope.track.uri;
43 | });
44 |
45 | var cleanUpCurrentTrackRequested = scope.$on('moped:currenttrackrequested', function(event, track) {
46 | scope.isPlaying = track.uri === scope.track.uri;
47 | util.safeApply(scope);
48 | });
49 |
50 | scope.$on('$destroy', function() {
51 | cleanUpTrackPlaybackStarted();
52 | cleanUpTrackPlaybackPaused();
53 | cleanUpCurrentTrackRequested();
54 | });
55 | }
56 | };
57 | });
--------------------------------------------------------------------------------
/src/app/widgets/track.tpl.html:
--------------------------------------------------------------------------------
1 |
2 | {{trackNo}}
3 | {{track.name}}
4 |
5 |
6 | {{artistsAsString()}}
7 | {{trackDuration()}}
8 |
9 |
10 | {{track.album.name}}
11 | {{track.album.date}}
12 | {{trackProvider()}}
13 |
--------------------------------------------------------------------------------
/src/assets/images/ajax-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/ajax-loader.gif
--------------------------------------------------------------------------------
/src/assets/images/android-chrome-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/android-chrome-144x144.png
--------------------------------------------------------------------------------
/src/assets/images/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/assets/images/android-chrome-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/android-chrome-36x36.png
--------------------------------------------------------------------------------
/src/assets/images/android-chrome-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/android-chrome-48x48.png
--------------------------------------------------------------------------------
/src/assets/images/android-chrome-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/android-chrome-72x72.png
--------------------------------------------------------------------------------
/src/assets/images/android-chrome-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/android-chrome-96x96.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/src/assets/images/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/assets/images/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #bb3333
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/images/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/favicon-16x16.png
--------------------------------------------------------------------------------
/src/assets/images/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/favicon-32x32.png
--------------------------------------------------------------------------------
/src/assets/images/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/favicon-96x96.png
--------------------------------------------------------------------------------
/src/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/favicon.ico
--------------------------------------------------------------------------------
/src/assets/images/library-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/library-icon.png
--------------------------------------------------------------------------------
/src/assets/images/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "My app",
3 | "icons": [
4 | {
5 | "src": "\/assets\/images\/android-chrome-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "\/assets\/images\/android-chrome-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image\/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "\/assets\/images\/android-chrome-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image\/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "\/assets\/images\/android-chrome-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image\/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "\/assets\/images\/android-chrome-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image\/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "\/assets\/images\/android-chrome-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image\/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/src/assets/images/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/mstile-144x144.png
--------------------------------------------------------------------------------
/src/assets/images/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/mstile-150x150.png
--------------------------------------------------------------------------------
/src/assets/images/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/mstile-310x150.png
--------------------------------------------------------------------------------
/src/assets/images/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/mstile-310x310.png
--------------------------------------------------------------------------------
/src/assets/images/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/mstile-70x70.png
--------------------------------------------------------------------------------
/src/assets/images/noalbum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/noalbum.png
--------------------------------------------------------------------------------
/src/assets/images/playlists-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/playlists-icon.png
--------------------------------------------------------------------------------
/src/assets/images/radio-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/radio-icon.png
--------------------------------------------------------------------------------
/src/assets/images/search-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/search-icon.png
--------------------------------------------------------------------------------
/src/assets/images/settings-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/settings-icon.png
--------------------------------------------------------------------------------
/src/assets/images/touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/touch-icon.png
--------------------------------------------------------------------------------
/src/assets/images/vinyl-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martijnboland/moped/7755ecc98536a96b9437b009c79197ed648a80ec/src/assets/images/vinyl-icon.png
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | <% styles.forEach( function ( file ) { %>
20 |
21 | <% }); %>
22 |
23 |
24 |
25 |
26 |
27 |
36 |
37 |
38 |
39 |
56 |
57 |
62 |
63 |
66 |
67 |
70 |
71 |
72 |
73 | <% scripts.forEach( function ( file ) { %>
74 |
75 | <% }); %>
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/less/README.md:
--------------------------------------------------------------------------------
1 | # The `src/less` Directory
2 |
3 | This folder is actually fairly self-explanatory: it contains your LESS/CSS files to be compiled during the build.
4 | The only important thing to note is that *only* `main.less` will be processed during the build, meaning that all
5 | other stylesheets must be *imported* into that one.
6 |
7 | This should operate somewhat like the routing; the `main.less` file contains all of the site-wide styles, while
8 | any styles that are route-specific should be imported into here from LESS files kept alongside the JavaScript
9 | and HTML sources of that component. For example, the `home` section of the site has some custom styles, which
10 | are imported like so:
11 |
12 | ```css
13 | @import '../app/home/home.less';
14 | ```
15 |
16 | The same principal, though not demonstrated in the code, would also apply to reusable components. CSS or LESS
17 | files from external components would also be imported. If, for example, we had a Twitter feed directive with
18 | an accompanying template and style, we would similarly import it:
19 |
20 | ```css
21 | @import '../common/twitterFeed/twitterFeedDirective.less';
22 | ```
23 |
24 | Using this decentralized approach for all our code (JavaScript, HTML, and CSS) creates a framework where a
25 | component's directory can be dragged and dropped into *any other project* and it will "just work".
26 |
27 | I would like to eventually automate the importing during the build so that manually importing it here would no
28 | longer be required, but more thought must be put in to whether this is the best approach.
29 |
--------------------------------------------------------------------------------
/src/less/main.less:
--------------------------------------------------------------------------------
1 | /**
2 | * This is the main application stylesheet. It should include or import all
3 | * stylesheets used throughout the application as this is the only stylesheet in
4 | * the Grunt configuration that is automatically processed.
5 | */
6 |
7 |
8 | /**
9 | * First, we include the Twitter Bootstrap LESS files. Only the ones used in the
10 | * project should be imported as the rest are just wasting space.
11 | */
12 |
13 | // Mixins
14 | @import '../../vendor/bootstrap/less/mixins.less';
15 |
16 | // Reset
17 | @import '../../vendor/bootstrap/less/normalize.less';
18 | @import '../../vendor/bootstrap/less/print.less';
19 |
20 | // Core CSS
21 | @import "../../vendor/bootstrap/less/scaffolding.less";
22 | @import "../../vendor/bootstrap/less/type.less";
23 | @import "../../vendor/bootstrap/less/code.less";
24 | @import "../../vendor/bootstrap/less/grid.less";
25 | @import "../../vendor/bootstrap/less/tables.less";
26 | @import "../../vendor/bootstrap/less/forms.less";
27 | @import "../../vendor/bootstrap/less/buttons.less";
28 |
29 | // Components
30 | @import "../../vendor/bootstrap/less/component-animations.less";
31 | @import "../../vendor/bootstrap/less/glyphicons.less";
32 | @import "../../vendor/bootstrap/less/dropdowns.less";
33 | @import "../../vendor/bootstrap/less/button-groups.less";
34 | @import "../../vendor/bootstrap/less/input-groups.less";
35 | @import "../../vendor/bootstrap/less/navs.less";
36 | @import "../../vendor/bootstrap/less/navbar.less";
37 | @import "../../vendor/bootstrap/less/breadcrumbs.less";
38 | @import "../../vendor/bootstrap/less/pagination.less";
39 | @import "../../vendor/bootstrap/less/pager.less";
40 | @import "../../vendor/bootstrap/less/labels.less";
41 | @import "../../vendor/bootstrap/less/badges.less";
42 | @import "../../vendor/bootstrap/less/jumbotron.less";
43 | @import "../../vendor/bootstrap/less/thumbnails.less";
44 | @import "../../vendor/bootstrap/less/alerts.less";
45 | @import "../../vendor/bootstrap/less/progress-bars.less";
46 | @import "../../vendor/bootstrap/less/media.less";
47 | @import "../../vendor/bootstrap/less/list-group.less";
48 | @import "../../vendor/bootstrap/less/panels.less";
49 | @import "../../vendor/bootstrap/less/wells.less";
50 | @import "../../vendor/bootstrap/less/close.less";
51 |
52 | // Components w/ JavaScript
53 | @import "../../vendor/bootstrap/less/modals.less";
54 | @import "../../vendor/bootstrap/less/tooltip.less";
55 | @import "../../vendor/bootstrap/less/popovers.less";
56 | @import "../../vendor/bootstrap/less/carousel.less";
57 |
58 | // Utility classes
59 | @import "../../vendor/bootstrap/less/utilities.less";
60 | @import "../../vendor/bootstrap/less/responsive-utilities.less";
61 |
62 |
63 | /**
64 | * This is our main variables file. It in turn imports the `variables` file from
65 | * Twitter Bootstrap. We must include it last so we can overwrite any variable
66 | * definitions in our imported stylesheets.
67 | */
68 |
69 | @import 'variables.less';
70 |
71 | // Application
72 | @import 'moped.less';
73 |
--------------------------------------------------------------------------------
/src/less/moped.less:
--------------------------------------------------------------------------------
1 | /* Generic pane rules */
2 | body { margin: 0; }
3 | .pane-row, .pane-col { overflow: hidden; position: absolute; }
4 | .pane-row { left: 0; right: 0; }
5 | .pane-col { top: 0; bottom: 0; }
6 | .scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }
7 | .scroll-y { overflow-y: auto; -webkit-overflow-scrolling: touch; }
8 |
9 | /* Bootstrap overrides */
10 | a:hover,
11 | a:active {
12 | text-decoration: none;
13 | }
14 |
15 | .navbar-toggle {
16 | border: none;
17 | margin: 0;
18 | float: left;
19 | z-index: 100;
20 | border-radius: 0;
21 | }
22 | .navbar-toggle .icon-bar {
23 | background-color: #999;
24 | }
25 | .navbar-toggle.outtaway {
26 | background-color: #333;
27 | }
28 |
29 | /* Custom styles */
30 | body {
31 | font-family: 'Open Sans', Helvetica, Arial, sans-serif;
32 | }
33 |
34 | .menu {
35 | width: 240px;
36 | background-color: #333;
37 | color: #eee;
38 | z-index: 0;
39 | }
40 |
41 | .menu .panel {
42 | background-color: transparent;
43 | border-radius: 0;
44 | margin-bottom: 10px;
45 | }
46 |
47 | .menu .panel-heading {
48 | background-color: #333;
49 | color: #fff;
50 | text-indent: 26px;
51 | background-repeat: no-repeat;
52 | background-position: 8px 8px;
53 | }
54 |
55 | .menu a > .panel-heading:hover {
56 | background-color: #555;
57 | }
58 |
59 | .menu .panel-heading.search {
60 | background-image: url(../images/search-icon.png);
61 | }
62 |
63 | .menu .panel-heading.library {
64 | background-image: url(../images/library-icon.png);
65 | }
66 |
67 | .menu .panel-heading.playlists {
68 | background-image: url(../images/playlists-icon.png);
69 | }
70 |
71 | .menu .panel-heading.radio {
72 | background-image: url(../images/radio-icon.png);
73 | }
74 |
75 | .menu .panel-heading.settings {
76 | background-image: url(../images/settings-icon.png);
77 | }
78 |
79 | .menu .panel-body {
80 | background-color: #444;
81 | color: #fff;
82 | }
83 |
84 | .menu .list-group {
85 | background-color: #444;
86 | font-size: 90%;
87 | border: none;
88 | }
89 |
90 | .menu .list-group-item {
91 | color: #ccc;
92 | background-color: transparent;
93 | border: none;
94 | padding: 0;
95 | }
96 |
97 | .menu .list-group-item a:hover {
98 | background-color: #555;
99 | }
100 |
101 | .menu .glyphicon {
102 | color: #fff;
103 | }
104 |
105 | .menu a {
106 | display: block;
107 | padding: 10px 15px;
108 | color: #ccc;
109 | }
110 |
111 | .menu ul ul {
112 | list-style: none;
113 | padding-left: 16px;
114 | }
115 |
116 | .menu .btn-search {
117 | position: absolute;
118 | left: -9999px;
119 | width: 1px;
120 | height: 1px;
121 | }
122 |
123 | .main {
124 | left: 0;
125 | right: auto;
126 | width: 100%;
127 | background-color: #fff;
128 | -webkit-transform: translate3d(0, 0, 0);
129 | -moz-transform: translate3d(0, 0, 0);
130 | -ms-transform: translate3d(0, 0, 0);
131 | -o-transform: translate3d(0, 0, 0);
132 | transform: translate3d(0, 0, 0);
133 | -webkit-transition: all 0.25s ease-out;
134 | -moz-transition: all 0.25s ease-out;
135 | -ms-transition: all 0.25s ease-out;
136 | -o-transition: all 0.25s ease-out;
137 | transition: all 0.25s ease-out;
138 | }
139 |
140 | .main .container {
141 | margin-left: 0;
142 | margin-right: auto;
143 | }
144 |
145 | .title-bar {
146 | background-color: #eee;
147 | color: #666;
148 | line-height: 32px;
149 | padding: 0 10px;
150 | }
151 |
152 | .title-bar a {
153 | margin: 0 5px;
154 | }
155 |
156 | .title-bar .working {
157 | height: 24px;
158 | width: 50px;
159 | margin-top: 4px;
160 | background: url(../images/ajax-loader.gif) no-repeat
161 | }
162 |
163 | .main.outtaway {
164 | -webkit-transform: translate3d(240px, 0, 0);
165 | -moz-transform: translate3d(240px, 0, 0);
166 | -ms-transform: translate3d(240px, 0, 0);
167 | -o-transform: translate3d(240px, 0, 0);
168 | transform: translate3d(240px, 0, 0);
169 | }
170 |
171 | .maincontent {
172 | top: 32px;
173 | bottom: 186px;
174 | padding: 0 10px;
175 | }
176 |
177 | .view-title h3 {
178 | margin: 10px 0;
179 | }
180 |
181 | .view-title h4 {
182 | margin: 14px 0;
183 | }
184 |
185 | .view-title img {
186 | margin: 0 15px;
187 | }
188 |
189 | .view-title .title-date {
190 | margin: 16px 10px;
191 | }
192 |
193 | .albumimagelist,
194 | .artistimagelist {
195 | list-style: none;
196 | padding: 0;
197 | margin: 0;
198 | }
199 |
200 | .albumimagelist li,
201 | .artistimagelist li {
202 | list-style: none;
203 | float: left;
204 | width: 176px;
205 | height: 176px;
206 | overflow: hidden;
207 | font-size: 90%;
208 | position: relative;
209 | }
210 |
211 |
212 | .albumimagelist img {
213 | display: block;
214 | margin: 1px;
215 | }
216 |
217 | .artistimagelist img {
218 | display: block;
219 | width: 174px;
220 | margin: 1px;
221 | }
222 |
223 | .imgoverlay {
224 | position: absolute;
225 | bottom: 0;
226 | color: #fff;
227 | background: #000;
228 | background: rgba(0, 0, 0, 0.5);
229 | width: 174px;
230 | margin: 1px;
231 | padding: 3px;
232 | }
233 |
234 | .nowplaying {
235 | height: 136px;
236 | bottom: 50px;
237 | background-color: #333;
238 | color: #fff;
239 | }
240 |
241 | .nowplaying-thumb {
242 | float: left;
243 | height: 64px;
244 | width: 64px;
245 | margin: 2px 10px 2px -12px;
246 | }
247 |
248 | .nowplaying h5 {
249 | margin: 5px 0;
250 | }
251 |
252 | .nowplaying a {
253 | color: #fff;
254 | }
255 |
256 | .nowplaying .time {
257 | margin: 5px 0 6px;
258 | }
259 |
260 | .nowplaying .time-slider,
261 | .nowplaying .slider-horizontal {
262 | width: 100%;
263 | }
264 |
265 | .controls {
266 | height: 50px;
267 | bottom: 0;
268 | background-color: #933;
269 | }
270 |
271 | .controls ul {
272 | margin: 10px 3px;
273 | }
274 |
275 | .controls a {
276 | display: inline-block;
277 | }
278 |
279 | .controls a:active,
280 | .controls a.active {
281 | color: #333;
282 | }
283 |
284 | .controls .glyphicon {
285 | color: #fff;
286 | font-size: 20px;
287 | }
288 |
289 | .controls .volume-slider {
290 | width: 100px;
291 | }
292 |
293 | .controls .slider.slider-horizontal .slider-track {
294 | top: 30%;
295 | }
296 |
297 | .slider-selection {
298 | background: #6ce;
299 | }
300 |
301 | .albumcover {
302 | margin-bottom: 10px;
303 | }
304 |
305 | /* Responsive adaptations */
306 | @media screen and (min-width: 768px) {
307 | .main { left: 240px; right: 0; width: auto; }
308 | .maincontent { bottom: 118px; }
309 | .nowplaying { height: 68px;}
310 | }
311 |
312 | // Overrides
313 |
314 | // Small grid
315 | @media (min-width: @screen-sm-min) {
316 | .container {
317 | max-width: @container-sm;
318 | width: auto;
319 | }
320 | }
321 |
322 | // Medium grid
323 | @media (min-width: @screen-md-min) {
324 | .container {
325 | max-width: @container-md;
326 | width: auto;
327 | }
328 | }
329 |
330 | // Large grid
331 | @media (min-width: @screen-lg-min) {
332 | .container {
333 | max-width: @container-lg;
334 | width: auto;
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/src/less/variables.less:
--------------------------------------------------------------------------------
1 | /**
2 | * These are the variables used throughout the application. This is where
3 | * overwrites that are not specific to components should be maintained.
4 | */
5 |
6 | @import '../../vendor/bootstrap/less/variables.less';
7 |
8 |
9 | /**
10 | * Typography-related.
11 | */
12 |
13 | @sansFontFamily: 'Roboto', sans-serif;
14 |
15 |
--------------------------------------------------------------------------------
/vendor/bootstrap-slider/bootstrap-slider.js:
--------------------------------------------------------------------------------
1 | /* =========================================================
2 | * bootstrap-slider.js v2.0.0
3 | * http://www.eyecon.ro/bootstrap-slider
4 | * =========================================================
5 | * Copyright 2012 Stefan Petre
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================= */
19 |
20 | !function( $ ) {
21 |
22 | var Slider = function(element, options) {
23 | this.element = $(element);
24 | this.picker = $(''+
25 | '
'+
26 | '
'+
27 | '
'+
28 | '
'+
29 | '
'+
30 | '
'+
31 | '
')
32 | .insertBefore(this.element)
33 | .append(this.element);
34 | this.id = this.element.data('slider-id')||options.id;
35 | if (this.id) {
36 | this.picker[0].id = this.id;
37 | }
38 |
39 | if (typeof Modernizr !== 'undefined' && Modernizr.touch) {
40 | this.touchCapable = true;
41 | }
42 |
43 | var tooltip = this.element.data('slider-tooltip')||options.tooltip;
44 |
45 | this.tooltip = this.picker.find('.tooltip');
46 | this.tooltipInner = this.tooltip.find('div.tooltip-inner');
47 |
48 | this.orientation = this.element.data('slider-orientation')||options.orientation;
49 | switch(this.orientation) {
50 | case 'vertical':
51 | this.picker.addClass('slider-vertical');
52 | this.stylePos = 'top';
53 | this.mousePos = 'pageY';
54 | this.sizePos = 'offsetHeight';
55 | this.tooltip.addClass('right')[0].style.left = '100%';
56 | break;
57 | default:
58 | this.picker
59 | .addClass('slider-horizontal')
60 | .css('width', this.element.outerWidth());
61 | this.orientation = 'horizontal';
62 | this.stylePos = 'left';
63 | this.mousePos = 'pageX';
64 | this.sizePos = 'offsetWidth';
65 | this.tooltip.addClass('top')[0].style.top = -this.tooltip.outerHeight() - 14 + 'px';
66 | break;
67 | }
68 |
69 | this.min = this.element.data('slider-min')||options.min;
70 | this.max = this.element.data('slider-max')||options.max;
71 | this.step = this.element.data('slider-step')||options.step;
72 | this.value = this.element.data('slider-value')||options.value;
73 | if (this.value[1]) {
74 | this.range = true;
75 | }
76 |
77 | this.selection = this.element.data('slider-selection')||options.selection;
78 | this.selectionEl = this.picker.find('.slider-selection');
79 | if (this.selection === 'none') {
80 | this.selectionEl.addClass('hide');
81 | }
82 | this.selectionElStyle = this.selectionEl[0].style;
83 |
84 |
85 | this.handle1 = this.picker.find('.slider-handle:first');
86 | this.handle1Stype = this.handle1[0].style;
87 | this.handle2 = this.picker.find('.slider-handle:last');
88 | this.handle2Stype = this.handle2[0].style;
89 |
90 | var handle = this.element.data('slider-handle')||options.handle;
91 | switch(handle) {
92 | case 'round':
93 | this.handle1.addClass('round');
94 | this.handle2.addClass('round');
95 | break
96 | case 'triangle':
97 | this.handle1.addClass('triangle');
98 | this.handle2.addClass('triangle');
99 | break
100 | }
101 |
102 | if (this.range) {
103 | this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0]));
104 | this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1]));
105 | } else {
106 | this.value = [ Math.max(this.min, Math.min(this.max, this.value))];
107 | this.handle2.addClass('hide');
108 | if (this.selection == 'after') {
109 | this.value[1] = this.max;
110 | } else {
111 | this.value[1] = this.min;
112 | }
113 | }
114 | this.diff = this.max - this.min;
115 | this.percentage = [
116 | (this.value[0]-this.min)*100/this.diff,
117 | (this.value[1]-this.min)*100/this.diff,
118 | this.step*100/this.diff
119 | ];
120 |
121 | this.offset = this.picker.offset();
122 | this.size = this.picker[0][this.sizePos];
123 |
124 | this.formater = options.formater;
125 |
126 | this.layout();
127 |
128 | if (this.touchCapable) {
129 | // Touch: Bind touch events:
130 | this.picker.on({
131 | touchstart: $.proxy(this.mousedown, this)
132 | });
133 | } else {
134 | this.picker.on({
135 | mousedown: $.proxy(this.mousedown, this)
136 | });
137 | }
138 |
139 | if (tooltip === 'show') {
140 | this.picker.on({
141 | mouseenter: $.proxy(this.showTooltip, this),
142 | mouseleave: $.proxy(this.hideTooltip, this)
143 | });
144 | } else {
145 | this.tooltip.addClass('hide');
146 | }
147 | };
148 |
149 | Slider.prototype = {
150 | constructor: Slider,
151 |
152 | over: false,
153 | inDrag: false,
154 |
155 | showTooltip: function(){
156 | this.tooltip.addClass('in');
157 | //var left = Math.round(this.percent*this.width);
158 | //this.tooltip.css('left', left - this.tooltip.outerWidth()/2);
159 | this.over = true;
160 | },
161 |
162 | hideTooltip: function(){
163 | if (this.inDrag === false) {
164 | this.tooltip.removeClass('in');
165 | }
166 | this.over = false;
167 | },
168 |
169 | layout: function(){
170 | this.handle1Stype[this.stylePos] = this.percentage[0]+'%';
171 | this.handle2Stype[this.stylePos] = this.percentage[1]+'%';
172 | if (this.orientation == 'vertical') {
173 | this.selectionElStyle.top = Math.min(this.percentage[0], this.percentage[1]) +'%';
174 | this.selectionElStyle.height = Math.abs(this.percentage[0] - this.percentage[1]) +'%';
175 | } else {
176 | this.selectionElStyle.left = Math.min(this.percentage[0], this.percentage[1]) +'%';
177 | this.selectionElStyle.width = Math.abs(this.percentage[0] - this.percentage[1]) +'%';
178 | }
179 | if (this.range) {
180 | this.tooltipInner.text(
181 | this.formater(this.value[0]) +
182 | ' : ' +
183 | this.formater(this.value[1])
184 | );
185 | this.tooltip[0].style[this.stylePos] = this.size * (this.percentage[0] + (this.percentage[1] - this.percentage[0])/2)/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px';
186 | } else {
187 | this.tooltipInner.text(
188 | this.formater(this.value[0])
189 | );
190 | this.tooltip[0].style[this.stylePos] = this.size * this.percentage[0]/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px';
191 | }
192 | },
193 |
194 | mousedown: function(ev) {
195 |
196 | // Touch: Get the original event:
197 | if (this.touchCapable && ev.type === 'touchstart') {
198 | ev = ev.originalEvent;
199 | }
200 |
201 | this.offset = this.picker.offset();
202 | this.size = this.picker[0][this.sizePos];
203 |
204 | var percentage = this.getPercentage(ev);
205 |
206 | if (this.range) {
207 | var diff1 = Math.abs(this.percentage[0] - percentage);
208 | var diff2 = Math.abs(this.percentage[1] - percentage);
209 | this.dragged = (diff1 < diff2) ? 0 : 1;
210 | } else {
211 | this.dragged = 0;
212 | }
213 |
214 | this.percentage[this.dragged] = percentage;
215 | this.layout();
216 |
217 | if (this.touchCapable) {
218 | // Touch: Bind touch events:
219 | $(document).on({
220 | touchmove: $.proxy(this.mousemove, this),
221 | touchend: $.proxy(this.mouseup, this)
222 | });
223 | } else {
224 | $(document).on({
225 | mousemove: $.proxy(this.mousemove, this),
226 | mouseup: $.proxy(this.mouseup, this)
227 | });
228 | }
229 |
230 | this.inDrag = true;
231 | var val = this.calculateValue();
232 | this.element.trigger({
233 | type: 'slideStart',
234 | value: val
235 | }).trigger({
236 | type: 'slide',
237 | value: val
238 | });
239 | return false;
240 | },
241 |
242 | mousemove: function(ev) {
243 |
244 | // Touch: Get the original event:
245 | if (this.touchCapable && ev.type === 'touchmove') {
246 | ev = ev.originalEvent;
247 | }
248 |
249 | var percentage = this.getPercentage(ev);
250 | if (this.range) {
251 | if (this.dragged === 0 && this.percentage[1] < percentage) {
252 | this.percentage[0] = this.percentage[1];
253 | this.dragged = 1;
254 | } else if (this.dragged === 1 && this.percentage[0] > percentage) {
255 | this.percentage[1] = this.percentage[0];
256 | this.dragged = 0;
257 | }
258 | }
259 | this.percentage[this.dragged] = percentage;
260 | this.layout();
261 | var val = this.calculateValue();
262 | this.element
263 | .trigger({
264 | type: 'slide',
265 | value: val
266 | })
267 | .data('value', val)
268 | .prop('value', val);
269 | return false;
270 | },
271 |
272 | mouseup: function(ev) {
273 | if (this.touchCapable) {
274 | // Touch: Bind touch events:
275 | $(document).off({
276 | touchmove: this.mousemove,
277 | touchend: this.mouseup
278 | });
279 | } else {
280 | $(document).off({
281 | mousemove: this.mousemove,
282 | mouseup: this.mouseup
283 | });
284 | }
285 |
286 | this.inDrag = false;
287 | if (this.over == false) {
288 | this.hideTooltip();
289 | }
290 | this.element;
291 | var val = this.calculateValue();
292 | this.element
293 | .trigger({
294 | type: 'slideStop',
295 | value: val
296 | })
297 | .data('value', val)
298 | .prop('value', val);
299 | return false;
300 | },
301 |
302 | calculateValue: function() {
303 | var val;
304 | if (this.range) {
305 | val = [
306 | (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step),
307 | (this.min + Math.round((this.diff * this.percentage[1]/100)/this.step)*this.step)
308 | ];
309 | this.value = val;
310 | } else {
311 | val = (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step);
312 | this.value = [val, this.value[1]];
313 | }
314 | return val;
315 | },
316 |
317 | getPercentage: function(ev) {
318 | if (this.touchCapable) {
319 | ev = ev.touches[0];
320 | }
321 | var percentage = (ev[this.mousePos] - this.offset[this.stylePos])*100/this.size;
322 | percentage = Math.round(percentage/this.percentage[2])*this.percentage[2];
323 | return Math.max(0, Math.min(100, percentage));
324 | },
325 |
326 | getValue: function() {
327 | if (this.range) {
328 | return this.value;
329 | }
330 | return this.value[0];
331 | },
332 |
333 | setValue: function(val) {
334 | this.value = val;
335 |
336 | if (this.range) {
337 | this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0]));
338 | this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1]));
339 | } else {
340 | this.value = [ Math.max(this.min, Math.min(this.max, this.value))];
341 | this.handle2.addClass('hide');
342 | if (this.selection == 'after') {
343 | this.value[1] = this.max;
344 | } else {
345 | this.value[1] = this.min;
346 | }
347 | }
348 | this.diff = this.max - this.min;
349 | this.percentage = [
350 | (this.value[0]-this.min)*100/this.diff,
351 | (this.value[1]-this.min)*100/this.diff,
352 | this.step*100/this.diff
353 | ];
354 | this.layout();
355 | }
356 | };
357 |
358 | $.fn.slider = function ( option, val ) {
359 | return this.each(function () {
360 | var $this = $(this),
361 | data = $this.data('slider'),
362 | options = typeof option === 'object' && option;
363 | if (!data) {
364 | $this.data('slider', (data = new Slider(this, $.extend({}, $.fn.slider.defaults,options))));
365 | }
366 | if (typeof option == 'string') {
367 | data[option](val);
368 | }
369 | })
370 | };
371 |
372 | $.fn.slider.defaults = {
373 | min: 0,
374 | max: 10,
375 | step: 1,
376 | orientation: 'horizontal',
377 | value: 5,
378 | selection: 'before',
379 | tooltip: 'show',
380 | handle: 'round',
381 | formater: function(value) {
382 | return value;
383 | }
384 | };
385 |
386 | $.fn.slider.Constructor = Slider;
387 |
388 | }( window.jQuery );
--------------------------------------------------------------------------------
/vendor/bootstrap-slider/slider.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Slider for Bootstrap
3 | *
4 | * Copyright 2012 Stefan Petre
5 | * Licensed under the Apache License v2.0
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | */
9 | .slider {
10 | display: inline-block;
11 | vertical-align: middle;
12 | position: relative;
13 | }
14 | .slider.slider-horizontal {
15 | width: 210px;
16 | height: 20px;
17 | }
18 | .slider.slider-horizontal .slider-track {
19 | height: 10px;
20 | width: 100%;
21 | margin-top: -5px;
22 | top: 50%;
23 | left: 0;
24 | }
25 | .slider.slider-horizontal .slider-selection {
26 | height: 100%;
27 | top: 0;
28 | bottom: 0;
29 | }
30 | .slider.slider-horizontal .slider-handle {
31 | margin-left: -10px;
32 | margin-top: -5px;
33 | }
34 | .slider.slider-horizontal .slider-handle.triangle {
35 | border-width: 0 10px 10px 10px;
36 | width: 0;
37 | height: 0;
38 | border-bottom-color: #0480be;
39 | margin-top: 0;
40 | }
41 | .slider.slider-vertical {
42 | height: 210px;
43 | width: 20px;
44 | }
45 | .slider.slider-vertical .slider-track {
46 | width: 10px;
47 | height: 100%;
48 | margin-left: -5px;
49 | left: 50%;
50 | top: 0;
51 | }
52 | .slider.slider-vertical .slider-selection {
53 | width: 100%;
54 | left: 0;
55 | top: 0;
56 | bottom: 0;
57 | }
58 | .slider.slider-vertical .slider-handle {
59 | margin-left: -5px;
60 | margin-top: -10px;
61 | }
62 | .slider.slider-vertical .slider-handle.triangle {
63 | border-width: 10px 0 10px 10px;
64 | width: 1px;
65 | height: 1px;
66 | border-left-color: #0480be;
67 | margin-left: 0;
68 | }
69 | .slider input {
70 | display: none;
71 | }
72 | .slider .tooltip-inner {
73 | white-space: nowrap;
74 | }
75 | .slider-track {
76 | position: absolute;
77 | cursor: pointer;
78 | background-color: #f7f7f7;
79 | background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9);
80 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));
81 | background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9);
82 | background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9);
83 | background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
84 | background-repeat: repeat-x;
85 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
86 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
87 | -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
88 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
89 | -webkit-border-radius: 4px;
90 | -moz-border-radius: 4px;
91 | border-radius: 4px;
92 | }
93 | .slider-selection {
94 | position: absolute;
95 | background-color: #f7f7f7;
96 | background-image: -moz-linear-gradient(top, #f9f9f9, #f5f5f5);
97 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f9f9f9), to(#f5f5f5));
98 | background-image: -webkit-linear-gradient(top, #f9f9f9, #f5f5f5);
99 | background-image: -o-linear-gradient(top, #f9f9f9, #f5f5f5);
100 | background-image: linear-gradient(to bottom, #f9f9f9, #f5f5f5);
101 | background-repeat: repeat-x;
102 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
103 | -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
104 | -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
105 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
106 | -webkit-box-sizing: border-box;
107 | -moz-box-sizing: border-box;
108 | box-sizing: border-box;
109 | -webkit-border-radius: 4px;
110 | -moz-border-radius: 4px;
111 | border-radius: 4px;
112 | }
113 | .slider-handle {
114 | position: absolute;
115 | width: 20px;
116 | height: 20px;
117 | background-color: #0e90d2;
118 | background-image: -moz-linear-gradient(top, #149bdf, #0480be);
119 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));
120 | background-image: -webkit-linear-gradient(top, #149bdf, #0480be);
121 | background-image: -o-linear-gradient(top, #149bdf, #0480be);
122 | background-image: linear-gradient(to bottom, #149bdf, #0480be);
123 | background-repeat: repeat-x;
124 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
125 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
126 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
127 | box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
128 | opacity: 0.8;
129 | border: 0px solid transparent;
130 | }
131 | .slider-handle.round {
132 | -webkit-border-radius: 20px;
133 | -moz-border-radius: 20px;
134 | border-radius: 20px;
135 | }
136 | .slider-handle.triangle {
137 | background: transparent none;
138 | }
--------------------------------------------------------------------------------