Should be nearly finished now - includes volume control, length measurement, and seems pretty stable
authorDan
Sun, 23 Mar 2008 20:24:33 -0400
changeset 2 860ba7141641
parent 1 cddc2ba706d6
child 3 e7447a6044ec
Should be nearly finished now - includes volume control, length measurement, and seems pretty stable
ajax.php
functions.php
json.php
playlist.php
scripts/ajax.js
scripts/domutils.js
scripts/volume.js
themes/funkymonkey/images/ajax.gif
themes/funkymonkey/images/amarok.gif
themes/funkymonkey/images/volume.png
themes/funkymonkey/playlist.tpl
themes/funkymonkey/style.css
webcontrol.php
--- a/ajax.php	Sun Mar 23 15:24:06 2008 -0400
+++ b/ajax.php	Sun Mar 23 20:24:33 2008 -0400
@@ -9,5 +9,104 @@
  * This script is in the public domain. Use it for good, not evil.
  */
 
+status('initializing Services_JSON');
+$json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
 
+// keep track of playlist refresh
+$playlist_last_refresh = time();
 
+/**
+ * Terminate a request with an error string formatted as JSON.
+ * @param string Error message
+ */
+
+function json_die($msg)
+{
+  global $json;
+  echo $json->encode(array(
+      'mode' => 'error',
+      'error' => $msg
+    ));
+  return true;
+}
+
+function ajax_request_handler($httpd)
+{
+  global $playlist, $mime_types, $json;
+  
+  // Set content type
+  $httpd->header("Content-type: {$mime_types['js']}");
+  
+  // get PATH_INFO
+  $pathinfo = @substr(@substr($_SERVER['REQUEST_URI'], 1), @strpos(@substr($_SERVER['REQUEST_URI'], 1), '/')+1);
+  if ( empty($pathinfo) )
+  {
+    return json_die('No action specified on URI');
+  }
+  
+  $params = explode('/', $pathinfo);
+  $action =& $params[0];
+  switch ( $action )
+  {
+    case 'stop':
+    case 'next':
+    case 'prev':
+      echo dcop_action('player', 'stop');
+      break;
+    case 'play':
+      echo dcop_action('player', 'playPause');
+      break;
+    case 'jump':
+      $tid =& $params[1];
+      if ( !preg_match('/^[0-9]+$/', $tid) )
+      {
+        return die_json('Invalid track ID');
+      }
+      $tid = intval($tid);
+      dcop_action('playlist', "playByIndex $tid");
+      $return = array(
+        'current_track_length' => $playlist[$tid]['length_int'],
+        'current_track_pos' => 0
+      );
+      echo $json->encode($return);
+      break;
+    case 'volume':
+      $volume =& $params[1];
+      if ( !preg_match('/^[0-9]+$/', $volume) )
+      {
+        return die_json('Invalid track ID');
+      }
+      $volume = intval($volume);
+      dcop_action('player', "setVolume $volume");
+      $return = array(
+        'volume' => $volume
+        );
+      echo $json->encode($return);
+      break;
+    case 'refresh':
+      global $playlist_last_refresh, $playlist, $playlist_last_md5;
+      if ( $playlist_last_refresh + 60 < time() )
+      {
+        rebuild_playlist();
+      }
+      $current_track = dcop_action('playlist', 'getActiveIndex');
+      $return = array(
+          'is_playing' => dcop_action('player', 'isPlaying'),
+          'current_track' => $current_track,
+          'volume' => dcop_action('player', 'getVolume'),
+          // include the MD5 of the playlist so that if it changes, the
+          // client can refresh (otherwise things get madly corrupted)
+          'playlist_hash' => $playlist_last_md5
+        );
+      if ( isset($playlist[$current_track]) )
+      {
+        $return['current_track_length'] = $playlist[$current_track]['length_int'];
+        $return['current_track_pos'] = dcop_action('player', 'trackCurrentTime');
+      }
+      echo $json->encode($return);
+      break;
+    default:
+      return json_die("Undefined action: $action");
+  }
+}
+
--- a/functions.php	Sun Mar 23 15:24:06 2008 -0400
+++ b/functions.php	Sun Mar 23 20:24:33 2008 -0400
@@ -68,12 +68,21 @@
  * Rebuilds the copy of the playlist in RAM
  */
 
+$playlist_last_md5 = '';
+
 function rebuild_playlist()
 {
   // import what we need
   global $homedir, $playlist;
   // sync and load the playlist file
   $playlist_file = dcop_action('playlist', 'saveCurrentPlaylist');
+  // check MD5 - if it's not changed, exit to save CPU cycles
+  global $playlist_last_md5;
+  if ( $playlist_last_md5 == @md5_file($playlist_file) )
+  {
+    return true;
+  }
+  $playlist_last_md5 = @md5_file($playlist_file);
   // start XML parser
   try
   {
@@ -97,7 +106,8 @@
         'title'  => strval($child->Title),
         'artist' => strval($child->Artist),
         'album'  => strval($child->Album),
-        'length' => seconds_to_str(intval($child->Length))
+        'length' => seconds_to_str(intval($child->Length)),
+        'length_int' => intval($child->Length)
       );
     $playlist[] = $item;
   }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/json.php	Sun Mar 23 20:24:33 2008 -0400
