diff --git a/.csslintrc b/.csslintrc
new file mode 100644
index 0000000..a271b01
--- /dev/null
+++ b/.csslintrc
@@ -0,0 +1 @@
+--format=compact
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..5b838e3
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,12 @@
+{
+ "extends": "standard",
+ "env": {
+ "jquery": true
+ },
+ "rules": {
+ "indent": [2, 4, {"SwitchCase": 1}],
+ "no-undef": 0, // TODO: Set this to '2' once Javascript has been modularised.
+ "no-unused-vars": 0, // TODO: Set this to '2' once Javascript has been modularised.
+ "camelcase": 1,
+ }
+}
diff --git a/.gitignore b/.gitignore
index 1b83512..8c6a739 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,12 +8,16 @@
MANIFEST
build/
cover/
+.karma_coverage/
coverage.xml
dist/
docs/_build/
mopidy.log*
node_modules/
nosetests.xml
+npm-debug.log
.project
*.pbxproj
*.egg-info
+.cache
+.eggs
diff --git a/.travis.yml b/.travis.yml
index 637d177..ec29c38 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,29 @@
-sudo: false
+sudo: required
+dist: trusty
language: python
python:
- "2.7_with_system_site_packages"
+addons:
+ apt:
+ sources:
+ - mopidy-stable
+ packages:
+ - mopidy
+
env:
- - TOX_ENV=py27
+ # Need to run 'py27' and 'test' together so that we can share coverage reports.
+ - TOX_ENV=py27,test
- TOX_ENV=flake8
+ - TOX_ENV=eslint
+ - TOX_ENV=csslint
+ - TOX_ENV=tidy
+
+before_install:
+ - "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround https://github.com/tornadoweb/tornado/issues/1573
+ - "sudo apt-get update -qq"
install:
- "pip install tox"
@@ -16,4 +32,5 @@ script:
- "tox -e $TOX_ENV"
after_success:
- - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi"
+ # We just use 'coveralls-lcov' to convert lcov.info to JSON format.
+ - "if [ $TOX_ENV == 'py27,test' ]; then gem install coveralls-lcov; coveralls-lcov -v -n .karma_coverage/lcov.info > .karma_coverage/lcov.json; pip install coveralls; coveralls --merge=.karma_coverage/lcov.json; fi"
diff --git a/README.rst b/README.rst
index 1dc0007..a66f594 100644
--- a/README.rst
+++ b/README.rst
@@ -10,27 +10,52 @@ Mopidy-MusicBox-Webclient
:target: https://pypi.python.org/pypi/Mopidy-MusicBox-Webclient/
:alt: Number of PyPI downloads
-With Mopidy MusicBox Webclient, you can play your music on your computer (`Rapsberry Pi `_)
-and remotely control it using your computer, tablet or phone.
+.. image:: https://img.shields.io/travis/pimusicbox/mopidy-musicbox-webclient/develop.svg?style=flat
+ :target: https://travis-ci.org/pimusicbox/mopidy-musicbox-webclient
+ :alt: Travis CI build status
-This is a responsive webclient especially written for Mopidy, a music server. Responsive, so it works on desktop and
-mobile browsers. You can browse, search and play albums, artists, playlists, and it has cover art from Last.fm.
+.. image:: https://img.shields.io/coveralls/pimusicbox/mopidy-musicbox-webclient/develop.svg?style=flat
+ :target: https://coveralls.io/r/pimusicbox/mopidy-musicbox-webclient?branch=develop
+ :alt: Test coverage
-`Mopidy `_ is a music server which can play music from Spotify, Google Music, SoundCloud, etc.
-or from your hard drive.
+.. image:: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat
+ :target: http://standardjs.com/
+ :alt: JavaScript Standard Style
-If you want to run Mopidy with this webclient on a Raspberry Pi, do yourself a favor and use my custom built SD-image:
-`Pi MusicBox `_.
+Mopidy MusicBox Webclient (MMW) is a frontend extension and JavaScript-based web client especially written for
+`Mopidy `_.
-.. image:: https://github.com/pimusicbox/mopidy-musicbox-webclient/raw/master/screenshots/playlists_desktop.png
+Features
+========
+- Responsive design that works equally well on desktop and mobile browsers.
+- Browse content provided by any Mopidy backend extension.
+- Add one or more tracks or entire albums to the queue.
+- Save the current queue to an easily accessible playlist.
+- Search for tracks, albums, or artists from specific backends or all of Mopidy.
+- Shows detailed track and album information during playback, with album cover retrieval from Last.fm.
+- Seek tracks during playback.
+- Support for all of the Mopidy playback controls (consume mode, repeat, shuffle, etc.)
+- Deep integration with, and additional features for, the `Pi MusicBox `_.
+- Fullscreen mode.
+
+.. image:: https://github.com/pimusicbox/mopidy-musicbox-webclient/raw/develop/screenshots/queue_desktop.png
+
+Dependencies
+============
+
+- MMW has been tested on the major browsers (Chrome, IE, Firefox, Safari, iOS). It *may* also work on other browsers
+ that support websockets, cookies, and JavaScript.
+
+- ``Mopidy`` >= 1.1.0. An extensible music server that plays music from local disk, Spotify, SoundCloud, Google
+ Play Music, and more.
Installation
============
Install by running::
- pip install Mopidy-MusicBox-Webclient
+ pip install mopidy-musicbox-webclient
Alternatively, clone the repository and run ``sudo python setup.py install`` from within the project directory. e.g. ::
@@ -40,11 +65,38 @@ Alternatively, clone the repository and run ``sudo python setup.py install`` fro
$ sudo python setup.py install
+Configuration
+=============
+
+MMW is shipped with default settings that should work straight out of the box for most users::
+
+ [musicbox_webclient]
+ enabled = true
+ musicbox = false
+ websocket_host =
+ websocket_port =
+ on_track_click = PLAY_ALL
+
+The following configuration values are available should you wish to customize your installation further:
+
+- ``musicbox_webclient/enabled``: If the MMW extension should be enabled or not. Defaults to ``true``.
+
+- ``musicbox_webclient/musicbox``: Set this to ``true`` if you are connecting to a Mopidy instance running on a
+ Pi Musicbox. Expands the MMW user interface to include features for rebooting the Pi, changing configuration values
+ via a web interface, etc.
+
+- ``musicbox_webclient/websocket_host``: Optional setting that can be used to specify the target host for Mopidy websocket connections.
+
+- ``musicbox_webclient/websocket_port``: Optional setting that can be used to specify the target port for Mopidy websocket connections.
+
+- ``musicbox_webclient/on_track_click``: Specifies the default action that should be performed when the user clicks on
+ a track that is displayed in the Browse pane or as part of Search results. Valid options are: ``PLAY_NOW``,
+ ``PLAY_NEXT``, ``ADD_THIS_BOTTOM``, ``ADD_ALL_BOTTOM``, ``PLAY_ALL`` (default), and ``DYNAMIC`` (repeats last action).
+
Usage
=====
-Point your (modern) browser at Mopidy-MusicBox-Webclient running on your Mopidy server e.g.
-http://localhost:6680/musicbox_webclient.
+Enter the address of the Mopidy server that you are connecting to in your browser (e.g. http://localhost:6680/musicbox_webclient)
Project resources
@@ -58,6 +110,44 @@ Project resources
Changelog
=========
+v2.3.0 (UNRELEASED)
+-------------------
+
+- Enhance build workflow to include style checks and syntax validation for HTML, CSS, and Javascript.
+- Now displays album and artist info when browsing tracks. (Addresses: `#99 `_).
+- Now remembers which backend was searched previously, and automatically selects that backend as the default search target.
+ (Addresses: `#130 `_).
+- Upgrade Media Progress Timer to version 3.0.0.
+- Now retrieves album cover and artist images using MusicBrainzID, if available.
+- New configuration parameter ``on_track_click`` can be used to customize the action that is performed when the
+ user clicks on a track in a list. Valid options are: ``PLAY_NOW``, ``PLAY_NEXT``, ``ADD_THIS_BOTTOM``,
+ ``ADD_ALL_BOTTOM``, ``PLAY_ALL`` (default), and ``DYNAMIC`` (repeats last action).
+ (Addresses: `#133 `_).
+- Optimized updating of 'now playing' icons in tracklists.
+ (Addresses: `#184 `_).
+- Optimized rendering of large lists of tracks to make UI more responsive.
+- Added 'Folder' FontAwesome icon on the Browse pane for browsing the filesystem.
+- New icons for 'PLAY' and 'PLAY_ALL' actions. In general, icons with an empty background will perform an action only
+ on the selected track, while icons with a filled background will apply the action to all tracks in the list.
+- Standardize popup dialog layout convention: Sentence fragments have no punctuation, buttons that confirm a
+ destructive action go on the left.
+
+**Fixes**
+
+- Don't create Mopidy models manually. (Fixes: `#172 `_).
+- Context menu is now available for all tracks in browse pane. (Fixes: `#126 `_).
+- last.fm artist image lookups should now always return the correct image for similarly named artists.
+- Ensure that browsed tracks are always added to the queue using the track URI rather than the track's position in the folder.
+ (Fixes: `#124 `_).
+- Fixed an issue where searches would be performed as soon as the user switches to the 'Search' pane,
+ instead of waiting for the 'Search!' button to be clicked.
+- Fixed an issue where the last track in an album was not grouped properly with the rest of the results, and would have
+ a small divider rendered above it. (Fixes: `#196 `_).
+- Replaced JavaScript confirmation prompt on 'Streams' pane with jQuery equivalent.
+ (Fixes: `#191 `_).
+- Clearing the queue should no longer trigger an album cover image lookup.
+ (Fixes: `#201 `_).
+
v2.2.0 (2016-03-01)
-------------------
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..7f456f9
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,80 @@
+// Karma configuration
+
+module.exports = function (config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['browserify', 'mocha'],
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'mopidy_musicbox_webclient/static/vendors/**/*.js',
+ 'mopidy_musicbox_webclient/static/js/**/*.js',
+ 'tests/**/test_*.js'
+ ],
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ 'tests/**/test_*.js': [ 'browserify' ],
+ 'mopidy_musicbox_webclient/static/js/**/*.js': ['coverage']
+ },
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['progress', 'coverage'],
+
+ // web server port
+ port: 9876,
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['PhantomJS'],
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: true,
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: Infinity,
+
+ // add additional browserify configuration properties here
+ // such as transform and/or debug=true to generate source maps
+ browserify: {
+ debug: true,
+ transform: [
+ 'babelify',
+ ['browserify-istanbul', { instrumenter: require('isparta') }]
+ ]
+ },
+
+ coverageReporter: {
+ // specify a common output directory
+ dir: '.karma_coverage/',
+ reporters: [
+ { type: 'lcov', subdir: '.' },
+ { type: 'text' }
+ ]
+ }
+ })
+}
diff --git a/mopidy_musicbox_webclient/__init__.py b/mopidy_musicbox_webclient/__init__.py
index 1884794..2bc628e 100644
--- a/mopidy_musicbox_webclient/__init__.py
+++ b/mopidy_musicbox_webclient/__init__.py
@@ -7,7 +7,7 @@ from mopidy import config, ext
__version__ = '2.2.0'
-class MusicBoxExtension(ext.Extension):
+class Extension(ext.Extension):
dist_name = 'Mopidy-MusicBox-Webclient'
ext_name = 'musicbox_webclient'
@@ -18,10 +18,17 @@ class MusicBoxExtension(ext.Extension):
return config.read(conf_file)
def get_config_schema(self):
- schema = super(MusicBoxExtension, self).get_config_schema()
+ schema = super(Extension, self).get_config_schema()
schema['musicbox'] = config.Boolean(optional=True)
schema['websocket_host'] = config.Hostname(optional=True)
schema['websocket_port'] = config.Port(optional=True)
+ schema['on_track_click'] = config.String(optional=True,
+ choices=['PLAY_NOW',
+ 'PLAY_NEXT',
+ 'ADD_THIS_BOTTOM',
+ 'ADD_ALL_BOTTOM',
+ 'PLAY_ALL',
+ 'DYNAMIC'])
return schema
def setup(self, registry):
diff --git a/mopidy_musicbox_webclient/ext.conf b/mopidy_musicbox_webclient/ext.conf
index fa7898e..f5d8a17 100644
--- a/mopidy_musicbox_webclient/ext.conf
+++ b/mopidy_musicbox_webclient/ext.conf
@@ -3,3 +3,4 @@ enabled = true
musicbox = false
websocket_host =
websocket_port =
+on_track_click = PLAY_ALL
diff --git a/mopidy_musicbox_webclient/static/css/webclient.css b/mopidy_musicbox_webclient/static/css/webclient.css
index 68ece16..48d4e5a 100644
--- a/mopidy_musicbox_webclient/static/css/webclient.css
+++ b/mopidy_musicbox_webclient/static/css/webclient.css
@@ -36,7 +36,7 @@
margin-left: 10px;
}
- #playlisttracksback {
+ .backnav-optional {
display: none;
}
@@ -74,7 +74,7 @@
width: 100%;
}
- #playlisttracksback {
+ .backnav-optional {
display: block;
}
@@ -244,8 +244,8 @@
}
.smalldivider {
- font-size: 25% !important;
- height: 5px !important;
+ font-size: 10%;
+ height: 2px;
background-color: #ddd !important;
}
@@ -293,13 +293,22 @@
margin-left: 20px;
}
-.song .moreBtn{
+.song .moreBtn {
float: right;
padding: 15px 18px 12px 22px;
display: inline-block;
line-height: 100%;
}
+.moreBtn i {
+ color: #ddd;
+ font-size: initial;
+}
+
+.backnav {
+ background-color: #ccc !important;
+}
+
/**********************
* Now Playing area *
@@ -338,6 +347,11 @@
text-decoration: none;
}
+.popupArtistLi,
+.popupAlbumLi {
+ display: none
+}
+
.popupArtistName,
.popupTrackName,
.popupAlbumName,
@@ -357,7 +371,7 @@
margin-top: 10px;
}
-#controlspopupimage,
+#albumCoverImg,
#coverpopupimage,
#artistpopupimage {
display: block;
@@ -368,18 +382,61 @@
max-height: 90%;
}
-#popupTracksLv li,
-#popupQueueLv li {
- border-bottom: 1px solid #aaa;
+/* Override to make buttons more visible in popups.*/
+#popupTracks .ui-btn-up-c,
+#popupQueue .ui-btn-up-c {
+ background: white;
}
-#popupTracksLv,
-#popupQueueLv {
- border: 1px solid #aaa;
+/* Custom icons for popup listviews - see http://demos.jquerymobile.com/1.3.2/#CustomIcons */
+.ui-icon-playAll:after,
+.ui-icon-play:after,
+.ui-icon-playNext:after,
+.ui-icon-add:after,
+.ui-icon-addAll:after,
+.ui-icon-remove:after {
+ color: #34495e;
+ font-family: 'FontAwesome';
+}
+
+.ui-icon-playAll:after {
+ content: '\f144';
+}
+
+.ui-icon-play:after {
+ content: '\f01d';
+}
+
+.ui-icon-playNext:after {
+ content: '\f149';
+}
+
+.ui-icon-add:after {
+ content: '\f196';
+}
+
+.ui-icon-addAll:after {
+ content: '\f0fe';
+}
+
+.ui-icon-remove:after {
+ content: '\f00d';
+}
+
+.ui-icon-playAll,
+.ui-icon-play,
+.ui-icon-playNext,
+.ui-icon-add,
+.ui-icon-addAll,
+.ui-icon-remove {
+ background-color: unset;
+ background-image: none;
+ font-weight: normal;
}
.popupDialog {
padding: 10px;
+ text-align: center;
}
/*dont hide clear buttons in text input */
@@ -517,7 +574,7 @@ a {
overflow: hidden;
}
- #controlspopupimage {
+ #albumCoverImg {
max-width: 90%;
max-height: 90%;
margin-bottom: 3px;
diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html
index 22a9158..db57490 100644
--- a/mopidy_musicbox_webclient/static/index.html
+++ b/mopidy_musicbox_webclient/static/index.html
@@ -1,30 +1,14 @@
- Musicbox
+ {{ title }}
-
-
-
+
+
+
+
+
@@ -52,11 +36,10 @@
-
+
Search for artists, albums, or specific tracks.
-
+
-
@@ -454,18 +421,18 @@
Play a specific stream/track and optionally save it to your favourites.
-
+
Get currently playing
-
+ onkeypress="return controls.streamPressed(event.keyCode);" type="text"/>
+
Play
-
+ onkeypress="return controls.streamPressed(event.keyCode);" type="text"/>
+
Save
@@ -477,7 +444,6 @@
-
-
+
+
-
-
-
-
+
diff --git a/mopidy_musicbox_webclient/static/js/controls.js b/mopidy_musicbox_webclient/static/js/controls.js
index 2638886..41d5471 100644
--- a/mopidy_musicbox_webclient/static/js/controls.js
+++ b/mopidy_musicbox_webclient/static/js/controls.js
@@ -1,665 +1,658 @@
-/***********************************
- * play tracks from a browse list *
- ***********************************/
-function playBrowsedTracks(action, trackIndex) {
- $('#popupBrowse').popup('close');
- toast('Loading...');
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory)
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = factory()
+ } else {
+ root.controls = factory()
+ }
+}(this, function () {
+ 'use strict'
- if (typeof trackIndex === 'undefined') {
- trackIndex = $('#popupBrowse').data("tlid");
- }
- if (action == PLAY_ALL) {
- mopidy.tracklist.clear();
- // Default for radio streams is to just add the selected URI.
- if (isStreamUri(browseTracks[trackIndex].uri)) {
- action = PLAY_NOW;
- }
- }
- var trackUris = [];
- switch (action) {
- case PLAY_NOW:
- case PLAY_NEXT:
- case ADD_THIS_BOTTOM:
- trackUris.push(browseTracks[trackIndex].uri);
- break;
- case PLAY_ALL:
- case ADD_ALL_BOTTOM:
- trackUris = getUris(browseTracks);
- break;
- default:
- break;
- }
- var maybePlay = function(tlTracks) {
- if (action === PLAY_NOW || action === PLAY_ALL) {
- var playIndex = (action === PLAY_ALL) ? trackIndex : 0;
- mopidy.playback.play({'tl_track': tlTracks[playIndex]});
- }
- };
-
- switch (action) {
- case PLAY_NOW:
- case PLAY_NEXT:
- mopidy.tracklist.index().then(function (currentIndex) {
- mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris}).then(maybePlay);
- });
- break;
- case ADD_THIS_BOTTOM:
- case ADD_ALL_BOTTOM:
- case PLAY_ALL:
- mopidy.tracklist.add({'uris': trackUris}).then(maybePlay);
- break;
- default:
- break;
- }
- return false;
-}
+ var controls = {
-/*********************************
- * play an uri from a tracklist *
- *********************************/
-function playTrack(action) {
- var hash = document.location.hash.split('?');
- var divid = hash[0].substr(1);
+ /**
+ * 'onClick' handler for tracks that are rendered in a list.
+ *
+ * Adds tracks to current tracklist and starts playback if necessary.
+ *
+ * @param {string} action - The action to perform. Valid actions are:
+ * PLAY_NOW: add the track at 'trackIndex' and start playback.
+ * PLAY_NEXT: insert track after currently playing track.
+ * ADD_THIS_BOTTOM: add track to bottom of tracklist.
+ * ADD_ALL_BOTTOM: add all tracks in in the list to bottom of
+ * tracklist.
+ * PLAY_ALL: clear tracklist and start playback of the track
+ * with URI provided in 'trackUri'.
+ * @param {object} mopidy - The Mopidy.js object that should be used to communicate with the
+ * Mopidy server.
+ * @param {string} trackUri - (Optional) The URI of the specific track that the action should
+ * be performed on. If no URI is provided then the 'data' attribute
+ * of the popup DIV is assumed to contain the track URI.
+ * @param {string} playlistUri - (Optional) The URI of the playlist containing the tracks
+ * to be played. If no URI is provided then the 'list' attribute
+ * of the popup DIV is assumed to contain the playlist URI.
+ */
- // Search page default click behaviour adds and plays selected track only.
- if (action == PLAY_NOW && divid == 'search') {
- action = PLAY_NOW_SEARCH;
- }
-
- $('#popupTracks').popup('close');
- $('#controlspopup').popup('close');
- toast('Loading...');
+ playTracks: function (action, mopidy, trackUri, playlistUri) {
+ $('#popupTracks').popup('close')
+ toast('Loading...')
- playlisturi = $('#popupTracks').data("list");
- uri = $('#popupTracks').data("track");
+ trackUri = trackUri || $('#popupTracks').data('track')
+ if (typeof trackUri === 'undefined') {
+ throw new Error('No track URI provided for playback.')
+ }
+ playlistUri = playlistUri || $('#popupTracks').data('list')
+ if (typeof playlistUri === 'undefined') {
+ throw new Error('No playlist URI provided for playback.')
+ }
- var trackUris = getTracksFromUri(playlisturi);
- //find track that was selected
- for (var selected = 0; selected < trackUris.length; selected++) {
- if (trackUris[selected] == uri) {
- break;
- }
- }
- switch (action) {
- case ADD_THIS_BOTTOM:
- case PLAY_NEXT:
- case PLAY_NOW_SEARCH:
- trackUris = [trackUris[selected]];
- selected = 0;
- }
- switch (action) {
- case PLAY_NOW:
- case PLAY_NOW_SEARCH:
+ action = controls.getAction(action)
+
+ if (action === PLAY_ALL) {
+ mopidy.tracklist.clear()
+ }
+
+ var trackUris = controls._getTrackURIsForAction(action, trackUri, playlistUri)
+ // Add the tracks and start playback if necessary.
+ switch (action) {
+ case PLAY_NOW:
+ case PLAY_NEXT:
+ // Find track that is currently playing.
+ mopidy.tracklist.index().then(function (currentIndex) {
+ // Add browsed track just below it.
+ mopidy.tracklist.add({at_position: currentIndex + 1, uris: trackUris}).then(function (tlTracks) {
+ if (action === PLAY_NOW) { // Start playback immediately.
+ mopidy.playback.stop().then(function () {
+ mopidy.playback.play({tlid: tlTracks[0].tlid})
+ })
+ }
+ })
+ })
+ break
+ case ADD_THIS_BOTTOM:
+ case ADD_ALL_BOTTOM:
+ case PLAY_ALL:
+ mopidy.tracklist.add({uris: trackUris}).then(function (tlTracks) {
+ if (action === PLAY_ALL) { // Start playback of selected track immediately.
+ mopidy.tracklist.filter({uri: [trackUri]}).then(function (tlTracks) {
+ mopidy.playback.stop().then(function () {
+ mopidy.playback.play({tlid: tlTracks[0].tlid})
+ })
+ })
+ }
+ })
+ break
+ default:
+ throw new Error('Unexpected tracklist action identifier: ' + action)
+ }
+
+ if (window[$(document.body).data('on-track-click')] === DYNAMIC) {
+ // Save last 'action' - will become default for future 'onClick' events
+ var previousAction = $.cookie('onTrackClick')
+ if (typeof previousAction === 'undefined' || action !== previousAction) {
+ $.cookie('onTrackClick', action, { expires: 365 })
+ updatePlayIcons('', '', controls.getIconForAction(action))
+ }
+ }
+ },
+
+ /* Getter function for 'action' variable. Also checks config settings and cookies if required. */
+ getAction: function (action) {
+ if (typeof action === 'undefined' || action.length === 0) { // Action parameter not provided, use defaults
+ action = window[$(document.body).data('on-track-click')]
+ }
+ if (action === DYNAMIC) { // Re-use last action stored in cookie.
+ action = $.cookie('onTrackClick')
+ if (typeof action === 'undefined') {
+ action = PLAY_ALL // Backwards-compatible default value.
+ }
+ }
+ return action
+ },
+
+ /* Retrieves the Font Awesome character for the given action. */
+ getIconForAction: function (action) {
+ action = controls.getAction(action)
+
+ switch (parseInt(action)) {
+ case PLAY_ALL:
+ return 'fa fa-play-circle'
+ case PLAY_NOW:
+ return 'fa fa-play-circle-o'
+ case PLAY_NEXT:
+ return 'fa fa-level-down'
+ case ADD_THIS_BOTTOM:
+ return 'fa fa-plus-square-o'
+ case ADD_ALL_BOTTOM:
+ return 'fa fa-plus-square'
+ default:
+ throw new Error('Unkown tracklist action identifier: ' + action)
+ }
+ },
+
+ /* Retrieves the relevant track URIs for the given action. */
+ _getTrackURIsForAction: function (action, trackUri, playlistUri) {
+ var trackUris = []
+ // Fill 'trackUris', by determining which tracks should be added.
+ switch (parseInt(action)) {
+ case PLAY_NOW:
+ case PLAY_NEXT:
+ case ADD_THIS_BOTTOM:
+ // Process single track
+ trackUris.push(trackUri)
+ break
+ case PLAY_ALL:
+ case ADD_ALL_BOTTOM:
+ // Process all tracks in playlist
+ trackUris = getTracksFromUri(playlistUri, false)
+ break
+ default:
+ throw new Error('Unexpected tracklist action identifier: ' + action)
+ }
+ return trackUris
+ },
+
+ /** ******************************************************
+ * play an uri from the queue
+ *********************************************************/
+
+ /** *
+ * Plays a Track from a Playlist.
+ * @param tlid
+ * @returns {boolean}
+ */
+ playQueueTrack: function (tlid) {
+ // Stop directly, for user feedback
+ mopidy.playback.stop()
+ $('#popupQueue').popup('close')
+ toast('Loading...')
+
+ tlid = tlid || $('#popupQueue').data('tlid')
+ mopidy.playback.play({'tlid': parseInt(tlid)})
+ },
+
+ /** *********************************
+ * remove a track from the queue *
+ ***********************************/
+ removeTrack: function (tlid) {
+ $('#popupQueue').popup('close')
+ toast('Deleting...')
+
+ tlid = tlid || $('#popupQueue').data('tlid')
+ mopidy.tracklist.remove({'tlid': [parseInt(tlid)]})
+ },
+
+ clearQueue: function () {
mopidy.tracklist.clear().then(
- mopidy.tracklist.add({'uris': trackUris}).then(
- function(tlTracks) {
- mopidy.playback.play({'tl_track': tlTracks[selected]});
- }
- )
- );
- break;
- case PLAY_NEXT:
- mopidy.tracklist.index().then(function(currentIndex) {
- mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris});
- });
- break;
- case ADD_THIS_BOTTOM:
- case ADD_ALL_BOTTOM:
- mopidy.tracklist.add({'uris': trackUris});
- break;
- }
- return false;
-}
+ resetSong()
+ )
+ return false
+ },
-/***
- * Plays a Track given by an URI from the given playlist URI.
- * @param track_uri, playlist_uri
- * @returns {boolean}
- */
-function playTrackByUri(track_uri, playlist_uri) {
- function findAndPlayTrack(tlTracks) {
- if (tlTracks.length > 0) {
- // Find track that was selected
- for (var selected = 0; selected < tlTracks.length; selected++) {
- if (tlTracks[selected].track.uri == track_uri) {
- mopidy.playback.play({'tl_track': tlTracks[selected]});
- return;
+ savePressed: function (key) {
+ if (key === 13) {
+ controls.saveQueue()
+ return false
+ }
+ return true
+ },
+
+ showSavePopup: function () {
+ mopidy.tracklist.getTracks().then(function (tracks) {
+ if (tracks.length > 0) {
+ $('#saveinput').val('')
+ $('#popupSave').popup('open')
}
+ })
+ },
+
+ saveQueue: function () {
+ mopidy.tracklist.getTracks().then(function (tracks) {
+ var playlistName = $('#saveinput').val().trim()
+ if (playlistName !== null && playlistName !== '') {
+ controls.getPlaylistByName(playlistName, 'm3u', false).then(function (exists) {
+ if (exists) {
+ $('#popupSave').popup('close')
+ $('#popupOverwrite').popup('open')
+ $('#overwriteConfirmBtn').click(function () {
+ controls.initSave(playlistName, tracks)
+ })
+ } else {
+ controls.initSave(playlistName, tracks)
+ }
+ })
+ }
+ })
+ return false
+ },
+
+ initSave: function (playlistName, tracks) {
+ $('#popupOverwrite').popup('close')
+ $('#popupSave').popup('close')
+ $('#saveinput').val('')
+ toast('Saving...')
+ mopidy.playlists.create({'name': playlistName, 'uri_scheme': 'm3u'}).then(function (playlist) {
+ playlist.tracks = tracks
+ mopidy.playlists.save({'playlist': playlist}).then()
+ })
+ },
+
+ refreshPlaylists: function () {
+ mopidy.playlists.refresh().then(function () {
+ playlists = {}
+ $('#playlisttracksdiv').hide()
+ $('#playlistslistdiv').show()
+ })
+ return false
+ },
+
+ /** ***********
+ * Buttons *
+ *************/
+
+ doShuffle: function () {
+ mopidy.playback.stop()
+ mopidy.tracklist.shuffle()
+ mopidy.playback.play()
+ },
+
+ /* Toggle state of play button */
+ setPlayState: function (nwplay) {
+ if (nwplay) {
+ $('#btplayNowPlaying >i').removeClass('fa-play').addClass('fa-pause')
+ $('#btplayNowPlaying').attr('title', 'Pause')
+ $('#btplay >i').removeClass('fa-play').addClass('fa-pause')
+ $('#btplay').attr('title', 'Pause')
+ mopidy.playback.getTimePosition().then(processCurrentposition, console.error)
+ syncedProgressTimer.start()
+ } else {
+ $('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play')
+ $('#btplayNowPlaying').attr('title', 'Play')
+ $('#btplay >i').removeClass('fa-pause').addClass('fa-play')
+ $('#btplay').attr('title', 'Play')
+ syncedProgressTimer.stop()
}
- }
- console.error('Failed to find and play selected track ', track_uri);
- return;
- }
+ play = nwplay
+ },
- // Stop directly, for user feedback
- mopidy.tracklist.clear();
-
- //this is deprecated, remove when popuptracks is removed completly
- $('#popupTracks').popup('close');
- $('#controlspopup').popup('close');
- //end of deprecated
-
- toast('Loading...');
-
- mopidy.tracklist.add({'uris': [playlist_uri]}).then(function(tlTracks) {
- // Can fail for all sorts of reasons. If so, just add individually.
- if (tlTracks.length === 0) {
- var trackUris = getTracksFromUri(playlist_uri, false);
- mopidy.tracklist.add({'uris': trackUris}).then(findAndPlayTrack);
- } else {
- findAndPlayTrack(tlTracks);
- }
- });
- return false;
-}
-
-/********************************************************
- * play an uri from the queue
- *********************************************************/
-
-/***
- * Plays a Track from a Playlist.
- * @param uri
- * @param tlid
- * @returns {boolean}
- */
-function playTrackQueueByTlid(uri, tlid) {
- //stop directly, for user feedback
- mopidy.playback.stop();
- $('#popupQueue').popup('close');
- toast('Loading...');
-
- tlid = parseInt(tlid);
- mopidy.tracklist.filter({
- 'tlid': [tlid]
- }).then(
- function(tlTracks) {
- if (tlTracks.length > 0) {
- mopidy.playback.play({'tl_track': tlTracks[0]});
- return;
- }
- console.log('Failed to play selected track ', tlid);
- }
- );
- return false;
-}
-
-/***
- * @deprecated
- * @returns {boolean}
- */
-function playTrackQueue() {
- uri = $('#popupQueue').data("track");
- tlid = $('#popupQueue').data("tlid");
- return playTrackQueueByTlid(uri, tlid);
-}
-
-/***********************************
- * remove a track from the queue *
- ***********************************/
-function removeTrack() {
- $('#popupQueue').popup('close');
- toast('Deleting...');
-
- tlid = parseInt($('#popupQueue').data("tlid"));
- console.log(tlid);
- mopidy.tracklist.remove({'tlid': [tlid]});
-}
-
-function clearQueue() {
- mopidy.tracklist.clear().then(
- resetSong()
- );
- return false;
-}
-
-function savePressed(key) {
- if (key == 13) {
- saveQueue();
- return false;
- }
- return true;
-}
-
-function showSavePopup(){
- mopidy.tracklist.getTracks().then(function(tracks) {
- if (tracks.length > 0) {
- $('#saveinput').val('');
- $('#popupSave').popup('open');
- }
- });
-
-}
-
-function saveQueue() {
- mopidy.tracklist.getTracks().then(function(tracks) {
- var playlistName = $('#saveinput').val().trim();
- if (playlistName !== null && playlistName !== "") {
- getPlaylistByName(playlistName, 'm3u', false).then(function(exists) {
- if (exists) {
- $('#popupSave').popup('close');
- $('#popupOverwrite').popup('open');
- $('#overwriteConfirmBtn').click(function() {
- initSave(playlistName, tracks);
- });
+ // play or pause
+ doPlay: function () {
+ toast('Please wait...', 250)
+ if (!play) {
+ mopidy.playback.play()
+ } else {
+ if (isStreamUri(songdata.track.uri)) {
+ mopidy.playback.stop()
} else {
- initSave(playlistName, tracks);
+ mopidy.playback.pause()
}
- });
- }
- });
- return false;
-}
-
-function initSave(playlistName, tracks) {
- $('#popupOverwrite').popup('close');
- $('#popupSave').popup('close');
- $('#saveinput').val('');
- toast('Saving...');
- mopidy.playlists.create({'name': playlistName, 'uri_scheme': "m3u"}).then(function(playlist) {
- playlist.tracks = tracks;
- mopidy.playlists.save({'playlist': playlist}).then();
- });
-}
-
-function refreshPlaylists() {
- mopidy.playlists.refresh().then(function() {
- playlists = {};
- $('#playlisttracksdiv').hide();
- $('#playlistslistdiv').show();
- });
- return false;
-}
-
-/*************
- * Buttons *
- *************/
-
-function doShuffle() {
- mopidy.playback.stop();
- mopidy.tracklist.shuffle();
- mopidy.playback.play();
-}
-
-/* Toggle state of play button */
-function setPlayState(nwplay) {
- if (nwplay) {
- $("#btplayNowPlaying >i").removeClass('fa-play').addClass('fa-pause');
- $("#btplayNowPlaying").attr('title', 'Pause');
- $("#btplay >i").removeClass('fa-play').addClass('fa-pause');
- $("#btplay").attr('title', 'Pause');
- mopidy.playback.getTimePosition().then(processCurrentposition, console.error);
- startProgressTimer();
- } else {
- $("#btplayNowPlaying >i").removeClass('fa-pause').addClass('fa-play');
- $("#btplayNowPlaying").attr('title', 'Play');
- $("#btplay >i").removeClass('fa-pause').addClass('fa-play');
- $("#btplay").attr('title', 'Play');
- progressTimer.stop();
- }
- play = nwplay;
-}
-
-//play or pause
-function doPlay() {
- toast('Please wait...', 250);
- if (!play) {
- mopidy.playback.play();
- } else {
- if(isStreamUri(songdata.track.uri)) {
- mopidy.playback.stop();
- } else {
- mopidy.playback.pause();
- }
- }
- setPlayState(!play);
-}
-
-function doPrevious() {
- toast('Playing previous track...');
- mopidy.playback.previous();
-}
-
-function doNext() {
- toast('Playing next track...');
- mopidy.playback.next();
-}
-
-function backbt() {
- history.back();
- return false;
-}
-
-/*************
- * Options *
- *************/
-function setTracklistOption(name, new_value) {
- if (!new_value) {
- $("#"+name+"bt").attr('style', 'color:#2489ce');
- } else {
- $("#"+name+"bt").attr('style', 'color:#66DD33');
- }
- return new_value;
-}
-
-function setRepeat(nwrepeat) {
- if (repeat != nwrepeat) {
- repeat = setTracklistOption("repeat", nwrepeat);
- }
-}
-
-function setRandom(nwrandom) {
- if (random != nwrandom) {
- random = setTracklistOption("random", nwrandom);
- }
-}
-
-function setConsume(nwconsume) {
- if (consume != nwconsume) {
- consume = setTracklistOption("consume", nwconsume);
- }
-}
-
-function setSingle(nwsingle) {
- if (single != nwsingle) {
- single = setTracklistOption("single", nwsingle);
- }
-}
-
-function doRandom() {
- mopidy.tracklist.setRandom({'value': !random}).then();
-}
-
-function doRepeat() {
- mopidy.tracklist.setRepeat({'value': !repeat}).then();
-}
-
-function doConsume() {
- mopidy.tracklist.setConsume({'value': !consume}).then();
-}
-
-function doSingle() {
- mopidy.tracklist.setSingle({'value': !single}).then();
-}
-
-
-/***********************************************
- * Track Slider *
- * Use a timer to prevent looping of commands *
- ***********************************************/
-function doSeekPos(value) {
- if (!positionChanging) {
- positionChanging = value;
- mopidy.playback.seek({'time_position': Math.round(value)}).then( function() {
- positionChanging = null;
- });
- }
-}
-
-function setPosition(pos) {
- if (!positionChanging && $("#trackslider").val() != pos) {
- setProgressTimer(pos);
- }
-}
-
-/***********************************************
- * Volume slider *
- * Use a timer to prevent looping of commands *
- ***********************************************/
-
-function setVolume(value) {
- if (!volumeChanging && !volumeSliding && $("#volumeslider").val() != value) {
- $( "#volumeslider" ).off( "change");
- $( "#volumeslider" ).val(value).slider('refresh');
- $( "#volumeslider" ).on( "change", function() { doVolume( $(this).val() ); } );
- }
-}
-
-function doVolume(value) {
- if (!volumeChanging) {
- volumeChanging = value;
- mopidy.playback.setVolume({'volume': parseInt(volumeChanging)}).then( function() {
- volumeChanging = null;
- });
- }
-}
-
-function setMute(nwmute) {
- if (mute != nwmute) {
- mute = nwmute;
- if (mute) {
- $("#mutebt").attr('class', 'fa fa-volume-off');
- } else {
- $("#mutebt").attr('class', 'fa fa-volume-up');
- }
- }
-}
-
-function doMute() {
- mopidy.mixer.setMute({'mute': !mute});
-}
-
-/************
- * Stream *
- ************/
-function streamPressed(key) {
- if (key == 13) {
- playStreamUri();
- return false;
- }
- return true;
-}
-
-function playStreamUri(uri) {
- //value of name is based on the passing of an uri as a parameter or not
- var nwuri = uri || $('#streamuriinput').val().trim();
- var service = $('#selectstreamservice').val();
- if (!uri && service) {
- nwuri = service + ':' + nwuri;
- }
- if (isServiceUri(nwuri) || isStreamUri(nwuri) || validUri(nwuri)) {
- toast('Playing...');
- //stop directly, for user feedback
- mopidy.playback.stop();
- //hide ios/android keyboard
- document.activeElement.blur();
- clearQueue();
- $("input").blur();
- mopidy.tracklist.add({'uris': [nwuri]});
- mopidy.playback.play();
- } else {
- toast('No valid url!');
- }
- return false;
-}
-
-function getCurrentlyPlaying() {
- $('#streamuriinput').val(songdata.track.uri);
- var name = songdata.track.name;
- if (songdata.track.artists) {
- var artistStr = artistsToString(songdata.track.artists);
- if (artistStr) {
- name = artistStr + ' - ' + name;
- }
- }
- $('#streamnameinput').val(name);
- return true;
-}
-
-function getUriSchemes() {
- uriSchemes = {};
- return mopidy.getUriSchemes().then(function(schemes) {
- for (var i = 0; i < schemes.length; i++) {
- uriSchemes[schemes[i].toLowerCase()] = true;
- }
- });
-}
-
-function getPlaylistByName(name, scheme, create) {
- var uri_scheme = scheme || '';
- var uri = '';
- if (uri_scheme && !uriSchemes[uri_scheme]) {
- return Mopidy.when(false);
- }
- return mopidy.playlists.asList().catch(console.error.bind(console)).then(function(plists) {
- for (var i = 0; i < plists.length; i++) {
- if ((plists[i].name === name) && (uri_scheme === '' || getScheme(plists[i].uri) === uri_scheme)) {
- return plists[i];
}
- }
- if (create) {
- return mopidy.playlists.create({'name': name, 'uri_scheme': uri_scheme}).done(function(plist) {
- console.log("Created playlist '%s'", plist.name);
- return plist;
- });
- }
- console.log("Can't find playist '%s", name);
- return Mopidy.when(false);
- });
-}
+ controls.setPlayState(!play)
+ },
-function getPlaylistFull(uri) {
- return mopidy.playlists.lookup({'uri': uri}).then(function(pl) {
- playlists[uri] = pl;
- return pl;
- });
-}
+ doPrevious: function () {
+ toast('Playing previous track...')
+ mopidy.playback.previous()
+ },
-function getFavourites() {
- return getPlaylistByName(STREAMS_PLAYLIST_NAME,
- STREAMS_PLAYLIST_SCHEME,
- true).then(function(playlist) {
- if (playlist) {
- return getPlaylistFull(playlist.uri);
- }
- return Mopidy.when(false);
- });
-}
+ doNext: function () {
+ toast('Playing next track...')
+ mopidy.playback.next()
+ },
-function addToFavourites(newTracks) {
- getFavourites().catch(console.error.bind(console)).then(function(favourites) {
- if (favourites) {
- if (favourites.tracks) {
- Array.prototype.push.apply(favourites.tracks, newTracks);
+ backbt: function () {
+ history.back()
+ return false
+ },
+
+ /** ***********
+ * Options *
+ *************/
+ setTracklistOption: function (name, new_value) {
+ if (!new_value) {
+ $('#' + name + 'bt').attr('style', 'color:#2489ce')
} else {
- favourites.tracks = newTracks;
+ $('#' + name + 'bt').attr('style', 'color:#66DD33')
}
- mopidy.playlists.save({'playlist': favourites}).then(function(s) {
- showFavourites();
- });
- }
- });
-}
+ return new_value
+ },
-function addFavourite(uri, name) {
- var uri = uri || $('#streamuriinput').val().trim();
- var name = name || $('#streamnameinput').val().trim();
- mopidy.library.lookup({'uris': [uri]}).then(function(results) {
- var newTracks = results[uri];
- if (newTracks.length == 1) {
- // TODO: Supporting adding an entire playlist?
- if (name) {
- newTracks[0].name = name; // User overrides name.
+ setRepeat: function (nwrepeat) {
+ if (repeat !== nwrepeat) {
+ repeat = controls.setTracklistOption('repeat', nwrepeat)
}
- addToFavourites(newTracks);
- } else {
- if (newTracks.length === 0) {
- console.log('No tracks to add');
+ },
+
+ setRandom: function (nwrandom) {
+ if (random !== nwrandom) {
+ random = controls.setTracklistOption('random', nwrandom)
+ }
+ },
+
+ setConsume: function (nwconsume) {
+ if (consume !== nwconsume) {
+ consume = controls.setTracklistOption('consume', nwconsume)
+ }
+ },
+
+ setSingle: function (nwsingle) {
+ if (single !== nwsingle) {
+ single = controls.setTracklistOption('single', nwsingle)
+ }
+ },
+
+ doRandom: function () {
+ mopidy.tracklist.setRandom({'value': !random}).then()
+ },
+
+ doRepeat: function () {
+ mopidy.tracklist.setRepeat({'value': !repeat}).then()
+ },
+
+ doConsume: function () {
+ mopidy.tracklist.setConsume({'value': !consume}).then()
+ },
+
+ doSingle: function () {
+ mopidy.tracklist.setSingle({'value': !single}).then()
+ },
+
+ /** *********************************************
+ * Track Slider *
+ * Use a timer to prevent looping of commands *
+ ***********************************************/
+ doSeekPos: function (value) {
+ if (!positionChanging) {
+ positionChanging = value
+ mopidy.playback.seek({'time_position': Math.round(value)}).then(function () {
+ positionChanging = null
+ })
+ }
+ },
+
+ setPosition: function (pos) {
+ if (!positionChanging && $('#trackslider').val() !== pos) {
+ syncedProgressTimer.set(pos)
+ }
+ },
+
+ /** *********************************************
+ * Volume slider *
+ * Use a timer to prevent looping of commands *
+ ***********************************************/
+
+ setVolume: function (value) {
+ if (!volumeChanging && !volumeSliding && $('#volumeslider').val() !== value) {
+ $('#volumeslider').off('change')
+ $('#volumeslider').val(value).slider('refresh')
+ $('#volumeslider').on('change', function () { controls.doVolume($(this).val()) })
+ }
+ },
+
+ doVolume: function (value) {
+ if (!volumeChanging) {
+ volumeChanging = value
+ mopidy.playback.setVolume({'volume': parseInt(volumeChanging)}).then(function () {
+ volumeChanging = null
+ })
+ }
+ },
+
+ setMute: function (nwmute) {
+ if (mute !== nwmute) {
+ mute = nwmute
+ if (mute) {
+ $('#mutebt').attr('class', 'fa fa-volume-off')
+ } else {
+ $('#mutebt').attr('class', 'fa fa-volume-up')
+ }
+ }
+ },
+
+ doMute: function () {
+ mopidy.mixer.setMute({'mute': !mute})
+ },
+
+ /** **********
+ * Stream *
+ ************/
+ streamPressed: function (key) {
+ if (key === 13) {
+ controls.playStreamUri()
+ return false
+ }
+ return true
+ },
+
+ playStreamUri: function (uri) {
+ // value of name is based on the passing of an uri as a parameter or not
+ var nwuri = uri || $('#streamuriinput').val().trim()
+ var service = $('#selectstreamservice').val()
+ if (!uri && service) {
+ nwuri = service + ':' + nwuri
+ }
+ if (isServiceUri(nwuri) || isStreamUri(nwuri) || validUri(nwuri)) {
+ toast('Playing...')
+ // stop directly, for user feedback
+ mopidy.playback.stop()
+ // hide ios/android keyboard
+ document.activeElement.blur()
+ controls.clearQueue()
+ $('input').blur()
+ mopidy.tracklist.add({'uris': [nwuri]})
+ mopidy.playback.play()
} else {
- console.log('Too many tracks (%d) to add', tracks.length);
+ toast('No valid url!')
}
- }
- });
-}
+ return false
+ },
-function deleteFavourite(index) {
- getFavourites().then(function(favourites) {
- if (favourites && favourites.tracks && index < favourites.tracks.length) {
- var name = favourites.tracks[index].name;
- if (confirm("Are you sure you want to remove '" + name + "'?")) {
- favourites.tracks.splice(index, 1);
- mopidy.playlists.save({'playlist': favourites}).then(function(s) {
- showFavourites();
- });
+ getCurrentlyPlaying: function () {
+ $('#streamuriinput').val(songdata.track.uri)
+ var name = songdata.track.name
+ if (songdata.track.artists) {
+ var artistStr = artistsToString(songdata.track.artists)
+ if (artistStr) {
+ name = artistStr + ' - ' + name
+ }
}
- }
- });
-}
+ $('#streamnameinput').val(name)
+ return true
+ },
-function showFavourites() {
- $('#streamuristable').empty();
- getFavourites().then(function(favourites) {
- if (!favourites) {
- return;
- }
- var tmp = '';
-
- $.cookie.json = true;
- if ($.cookie('streamUris')) {
- tmp = 'Convert StreamUris';
- }
- if (favourites.tracks) {
- var child = '';
- for (var i = 0; i < favourites.tracks.length; i++) {
- child = '
'
+ return html
+}
+
+/* Tracklist renderer for track artist and album name. */
+function renderSongLiAlbumInfo (track, target) {
+ var html = renderSongLiTrackArtists(track)
+ if (track.album && track.album.name) {
+ html += ' - ' + track.album.name + ''
+ }
+ if (typeof target !== 'undefined' && target.length > 0) {
+ target = getjQueryID(target, track.uri, true)
+ $(target).children('a').eq(1).append(html)
+ }
+ return html
+}
+
+/* Tracklist renderer for track artist information. */
+function renderSongLiTrackArtists (track) {
+ var html = ''
+ if (track.artists) {
+ for (var i = 0; i < track.artists.length; i++) {
+ html += track.artists[i].name
+ html += (i === track.artists.length - 1) ? '' : ' / '
+ // Stop after 3
+ if (i > 2) {
+ html += '...'
+ break
+ }
+ }
+ }
+ return html
+}
+
+/* Tracklist renderer to insert dividers between albums. */
+function renderSongLiDivider (previousTrack, track, nextTrack, target) {
+ var html = ''
+ // Render differently if part of an album.
+ if (!hasSameAlbum(previousTrack, track) && hasSameAlbum(track, nextTrack)) {
+ // Large divider with album cover.
+ html +=
+ '
'
}
- $('.popupArtistsLi').hide();
- $('.popupArtistsLv').html(child).show();
- $('.popupArtistsDiv').show();
+ $('.popupArtistsLi').hide()
+ $('.popupArtistsLv').html(child).show()
+ $('.popupArtistsDiv').show()
// this makes the viewport of the window resize somehow
- $('.popupArtistsLv').listview("refresh");
+ $('.popupArtistsLv').listview('refresh')
}
} else {
- $('.popupArtistsDiv').hide();
- $('.popupArtistsLi').hide();
+ $('.popupArtistsDiv').hide()
+ $('.popupArtistsLi').hide()
}
- var hash = document.location.hash.split('?');
- var divid = hash[0].substr(1);
- var popupName = '';
- if (divid == 'current') {
- $(".addqueue").hide();
- popupName = '#popupQueue';
- } else if (divid == 'browse') {
- $(".addqueue").show();
- popupName = '#popupBrowse';
+ var hash = document.location.hash.split('?')
+ var divid = hash[0].substr(1)
+ var popupName = ''
+ if (divid === 'current') {
+ $('.addqueue').hide()
+ popupName = '#popupQueue'
} else {
- $(".addqueue").show();
- popupName = '#popupTracks';
+ $('.addqueue').show()
+ popupName = '#popupTracks'
}
- if (typeof tlid != 'undefined' && tlid !== '') {
- $(popupName).data("list", listuri).data("track", trackuri).data("tlid", tlid).popup("open", {
- x : e.pageX,
- y : e.pageY
- });
+ if (typeof tlid !== 'undefined' && tlid !== '') {
+ $(popupName).data('list', listuri).data('track', trackuri).data('tlid', tlid).popup('open', {
+ x: e.pageX,
+ y: e.pageY
+ })
} else {
- $(popupName).data("list", listuri).data("track", trackuri).popup("open", {
- x : e.pageX,
- y : e.pageY
- });
+ $(popupName).data('list', listuri).data('track', trackuri).popup('open', {
+ x: e.pageX,
+ y: e.pageY
+ })
}
- return false;
+ return false
}
-function showAlbumPopup(popupId) {
- uri = $(popupId).data("track");
- showAlbum(popupData[uri].album.uri);
+function showAlbumPopup (popupId) {
+ uri = $(popupId).data('track')
+ library.showAlbum(popupData[uri].album.uri)
}
-/**********************
+/** ********************
* initialize sockets *
**********************/
-function initSocketevents() {
- mopidy.on("state:online", function() {
- showOffline(false);
- getCurrentPlaylist();
- updateStatusOfAll();
- getPlaylists();
- getUriSchemes().then(function() {
- showFavourites();
- });
- getBrowseDir();
- getSearchSchemes();
- showLoading(false);
- $(window).hashchange();
- });
+function initSocketevents () {
+ mopidy.on('state:online', function () {
+ showOffline(false)
+ library.getCurrentPlaylist()
+ updateStatusOfAll()
+ library.getPlaylists()
+ controls.getUriSchemes().then(function () {
+ controls.showFavourites()
+ })
+ library.getBrowseDir()
+ library.getSearchSchemes(searchBlacklist, mopidy)
+ showLoading(false)
+ $(window).hashchange()
+ })
- mopidy.on("state:offline", function() {
- resetSong();
- showOffline(true);
- });
+ mopidy.on('state:offline', function () {
+ resetSong()
+ showOffline(true)
+ })
- mopidy.on("event:optionsChanged", updateOptions);
+ mopidy.on('event:optionsChanged', updateOptions)
- mopidy.on("event:trackPlaybackStarted", function(data) {
- setSongInfo(data.tl_track);
- setPlayState(true);
- });
+ mopidy.on('event:trackPlaybackStarted', function (data) {
+ setSongInfo(data.tl_track)
+ controls.setPlayState(true)
+ })
- mopidy.on("event:playlistsLoaded", function(data) {
- showLoading(true);
- getPlaylists();
- });
+ mopidy.on('event:playlistsLoaded', function (data) {
+ showLoading(true)
+ library.getPlaylists()
+ })
- mopidy.on("event:playlistChanged", function(data) {
- $('#playlisttracksdiv').hide();
- $('#playlistslistdiv').show();
- delete playlists[data.playlist.uri];
- getPlaylists();
- });
+ mopidy.on('event:playlistChanged', function (data) {
+ $('#playlisttracksdiv').hide()
+ $('#playlistslistdiv').show()
+ delete playlists[data.playlist.uri]
+ library.getPlaylists()
+ })
- mopidy.on("event:playlistDeleted", function(data) {
- $('#playlisttracksdiv').hide();
- $('#playlistslistdiv').show();
- delete playlists[data.uri];
- getPlaylists();
- });
+ mopidy.on('event:playlistDeleted', function (data) {
+ $('#playlisttracksdiv').hide()
+ $('#playlistslistdiv').show()
+ delete playlists[data.uri]
+ library.getPlaylists()
+ })
- mopidy.on("event:volumeChanged", function(data) {
- setVolume(data.volume);
- });
+ mopidy.on('event:volumeChanged', function (data) {
+ controls.setVolume(data.volume)
+ })
- mopidy.on("event:muteChanged", function(data) {
- setMute(data.mute);
- });
+ mopidy.on('event:muteChanged', function (data) {
+ controls.setMute(data.mute)
+ })
- mopidy.on("event:playbackStateChanged", function(data) {
+ mopidy.on('event:playbackStateChanged', function (data) {
switch (data.new_state) {
- case "paused":
- case "stopped":
- setPlayState(false);
- break;
- case "playing":
- setPlayState(true);
- break;
+ case 'paused':
+ case 'stopped':
+ controls.setPlayState(false)
+ break
+ case 'playing':
+ controls.setPlayState(true)
+ break
}
- });
+ })
- mopidy.on("event:tracklistChanged", function(data) {
- getCurrentPlaylist();
- });
+ mopidy.on('event:tracklistChanged', function (data) {
+ library.getCurrentPlaylist()
+ })
- mopidy.on("event:seeked", function(data) {
- setPosition(parseInt(data.time_position));
+ mopidy.on('event:seeked', function (data) {
+ controls.setPosition(parseInt(data.time_position))
if (play) {
- startProgressTimer();
+ syncedProgressTimer.start()
}
- });
+ })
- mopidy.on("event:streamTitleChanged", function(data) {
- setSongTitle(data.title, true);
- });
+ mopidy.on('event:streamTitleChanged', function (data) {
+ setSongTitle(data.title, true)
+ })
}
-$(document).bind("pageinit", function() {
- resizeMb();
+$(document).bind('pageinit', function () {
+ resizeMb()
+})
-});
-
-/**************
+/** ************
* gui stuff *
**************/
-function toggleFullscreen() {
- if (isMobileSafari) { alert ("To get this app in Full Screen, you have to add it to your home-screen using the Share button."); exit(); }
- if (!isFullscreen() ) { // current working methods
- var docElm = document.documentElement;
+function toggleFullscreen () {
+ if (isMobileSafari) { alert('To get this app in Full Screen, you have to add it to your home-screen using the Share button.'); exit() }
+ if (!isFullscreen()) { // current working methods
+ var docElm = document.documentElement
if (docElm.requestFullscreen) {
- docElm.requestFullscreen();
+ docElm.requestFullscreen()
} else if (docElm.msRequestFullscreen) {
- docElm.msRequestFullscreen();
+ docElm.msRequestFullscreen()
} else if (docElm.mozRequestFullScreen) {
- docElm.mozRequestFullScreen();
+ docElm.mozRequestFullScreen()
} else if (docElm.webkitRequestFullScreen) {
- docElm.webkitRequestFullScreen();
+ docElm.webkitRequestFullScreen()
}
} else {
if (document.exitFullscreen) {
- document.exitFullscreen();
+ document.exitFullscreen()
} else if (document.msExitFullscreen) {
- document.msExitFullscreen();
+ document.msExitFullscreen()
} else if (document.mozCancelFullScreen) {
- document.mozCancelFullScreen();
+ document.mozCancelFullScreen()
} else if (document.webkitCancelFullScreen) {
- document.webkitCancelFullScreen();
+ document.webkitCancelFullScreen()
}
}
}
-function isFullscreen() {
+function isFullscreen () {
return (document.fullscreenElement || // alternative standard method
- document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement ); // current working methods
+ document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) // current working methods
}
-function switchContent(divid, uri) {
- var hash = divid;
+function switchContent (divid, uri) {
+ var hash = divid
if (uri) {
- hash += "?" + uri;
+ hash += '?' + uri
}
- location.hash = "#" + hash;
+ location.hash = '#' + hash
}
-function setHeadline(site){
- site = site.trim();
- str = $('.mainNav').find('a[href$='+site+']').text();
- if(str === ""){
- str = site.charAt(0).toUpperCase() + site.slice(1);
+function setHeadline (site) {
+ site = site.trim()
+ str = $('.mainNav').find('a[href$=' + site + ']').text()
+ if (str === '') {
+ str = site.charAt(0).toUpperCase() + site.slice(1)
}
- $('#contentHeadline').html('' + str + '');
+ $('#contentHeadline').html('' + str + '')
}
-//update tracklist options.
-function updateOptions() {
- mopidy.tracklist.getRepeat().then(processRepeat, console.error);
- mopidy.tracklist.getRandom().then(processRandom, console.error);
- mopidy.tracklist.getConsume().then(processConsume, console.error);
- mopidy.tracklist.getSingle().then(processSingle, console.error);
+// update tracklist options.
+function updateOptions () {
+ mopidy.tracklist.getRepeat().then(processRepeat, console.error)
+ mopidy.tracklist.getRandom().then(processRandom, console.error)
+ mopidy.tracklist.getConsume().then(processConsume, console.error)
+ mopidy.tracklist.getSingle().then(processSingle, console.error)
}
-//update everything as if reloaded
-function updateStatusOfAll() {
- mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error);
- mopidy.playback.getTimePosition().then(processCurrentposition, console.error);
- mopidy.playback.getState().then(processPlaystate, console.error);
+// update everything as if reloaded
+function updateStatusOfAll () {
+ mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error)
+ mopidy.playback.getTimePosition().then(processCurrentposition, console.error)
+ mopidy.playback.getState().then(processPlaystate, console.error)
- updateOptions();
+ updateOptions()
- mopidy.playback.getVolume().then(processVolume, console.error);
- mopidy.mixer.getMute().then(processMute, console.error);
+ mopidy.playback.getVolume().then(processVolume, console.error)
+ mopidy.mixer.getMute().then(processMute, console.error)
}
-function locationHashChanged() {
- var hash = document.location.hash.split('?');
- //remove #
- var divid = hash[0].substr(1);
- setHeadline(divid);
+function locationHashChanged () {
+ var hash = document.location.hash.split('?')
+ // remove #
+ var divid = hash[0].substr(1)
+ setHeadline(divid)
-
- var uri = hash[1];
- $('.mainNav a').removeClass('ui-state-active ui-state-persist ui-btn-active');
- //i don't know why some li elements have those classes, but they do, so we need to remove them
- $('.mainNav li').removeClass('ui-state-active ui-state-persist ui-btn-active');
+ var uri = hash[1]
+ $('.mainNav a').removeClass('ui-state-active ui-state-persist ui-btn-active')
+ // i don't know why some li elements have those classes, but they do, so we need to remove them
+ $('.mainNav li').removeClass('ui-state-active ui-state-persist ui-btn-active')
if ($(window).width() < 560) {
- $("#panel").panel("close");
+ $('#panel').panel('close')
}
- $('.pane').hide();
+ $('.pane').hide()
- $('#' + divid + 'pane').show();
+ $('#' + divid + 'pane').show()
- switch(divid) {
+ switch (divid) {
case 'home':
- $('#navhome a').addClass('ui-state-active ui-state-persist ui-btn-active');
- break;
+ $('#navhome a').addClass('ui-state-active ui-state-persist ui-btn-active')
+ break
case 'nowPlaying':
- $('#navnowPlaying a').addClass('ui-state-active ui-state-persist ui-btn-active');
- break;
+ $('#navnowPlaying a').addClass('ui-state-active ui-state-persist ui-btn-active')
+ break
case 'current':
- $('#navcurrent a').addClass('ui-state-active ui-state-persist ui-btn-active');
- getCurrentPlaylist();
- break;
+ $('#navcurrent a').addClass('ui-state-active ui-state-persist ui-btn-active')
+ break
case 'playlists':
- $('#navplaylists a').addClass('ui-state-active ui-state-persist ui-btn-active');
- break;
+ $('#navplaylists a').addClass('ui-state-active ui-state-persist ui-btn-active')
+ break
case 'browse':
- $('#navbrowse a').addClass('ui-state-active ui-state-persist ui-btn-active');
- break;
+ $('#navbrowse a').addClass('ui-state-active ui-state-persist ui-btn-active')
+ break
case 'search':
- $('#navsearch a').addClass($.mobile.activeBtnClass);
- $("#searchinput").focus();
- if (customTracklists['mbw:allresultscache'] === '') {
- initSearch($('#searchinput').val());
- }
- break;
+ $('#navsearch a').addClass($.mobile.activeBtnClass)
+ $('#searchinput').focus()
+ break
case 'stream':
- $('#navstream a').addClass('ui-state-active ui-state-persist ui-btn-active');
- break;
+ $('#navstream a').addClass('ui-state-active ui-state-persist ui-btn-active')
+ break
case 'artists':
if (uri !== '') {
- showArtist(uri);
+ library.showArtist(uri)
}
- break;
+ break
case 'albums':
if (uri !== '') {
- showAlbum(uri);
+ library.showAlbum(uri)
}
- break;
+ break
}
- //switch the footer
- switch(divid) {
+ // switch the footer
+ switch (divid) {
case 'nowPlaying':
- $('#normalFooter').hide();
- $('#nowPlayingFooter').show();
- break;
+ $('#normalFooter').hide()
+ $('#nowPlayingFooter').show()
+ break
default:
- $('#normalFooter').show();
- $('#nowPlayingFooter').hide();
+ $('#normalFooter').show()
+ $('#nowPlayingFooter').hide()
}
// Set the page title based on the hash.
- document.title = PROGRAM_NAME;
- return false;
+ document.title = PROGRAM_NAME
+ return false
}
-/***********************
+/** *********************
* initialize software *
***********************/
-$(document).ready(function(event) {
- //check for websockets
+$(document).ready(function (event) {
+ // check for websockets
if (!window.WebSocket) {
- switchContent("playlists");
- $('#playlistspane').html('
Old Browser
Sorry. Your browser isn\'t modern enough for this webapp. Modern versions of Chrome, Firefox, Safari all will do. Maybe Opera and Internet Explorer 10 also work, but it\'s not tested.
Sorry. Your browser isn\'t modern enough for this webapp. Modern versions of Chrome, Firefox, Safari all will do. Maybe Opera and Internet Explorer 10 also work, but it\'s not tested.
')
+ exit
}
- //workaround for a bug in jQuery Mobile, without that the panel doesn't close on mobile devices...
- $('.ui-panel-dismiss').on( "tap", function() { $("#panel").panel("close"); } );
- //end of workaround
+ // workaround for a bug in jQuery Mobile, without that the panel doesn't close on mobile devices...
+ $('.ui-panel-dismiss').on('tap', function () { $('#panel').panel('close') })
+ // end of workaround
- $(window).hashchange();
+ $(window).hashchange()
// Connect to server
+ var websocketUrl = $(document.body).data('websocket-url')
if (websocketUrl) {
try {
mopidy = new Mopidy({
webSocketUrl: websocketUrl,
callingConvention: 'by-position-or-by-name'
- });
+ })
} catch (e) {
- showOffline(true);
+ showOffline(true)
}
} else {
try {
- mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'});
- } catch (e) {
- showOffline(true);
+ mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'})
+ } catch (e) {
+ showOffline(true)
}
}
- //initialize events
- initSocketevents();
+ // initialize events
+ initSocketevents()
- progressTimer = new ProgressTimer({
- callback: timerCallback,
- //updateRate: 2000,
- });
+ syncedProgressTimer = new SyncedProgressTimer(8, mopidy)
- resetSong();
+ resetSong()
if (location.hash.length < 2) {
- switchContent("home");
+ switchContent('home')
}
- initgui = false;
- window.onhashchange = locationHashChanged;
+ initgui = false
+ window.onhashchange = locationHashChanged
- //only show backbutton if in UIWebview
+ // only show backbutton if in UIWebview
if (window.navigator.standalone) {
- $("#btback").show();
+ $('#btback').show()
} else {
- $("#btback").hide();
+ $('#btback').hide()
}
- $(window).resize(function() {
- resizeMb();
- });
+ $(window).resize(function () {
+ resizeMb()
+ })
- //navigation temporary, rewrite this!
- $('#songinfo').click(function() {
- return switchContent('nowPlaying');
- });
- $('#controlspopupimage').click(function() {
- return switchContent('current');
- });
- $('#navToggleFullscreen').click(function() {
- toggleFullscreen();
- });
+ // navigation temporary, rewrite this!
+ $('#songinfo').click(function () {
+ return switchContent('nowPlaying')
+ })
+ $('#albumCoverImg').click(function () {
+ return switchContent('current')
+ })
+ $('#navToggleFullscreen').click(function () {
+ toggleFullscreen()
+ })
// event handlers for full screen mode
- $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange, MSFullscreenChange', function(e) {
+ $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange, MSFullscreenChange', function (e) {
if (isFullscreen()) {
- document.getElementById("toggletxt").innerHTML = "Exit Fullscreen";
+ document.getElementById('toggletxt').innerHTML = 'Exit Fullscreen'
} else {
- document.getElementById("toggletxt").innerHTML = "Fullscreen";
+ document.getElementById('toggletxt').innerHTML = 'Fullscreen'
}
- });
+ })
- // remove buttons only for MusicBox
- if (!isMusicBox) {
- $('#navSettings').hide();
- $('#navshutdown').hide();
- $('#homesettings').hide();
- $('#homeshutdown').hide();
+ // Remove MusicBox only content (e.g. settings, system pages)
+ if (!$(document.body).data('is-musicbox')) {
+ $('#navSettings').hide()
+ $('#navshutdown').hide()
+ $('#homesettings').hide()
+ $('#homeshutdown').hide()
}
- // remove Alarm Clock if it is not present
- if (!hasAlarmClock) {
- $('#navAlarmClock').hide();
- $('#homeAlarmClock').hide();
- $('#homeAlarmClock').nextAll().find('.ui-block-a, .ui-block-b').toggleClass('ui-block-a').toggleClass('ui-block-b');
+ // Remove Alarm Clock icons if it is not present
+ if (!$(document.body).data('has-alarmclock')) {
+ $('#navAlarmClock').hide()
+ $('#homeAlarmClock').hide()
+ $('#homeAlarmClock').nextAll().find('.ui-block-a, .ui-block-b').toggleClass('ui-block-a').toggleClass('ui-block-b')
}
- //navigation stuff
+ // navigation stuff
- $(document).keypress( function (event) {
- //console.log('kp: '+event);
- if (event.target.tagName != 'INPUT') {
- var unicode=event.keyCode? event.keyCode : event.charCode;
- var actualkey=String.fromCharCode(unicode);
- switch(actualkey) {
- case ' ':
- doPlay();
- event.preventDefault();
- break;
- case '>':
- doNext();
- event.preventDefault();
- break;
- case '<':
- doPrevious();
- event.preventDefault();
- break;
- }
- return true;
- }
- });
-
+ $(document).keypress(function (event) {
+ // console.log('kp: '+event);
+ if (event.target.tagName !== 'INPUT') {
+ var unicode = event.keyCode ? event.keyCode : event.charCode
+ var actualkey = String.fromCharCode(unicode)
+ switch (actualkey) {
+ case ' ':
+ controls.doPlay()
+ event.preventDefault()
+ break
+ case '>':
+ controls.doNext()
+ event.preventDefault()
+ break
+ case '<':
+ controls.doPrevious()
+ event.preventDefault()
+ break
+ }
+ return true
+ }
+ })
if ($(window).width() < 980) {
- $("#panel").panel("close");
- }else{
- $("#panel").panel("open");
+ $('#panel').panel('close')
+ } else {
+ $('#panel').panel('open')
}
- $.event.special.swipe.horizontalDistanceThreshold = 125; // (default: 30px) Swipe horizontal displacement must be more than this.
- $.event.special.swipe.verticalDistanceThreshold = 50; // (default: 75px) Swipe vertical displacement must be less than this.
- $.event.special.swipe.durationThreshold = 500;
+ $.event.special.swipe.horizontalDistanceThreshold = 125 // (default: 30px) Swipe horizontal displacement must be more than this.
+ $.event.special.swipe.verticalDistanceThreshold = 50 // (default: 75px) Swipe vertical displacement must be less than this.
+ $.event.special.swipe.durationThreshold = 500
// swipe songinfo and panel
- $( "#normalFooter, #nowPlayingFooter" ).on( "swiperight", doPrevious );
- $( "#normalFooter, #nowPlayingFooter" ).on( "swipeleft", doNext );
- $( "#nowPlayingpane, .ui-body-c, #header, #panel, .pane" ).on( "swiperight", function() {
- if(!$(event.target).is("#normalFooter") && !$(event.target).is("#nowPlayingFooter")) {
- $("#panel").panel("open");
- event.stopImmediatePropagation(); }
- } );
- $( "#nowPlayingpane, .ui-body-c, #header, #panel, .pane" ).on( "swipeleft", function() {
- if(!$(event.target).is("#normalFooter") && !$(event.target).is("#nowPlayingFooter")) {
- $("#panel").panel("close");
- event.stopImmediatePropagation(); }
- } );
-
- $( "#trackslider" ).on( "slidestart", function() {
- progressTimer.stop();
- $( "#trackslider" ).on( "change", function() { updatePosition( $(this).val() ); } );
- } );
-
- $( "#trackslider" ).on( "slidestop", function() {
- $( "#trackslider" ).off( "change");
- doSeekPos( $(this).val() );
- } );
-
- $( "#volumeslider" ).on( "slidestart", function() { volumeSliding = true; } );
- $( "#volumeslider" ).on( "slidestop", function() { volumeSliding = false; } );
- $( "#volumeslider" ).on( "change", function() { doVolume( $(this).val() ); } );
-});
-
-function updatePlayIcons (uri, tlid) {
- //update styles of listviews
- $('#currenttable li').each(function() {
- var eachTlid = $(this).attr('tlid');
- if (typeof eachTlid != 'undefined') {
- eachTlid = parseInt(eachTlid);
+ $('#normalFooter, #nowPlayingFooter').on('swiperight', controls.doPrevious)
+ $('#normalFooter, #nowPlayingFooter').on('swipeleft', controls.doNext)
+ $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function (event) {
+ if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) {
+ $('#panel').panel('open')
+ event.stopImmediatePropagation()
}
- if (this.id == 'currenttable-' + uri && eachTlid == tlid) {
- $(this).addClass('currenttrack');
- } else {
- $(this).removeClass("currenttrack");
- }
- });
+ })
+ $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swipeleft', function (event) {
+ if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) {
+ $('#panel').panel('close')
+ event.stopImmediatePropagation()
+ }
+ })
- $('#playlisttracks li').each(function() {
- if (this.id == 'playlisttracks-' + uri) {
- $(this).addClass('currenttrack2');
- } else {
- $(this).removeClass("currenttrack2");
- }
- });
+ $('#trackslider').on('slidestart', function () {
+ syncedProgressTimer.stop()
+ $('#trackslider').on('change', function () { syncedProgressTimer.updatePosition($(this).val()) })
+ })
- $('#trackresulttable li').each(function() {
- if (this.id == 'trackresulttable-' + uri) {
- $(this).addClass('currenttrack2');
- } else {
- $(this).removeClass("currenttrack2");
- }
- });
+ $('#trackslider').on('slidestop', function () {
+ $('#trackslider').off('change')
+ syncedProgressTimer.updatePosition($(this).val())
+ controls.doSeekPos($(this).val())
+ })
- $('#artiststable li').each(function() {
- if (this.id == 'artiststable-' + uri) {
- $(this).addClass('currenttrack2');
- } else {
- $(this).removeClass("currenttrack2");
- }
- });
+ $('#volumeslider').on('slidestart', function () { volumeSliding = true })
+ $('#volumeslider').on('slidestop', function () { volumeSliding = false })
+ $('#volumeslider').on('change', function () { controls.doVolume($(this).val()) })
+})
- $('#albumstable li').each(function() {
- if (this.id == 'albumstable-' + uri) {
- $(this).addClass('currenttrack2');
- } else {
- $(this).removeClass("currenttrack2");
- }
- });
- $('#browselist li').each(function() {
- if (this.id == 'browselisttracks-' + uri) {
- $(this).addClass('currenttrack2');
- } else {
- $(this).removeClass("currenttrack2");
- }
- });
+function updatePlayIcons (uri, tlid, popupMenuIcon) {
+ // Update styles of listviews
+ if (arguments.length < 3) {
+ throw new Error('Missing parameters for "updatePlayIcons" function call.')
+ }
+ var listviews = [PLAYLIST_TABLE, SEARCH_TRACK_TABLE, ARTIST_TABLE, ALBUM_TABLE, BROWSE_TABLE]
+ var target = CURRENT_PLAYLIST_TABLE.substr(1)
+ if (uri && typeof tlid === 'number' && tlid >= 0) {
+ $(CURRENT_PLAYLIST_TABLE).children('li.song.albumli').each(function () {
+ var eachTlid = $(this).attr('tlid')
+ if (typeof eachTlid !== 'undefined') {
+ eachTlid = parseInt(eachTlid)
+ }
+ if (this.id === getjQueryID(target, uri) && eachTlid === tlid) {
+ if (!$(this).hasClass('currenttrack')) {
+ $(this).addClass('currenttrack')
+ }
+ } else if ($(this).hasClass('currenttrack')) {
+ $(this).removeClass('currenttrack')
+ }
+ })
+ }
+
+ var popupElement
+
+ for (var i = 0; i < listviews.length; i++) {
+ target = listviews[i].substr(1)
+ $(listviews[i]).children('li.song.albumli').each(function () {
+ if (uri) {
+ if (this.id === getjQueryID(target, uri)) {
+ $(this).addClass('currenttrack2')
+ } else {
+ $(this).removeClass('currenttrack2')
+ }
+ }
+ if (popupMenuIcon) {
+ popupElement = $(this).children('a.moreBtn').children('i').first()
+ if (!popupElement.hasClass(popupMenuIcon)) {
+ popupElement.removeClass()
+ popupElement.addClass(popupMenuIcon)
+ }
+ }
+ })
+ }
}
diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js
index 652fae3..19f0c28 100644
--- a/mopidy_musicbox_webclient/static/js/images.js
+++ b/mopidy_musicbox_webclient/static/js/images.js
@@ -1,92 +1,282 @@
-/**
- * @author Wouter van Wijk
- */
-
-API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733';
-API_SECRET = '2c631802c2285d5d5d1502462fe42a2b';
-
-var fmcache;
-var lastfm;
-
-$(window).load(function () {
- // create a Cache object
- fmcache = new LastFMCache();
- // create a LastFM object
- lastfm = new LastFM({
- apiKey : API_KEY,
- apiSecret : API_SECRET,
- cache : fmcache
- });
-});
-
-function getCover(uri, images, size) {
- var defUrl = 'images/default_cover.png';
- $(images).attr('src', defUrl);
- if (!uri) {
- return;
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory)
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = factory()
+ } else {
+ root.images = factory()
}
+}(this, function () {
+ 'use strict'
- mopidy.library.getImages({'uris': [uri]}).then(function(imageResults) {
- var uri = Object.keys(imageResults)[0];
- if (imageResults[uri].length > 0) {
- $(images).attr('src', imageResults[uri][0].uri);
- } else {
- // Also check deprecated 'album.images' in case backend does not
- // implement mopidy.library.getImages yet...
- getCoverFromAlbum(uri, images, size);
- }
- });
-}
+ var API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733'
+ var API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'
-// Note that this approach has been deprecated in Mopidy
-// TODO: Remove when Mopidy no longer supports getting images
-// with 'album.images'.
-function getCoverFromAlbum(uri, images, size) {
- mopidy.library.lookup({'uris': [uri]}).then(function(resultDict) {
- var uri = Object.keys(resultDict)[0];
- var track = resultDict[uri][0];
- if (track.album && track.album.images && (track.album.images.length > 0) ) {
- $(images).attr('src', track.album.images[0]);
- } else {
- // Fallback to last.fm
- getCoverFromLastFm(track, images, size);
- }
- });
-}
+ var images = {
-function getCoverFromLastFm(track, images, size) {
- var defUrl = 'images/default_cover.png';
- if (!(track.album || track.artist)) {
- return;
- }
- var albumname = track.album.name || '';
- var artistname = '';
- if ( track.album.artists && (track.album.artists.length > 0) ) {
- // First look for the artist in the album
- artistname = track.album.artists[0].name;
- } else if (track.artists && (track.artists.length > 0) ) {
- // Fallback to using artists for specific track
- artistname = track.artists[0].name;
- }
+ DEFAULT_ALBUM_URL: 'images/default_cover.png',
+ DEFAULT_ARTIST_URL: 'images/user_24x32.png',
- lastfm.album.getInfo( {artist: artistname, album: albumname},
- { success: function(data) {
- for (var i = 0; i < data.album.image.length; i++) {
- if ( data.album.image[i].size == size) {
- $(images).attr('src', data.album.image[i]['#text'] || defUrl);
+ lastFM: new LastFM({
+ apiKey: API_KEY,
+ apiSecret: API_SECRET,
+ cache: new LastFMCache()
+ }),
+
+ /* Extract artist information from Mopidy track. */
+ _getArtistInfo: function (track) {
+ var artistName = ''
+ var musicBrainzID = ''
+
+ if (track && track.artists && (track.artists.length > 0)) {
+ // First look for the artist info in the track
+ artistName = track.artists[0].name
+ musicBrainzID = track.artists[0].musicbrainz_id
+ }
+
+ if ((!artistName || !musicBrainzID) && (track && track.album && track.album.artists && track.album.artists.length > 0)) {
+ // Fallback to using artist info contained in the track's album
+ artistName = artistName || track.album.artists[0].name
+ musicBrainzID = musicBrainzID || track.album.artists[0].musicbrainz_id
+ }
+
+ return {mbid: musicBrainzID, name: artistName}
+ },
+
+ /* Utility function for retrieving artist informaton for the given track from last.fm */
+ _getLastFmArtistInfo: function (track) {
+ var artist = images._getArtistInfo(track)
+ var artistPromise = $.Deferred()
+
+ if (!(track && (track.musicbrainz_id || (track.name && artist && artist.name)))) {
+ // Avoid expensive last.fm call if tag information is missing.
+ return artistPromise.reject('none', 'Not enough tag information available for track to make last.fm call.')
+ }
+
+ var params = {}
+ // Only add arguments to parameter object if values are available for them.
+ if (track.musicbrainz_id) {
+ params.mbid = track.musicbrainz_id
+ }
+ if (track.name && artist.name) {
+ params.track = track.name
+ params.artist = artist.name
+ }
+
+ images.lastFM.track.getInfo(params, {success: function (data) {
+ artistPromise.resolve(data.track.artist)
+ }, error: function (code, message) {
+ artistPromise.reject(code, message)
+ }})
+
+ return artistPromise
+ },
+
+ /* Utility function for retrieving information for the given track from last.fm. */
+ _getLastFmAlbumInfo: function (track) {
+ var artist = images._getArtistInfo(track)
+ var albumPromise = $.Deferred()
+
+ if (!(track && track.album && (track.album.musicbrainz_id || (track.album.name && artist && artist.name)))) {
+ // Avoid expensive last.fm call if tag information is missing.
+ return albumPromise.reject('none', 'Not enough tag information available for album to make last.fm call.')
+ }
+
+ var musicBrainzID = track.album.musicbrainz_id
+ var albumName = track.album.name
+ var artistName = images._getArtistInfo(track).name
+
+ var params = {}
+ // Only add arguments to parameter object if values are available for them.
+ if (musicBrainzID) {
+ params.mbid = musicBrainzID
+ }
+ if (artistName && albumName) {
+ params.artist = artistName
+ params.album = albumName
+ }
+
+ images.lastFM.album.getInfo(params, {success: function (data) {
+ albumPromise.resolve(data)
+ }, error: function (code, message) {
+ albumPromise.reject(code, message)
+ }})
+
+ return albumPromise
+ },
+
+ /**
+ * Sets an HTML image element to contain the album cover art of the relevant Mopidy track.
+ *
+ * Potential sources for the album image will be interrogated in the following order until
+ * a suitable image URI is found:
+ * 1.) mopidy.library.getImages
+ * 2.) mopidy.models.Track.album.images (DEPRECATED)
+ * 3.) last.fm using the album MusicBrainz ID
+ * 4.) last.fm using the album name and track artist name
+ * 5.) last.fm using the album name and album artist name
+ * 6.) a default image
+ *
+ * @param {string} uri - The URI of the Mopidy track to retrieve the album cover image for.
+ * @param {string} img_element - The identifier of the HTML image element that will be used
+ * to render the image.
+ * @param {object} mopidy - The Mopidy.js object that should be used to communicate with the
+ * Mopidy server.
+ * @param {string} size - (Optional) The preferred size of the image. This parameter is only
+ * used in the last.fm lookups if Mopidy does not provide the image
+ * directly. Can be one of 'small', 'medium', 'large',
+ * 'extralarge' (default), or 'mega'.
+ */
+ setAlbumImage: function (uri, img_element, mopidy, size) {
+ // Set default immediately while we're busy retrieving actual image.
+ $(img_element).attr('src', images.DEFAULT_ALBUM_URL)
+ if (!uri) {
+ return
+ }
+ size = size || 'extralarge'
+
+ mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) {
+ var uri = Object.keys(imageResults)[0]
+ if (imageResults[uri].length > 0) {
+ $(img_element).attr('src', imageResults[uri][0].uri)
+ } else {
+ // Also check deprecated 'album.images' in case backend does not
+ // implement mopidy.library.getImages yet...
+ images._setDeprecatedAlbumImage(uri, img_element, mopidy, size)
}
- }
- }
- });
-}
+ })
+ },
-function getArtistImage(nwartist, image, size) {
- var defUrl = 'images/user_24x32.png';
- lastfm.artist.getInfo({artist: nwartist}, {success: function(data){
- for (var i = 0; i < data.artist.image.length; i++) {
- if ( data.artist.image[i].size == size) {
- $(image).attr('src', data.artist.image[i]['#text'] || defUrl);
+ // Note that this approach has been deprecated in Mopidy
+ // TODO: Remove when Mopidy no longer supports retrieving images
+ // from 'album.images'.
+ /* Set album image using mopidy.album.images. */
+ _setDeprecatedAlbumImage: function (uri, img_element, mopidy, size) {
+ if (!uri) {
+ $(img_element).attr('src', images.DEFAULT_ALBUM_URL)
+ return
}
+ size = size || 'extralarge'
+
+ mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
+ var uri = Object.keys(resultDict)[0]
+ var track = resultDict[uri][0]
+ if (track && track.album && track.album.images && track.album.images.length > 0) {
+ $(img_element).attr('src', track.album.images[0])
+ } else {
+ // Fallback to last.fm
+ images._setLastFmAlbumImage(track, img_element, size)
+ }
+ })
+ },
+
+ /* Lookup album image on last.fm using the provided Mopidy track. */
+ _setLastFmAlbumImage: function (track, img_element, size) {
+ if (!track || !(track.album || track.artists)) {
+ $(img_element).attr('src', images.DEFAULT_ALBUM_URL)
+ return
+ }
+ size = size || 'extralarge'
+
+ images._getLastFmAlbumInfo(track).then(function (data) {
+ for (var i = 0; i < data.album.image.length; i++) {
+ if (data.album.image[i].size === size) {
+ $(img_element).attr('src', data.album.image[i]['#text'] || images.DEFAULT_ALBUM_URL)
+ break
+ }
+ }
+ }, function (code, message) {
+ $(img_element).attr('src', images.DEFAULT_ALBUM_URL)
+ console.error('Error getting album info from last.fm (%s: %s)', code, message)
+ })
+ },
+
+ /**
+ * Sets an HTML image element to contain the artist image of the relevant Mopidy track.
+ *
+ * Potential sources of the artist image will be interrogated in the following order until
+ * a suitable image URI is found:
+ * 1.) mopidy.library.getImages
+ * 2.) last.fm using the artist MusicBrainz ID. If no artist ID is provided, it will be
+ * looked up on last.fm first using the track and album details.
+ * 3.) a default image
+ *
+ * @param {string} artist_uri - The URI of the Mopidy artist to retrieve the image for.
+ * @param {string} track_uri - The URI of the Mopidy track that will be used as a fallback
+ * if the artist URI does not provide any image results.
+ * @param {string} img_element - The identifier of the HTML image element that will be used
+ * to render the image.
+ * @param {object} mopidy - The Mopidy.js object that should be used to communicate with the
+ * Mopidy server.
+ * @param {string} size - (Optional) The preferred size of the image. This parameter is only
+ * used in the last.fm lookups if Mopidy does not provide the image
+ * directly. Can be one of 'small', 'medium', 'large',
+ * 'extralarge' (default), or 'mega'.
+ */
+ setArtistImage: function (artist_uri, track_uri, img_element, mopidy, size) {
+ // Set default immediately while we're busy retrieving actual image.
+ $(img_element).attr('src', images.DEFAULT_ARTIST_URL)
+ if (!artist_uri && !track_uri) {
+ return
+ }
+ size = size || 'extralarge'
+
+ if (artist_uri) {
+ // Use artist as starting point for retrieving image.
+ mopidy.library.getImages({'uris': [artist_uri]}).then(function (imageResults) {
+ var uri = Object.keys(imageResults)[0]
+ if (imageResults[uri].length > 0) {
+ $(img_element).attr('src', imageResults[uri][0].uri)
+ } else {
+ // Fall back to using track as starting point for retrieving image.
+ images._setArtistImageFromTrack(track_uri, img_element, mopidy, size)
+ }
+ })
+ }
+ },
+
+ /* Set artist image using the supplied Mopidy track URI. */
+ _setArtistImageFromTrack: function (uri, img_element, mopidy, size) {
+ mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
+ var uri = Object.keys(resultDict)[0]
+ var track = resultDict[uri][0]
+ var artist = images._getArtistInfo(track)
+
+ if (artist.mbid) {
+ images._setLastFmArtistImage(artist.mbid, img_element, size)
+ } else {
+ // Look up unique MusicBrainz ID for artist first using the available track information
+ images._getLastFmArtistInfo(track).then(function (artist) {
+ images._setLastFmArtistImage(artist.mbid, img_element, size)
+ }, function (code, message) {
+ $(img_element).attr('src', images.DEFAULT_ARTIST_URL)
+ console.error('Error retrieving artist info from last.fm. (%s: %s)', code, message)
+ })
+ }
+ })
+ },
+
+ /* Set artist image using the supplied artist MusicBrainz ID. */
+ _setLastFmArtistImage: function (mbid, img_element, size) {
+ if (!mbid) {
+ // Avoid expensive last.fm call if tag information is missing.
+ $(img_element).attr('src', images.DEFAULT_ARTIST_URL)
+ return
+ }
+ size = size || 'extralarge'
+
+ images.lastFM.artist.getInfo({mbid: mbid}, {success: function (data) {
+ for (var i = 0; i < data.artist.image.length; i++) {
+ if (data.artist.image[i].size === size) {
+ $(img_element).attr('src', data.artist.image[i]['#text'] || images.DEFAULT_ARTIST_URL)
+ break
+ }
+ }
+ }, error: function (code, message) {
+ $(img_element).attr('src', images.DEFAULT_ARTIST_URL)
+ console.error('Error retrieving artist info from last.fm. (%s: %s)', code, message)
+ }})
}
- }});
-}
\ No newline at end of file
+ }
+ return images
+}))
diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js
index 3a62e05..13e63e8 100644
--- a/mopidy_musicbox_webclient/static/js/library.js
+++ b/mopidy_musicbox_webclient/static/js/library.js
@@ -1,344 +1,349 @@
-/*********************************
- * Search
- *********************************/
-function searchPressed(key) {
- var value = $('#searchinput').val();
- switchContent('search');
-
- if (key == 13) {
- initSearch();
- return false;
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory)
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = factory()
+ } else {
+ root.library = factory()
}
- return true;
-}
+}(this, function () {
+ 'use strict'
-//init search
-function initSearch() {
- var value = $('#searchinput').val();
- var searchService = $('#selectSearchService').val();
+ var library = {
- if ((value.length < 100) && (value.length > 0)) {
- showLoading(true);
- //hide ios/android keyboard
- document.activeElement.blur();
- $("input").blur();
+ /** *******************************
+ * Search
+ *********************************/
+ searchPressed: function (key) {
+ var value = $('#searchinput').val()
+ switchContent('search')
- delete customTracklists[URI_SCHEME+':allresultscache'];
- delete customTracklists[URI_SCHEME+':artistresultscache'];
- delete customTracklists[URI_SCHEME+':albumresultscache'];
- delete customTracklists[URI_SCHEME+':trackresultscache'];
- $("#searchartists").hide();
- $("#searchalbums").hide();
- $("#searchtracks").hide();
+ if (key === 13) {
+ library.initSearch()
+ return false
+ }
+ return true
+ },
- if (searchService != 'all') {
- mopidy.library.search({'query': {any:[value]}, 'uris': [searchService + ':']}).then(processSearchResults, console.error);
- } else {
- mopidy.getUriSchemes().then(function (schemes) {
- var query = {},
- uris = [];
+ // init search
+ initSearch: function () {
+ var value = $('#searchinput').val()
+ var searchService = $('#selectSearchService').val()
+ $.cookie('searchScheme', searchService, { expires: 365 })
- var regexp = $.map(schemes, function (scheme) {
- return '^' + scheme + ':';
- }).join('|');
+ if ((value.length < 100) && (value.length > 0)) {
+ showLoading(true)
+ // hide ios/android keyboard
+ document.activeElement.blur()
+ $('input').blur()
- var match = value.match(regexp);
- if (match) {
- var scheme = match[0];
- query = {uri: [value]};
- uris = [scheme];
+ delete customTracklists[URI_SCHEME + ':trackresultscache']
+ $('#searchartists').hide()
+ $('#searchalbums').hide()
+ $('#searchtracks').hide()
+
+ if (searchService !== 'all') {
+ mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(library.processSearchResults, console.error)
} else {
- query = {any: [value]};
- }
- mopidy.library.search({'query': query, 'uris': uris}).then(processSearchResults, console.error);
- });
- }
- }
-}
+ mopidy.getUriSchemes().then(function (schemes) {
+ var query = {}
+ var uris = []
-/********************************************************
- * process results of a search
- *********************************************************/
+ var regexp = $.map(schemes, function (scheme) {
+ return '^' + scheme + ':'
+ }).join('|')
-//# speed clone http://jsperf.com/cloning-an-object/2
-function clone(obj) {
- var target = {};
- for (var i in obj) {
- if (obj.hasOwnProperty(i)) {
- target[i] = obj[i];
- }
- }
- return target;
-}
-
-function processSearchResults(resultArr) {
- $(SEARCH_TRACK_TABLE).empty();
- $(SEARCH_ARTIST_TABLE).empty();
- $(SEARCH_ALBUM_TABLE).empty();
-
- // Merge results from different backends.
- // TODO should of coures have multiple tables
- var results = {'tracks': [], 'artists': [], 'albums': []};
- var j, emptyResult = true;
-
- for (var i = 0; i < resultArr.length; i++) {
- if (resultArr[i].tracks) {
- for (j = 0; j < resultArr[i].tracks.length; j++) {
- results.tracks.push(resultArr[i].tracks[j]);
- emptyResult = false;
- }
- }
- if (resultArr[i].artists) {
- for (j = 0; j < resultArr[i].artists.length; j++) {
- results.artists.push(resultArr[i].artists[j]);
- emptyResult = false;
- }
- }
- if (resultArr[i].albums) {
- for (j = 0; j < resultArr[i].albums.length; j++) {
- results.albums.push(resultArr[i].albums[j]);
- emptyResult = false;
- }
- }
- }
-
- customTracklists[URI_SCHEME+':trackresultscache'] = results.tracks;
-
- if (emptyResult) {
- toast('No results');
- showLoading(false);
- return false;
- }
-
- if (results.artists.length > 0) {
- $("#searchartists").show();
- }
-
- if (results.albums.length > 0) {
- $("#searchalbums").show();
- }
-
- if (results.tracks.length > 0) {
- $("#searchtracks").show();
- }
-
- // Returns a string where {x} in template is replaced by tokens[x].
- function theme(template, tokens) {
- return template.replace(/{[^}]+}/g, function(match) {
- return tokens[match.slice(1,-1)];
- });
- }
-
- // 'Show more' pattern
- var showMorePattern = '