]> git.wh0rd.org Git - chrome-ext/music-player-client.git/commitdiff
initial extension v1.0
authorMike Frysinger <vapier@gentoo.org>
Tue, 20 Aug 2013 04:30:26 +0000 (00:30 -0400)
committerMike Frysinger <vapier@gentoo.org>
Tue, 20 Aug 2013 04:30:26 +0000 (00:30 -0400)
13 files changed:
.gitignore [new file with mode: 0644]
TODO [new file with mode: 0644]
images/icon-128x128.png [new file with mode: 0644]
images/screenshot.png [new file with mode: 0644]
images/small-tile.png [new file with mode: 0644]
js/mpc.js [new file with mode: 0644]
js/tcp-client.js [new file with mode: 0644]
launcher.js [new file with mode: 0644]
main.html [new file with mode: 0644]
main.js [new file with mode: 0644]
makedist.sh [new file with mode: 0755]
manifest.files [new file with mode: 0644]
manifest.json [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..c4c4ffc
--- /dev/null
@@ -0,0 +1 @@
+*.zip
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..7d7c32d
--- /dev/null
+++ b/TODO
@@ -0,0 +1,5 @@
+make UI not so crappy
+
+support keyboard shortcuts
+
+playlist support
diff --git a/images/icon-128x128.png b/images/icon-128x128.png
new file mode 100644 (file)
index 0000000..4238d24
Binary files /dev/null and b/images/icon-128x128.png differ
diff --git a/images/screenshot.png b/images/screenshot.png
new file mode 100644 (file)
index 0000000..dc35ed0
Binary files /dev/null and b/images/screenshot.png differ
diff --git a/images/small-tile.png b/images/small-tile.png
new file mode 100644 (file)
index 0000000..ca586d9
Binary files /dev/null and b/images/small-tile.png differ
diff --git a/js/mpc.js b/js/mpc.js
new file mode 100644 (file)
index 0000000..bf2d969
--- /dev/null
+++ b/js/mpc.js
@@ -0,0 +1,187 @@
+// Written by Mike Frysinger <vapier@gmail.com>.  Released into the public domain.  Suck it.
+
+function Mpc(socket, cb_update_state) {
+       this._socket = socket;
+       this._cb_update_state = cb_update_state;
+       this._queue = ['init'];
+       this.state = {};
+}
+
+Mpc.log = function(msg, obj) {
+       console.log('mpc: ' + msg, obj);
+}
+
+Mpc.prototype.send = function(msg) {
+       this._queue.push(msg);
+       this._socket.send(msg, function(x) {
+               Mpc.log('send: ' + msg + ':', x);
+       });
+}
+
+Mpc.prototype.recv_msg = function(lines) {
+       curr = this._queue.shift();
+       Mpc.log('recv: [' + curr + ']:', lines.join('\n'));
+       curr = curr.split(' ');
+
+       switch (curr[0]) {
+       // Needs to return a list of dicts (see above for dicts).
+       //case 'playlistinfo':
+       case 'currentsong':
+       case 'stats':
+       case 'status':
+               state = {};
+               lines.forEach(function(line) {
+                       i = line.indexOf(':');
+                       if (i == -1)
+                               return; // Ignores the OK line
+                       key = line.substr(0, i);
+                       val = line.substr(i + 2);
+                       state[key] = val;
+               });
+               this.state = state;
+               this._cb_update_state(state);
+               break;
+       default:
+               this._cb_update_state(lines, curr);
+               break;
+       }
+}
+
+Mpc.prototype.recv = function(msg) {
+       /* We can get back a bunch of responses in a row, so parse them out */
+       /* XXX: Do we have to handle partial reads ?  like long playlists ... */
+       lines = msg.split('\n');
+       var i = 0;
+       while (i < lines.length) {
+               if (lines[i] == 'OK' || lines[i].substr(0, 3) == 'OK ') {
+                       this.recv_msg(lines.splice(0, i + 1));
+                       i = 0;
+               } else
+                       ++i;
+       }
+}
+
+/*
+ * Command generator helpers.
+ */
+
+Mpc.__make_send_void = function(cmd) {
+       return function() { this.send(cmd); }
+}
+
+Mpc.__make_send_arg1 = function(cmd) {
+       return function(a1) {
+               if (a1 === undefined)
+                       Mpc.log(cmd + ': function requires one argument');
+               else
+                       this.send(cmd + ' ' + a1);
+       }
+}
+
+Mpc.__make_send_arg2 = function(cmd) {
+       return function(a1, a2) {
+               if (a1 === undefined || a2 === undefined)
+                       Mpc.log(cmd + ': function requires two arguments');
+               else
+                       this.send(cmd + ' ' + a1 + ' ' + a2);
+       }
+}
+
+Mpc.__make_send_opt = function(cmd) {
+       return function(arg) {
+               if (arg === undefined)
+                       arg = '';
+               this.send(cmd + ' ' + arg);
+       };
+}
+
+Mpc.__make_send_range = function(cmd, min, max, def) {
+       return function(arg) {
+               if (arg === undefined)
+                       arg = def;
+               if (arg >= min && arg <= max)
+                       this.send(cmd + ' ' + arg);
+               else
+                       Mpc.log(cmd + ': arg must be [' + min + ',' + max + '] but got "' + arg + '"');
+       };
+}
+
+/*
+ * Querying MPD's status
+ * http://www.musicpd.org/doc/protocol/ch03.html#idp118752
+ */
+
+// clearerror
+Mpc.prototype.clearerror          = Mpc.__make_send_void('clearerror');
+// currentsong
+Mpc.prototype.currentsong         = Mpc.__make_send_void('currentsong');
+// idle [SUBSYSTEMS...]
+// TODO
+// status
+Mpc.prototype.status              = Mpc.__make_send_void('status');
+// stats
+Mpc.prototype.stats               = Mpc.__make_send_void('stats');
+
+/*
+ * Playback options
+ * http://www.musicpd.org/doc/protocol/ch03s02.html
+ */
+
+// consume {STATE}
+Mpc.prototype.consume             = Mpc.__make_send_range('consume', 0, 1, 1);
+// crossfade {SECONDS}
+Mpc.prototype.consume             = Mpc.__make_send_arg1('crossfade');
+// mixrampdb {deciBels}
+Mpc.prototype.mixrampdb           = Mpc.__make_send_arg1('mixrampdb');
+// mixrampdelay {SECONDS|nan}
+// Note: Probably should handle javascript NaN here.
+Mpc.prototype.mixrampdelay        = Mpc.__make_send_arg1('mixrampdelay');
+// random {STATE}
+Mpc.prototype.random              = Mpc.__make_send_range('random', 0, 1, 1);
+// repeat {STATE}
+Mpc.prototype.repeat              = Mpc.__make_send_range('repeat', 0, 1, 1);
+// setvol {VOL}
+Mpc.prototype.setvol              = Mpc.__make_send_range('setvol', 0, 100);
+// single {STATE}
+Mpc.prototype.single              = Mpc.__make_send_range('single', 0, 1, 1);
+// replay_gain_mode {MODE}
+Mpc.prototype.replay_gain_mode    = Mpc.__make_send_arg1('replay_gain_mode');
+// replay_gain_status
+
+/*
+ * Controlling playback
+ * http://www.musicpd.org/doc/protocol/ch03s03.html
+ */
+
+// next
+Mpc.prototype.next                = Mpc.__make_send_void('next');
+// pause {PAUSE}
+Mpc.prototype.pause               = Mpc.__make_send_range('pause', 0, 1, 1);
+// play [SONGPOS]
+Mpc.prototype.play                = Mpc.__make_send_opt('play');
+// playid [SONGID]
+Mpc.prototype.playid              = Mpc.__make_send_opt('playid');
+// previous
+Mpc.prototype.previous            = Mpc.__make_send_void('previous');
+// seek {SONGPOS} {TIME}
+Mpc.prototype.seek                = Mpc.__make_send_arg2('seek');
+// seekid {SONGID} {TIME}
+Mpc.prototype.seekid              = Mpc.__make_send_arg2('seekid');
+// seekcur {TIME}
+Mpc.prototype.seekcur             = Mpc.__make_send_arg1('seek');
+// stop
+Mpc.prototype.stop                = Mpc.__make_send_void('stop');
+
+/*
+ * Connection settings
+ * http://www.musicpd.org/doc/protocol/ch03s08.html
+ */
+
+// close
+Mpc.prototype.close               = Mpc.__make_send_void('close');
+// kill
+Mpc.prototype.kill                = Mpc.__make_send_void('kill');
+// password {PASSWORD}
+Mpc.prototype.password            = Mpc.__make_send_arg1('password');
+// ping
+Mpc.prototype.ping                = Mpc.__make_send_void('ping');
diff --git a/js/tcp-client.js b/js/tcp-client.js
new file mode 100644 (file)
index 0000000..8d1db93
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+Copyright 2012 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+Author: Boris Smus (smus@chromium.org)
+*/
+
+(function(exports) {
+
+  // Define some local variables here.
+  var socket = chrome.socket;
+
+  /**
+   * Creates an instance of the client
+   *
+   * @param {String} host The remote host to connect to
+   * @param {Number} port The port to connect to at the remote host
+   */
+  function TcpClient(host, port) {
+    this.host = host;
+    this.port = port;
+
+    // Callback functions.
+    this.callbacks = {
+      connect: null,    // Called when socket is connected.
+      disconnect: null, // Called when socket is disconnected.
+      recv: null,       // Called when client receives data from server.
+      sent: null        // Called when client sends data to server.
+    };
+
+    // Socket.
+    this.socketId = null;
+    this.isConnected = false;
+
+    log('initialized tcp client');
+  }
+
+  /**
+   * Connects to the TCP socket, and creates an open socket.
+   *
+   * @see http://developer.chrome.com/trunk/apps/socket.html#method-create
+   * @param {Function} callback The function to call on connection
+   */
+  TcpClient.prototype.connect = function(callback) {
+    socket.create('tcp', {}, this._onCreate.bind(this));
+
+    // Register connect callback.
+    this.callbacks.connect = callback;
+  };
+
+  /**
+   * Sends a message down the wire to the remote side
+   *
+   * @see http://developer.chrome.com/trunk/apps/socket.html#method-write
+   * @param {String} msg The message to send
+   * @param {Function} callback The function to call when the message has sent
+   */
+  TcpClient.prototype.sendMessage = function(msg, callback) {
+    this._stringToArrayBuffer(msg + '\n', function(arrayBuffer) {
+      socket.write(this.socketId, arrayBuffer, this._onWriteComplete.bind(this));
+    }.bind(this));
+
+    // Register sent callback.
+    this.callbacks.sent = callback;
+  };
+
+  /**
+   * Sets the callback for when a message is received
+   *
+   * @param {Function} callback The function to call when a message has arrived
+   */
+  TcpClient.prototype.addResponseListener = function(callback) {
+    // Register received callback.
+    this.callbacks.recv = callback;
+  };
+
+  /**
+   * Disconnects from the remote side
+   *
+   * @see http://developer.chrome.com/trunk/apps/socket.html#method-disconnect
+   */
+  TcpClient.prototype.disconnect = function() {
+    socket.disconnect(this.socketId);
+    this.isConnected = false;
+  };
+
+  /**
+   * The callback function used for when we attempt to have Chrome
+   * create a socket. If the socket is successfully created
+   * we go ahead and connect to the remote side.
+   *
+   * @private
+   * @see http://developer.chrome.com/trunk/apps/socket.html#method-connect
+   * @param {Object} createInfo The socket details
+   */
+  TcpClient.prototype._onCreate = function(createInfo) {
+    this.socketId = createInfo.socketId;
+    if (this.socketId > 0) {
+      socket.connect(this.socketId, this.host, this.port, this._onConnectComplete.bind(this));
+      this.isConnected = true;
+    } else {
+      error('Unable to create socket');
+    }
+  };
+
+  /**
+   * The callback function used for when we attempt to have Chrome
+   * connect to the remote side. If a successful connection is
+   * made then polling starts to check for data to read
+   *
+   * @private
+   * @param {Number} resultCode Indicates whether the connection was successful
+   */
+  TcpClient.prototype._onConnectComplete = function(resultCode) {
+    // Start polling for reads.
+    setInterval(this._periodicallyRead.bind(this), 500);
+
+    if (this.callbacks.connect) {
+      log('connect complete');
+      this.callbacks.connect();
+    }
+    log('onConnectComplete');
+  };
+
+  /**
+   * Checks for new data to read from the socket
+   *
+   * @see http://developer.chrome.com/trunk/apps/socket.html#method-read
+   */
+  TcpClient.prototype._periodicallyRead = function() {
+    socket.read(this.socketId, null, this._onDataRead.bind(this));
+  };
+
+  /**
+   * Callback function for when data has been read from the socket.
+   * Converts the array buffer that is read in to a string
+   * and sends it on for further processing by passing it to
+   * the previously assigned callback function.
+   *
+   * @private
+   * @see TcpClient.prototype.addResponseListener
+   * @param {Object} readInfo The incoming message
+   */
+  TcpClient.prototype._onDataRead = function(readInfo) {
+    // Call received callback if there's data in the response.
+    if (readInfo.resultCode > 0 && this.callbacks.recv) {
+      log('onDataRead');
+      // Convert ArrayBuffer to string.
+      this._arrayBufferToString(readInfo.data, function(str) {
+        this.callbacks.recv(str);
+      }.bind(this));
+    }
+  };
+
+  /**
+   * Callback for when data has been successfully
+   * written to the socket.
+   *
+   * @private
+   * @param {Object} writeInfo The outgoing message
+   */
+  TcpClient.prototype._onWriteComplete = function(writeInfo) {
+    log('onWriteComplete');
+    // Call sent callback.
+    if (this.callbacks.sent) {
+      this.callbacks.sent(writeInfo);
+    }
+  };
+
+  /**
+   * Converts an array buffer to a string
+   *
+   * @private
+   * @param {ArrayBuffer} buf The buffer to convert
+   * @param {Function} callback The function to call when conversion is complete
+   */
+  TcpClient.prototype._arrayBufferToString = function(buf, callback) {
+    var bb = new Blob([new Uint8Array(buf)]);
+    var f = new FileReader();
+    f.onload = function(e) {
+      callback(e.target.result);
+    };
+    f.readAsText(bb);
+  };
+
+  /**
+   * Converts a string to an array buffer
+   *
+   * @private
+   * @param {String} str The string to convert
+   * @param {Function} callback The function to call when conversion is complete
+   */
+  TcpClient.prototype._stringToArrayBuffer = function(str, callback) {
+    var bb = new Blob([str]);
+    var f = new FileReader();
+    f.onload = function(e) {
+        callback(e.target.result);
+    };
+    f.readAsArrayBuffer(bb);
+  };
+
+  /**
+   * Wrapper function for logging
+   */
+  function log(msg) {
+    //console.log('tcp-client: ', msg);
+  }
+
+  /**
+   * Wrapper function for error logging
+   */
+  function error(msg) {
+    console.error('tcp-client: ', msg);
+  }
+
+  exports.TcpClient = TcpClient;
+
+})(window);
diff --git a/launcher.js b/launcher.js
new file mode 100644 (file)
index 0000000..a4b91cc
--- /dev/null
@@ -0,0 +1,10 @@
+// Written by Mike Frysinger <vapier@gmail.com>.  Released into the public domain.  Suck it.
+
+chrome.app.runtime.onLaunched.addListener(function() {
+       chrome.app.window.create('main.html', {
+               bounds: {
+                       width: 300,
+                       height: 120
+               }
+       });
+});
diff --git a/main.html b/main.html
new file mode 100644 (file)
index 0000000..8f5e3c4
--- /dev/null
+++ b/main.html
@@ -0,0 +1,125 @@
+<!-- Written by Mike Frysinger <vapier@gmail.com>.  Released into the public domain.  Suck it. -->
+<!doctype html>
+<html>
+
+<head>
+<title>Media Player Client</title>
+<script src='js/tcp-client.js'></script>
+<script src='js/mpc.js'></script>
+<script src='main.js'></script>
+<style>
+table {
+       margin: 0px;
+       padding: 0px;
+       border-collapse: collapse;
+}
+body {
+       margin: 0px;
+       padding: 0px;
+}
+td {
+       white-space: nowrap;
+}
+div#status {
+       white-space: nowrap;
+}
+
+table.tabs td {
+       padding-left: 1em;
+       border: black solid 1px;
+       border-top: 0px;
+       font-size: smaller;
+       font-family: sans-serif;
+       background-color: #d0d0d0;
+}
+table.tabs td.selected {
+       background-color: white;
+}
+
+/* Get the tabs to float at the bottom */
+html, body {
+       height: 100%;
+}
+div#body {
+       min-height: 100%;
+}
+div.main {
+       overflow: auto;
+       padding-bottom: 1.3em;
+}
+div#footer {
+       position: relative;
+       margin-top: -1.3em;
+       height: 1.3em;
+       clear: both;
+}
+</style>
+</head>
+
+<body>
+
+<div id='body'>
+
+<div class='main' id='main.controls'>
+<table>
+ <tr>
+  <td>
+   <input type='button' id='previous' value='&#x21E6;'><input type='button' id='stop' value='&#x25FB;'><input type='button' id='pause' value='&#x25EB;'><input type='button' id='play' value='&#x25B7;'><input type='button' id='next' value='&#x21E8;'>
+  </td>
+  <td style='whitespace:collapse'>
+   <input type='button' id='repeat' value='&#x221E;'><input type='button' id='random' value='&reg;'><input type='button' id='single' value='&sect;'><input type='button' id='consume' value='&copy;'>
+  </td>
+ </tr>
+ <tr>
+  <td>
+   <input type='range' id='setvol' min='0' max='100' value='0'>
+  </td>
+  <td>
+   <input type='range' id='seekcur' min='0' max='100' value='0'>
+  </td>
+ </tr>
+ <tr>
+  <td colspan=2>
+   <span id='status'>Loading...</span>
+  </td>
+ </tr>
+</table>
+</div>
+
+<div class='main' id='main.options' style='display: none'>
+<table>
+ <tr>
+  <td>Host:</td>
+  <td><input type='text' id='host' value='192.168.0.2'></td>
+ </tr>
+ <tr>
+  <td>Port:</td>
+  <td><input type='number' id='port' value='6600'></td>
+ </tr>
+ <tr>
+  <td colspan=2>
+   <input type='button' id='connect' value='connect'>
+   &nbsp;&nbsp;&nbsp;
+   Sync Settings:<input type='checkbox' id='sync' checked></td>
+ </tr>
+</table>
+</div>
+
+<div class='main' id='main.metadata' style='display: none'>
+<div id='metadata'></div>
+</div>
+
+</div>
+
+<div id='footer'>
+<table class='tabs'>
+ <tr>
+  <td id='tab.controls' class='selected'>Controls</td>
+  <td id='tab.metadata'>Metadata</td>
+  <td id='tab.options'>Options</td>
+ </tr>
+</table>
+</div>
+
+</body>
+</html>
diff --git a/main.js b/main.js
new file mode 100644 (file)
index 0000000..08efb72
--- /dev/null
+++ b/main.js
@@ -0,0 +1,236 @@
+// Written by Mike Frysinger <vapier@gmail.com>.  Released into the public domain.  Suck it.
+
+/* Globals to allow easy manipulation via javascript console */
+var mpc;
+var tcpclient;
+
+function TcpClientSender(tcpclient) {
+       this.tcpclient = tcpclient;
+}
+TcpClientSender.prototype.send = function(data, cb) {
+       this.tcpclient.sendMessage(data, cb);
+}
+
+function tramp_mpc_recv(data) {
+       mpc.recv(data);
+}
+
+function sync_storage(sync) {
+       return sync ? chrome.storage.sync : chrome.storage.local;
+}
+
+window.onload = function() {
+       var local_keys = [
+               'sync',
+       ];
+       var sync_keys = [
+               'host', 'port',
+       ];
+       var options = {
+               'host': '192.168.0.2',
+               'port': 6600,
+               'sync': true,
+       };
+
+       chrome.storage.local.get(local_keys, function(settings) {
+               local_keys.forEach(function(key) {
+                       if (key in settings)
+                               options[key] = settings[key]
+               });
+
+               var storage = sync_storage(options['sync']);
+               storage.get(sync_keys, function(settings) {
+                       sync_keys.forEach(function(key) {
+                               if (key in settings)
+                                       options[key] = settings[key];
+                       });
+
+                       init_ui(local_keys, sync_keys, options);
+                       mpc_connect();
+               });
+       });
+};
+
+function mpc_refresh() {
+       mpc.status();
+       mpc.currentsong();
+}
+
+function mpc_connect(host, port) {
+       if (typeof(host) != 'string') {
+               host = window['opts_host'].value;
+               port = parseInt(window['opts_port'].value);
+       }
+
+       if (mpc != undefined) {
+               console.log('disconnecting');
+               update_ui('disconnect');
+               delete mpc;
+               tcpclient.disconnect();
+               delete tcpclient;
+       }
+
+       update_ui('init');
+       tcpclient = new TcpClient(host, port);
+       tcpclient.connect(function() {
+               var mpc_sender = new TcpClientSender(tcpclient);
+               tcpclient.addResponseListener(tramp_mpc_recv);
+               mpc = new Mpc(mpc_sender, update_ui);
+               console.log('connected to ' + host + ':' + port);
+               mpc_refresh();
+       });
+}
+
+function tramp_mpc_consume() {
+       var val = zo(!getToggleButton(this));
+       mpc.consume(val);
+       setToggleButton(this, val);
+}
+function tramp_mpc_next() { mpc.next(); }
+function tramp_mpc_pause() { mpc.pause(); }
+function tramp_mpc_play() { mpc.play(); }
+function tramp_mpc_previous() { mpc.previous(); }
+function tramp_mpc_random() {
+       var val = zo(!getToggleButton(this));
+       mpc.random(val);
+       setToggleButton(this, val);
+}
+function tramp_mpc_repeat() {
+       var val = zo(!getToggleButton(this));
+       mpc.repeat(val);
+       setToggleButton(this, val);
+}
+function tramp_mpc_seekcur() { mpc.seekcur(this.value); }
+function tramp_mpc_setvol() { mpc.setvol(this.value); }
+function tramp_mpc_single() {
+       var val = zo(!getToggleButton(this));
+       mpc.single(val);
+       setToggleButton(this, val);
+}
+function tramp_mpc_stop() { mpc.stop(); }
+
+function zo(val) {
+       return val ? 1 : 0;
+}
+function szo(val) {
+       return val == '0' ? 0 : 1;
+}
+function getToggleButton(btn) {
+       return btn.style.borderStyle == 'inset';
+}
+function setToggleButton(btn, val) {
+       if (val === undefined)
+               val = !getToggleButton(btn);
+       btn.style.borderStyle = val ? 'inset' : '';
+}
+
+function show_page(page) {
+       if (typeof(page) != 'string')
+               page = this.id.split('.')[1];
+
+       var eles = document.getElementsByClassName('main');
+       for (var i = 0; i < eles.length; ++i) {
+               var ele = eles[i];
+               var dis = 'none';
+               var cls = '';
+               if (ele.id == 'main.' + page) {
+                       dis = '';
+                       cls = 'selected';
+               }
+               ele.style.display = dis;
+               document.getElementById('tab.' + ele.id.split('.')[1]).className = cls;
+       }
+}
+
+function update_local_settings() {
+       var setting = {};
+       setting[this.id] = this.checked;
+       chrome.storage.local.set(setting);
+}
+
+function update_sync_settings() {
+       var setting = {};
+       setting[this.id] = this.value;
+       var storage = sync_storage(window['opts_sync'].checked);
+       storage.set(setting);
+}
+
+function init_ui(local_keys, sync_keys, options) {
+       /* Setup footer */
+       [
+               'controls', 'metadata', 'options',
+       ].forEach(function(id) {
+               document.getElementById('tab.' + id).onclick = show_page;
+       });
+
+       /* Setup control tab */
+       ui_mpc_status = document.getElementById('status');
+       ui_mpc_metadata = document.getElementById('metadata');
+       [
+               'consume', 'next', 'pause', 'play', 'previous', 'random', 'repeat',
+               'seekcur', 'setvol', 'single', 'stop',
+       ].forEach(function(id) {
+               var ele = window['ui_mpc_' + id] = document.getElementById(id);
+               ele.onclick = window['tramp_mpc_' + id];
+               ele.title = id;
+       });
+
+       /* Setup options tab */
+       document.getElementById('connect').onclick = mpc_connect;
+       local_keys.forEach(function(id) {
+               var ele = window['opts_' + id] = document.getElementById(id);
+               ele.checked = options[id];
+               ele.onchange = update_local_settings;
+       });
+       sync_keys.forEach(function(id) {
+               var ele = window['opts_' + id] = document.getElementById(id);
+               ele.value = options[id];
+               ele.oninput = update_sync_settings;
+       });
+}
+
+function update_ui(state, cmd) {
+       if (typeof(state) == 'string') {
+               ui_mpc_status.innerText = ({
+                       'disconnect': 'Disconnecting...',
+                       'init': 'Connecting...',
+               })[state];
+               return;
+       }
+
+       if (Array.isArray(state)) {
+               /*
+               switch (cmd[0]) {
+               case 'setvol':
+               case 'seekcur':
+                       break;
+               default:
+                       mpc_refresh();
+               }
+               */
+               return;
+       }
+
+       if ('file' in state) {
+               // Hack: should be a real object.
+               ui_mpc_metadata.innerText = state['file'];
+               return;
+       }
+
+       var time = state.time.split(':');
+       window['ui_mpc_seekcur'].max = time[1];
+       window['ui_mpc_seekcur'].value = time[0];
+
+       window['ui_mpc_setvol'].value = state.volume;
+       [
+               'consume', 'random', 'repeat', 'single',
+       ].forEach(function(id) {
+               setToggleButton(window['ui_mpc_' + id], szo(state[id]));
+       });
+
+       ui_mpc_status.innerText = ({
+               'play': 'Playing',
+               'pause': 'Paused',
+               'stop': 'Stopped',
+       })[state.state];
+}
diff --git a/makedist.sh b/makedist.sh
new file mode 100755 (executable)
index 0000000..76be818
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/bash -e
+# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+case $1 in
+-h|--help)
+  echo "Usage: $0 [rev]"
+  exit 0
+  ;;
+esac
+
+json_value() {
+  local key=$1
+  sed -n -r \
+    -e '/^[[:space:]]*"'"${key}"'"/s|.*:[[:space:]]*"([^"]*)",?$|\1|p' \
+    manifest.json
+}
+
+PN=$(json_value name | sed 's:[[:space:]]:_:g' | tr '[:upper:]' '[:lower:]')
+PV=$(json_value version)
+rev=${1:-0}
+PVR="${PV}.${rev}"
+P="${PN}-${PVR}"
+
+rm -rf "${P}"
+mkdir "${P}"
+
+while read line ; do
+  [[ ${line} == */* ]] && mkdir -p "${P}/${line%/*}"
+  ln "${line}" "${P}/${line}"
+done < <(sed 's:#.*::' manifest.files)
+cp manifest.json "${P}/"
+
+sed -i \
+  -e '/"version"/s:"[^"]*",:"'${PVR}'",:' \
+  "${P}/manifest.json"
+
+zip="${P}.zip"
+zip -r "${zip}" "${P}"
+rm -rf "${P}"
+du -b "${zip}"
diff --git a/manifest.files b/manifest.files
new file mode 100644 (file)
index 0000000..e391a0b
--- /dev/null
@@ -0,0 +1,6 @@
+images/icon-128x128.png
+js/mpc.js
+js/tcp-client.js
+launcher.js
+main.js
+main.html
diff --git a/manifest.json b/manifest.json
new file mode 100644 (file)
index 0000000..14453b1
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "manifest_version": 2,
+  "minimum_chrome_version": "24",
+  "name": "Music Player Client",
+  "version": "1.0.1",
+  "description": "Control a Music Player Daemon (MPD)",
+  "icons": {
+    "128": "images/icon-128x128.png"
+  },
+  "app": {
+    "background": {
+      "scripts": ["launcher.js"]
+    }
+  },
+  "offline_enabled": true,
+  "permissions": [
+    "storage",
+    {"socket": ["tcp-connect"]}
+  ]
+}