@@ -0,0 +1,805 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+* Converts to and from JSON format.
+*
+* JSON (JavaScript Object Notation) is a lightweight data-interchange
+* format. It is easy for humans to read and write. It is easy for machines
+* to parse and generate. It is based on a subset of the JavaScript
+* Programming Language, Standard ECMA-262 3rd Edition - December 1999.
+* This feature can also be found in  Python. JSON is a text format that is
+* completely language independent but uses conventions that are familiar
+* to programmers of the C-family of languages, including C, C++, C#, Java,
+* JavaScript, Perl, TCL, and many others. These properties make JSON an
+* ideal data-interchange language.
+*
+* This package provides a simple encoder and decoder for JSON notation. It
+* is intended for use with client-side Javascript applications that make
+* use of HTTPRequest to perform server communication functions - data can
+* be encoded into JSON notation for use in a client-side javascript, or
+* decoded from incoming Javascript requests. JSON format is native to
+* Javascript, and can be directly eval()'ed with no further parsing
+* overhead
+*
+* All strings should be in ASCII or UTF-8 format!
+*
+* LICENSE: Redistribution and use in source and binary forms, with or
+* without modification, are permitted provided that the following
+* conditions are met: Redistributions of source code must retain the
+* above copyright notice, this list of conditions and the following
+* disclaimer. Redistributions in binary form must reproduce the above
+* copyright notice, this list of conditions and the following disclaimer
+* in the documentation and/or other materials provided with the
+* distribution.
+*
+* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+* NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+* DAMAGE.
+*
+* @category
+* @package     Services_JSON
+* @author      Michal Migurski <mike-json@teczno.com>
+* @author      Matt Knapp <mdknapp[at]gmail[dot]com>
+* @author      Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
+* @copyright   2005 Michal Migurski
+* @version     CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
+* @license     http://www.opensource.org/licenses/bsd-license.php
+* @link        http://pear.php.net/pepr/pepr-proposal-show.php?id=198
+*/
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_SLICE',   1);
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_IN_STR',  2);
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_IN_ARR',  3);
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_IN_OBJ',  4);
+
+/**
+* Marker constant for Services_JSON::decode(), used to flag stack state
+*/
+define('SERVICES_JSON_IN_CMT', 5);
+
+/**
+* Behavior switch for Services_JSON::decode()
+*/
+define('SERVICES_JSON_LOOSE_TYPE', 16);
+
+/**
+* Behavior switch for Services_JSON::decode()
+*/
+define('SERVICES_JSON_SUPPRESS_ERRORS', 32);
+
+/**
+ * Converts to and from JSON format.
+ *
+ * @example <code>
+   // create a new instance of Services_JSON
+   $json = new Services_JSON();
+  
+   // convert a complexe value to JSON notation, and send it to the browser
+   $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
+   $output = $json->encode($value);
+  
+   print($output);
+   // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
+  
+   // accept incoming POST data, assumed to be in JSON notation
+   $input = file_get_contents('php://input', 1000000);
+   $value = $json->decode($input);
+   </code>
+ *
+ */
+class Services_JSON
+{
+   /**
+    * constructs a new JSON instance
+    *
+    * @param    int     $use    object behavior flags; combine with boolean-OR
+    *
+    *                           possible values:
+    *                           - SERVICES_JSON_LOOSE_TYPE:  loose typing.
+    *                                   "{...}" syntax creates associative arrays
+    *                                   instead of objects in decode().
+    *                           - SERVICES_JSON_SUPPRESS_ERRORS:  error suppression.
+    *                                   Values which can't be encoded (e.g. resources)
+    *                                   appear as NULL instead of throwing errors.
+    *                                   By default, a deeply-nested resource will
+    *                                   bubble up with an error, so all return values
+    *                                   from encode() should be checked with isError()
+    */
+    function Services_JSON($use = 0)
+    {
+        $this->use = $use;
+    }
+
+   /**
+    * convert a string from one UTF-16 char to one UTF-8 char
+    *
+    * Normally should be handled by mb_convert_encoding, but
+    * provides a slower PHP-only method for installations
+    * that lack the multibye string extension.
+    *
+    * @param    string  $utf16  UTF-16 character
+    * @return   string  UTF-8 character
+    * @access   private
+    */
+    function utf162utf8($utf16)
+    {
+        // oh please oh please oh please oh please oh please
+        if(function_exists('mb_convert_encoding')) {
+            return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
+        }
+
+        $bytes = (ord($utf16{0}) << 8) | ord($utf16{1});
+
+        switch(true) {
+            case ((0x7F & $bytes) == $bytes):
+                // this case should never be reached, because we are in ASCII range
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr(0x7F & $bytes);
+
+            case (0x07FF & $bytes) == $bytes:
+                // return a 2-byte UTF-8 character
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr(0xC0 | (($bytes >> 6) & 0x1F))
+                     . chr(0x80 | ($bytes & 0x3F));
+
+            case (0xFFFF & $bytes) == $bytes:
+                // return a 3-byte UTF-8 character
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr(0xE0 | (($bytes >> 12) & 0x0F))
+                     . chr(0x80 | (($bytes >> 6) & 0x3F))
+                     . chr(0x80 | ($bytes & 0x3F));
+        }
+
+        // ignoring UTF-32 for now, sorry
+        return '';
+    }
+
+   /**
+    * convert a string from one UTF-8 char to one UTF-16 char
+    *
+    * Normally should be handled by mb_convert_encoding, but
+    * provides a slower PHP-only method for installations
+    * that lack the multibye string extension.
+    *
+    * @param    string  $utf8   UTF-8 character
+    * @return   string  UTF-16 character
+    * @access   private
+    */
+    function utf82utf16($utf8)
+    {
+        // oh please oh please oh please oh please oh please
+        if(function_exists('mb_convert_encoding')) {
+            return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
+        }
+
+        switch(strlen($utf8)) {
+            case 1:
+                // this case should never be reached, because we are in ASCII range
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return $utf8;
+
+            case 2:
+                // return a UTF-16 character from a 2-byte UTF-8 char
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr(0x07 & (ord($utf8{0}) >> 2))
+                     . chr((0xC0 & (ord($utf8{0}) << 6))
+                         | (0x3F & ord($utf8{1})));
+
+            case 3:
+                // return a UTF-16 character from a 3-byte UTF-8 char
+                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                return chr((0xF0 & (ord($utf8{0}) << 4))
+                         | (0x0F & (ord($utf8{1}) >> 2)))
+                     . chr((0xC0 & (ord($utf8{1}) << 6))
+                         | (0x7F & ord($utf8{2})));
+        }
+
+        // ignoring UTF-32 for now, sorry
+        return '';
+    }
+
+   /**
+    * encodes an arbitrary variable into JSON format
+    *
+    * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
+    *                           see argument 1 to Services_JSON() above for array-parsing behavior.
+    *                           if var is a strng, note that encode() always expects it
+    *                           to be in ASCII or UTF-8 format!
+    *
+    * @return   mixed   JSON string representation of input var or an error if a problem occurs
+    * @access   public
+    */
+    function encode($var)
+    {
+        switch (gettype($var)) {
+            case 'boolean':
+                return $var ? 'true' : 'false';
+
+            case 'NULL':
+                return 'null';
+
+            case 'integer':
+                return (int) $var;
+
+            case 'double':
+            case 'float':
+                return (float) $var;
+
+            case 'string':
+                // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
+                $ascii = '';
+                $strlen_var = strlen($var);
+
+               /*
+                * Iterate over every character in the string,
+                * escaping with a slash or encoding to UTF-8 where necessary
+                */
+                for ($c = 0; $c < $strlen_var; ++$c) {
+
+                    $ord_var_c = ord($var{$c});
+
+                    switch (true) {
+                        case $ord_var_c == 0x08:
+                            $ascii .= '\b';
+                            break;
+                        case $ord_var_c == 0x09:
+                            $ascii .= '\t';
+                            break;
+                        case $ord_var_c == 0x0A:
+                            $ascii .= '\n';
+                            break;
+                        case $ord_var_c == 0x0C:
+                            $ascii .= '\f';
+                            break;
+                        case $ord_var_c == 0x0D:
+                            $ascii .= '\r';
+                            break;
+
+                        case $ord_var_c == 0x22:
+                        case $ord_var_c == 0x2F:
+                        case $ord_var_c == 0x5C:
+                            // double quote, slash, slosh
+                            $ascii .= '\\'.$var{$c};
+                            break;
+
+                        case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
+                            // characters U-00000000 - U-0000007F (same as ASCII)
+                            $ascii .= $var{$c};
+                            break;
+
+                        case (($ord_var_c & 0xE0) == 0xC0):
+                            // characters U-00000080 - U-000007FF, mask 110XXXXX
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
+                            $c += 1;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+
+                        case (($ord_var_c & 0xF0) == 0xE0):
+                            // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c,
+                                         ord($var{$c + 1}),
+                                         ord($var{$c + 2}));
+                            $c += 2;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+
+                        case (($ord_var_c & 0xF8) == 0xF0):
+                            // characters U-00010000 - U-001FFFFF, mask 11110XXX
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c,
+                                         ord($var{$c + 1}),
+                                         ord($var{$c + 2}),
+                                         ord($var{$c + 3}));
+                            $c += 3;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+
+                        case (($ord_var_c & 0xFC) == 0xF8):
+                            // characters U-00200000 - U-03FFFFFF, mask 111110XX
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c,
+                                         ord($var{$c + 1}),
+                                         ord($var{$c + 2}),
+                                         ord($var{$c + 3}),
+                                         ord($var{$c + 4}));
+                            $c += 4;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+
+                        case (($ord_var_c & 0xFE) == 0xFC):
+                            // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                            $char = pack('C*', $ord_var_c,
+                                         ord($var{$c + 1}),
+                                         ord($var{$c + 2}),
+                                         ord($var{$c + 3}),
+                                         ord($var{$c + 4}),
+                                         ord($var{$c + 5}));
+                            $c += 5;
+                            $utf16 = $this->utf82utf16($char);
+                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
+                            break;
+                    }
+                }
+
+                return '"'.$ascii.'"';
+
+            case 'array':
+               /*
+                * As per JSON spec if any array key is not an integer
+                * we must treat the the whole array as an object. We
+                * also try to catch a sparsely populated associative
+                * array with numeric keys here because some JS engines
+                * will create an array with empty indexes up to
+                * max_index which can cause memory issues and because
+                * the keys, which may be relevant, will be remapped
+                * otherwise.
+                *
+                * As per the ECMA and JSON specification an object may
+                * have any string as a property. Unfortunately due to
+                * a hole in the ECMA specification if the key is a
+                * ECMA reserved word or starts with a digit the
+                * parameter is only accessible using ECMAScript's
+                * bracket notation.
+                */
+
+                // treat as a JSON object
+                if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
+                    $properties = array_map(array($this, 'name_value'),
+                                            array_keys($var),
+                                            array_values($var));
+
+                    foreach($properties as $property) {
+                        if(Services_JSON::isError($property)) {
+                            return $property;
+                        }
+                    }
+
+                    return '{' . join(',', $properties) . '}';
+                }
+
+                // treat it like a regular array
+                $elements = array_map(array($this, 'encode'), $var);
+
+                foreach($elements as $element) {
+                    if(Services_JSON::isError($element)) {
+                        return $element;
+                    }
+                }
+
+                return '[' . join(',', $elements) . ']';
+
+            case 'object':
+                $vars = get_object_vars($var);
+
+                $properties = array_map(array($this, 'name_value'),
+                                        array_keys($vars),
+                                        array_values($vars));
+
+                foreach($properties as $property) {
+                    if(Services_JSON::isError($property)) {
+                        return $property;
+                    }
+                }
+
+                return '{' . join(',', $properties) . '}';
+
+            default:
+                return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
+                    ? 'null'
+                    : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
+        }
+    }
+
+   /**
+    * array-walking function for use in generating JSON-formatted name-value pairs
+    *
+    * @param    string  $name   name of key to use
+    * @param    mixed   $value  reference to an array element to be encoded
+    *
+    * @return   string  JSON-formatted name-value pair, like '"name":value'
+    * @access   private
+    */
+    function name_value($name, $value)
+    {
+        $encoded_value = $this->encode($value);
+
+        if(Services_JSON::isError($encoded_value)) {
+            return $encoded_value;
+        }
+
+        return $this->encode(strval($name)) . ':' . $encoded_value;
+    }
+
+   /**
+    * reduce a string by removing leading and trailing comments and whitespace
+    *
+    * @param    $str    string      string value to strip of comments and whitespace
+    *
+    * @return   string  string value stripped of comments and whitespace
+    * @access   private
+    */
+    function reduce_string($str)
+    {
+        $str = preg_replace(array(
+
+                // eliminate single line comments in '// ...' form
+                '#^\s*//(.+)$#m',
+
+                // eliminate multi-line comments in '/* ... */' form, at start of string
+                '#^\s*/\*(.+)\*/#Us',
+
+                // eliminate multi-line comments in '/* ... */' form, at end of string
+                '#/\*(.+)\*/\s*$#Us'
+
+            ), '', $str);
+
+        // eliminate extraneous space
+        return trim($str);
+    }
+
+   /**
+    * decodes a JSON string into appropriate variable
+    *
+    * @param    string  $str    JSON-formatted string
+    *
+    * @return   mixed   number, boolean, string, array, or object
+    *                   corresponding to given JSON input string.
+    *                   See argument 1 to Services_JSON() above for object-output behavior.
+    *                   Note that decode() always returns strings
+    *                   in ASCII or UTF-8 format!
+    * @access   public
+    */
+    function decode($str)
+    {
+        $str = $this->reduce_string($str);
+
+        switch (strtolower($str)) {
+            case 'true':
+                return true;
+
+            case 'false':
+                return false;
+
+            case 'null':
+                return null;
+
+            default:
+                $m = array();
+
+                if (is_numeric($str)) {
+                    // Lookie-loo, it's a number
+
+                    // This would work on its own, but I'm trying to be
+                    // good about returning integers where appropriate:
+                    // return (float)$str;
+
+                    // Return float or int, as appropriate
+                    return ((float)$str == (integer)$str)
+                        ? (integer)$str
+                        : (float)$str;
+
+                } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
+                    // STRINGS RETURNED IN UTF-8 FORMAT
+                    $delim = substr($str, 0, 1);
+                    $chrs = substr($str, 1, -1);
+                    $utf8 = '';
+                    $strlen_chrs = strlen($chrs);
+
+                    for ($c = 0; $c < $strlen_chrs; ++$c) {
+
+                        $substr_chrs_c_2 = substr($chrs, $c, 2);
+                        $ord_chrs_c = ord($chrs{$c});
+
+                        switch (true) {
+                            case $substr_chrs_c_2 == '\b':
+                                $utf8 .= chr(0x08);
+                                ++$c;
+                                break;
+                            case $substr_chrs_c_2 == '\t':
+                                $utf8 .= chr(0x09);
+                                ++$c;
+                                break;
+                            case $substr_chrs_c_2 == '\n':
+                                $utf8 .= chr(0x0A);
+                                ++$c;
+                                break;
+                            case $substr_chrs_c_2 == '\f':
+                                $utf8 .= chr(0x0C);
+                                ++$c;
+                                break;
+                            case $substr_chrs_c_2 == '\r':
+                                $utf8 .= chr(0x0D);
+                                ++$c;
+                                break;
+
+                            case $substr_chrs_c_2 == '\\"':
+                            case $substr_chrs_c_2 == '\\\'':
+                            case $substr_chrs_c_2 == '\\\\':
+                            case $substr_chrs_c_2 == '\\/':
+                                if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
+                                   ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
+                                    $utf8 .= $chrs{++$c};
+                                }
+                                break;
+
+                            case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
+                                // single, escaped unicode character
+                                $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
+                                       . chr(hexdec(substr($chrs, ($c + 4), 2)));
+                                $utf8 .= $this->utf162utf8($utf16);
+                                $c += 5;
+                                break;
+
+                            case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
+                                $utf8 .= $chrs{$c};
+                                break;
+
+                            case ($ord_chrs_c & 0xE0) == 0xC0:
+                                // characters U-00000080 - U-000007FF, mask 110XXXXX
+                                //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 2);
+                                ++$c;
+                                break;
+
+                            case ($ord_chrs_c & 0xF0) == 0xE0:
+                                // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 3);
+                                $c += 2;
+                                break;
+
+                            case ($ord_chrs_c & 0xF8) == 0xF0:
+                                // characters U-00010000 - U-001FFFFF, mask 11110XXX
+                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 4);
+                                $c += 3;
+                                break;
+
+                            case ($ord_chrs_c & 0xFC) == 0xF8:
+                                // characters U-00200000 - U-03FFFFFF, mask 111110XX
+                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 5);
+                                $c += 4;
+                                break;
+
+                            case ($ord_chrs_c & 0xFE) == 0xFC:
+                                // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+                                $utf8 .= substr($chrs, $c, 6);
+                                $c += 5;
+                                break;
+
+                        }
+
+                    }
+
+                    return $utf8;
+
+                } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
+                    // array, or object notation
+
+                    if ($str{0} == '[') {
+                        $stk = array(SERVICES_JSON_IN_ARR);
+                        $arr = array();
+                    } else {
+                        if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+                            $stk = array(SERVICES_JSON_IN_OBJ);
+                            $obj = array();
+                        } else {
+                            $stk = array(SERVICES_JSON_IN_OBJ);
+                            $obj = new stdClass();
+                        }
+                    }
+
+                    array_push($stk, array('what'  => SERVICES_JSON_SLICE,
+                                           'where' => 0,
+                                           'delim' => false));
+
+                    $chrs = substr($str, 1, -1);
+                    $chrs = $this->reduce_string($chrs);
+
+                    if ($chrs == '') {
+                        if (reset($stk) == SERVICES_JSON_IN_ARR) {
+                            return $arr;
+
+                        } else {
+                            return $obj;
+
+                        }
+                    }
+
+                    //print("\nparsing {$chrs}\n");
+
+                    $strlen_chrs = strlen($chrs);
+
+                    for ($c = 0; $c <= $strlen_chrs; ++$c) {
+
+                        $top = end($stk);
+                        $substr_chrs_c_2 = substr($chrs, $c, 2);
+
+                        if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
+                            // found a comma that is not inside a string, array, etc.,
+                            // OR we've reached the end of the character list
+                            $slice = substr($chrs, $top['where'], ($c - $top['where']));
+                            array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
+                            //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+                            if (reset($stk) == SERVICES_JSON_IN_ARR) {
+                                // we are in an array, so just push an element onto the stack
+                                array_push($arr, $this->decode($slice));
+
+                            } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
+                                // we are in an object, so figure
+                                // out the property name and set an
+                                // element in an associative array,
+                                // for now
+                                $parts = array();
+                                
+                                if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+                                    // "name":value pair
+                                    $key = $this->decode($parts[1]);
+                                    $val = $this->decode($parts[2]);
+
+                                    if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+                                        $obj[$key] = $val;
+                                    } else {
+                                        $obj->$key = $val;
+                                    }
+                                } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+                                    // name:value pair, where name is unquoted
+                                    $key = $parts[1];
+                                    $val = $this->decode($parts[2]);
+
+                                    if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+                                        $obj[$key] = $val;
+                                    } else {
+                                        $obj->$key = $val;
+                                    }
+                                }
+
+                            }
+
+                        } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
+                            // found a quote, and we are not inside a string
+                            array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
+                            //print("Found start of string at {$c}\n");
+
+                        } elseif (($chrs{$c} == $top['delim']) &&
+                                 ($top['what'] == SERVICES_JSON_IN_STR) &&
+                                 ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) {
+                            // found a quote, we're in a string, and it's not escaped
+                            // we know that it's not escaped becase there is _not_ an
+                            // odd number of backslashes at the end of the string so far
+                            array_pop($stk);
+                            //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
+
+                        } elseif (($chrs{$c} == '[') &&
+                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+                            // found a left-bracket, and we are in an array, object, or slice
+                            array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
+                            //print("Found start of array at {$c}\n");
+
+                        } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
+                            // found a right-bracket, and we're in an array
+                            array_pop($stk);
+                            //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+                        } elseif (($chrs{$c} == '{') &&
+                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+                            // found a left-brace, and we are in an array, object, or slice
+                            array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
+                            //print("Found start of object at {$c}\n");
+
+                        } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
+                            // found a right-brace, and we're in an object
+                            array_pop($stk);
+                            //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+                        } elseif (($substr_chrs_c_2 == '/*') &&
+                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+                            // found a comment start, and we are in an array, object, or slice
+                            array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
+                            $c++;
+                            //print("Found start of comment at {$c}\n");
+
+                        } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
+                            // found a comment end, and we're in one now
+                            array_pop($stk);
+                            $c++;
+
+                            for ($i = $top['where']; $i <= $c; ++$i)
+                                $chrs = substr_replace($chrs, ' ', $i, 1);
+
+                            //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+                        }
+
+                    }
+
+                    if (reset($stk) == SERVICES_JSON_IN_ARR) {
+                        return $arr;
+
+                    } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
+                        return $obj;
+
+                    }
+
+                }
+        }
+    }
+
+    /**
+     * @todo Ultimately, this should just call PEAR::isError()
+     */
+    function isError($data, $code = null)
+    {
+        if (class_exists('pear')) {
+            return PEAR::isError($data, $code);
+        } elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
+                                 is_subclass_of($data, 'services_json_error'))) {
+            return true;
+        }
+
+        return false;
+    }
+}
+
+if (class_exists('PEAR_Error')) {
+
+    class Services_JSON_Error extends PEAR_Error
+    {
+        function Services_JSON_Error($message = 'unknown error', $code = null,
+                                     $mode = null, $options = null, $userinfo = null)
+        {
+            parent::PEAR_Error($message, $code, $mode, $options, $userinfo);
+        }
+    }
+
+} else {
+
+    /**
+     * @todo Ultimately, this class shall be descended from PEAR_Error
+     */
+    class Services_JSON_Error
+    {
+        function Services_JSON_Error($message = 'unknown error', $code = null,
+                                     $mode = null, $options = null, $userinfo = null)
+        {
+
+        }
+    }
+
+}
+    
+?>
\ No newline at end of file
--- a/playlist.php	Sun Mar 23 15:24:06 2008 -0400
+++ b/playlist.php	Sun Mar 23 20:24:33 2008 -0400
@@ -19,6 +19,8 @@
   $smarty->assign('active', $active);
   $smarty->assign('scripts', array(
       'ajax.js',
+      'domutils.js',
+      'volume.js'
     ));
   $smarty->display('playlist.tpl');
 }
