├── .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 |
40 | 46 |
47 |
48 | Status: {{connectionState}} 49 |
50 |
51 |
52 | < Back 53 | Home 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 |
68 |
69 |
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 |
40 | 46 |
47 |
48 | Status: {{connectionState}} 49 |
50 |
51 |
52 | < Back 53 | Home 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 |
68 |
69 |
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 |
    28 |
  • 29 |
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 |
10 | 11 |
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 |
2 |
Browse
3 |
4 | 9 |
10 |
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 |
3 | 4 |
{{currentTrack}}
5 |
6 | {{artist.name}} 7 |
8 |
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 |
    2 |
  • 3 |
  • 4 |
  • 5 |
  • 6 |
  • 7 | 8 |
  • 9 |
  • 10 | 11 |
  • 12 |
  • 13 |
-------------------------------------------------------------------------------- /src/app/playlists/list.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |

{{playlist.name}}

3 |
4 |
5 | 6 |
-------------------------------------------------------------------------------- /src/app/playlists/menu.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |
Playlists
3 |
4 | 10 |
11 |
-------------------------------------------------------------------------------- /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 |
2 |
Radio
3 |
4 | 7 |
8 |
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 |
3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 |
15 | 16 | 19 | 21 |
22 |
23 |
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 |

4 |
5 |

Artists

6 | 13 |
14 |
15 |
16 |

Albums

17 |
    18 |
  • 19 |
  • 20 |
21 |
22 |
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 |
2 | 3 |
4 |
5 |
6 | 7 | 8 |
9 |
10 |
11 |
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 |
2 |

Settings

3 | 4 |
5 |
6 | 7 | Leave empty for default, example url: ws://hostname:6680/mopidy/ws/ 8 |
9 |
10 | 11 |
12 |
13 | 14 |
-------------------------------------------------------------------------------- /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 |

4 | {{album.name}} 5 | {{album.name}} 6 |

7 |
{{album.date}}
8 |

9 | {{artist.name}} 10 |

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 | 3 |

{{track.name}}

4 |
{{trackDuration()}}
5 |
6 |
{{artistsAsString()}}
7 | 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 | 11 | 12 | 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 |
40 | 46 |
47 |
48 | Status: {{connectionState}} 49 |
50 |
51 |
52 | < Back 53 | Home 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 |
68 |
69 |
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 | } --------------------------------------------------------------------------------