--- a/scripts/ajax.js	Sun Mar 23 15:24:06 2008 -0400
+++ b/scripts/ajax.js	Sun Mar 23 20:24:33 2008 -0400
@@ -1,1 +1,244 @@
-// Hello world
+/**
+ * AJAX functions
+ * 
+ * Web control interface script for Amarok
+ * Written by Dan Fuhry - 2008
+ *
+ * This script is in the public domain. Use it for good, not evil.
+ */
+
+var ajax;
+var is_playing = false, current_track = -1, current_track_length, current_track_pos, ct_advance_timeout = false, ct_counter = false, playlist_md5 = false;
+
+function ajaxGet(uri, f)
+{
+  if (window.XMLHttpRequest)
+  {
+    ajax = new XMLHttpRequest();
+  }
+  else
+  {
+    if (window.ActiveXObject) {           
+      ajax = new ActiveXObject("Microsoft.XMLHTTP");
+    }
+    else
+    {
+      alert('AmaroK client-side runtime error: No AJAX support, unable to continue');
+      return;
+    }
+  }
+  ajax.onreadystatechange = f;
+  ajax.open('GET', uri, true);
+  ajax.setRequestHeader( "If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT" );
+  ajax.send(null);
+}
+
+function ajaxPost(uri, parms, f)
+{
+  if (window.XMLHttpRequest)
+  {
+    ajax = new XMLHttpRequest();
+  }
+  else
+  {
+    if (window.ActiveXObject)
+    {           
+      ajax = new ActiveXObject("Microsoft.XMLHTTP");
+    }
+    else
+    {
+      alert('AmaroK client-side runtime error: No AJAX support, unable to continue');
+      return;
+    }
+  }
+  ajax.onreadystatechange = f;
+  ajax.open('POST', uri, true);
+  ajax.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+  // Setting Content-length in Safari triggers a warning
+  if ( !is_Safari )
+  {
+    ajax.setRequestHeader("Content-length", parms.length);
+  }
+  ajax.setRequestHeader("Connection", "close");
+  ajax.send(parms);
+}
+
+function setAjaxLoading()
+{
+  $('ajax_status').object.src = img_ajax;
+}
+
+function unsetAjaxLoading()
+{
+  $('ajax_status').object.src = 'about:blank';
+}
+
+var refresh_playlist = function()
+{
+  setAjaxLoading();
+  ajaxGet('/action.json/refresh', function()
+    {
+      if ( ajax.readyState == 4 && ajax.status == 200 )
+      {
+        unsetAjaxLoading();
+        var response = (' ' + ajax.responseText).substr(1);
+        // quickie JSON parser :)
+        response = eval('(' + response + ')');
+        // has the playlist been modified?
+        if ( playlist_md5 )
+        {
+          if ( response.playlist_hash != playlist_md5 )
+          {
+            // playlist has changed, reload
+            window.location.reload();
+            return false;
+          }
+        }
+        playlist_md5 = response.playlist_hash;
+        // update track number
+        if ( response.current_track != current_track )
+        {
+          var ot_id = 'track_' + current_track;
+          var nt_id = 'track_' + response.current_track;
+          current_track = response.current_track;
+          if ( $(ot_id).hasClass('current') )
+          {
+            $(ot_id).rmClass('current');
+          }
+          if ( ! $(nt_id).hasClass('current') )
+          {
+            $(nt_id).addClass('current');
+          }
+        }
+        // update playing status
+        var img = $('btn_playpause').object.getElementsByTagName('img')[0];
+        is_playing = response.is_playing;
+        if ( is_playing )
+        {
+          img.src = img_pause;
+        }
+        else
+        {
+          img.src = img_play;
+        }
+        // update volume
+        if ( response.volume != current_volume )
+        {
+          set_volume_fill(response.volume);
+          current_volume = response.volume;
+        }
+        // auto-refresh on track advance
+        if ( ct_advance_timeout )
+        {
+          clearTimeout(ct_advance_timeout);
+        }
+        var time_remaining = response.current_track_length - response.current_track_pos;
+        current_track_length = response.current_track_length;
+        current_track_pos = response.current_track_pos;
+        if ( ct_counter )
+          clearInterval(ct_counter);
+        update_clock();
+        if ( is_playing )
+        {
+          ct_advance_timeout = setTimeout(refresh_playlist, ( 1000 * time_remaining ));
+          ct_counter = setInterval(update_clock, 1000);
+        }
+      }
+    });
+}
+
+function player_action(action)
+{
+  var act2 = action;
+  setAjaxLoading();
+  ajaxGet('/action.json/' + action, function()
+    {
+      if ( ajax.readyState == 4 && ajax.status == 200 )
+      {
+        unsetAjaxLoading();
+        refresh_playlist();
+      }
+    });
+}
+
+function jump_to_song(tid)
+{
+  setAjaxLoading();
+  if ( tid == current_track )
+    return false;
+  ajaxGet('/action.json/jump/' + tid, function()
+    {
+      if ( ajax.readyState == 4 && ajax.status == 200 )
+      {
+        unsetAjaxLoading();
+        var response = (' ' + ajax.responseText).substr(1);
+        // quickie JSON parser :)
+        response = eval('(' + response + ')');
+        
+        // update track number
+        var ot_id = 'track_' + current_track;
+        var nt_id = 'track_' + tid;
+        current_track = tid;
+        if ( $(ot_id).hasClass('current') )
+        {
+          $(ot_id).rmClass('current');
+        }
+        if ( ! $(nt_id).hasClass('current') )
+        {
+          $(nt_id).addClass('current');
+        }
+        // update playing status
+        var img = $('btn_playpause').object.getElementsByTagName('img')[0];
+        is_playing = true;
+        img.src = img_play;
+        // auto-refresh on track advance
+        if ( ct_advance_timeout )
+        {
+          clearTimeout(ct_advance_timeout);
+        }
+        if ( ct_counter )
+          clearInterval(ct_counter);
+        var time_remaining = response.current_track_length - response.current_track_pos;
+        current_track_length = response.current_track_length;
+        current_track_pos = response.current_track_pos;
+        if ( is_playing )
+        {
+          ct_advance_timeout = setTimeout(refresh_playlist, ( 1000 * time_remaining ));
+          update_clock();
+          ct_counter = setInterval(update_clock, 1000);
+        }
+      }
+    });
+}
+
+function update_clock()
+{
+  var str = secs_to_string(current_track_pos) + '/' + secs_to_string(current_track_length);
+  $('playmeter').object.innerHTML = str;
+  current_track_pos++;
+}
+
+function secs_to_string(time)
+{
+  var count_seconds = time % 60;
+  var count_minutes = ( time - count_seconds ) / 60;
+  return fill_zeroes(count_minutes) + ':' + fill_zeroes(count_seconds);
+}
+
+function fill_zeroes(str, len)
+{
+  if ( !len )
+    len = 2;
+  if ( typeof(str) == 'number' && str == 0 )
+    str = '0';
+  str = String(str);
+  while ( str.length < len )
+  {
+    str = '0' + str;
+  }
+  return str;
+}
+
+window.onload = refresh_playlist;
+setInterval(refresh_playlist, 10000);
+
--- a/scripts/domutils.js	Sun Mar 23 15:24:06 2008 -0400
+++ b/scripts/domutils.js	Sun Mar 23 20:24:33 2008 -0400
@@ -11,23 +11,11 @@
   this.object = ( typeof(id) == 'object' ) ? id : document.getElementById(id);
   if ( !this.object )
   {
-    console.warn('Dynano: requested object is bad. id parameter follows.');
-    console.debug(id);
-    console.debug(tinyMCE.getInstanceById(id));
     this.object = false;
     return this;
   }
   this.height = __DNObjGetHeight(this.object);
   this.width = __DNObjGetWidth(this.object);
-  
-  if ( this.object.tagName == 'TEXTAREA' && typeof(tinyMCE) == 'object' )
-  {
-    this.object.dnIsMCE = 'no';
-    this.switchToMCE = DN_switchToMCE;
-    this.destroyMCE = DN_destroyMCE;
-    this.getContent = DN_mceFetchContent;
-    this.setContent = DN_mceSetContent;
-  }
 }
 function __DNObjGetHeight(o) {
   return o.offsetHeight;
@@ -91,114 +79,6 @@
   return left_offset;
 }
 
-function DN_switchToMCE(performWikiTransform)
-{
-  if ( !this.object.id )
-    this.object.id = 'textarea_' + Math.floor(Math.random() * 1000000);
-  if ( !this.object.name )
-    this.object.name = 'textarea_' + Math.floor(Math.random() * 1000000);
-  // Updated for TinyMCE 3.x
-  if ( performWikiTransform )
-  {
-    this.object.value = DN_WikitextToXHTML(this.object.value);
-  }
-  // If tinyMCE init hasn't been called yet, do it now.
-  if ( !tinymce_initted )
-  {
-    enano_tinymce_options.mode = 'exact';
-    enano_tinymce_options.elements = this.object.id;
-    initTinyMCE();
-    this.object.dnIsMCE = 'yes';
-    return true;
-  }
-  else
-  {
-    tinyMCE.execCommand("mceAddControl", true, this.object.id);
-    this.object.dnIsMCE = 'yes';
-  }
-  return this;
-}
-
-function DN_destroyMCE(performWikiTransform)
-{
-  //if ( !this.object.dn_is_mce )
-  //  return this;
-  if ( this.object.id )
-  {
-    // TinyMCE 2.x
-    // tinyMCE.removeMCEControl(this.object.name);
-    // TinyMCE 3.x
-    var ed = tinyMCE.getInstanceById(this.object.id);
-    if ( ed )
-    {
-      if ( !tinyMCE.execCommand("mceRemoveEditor", false, this.object.id) )
-        alert('could not destroy editor');
-      if ( performWikiTransform )
-      {
-        this.object.value = DN_XHTMLToWikitext(this.object.value);
-      }
-    }
-  }
-  this.object.dnIsMCE = 'no';
-  return this;
-}
-
-function DN_mceFetchContent()
-{
-  if ( this.object.name )
-  {
-    var text = this.object.value;
-    if ( tinyMCE.get(this.object.id) )
-    {
-      var editor = tinyMCE.get(this.object.id);
-      text = editor.getContent();
-    }
-    return text;
-  }
-  else
-  {
-    return this.object.value;
-  }
-}
-
-function DN_mceSetContent(text)
-{
-  if ( this.object.name )
-  {
-    this.object.value = text;
-    if ( tinyMCE.get(this.object.id) )
-    {
-      var editor = tinyMCE.get(this.object.id);
-      editor.setContent(text);
-    }
-  }
-  else
-  {
-    this.object.value = text;
-  }
-}
-
-// A basic Wikitext to XHTML converter
-function DN_WikitextToXHTML(text)
-{
-  text = text.replace(/^===[\s]*(.+?)[\s]*===$/g, '<h3>$1</h3>');
-  text = text.replace(/'''(.+?)'''/g, '<b>$1</b>');
-  text = text.replace(/''(.+?)''/g, '<i>$1</i>');
-  text = text.replace(/\[(http|ftp|irc|mailto):([^ \]])+ ([^\]]+?)\]/g, '<a href="$1:$2">$4</a>');
-  return text;
-}
-
-// Inverse of the previous function
-function DN_XHTMLToWikitext(text)
-{
-  text = text.replace(/<h3>(.+?)<\/h3>/g, '=== $1 ===');
-  text = text.replace(/<(b|strong)>(.+?)<\/(b|strong)>/g, "'''$2'''");
-  text = text.replace(/<(i|em)>(.+?)<\/(i|em)>/g, "''$2''");
-  text = text.replace(/<a href="([^" ]+)">(.+?)<\/a>/g, '[$1 $2]');
-  text = text.replace(/<\/?p>/g, '');
-  return text;
-}
-
 DNobj.prototype.addClass = function(clsname) { addClass(this.object, clsname); return this; };
 DNobj.prototype.rmClass  = function(clsname) { rmClass( this.object, clsname); return this; };
 DNobj.prototype.hasClass = function(clsname) { return hasClass(this.object, clsname); };
@@ -207,3 +87,11 @@
 DNobj.prototype.Left     = function()        { /* return this.object.offsetLeft; */ return __DNObjGetLeft(this.object); }
 DNobj.prototype.Top      = function()        { /* return this.object.offsetTop;  */ return __DNObjGetTop( this.object); }
 
+// Equivalent to PHP trim() function
+function trim(text)
+{
+  text = text.replace(/^([\s]+)/, '');
+  text = text.replace(/([\s]+)$/, '');
+  return text;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/volume.js	Sun Mar 23 20:24:33 2008 -0400
@@ -0,0 +1,65 @@
+/**
+ * Volume widget presentation code
+ * 
+ * Web control interface script for Amarok
+ * Written by Dan Fuhry - 2008
+ *
+ * This script is in the public domain. Use it for good, not evil.
+ */
+
+var current_volume = 0;
+
+function set_volume_fill(amount)
+{
+  amount = 10 * ( Math.round(amount / 10) );
+  if ( amount == 0 )
+    amount = -10;
+  for ( var i = 0; i <= amount; i += 10 )
+  {
+    if ( !$('volbtn_' + i).hasClass('volume_button_active') )
+    {
+      $('volbtn_' + i).addClass('volume_button_active');
+    }
+  }
+  for ( ; i <= 100; i += 10 )
+  {
+    if ( $('volbtn_' + i).hasClass('volume_button_active') )
+    {
+      $('volbtn_' + i).rmClass('volume_button_active');
+    }
+  }
+}
+
+function volume_over(amount)
+{
+  set_volume_fill(amount);
+}
+
+function volume_out()
+{
+  set_volume_fill(current_volume);
+}
+
+function set_volume(level)
+{
+  setAjaxLoading();
+  if ( level == current_volume )
+    return false;
+  ajaxGet('/action.json/volume/' + level, function()
+    {
+      if ( ajax.readyState == 4 && ajax.status == 200 )
+      {
+        unsetAjaxLoading();
+        var response = (' ' + ajax.responseText).substr(1);
+        // quickie JSON parser :)
+        response = eval('(' + response + ')');
+        // update volume
+        if ( response.volume != current_volume )
+        {
+          set_volume_fill(response.volume);
+          current_volume = response.volume;
+        }
+      }
+    });
+}
+
Binary file themes/funkymonkey/images/ajax.gif has changed
Binary file themes/funkymonkey/images/amarok.gif has changed
Binary file themes/funkymonkey/images/volume.png has changed
--- a/themes/funkymonkey/playlist.tpl	Sun Mar 23 15:24:06 2008 -0400
+++ b/themes/funkymonkey/playlist.tpl	Sun Mar 23 20:24:33 2008 -0400
@@ -12,6 +12,12 @@
     <title>AmaroK playlist</title>
     <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
     <link rel="stylesheet" type="text/css" href="/themes/{$theme|escape}/style.css" />
+    <script type="text/javascript">
+    var img_play = '/themes/{$theme|escape}/images/play.png';
+    var img_pause = '/themes/{$theme|escape}/images/pause.png';
+    var img_ajax = '/themes/{$theme|escape}/images/ajax.gif';
+    var class_current = 'current';
+    </script>
     {foreach from=$scripts item=script}
     <script type="text/javascript" src="/scripts/{$script}"></script>
     {/foreach}
@@ -19,11 +25,29 @@
   <body>
     <div id="playbar">
       <div class="playbar-inner">
-        <a href="#action:prev" onclick="return false;"><img alt="&laquo; PrevTrk" src="/themes/{$theme|escape}/images/prev.png" /></a>
-        <a href="#action:play" onclick="return false;"><img alt="Play" src="/themes/{$theme|escape}/images/play.png" /></a>
-        <a href="#action:pause" onclick="return false;"><img alt="Pause" src="/themes/{$theme|escape}/images/pause.png" /></a>
-        <a href="#action:stop" onclick="return false;"><img alt="Stop" src="/themes/{$theme|escape}/images/stop.png" /></a>
-        <a href="#action:next" onclick="return false;"><img alt="NextTrk &raquo;" src="/themes/{$theme|escape}/images/next.png" /></a>
+        <img alt=" " id="ajax_status" style="display: block; float: right; margin: 3px 0;" src="about:blank" />
+        <img alt="AmaroK web control" src="/themes/{$theme|escape}/images/amarok.gif" style="margin-right: 20px;" />
+        <a href="#action:prev" onclick="player_action('prev'); return false;"><img alt="&laquo; PrevTrk" src="/themes/{$theme|escape}/images/prev.png" /></a>
+        <a href="#action:play" onclick="player_action('play'); return false;" id="btn_playpause"><img alt="Play" src="/themes/{$theme|escape}/images/play.png" /></a>
+        <a href="#action:stop" onclick="player_action('stop'); return false;"><img alt="Stop" src="/themes/{$theme|escape}/images/stop.png" /></a>
+        <a href="#action:next" onclick="player_action('next'); return false;"><img alt="NextTrk &raquo;" src="/themes/{$theme|escape}/images/next.png" /></a>
+        <span style="margin-right: 50px;">&nbsp;</span>
+        <span id="playmeter">--:--/--:--</span>
+        <span style="margin-right: 50px;">&nbsp;</span>
+        <img hspace="4" alt="Volume: " src="/themes/{$theme|escape}/images/volume.png" />
+        <span id="volume_wrap"><a
+          class="volume_button" href="#volume:0"   onmouseover="volume_over(0);"   onmouseout="volume_out();" id="volbtn_0"   onclick="set_volume(0);   return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:10"  onmouseover="volume_over(10);"  onmouseout="volume_out();" id="volbtn_10"  onclick="set_volume(10);  return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:20"  onmouseover="volume_over(20);"  onmouseout="volume_out();" id="volbtn_20"  onclick="set_volume(20);  return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:30"  onmouseover="volume_over(30);"  onmouseout="volume_out();" id="volbtn_30"  onclick="set_volume(30);  return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:40"  onmouseover="volume_over(40);"  onmouseout="volume_out();" id="volbtn_40"  onclick="set_volume(40);  return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:50"  onmouseover="volume_over(50);"  onmouseout="volume_out();" id="volbtn_50"  onclick="set_volume(50);  return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:60"  onmouseover="volume_over(60);"  onmouseout="volume_out();" id="volbtn_60"  onclick="set_volume(60);  return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:70"  onmouseover="volume_over(70);"  onmouseout="volume_out();" id="volbtn_70"  onclick="set_volume(70);  return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:80"  onmouseover="volume_over(80);"  onmouseout="volume_out();" id="volbtn_80"  onclick="set_volume(80);  return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:90"  onmouseover="volume_over(90);"  onmouseout="volume_out();" id="volbtn_90"  onclick="set_volume(90);  return false;">&nbsp;</a><a
+          class="volume_button" href="#volume:100" onmouseover="volume_over(100);" onmouseout="volume_out();" id="volbtn_100" onclick="set_volume(100); return false;">&nbsp;</a>
+        </span>
       </div>
     </div>
     <div class="tblholder" id="playlist">
@@ -37,12 +61,12 @@
         </tr>
         {foreach key=tid item=track from=$playlist}
         {strip}
-        <tr class="{cycle values="row1,row2"}{if $active == $tid} current{/if}">
+        <tr class="{cycle values="row1,row2"}{if $active == $tid} current{/if}" id="track_{$tid}" amarok:length_sec="{$track.length_int}">
           <td style="text-align: center;">{$tid+1}</td>
           <td>
-            {if $active != $tid}<a class="tracklink" href="#action:jump;tid:{$tid}" onclick="return false;">{/if}
+            <a class="tracklink" href="#action:jump;tid:{$tid}" onclick="jump_to_song({$tid}); return false;">
               {$track.title|escape}
-            {if $active != $tid}</a>{/if}
+            </a>
           </td>
           <td>{$track.artist|escape}</td>
           <td>{$track.album|escape}</td>
--- a/themes/funkymonkey/style.css	Sun Mar 23 15:24:06 2008 -0400
+++ b/themes/funkymonkey/style.css	Sun Mar 23 20:24:33 2008 -0400
@@ -57,6 +57,7 @@
 
 div.playbar-inner {
   padding: 5px;
+  line-height: 22px;
 }
 
 a img {
@@ -72,3 +73,24 @@
   color: white;
 }
 
+div#playlist tr.current a.tracklink {
+  color: #ffff00;
+}
+
+span#playmeter, span#volume_wrap {
+  position: relative;
+  top: -7px;
+}
+
+a.volume_button {
+  padding-right: 10px;
+  margin-right: 1px;
+  background-color: #33395d;
+  border: 1px solid #909090;
+}
+
+a.volume_button_active {
+  background-color: #9090c2;
+  border-color: #d0d0d0;
+}
+
--- a/webcontrol.php	Sun Mar 23 15:24:06 2008 -0400
+++ b/webcontrol.php	Sun Mar 23 20:24:33 2008 -0400
@@ -25,6 +25,7 @@
 define('SMARTY_DIR', './smarty/');
 require('smarty/Smarty.class.php');
 require('playlist.php');
+require('json.php');
 require('ajax.php');
 
 status('initializing Smarty');
@@ -63,6 +64,7 @@
   // setup handlers
   status('initializing handlers');
   $httpd->add_handler('index', 'function', 'amarok_playlist');
+  $httpd->add_handler('action.json', 'function', 'ajax_request_handler');
   $httpd->add_handler('scripts', 'dir', './scripts');
   $httpd->add_handler("themes/$theme", 'dir', "./themes/$theme");
   $httpd->allow_dir_list = true;