diff --git a/lsp/functions.js b/lsp/functions.js index 81993f1d..30ba1a0f 100644 --- a/lsp/functions.js +++ b/lsp/functions.js @@ -1,18 +1,18 @@ - /** - * Show a confirm dialog - * @param question the question displayed - */ + /** + * Show a confirm dialog + * @param question the question displayed + */ function confirmDelete(question) { return confirm(question); } - /** - * Format a date to mm/dd/yyyy hh:mm:ss format - * @param date the date to format (timestamp) - */ + /** + * Format a date to mm/dd/yyyy hh:mm:ss format + * @param date the date to format (timestamp) + */ function formatDate(date) { var d = new Date(date * 1000); @@ -29,10 +29,10 @@ } - /** - * Find out what kind of resource an URI is - * @param uri the URI to check. If it start with a protocol (ebut not file://) return 'Live', else 'Recorded' - */ + /** + * Find out what kind of resource an URI is + * @param uri the URI to check. If it start with a protocol (ebut not file://) return 'Live', else 'Recorded' + */ function TypeofResource(uri) { var protocol = /([a-zA-Z]+):\/\//.exec(uri); @@ -46,10 +46,10 @@ } - /** - * convert a short limit name to a long one using the table above - * @param name the short name of a limit - */ + /** + * convert a short limit name to a long one using the table above + * @param name the short name of a limit + */ function shortToLongLimit(name) { var i; @@ -66,13 +66,13 @@ } - /** - * forse the server to save to the config file - * @param callback function to call after the command is send - */ + /** + * forse the server to save to the config file + * @param callback function to call after the command is send + */ function forceJSONSave(callback) { - // build the object to send to the server + // build the object to send to the server var data = { 'authorize': @@ -83,7 +83,7 @@ 'save': 1 }; - // make the XHR call + // make the XHR call $.ajax( { 'url': settings.server, @@ -99,11 +99,11 @@ } - /** - * retrieves data from the server ;) - * note: does not authenticate first. Assumes user is logged in. - * @param callback the function to call when the data has been retrieved. This callback has 1 parameter, the data retrieved. - */ + /** + * retrieves data from the server ;) + * note: does not authenticate first. Assumes user is logged in. + * @param callback the function to call when the data has been retrieved. This callback has 1 parameter, the data retrieved. + */ function getData(callback) { var data = @@ -113,7 +113,7 @@ 'username': settings.credentials.username, 'password': (settings.credentials.authstring != "" ? MD5(MD5(settings.credentials.password) + settings.credentials.authstring) : "" ) }, - 'capabilities': {} + 'capabilities': {} }; $.ajax( @@ -132,11 +132,12 @@ var ret = $.extend(true, { "streams": {}, - "capabilities": {}, + "capabilities": {}, "statistics": {} }, d); - - console.log('[651] RECV', ret); + + //IE breaks if the console isn't opened, so keep commented when committing + //console.log('[651] RECV', ret); if(callback) { @@ -147,113 +148,113 @@ } - /** - * retrieved the status and number of viewers from all streams - * @param callback function that is called when the data is collected. Has one parameter, the data retrieved - */ - function getStreamsData(callback) - { + /** + * retrieved the status and number of viewers from all streams + * @param callback function that is called when the data is collected. Has one parameter, the data retrieved + */ + function getStreamsData(callback) + { - getData(function(data) - { - var streams = {}; // streamID: [status, numViewers]; - var cnt = 0; + getData(function(data) + { + var streams = {}; // streamID: [status, numViewers]; + var cnt = 0; - for(var stream in data.streams) - { - streams[stream] = [data.streams[stream].online, 0]; - cnt++; - } + for(var stream in data.streams) + { + streams[stream] = [data.streams[stream].online, 0]; + cnt++; + } - if(cnt === 0) - { - return; // if there are no streams, don't collect data and just return - } + if(cnt === 0) + { + return; // if there are no streams, don't collect data and just return + } - for(stream in data.statistics) - { - if(data.statistics[stream].curr) - { - for(var viewer in data.statistics[stream].curr) - { - streams[stream][1]++; - } - } - } + for(stream in data.statistics) + { + if(data.statistics[stream].curr) + { + for(var viewer in data.statistics[stream].curr) + { + streams[stream][1]++; + } + } + } - callback(streams); - }); - } + callback(streams); + }); + } - /** - * parses an url and returns the parts of it. - * @return object containing the parts of the URL: protocol, host and port. - */ + /** + * parses an url and returns the parts of it. + * @return object containing the parts of the URL: protocol, host and port. + */ function parseURL(url) - { - var pattern = /(https?)\:\/\/([^:\/]+)\:(\d+)?/i; - - var retobj = {protocol: '', host: '', port: ''}; - var results = url.match(pattern); + { + var pattern = /(https?)\:\/\/([^:\/]+)\:(\d+)?/i; + + var retobj = {protocol: '', host: '', port: ''}; + var results = url.match(pattern); - if(results != null) - { - retobj.protocol = results[1]; - retobj.host = results[2]; - retobj.port = results[3]; - } + if(results != null) + { + retobj.protocol = results[1]; + retobj.host = results[2]; + retobj.port = results[3]; + } - return retobj; - } + return retobj; + } - /** - * go figure. - * @return true if there is a HTTP connector... and false if there isn't. - */ - function isThereAHTTPConnector() - { + /** + * go figure. + * @return true if there is a HTTP connector... and false if there isn't. + */ + function isThereAHTTPConnector() + { var i, len = (settings.settings.config.protocols ? settings.settings.config.protocols.length : 0); - for(i = 0; i < len; i++) - { - if(settings.settings.config.protocols[i].connector == 'HTTP') - { - return true; - } - } + for(i = 0; i < len; i++) + { + if(settings.settings.config.protocols[i].connector == 'HTTP') + { + return true; + } + } - return false; - } + return false; + } - /** - * retrieve port of the http connector - * @return the port number - */ - function getHTTPControllerPort() - { + /** + * retrieve port of the http connector + * @return the port number + */ + function getHTTPControllerPort() + { var i, len = (settings.settings.config.protocols ? settings.settings.config.protocols.length : 0); - for(i = 0; i < len; i++) - { - if(settings.settings.config.protocols[i].connector == 'HTTP') - { - return settings.settings.config.protocols[i].port; - } - } + for(i = 0; i < len; i++) + { + if(settings.settings.config.protocols[i].connector == 'HTTP') + { + return settings.settings.config.protocols[i].port; + } + } - return 0; - } + return 0; + } - /** - * retrieves the stream status (online and total number of streams) and viewer info (total number of viewers). - * @param callback function that is called when data is retrieved. Has one parameter, the retrieved data. - */ + /** + * retrieves the stream status (online and total number of streams) and viewer info (total number of viewers). + * @param callback function that is called when data is retrieved. Has one parameter, the retrieved data. + */ function getStatData(callback) { getData(function(data) @@ -290,14 +291,14 @@ } - /** - * Connect to the server and retrieve the data - * @param callback the function to call when connected. Has one parameter, an optional error string. - */ + /** + * Connect to the server and retrieve the data + * @param callback the function to call when connected. Has one parameter, an optional error string. + */ function loadSettings(callback) { - // display 'loading, please wait' while retrieving data - $('body').append( $('<div>').attr('id', 'shield').text('Loading, please wait...') ); + // display 'loading, please wait' while retrieving data + $('body').append( $('<div>').attr('id', 'shield').text('Loading, please wait...') ); var errorstr = '', data = $.extend(settings.settings, @@ -311,8 +312,9 @@ delete data.log; // don't send the logs back to the server delete data.statistics; // same goes for the stats - - console.log('[763] SEND', data); + + //IE breaks if the console isn't opened, so keep commented when committing + //console.log('[763] SEND', data); $.ajax( { @@ -328,13 +330,14 @@ 'error': function() { showTab('disconnect'); - $('#shield').remove(); // remove loading display + $('#shield').remove(); // remove loading display }, 'success': function(d) { - $('#shield').remove(); // remove loading display + $('#shield').remove(); // remove loading display - console.log('[785] RECV', d); + //IE breaks if the console isn't opened, so keep commented when committing + //console.log('[785] RECV', d); if(d && d['authorize'] && d['authorize']['challenge']) { @@ -358,7 +361,7 @@ "version": "" }, "streams": {}, - "capabilities": {}, + "capabilities": {}, "log": {}, "statistics": {} }, d); @@ -372,10 +375,10 @@ } - /** - * Sets the page's header text (loging in, connected, disconnected), title and pretty colors (!) - * @param state the state of the header. Possible are 'logingin', 'disconnected' or 'connected'. - */ + /** + * Sets the page's header text (loging in, connected, disconnected), title and pretty colors (!) + * @param state the state of the header. Possible are 'logingin', 'disconnected' or 'connected'. + */ function setHeaderState(state) { var text, cname, title; @@ -396,22 +399,22 @@ - /** - * Formats the status property to a string (with colors!) - * @param status, the status property of a stream - */ + /** + * Formats the status property to a string (with colors!) + * @param status, the status property of a stream + */ function formatStatus(status) { - if(status == undefined) - { - return "<span>Unknown, checking...</span>"; - } + if(status == undefined) + { + return "<span>Unknown, checking...</span>"; + } - switch(status) - { - case 1: return "<span class='green'>Running</span>"; break; - case 0: return "<span class='red'>Offline</span>"; break; - default: return "<span class='green'>" + status + "</span>"; break; - } + switch(status) + { + case 1: return "<span class='green'>Running</span>"; break; + case 0: return "<span class='red'>Offline</span>"; break; + default: return "<span class='green'>" + status + "</span>"; break; + } } diff --git a/lsp/graphics/sort.png b/lsp/graphics/sort.png new file mode 100644 index 00000000..b0802bd3 Binary files /dev/null and b/lsp/graphics/sort.png differ diff --git a/lsp/graphics/sort_asc.png b/lsp/graphics/sort_asc.png new file mode 100644 index 00000000..fcc1b805 Binary files /dev/null and b/lsp/graphics/sort_asc.png differ diff --git a/lsp/graphics/sort_desc.png b/lsp/graphics/sort_desc.png new file mode 100644 index 00000000..340def41 Binary files /dev/null and b/lsp/graphics/sort_desc.png differ diff --git a/lsp/main.js b/lsp/main.js index 7360859f..a5f1b5f5 100644 --- a/lsp/main.js +++ b/lsp/main.js @@ -1,15 +1,15 @@ - /** - * Local settings page - * DDVTECH - * for more information, see http://mistserver.org/index.php?title=Stand-alone_Configuration_Page - */ - - - /** - * Store a local copy of the data send by the server. - * server is the URL, credentials hold the username, password and authstring and settings is the local copy. - */ + /** + * Local settings page + * DDVTECH + * for more information, see http://mistserver.org/index.php?title=Stand-alone_Configuration_Page + */ + + + /** + * Store a local copy of the data send by the server. + * server is the URL, credentials hold the username, password and authstring and settings is the local copy. + */ var settings = { server: '', @@ -21,11 +21,11 @@ }, settings: {} }; - - - /** - * Table for long/short limit names - */ + + + /** + * Table for long/short limit names + */ var ltypes = [ ['kb_total', 'Total bandwidth'], @@ -39,11 +39,11 @@ ['str_kbps_min', 'Minimum bitrate'], ['str_kbps_max', 'Maximum bitrate'] ]; - - - /** - * When the page loads, fix the menu and show the correct tab - */ + + + /** + * When the page loads, fix the menu and show the correct tab + */ $(document).ready(function() { $('#nav').children().each(function() @@ -55,71 +55,73 @@ { $(this).attr('class', ''); }); - + // select this one $(this).attr('class', 'selected'); - + // show correct tab showTab($(this).text()); }); }); - + // show login 'tab' and hide menu showTab('login'); $('#nav').css('visibility', 'hidden'); }); - - - + + // used on the overview and streams page. cleared when switching to another 'tab'. var sinterval = null; - + + // and this one is used on the protocols page. + var pinterval = null; + // what kind of streams should be displayed? Format is [recorded, live]; var streamsdisplay = [true, true]; - - - - /** - * Display a certain page. It contains a (giant) switch-statement, that builds a page depending on the tab requested - * @param name the name of the tab - * @param streamname only used when editing streams, the name of the edited (or new) stream. Also used with the 'embed' tab - */ + + + /** + * Display a certain page. It contains a (giant) switch-statement, that builds a page depending on the tab requested + * @param name the name of the tab + * @param streamname only used when editing streams, the name of the edited (or new) stream. Also used with the 'embed' tab + */ function showTab(name, streamname) { // clear page and refresh interval $('#page').html(''); clearInterval(sinterval); - + clearInterval(pinterval); + switch(name) { case 'login': - + var host = $('<input>').attr('type', 'text').attr('placeholder', 'HTTP://' + (location.host == '' ? 'localhost:4242' : location.host) + '/api'); var user = $('<input>').attr('type', 'text').attr('placeholder', 'USERNAME'); var pass = $('<input>').attr('type', 'password').attr('placeholder', 'PASSWORD'); - var conn = $('<button>').click(function() + var conn = $('<button>').click(function() { // get login info settings.credentials.username = user.val(); settings.credentials.password = pass.val(); settings.server = host.val() == '' ? host.attr('placeholder') : host.val().toLowerCase(); - + // save username, URL in address location.hash = user.val() + '@' + settings.server; - + // try to login setHeaderState('logingin'); - + loadSettings(function(errorstr) { if(errorstr == '') { setHeaderState('connected'); - + $('#nav').css('visibility', 'visible'); - + showTab('overview'); - + // show overview as current tab - this only happens when logging out and then in $('#nav').children().each(function() { @@ -130,18 +132,19 @@ $(this).attr('class', 'selected'); } }); - + }else{ setHeaderState('disconnected'); $('#header-host').text(''); + $('#page').append($('<p>').text(errorstr)); } }); }).text('login'); - + $('#page').append( $('<div>').attr('id', 'login').append(host).append(user).append(pass).append(conn) ); - + // if we 'enter' in host, user or pass we should try to login. function hand(e) { @@ -150,28 +153,25 @@ conn.trigger('click'); // conn = login button } } - + host.keypress(hand); user.keypress(hand); pass.keypress(hand); - + // retrieve address hash from URL var adr = location.hash.replace('#', '').split('@'); - + if(adr.length == 2) { // put it in the page host.val(adr[1]); user.val(adr[0]); } - + break; - - - - + case 'overview': - + $('#page').append( $('<div>').attr('id', 'editserver').append( $('<label>').attr('for', 'config-host').text('host').append( @@ -199,7 +199,7 @@ ) ) ); - + function showStats() { getStatData(function(data) @@ -208,35 +208,33 @@ $('#cur_num_viewers').html('').text(data.viewers); }); } - - // refresh the stream status + viewers + + // refresh the stream status + viewers sinterval = setInterval(function() { showStats(); }, 10000); - + showStats(); - - + $('#editserver').append( $('<button>').attr('class', 'floatright').click(function() { var host = $('#config-host').val(); var name = $('#config-name').val(); - + settings.settings.config.host = host; settings.settings.config.name = name; - + loadSettings(function() { showTab('overview'); }); }).text( 'save' ) ); - - + var forcesave = $('<div>').attr('id', 'forcesave'); - + forcesave.append( $('<p>').text('Click the button below to force an immediate settings save. This differs from a regular save to memory and file save on exit by saving directly to file while operating. This may slow server processes for a short period of time.') ).append( @@ -248,325 +246,339 @@ } }).text( 'force save to JSON file' ) ); - + $('#page').append(forcesave); - + break; - - - - + + case 'protocols': - + $table = $('<table>'); - $table.html("<thead><th>Protocol</th><th>Settings</th><th></th></thead>"); + $table.html("<thead><th>Protocol</th><th>Status</th><th>Settings</th><th></th></thead>"); $tbody = $('<tbody>'); - + var tr, i, protocol, - len = (settings.settings.config.protocols ? settings.settings.config.protocols.length : 0); - + len = (settings.settings.config.protocols ? settings.settings.config.protocols.length : 0); + $tbody.html(''); - + pids = []; + for(i = 0; i < len; i++) { protocol = settings.settings.config.protocols[i]; // local copy - + pids.push(i); + tr = $('<tr>').attr('id', 'protocol-' + i); - + tr.append( $('<td>').text( protocol.connector ) ); - - s = ""; - for (option in protocol) { - if ((option != 'connector') && (option != 'online')) { - u = - s += option+': '+((protocol[option] == '') ? 'default' : protocol[option] )+', '; - } - } - s = s.slice(0,-2); - - tr.append( $('<td>').text( s ) ); - + + tr.append( $('<td>').html( formatStatus( protocol.online ) ) ); + + s = ""; + for (option in protocol) + { + if ((option != 'connector') && (option != 'online')) + { + s += option+': '+((protocol[option] == '') || (protocol[option] == 0) ? 'default' : protocol[option] )+', '; + } + } + s = s.slice(0,-2); + + tr.append( $('<td>').text( s ) ); + tr.append( $('<td>').attr('class', 'center').append( $('<button>').text('edit').click(function() - { - id = $(this).parent().parent().attr('id').replace('protocol-', ''); - showTab('editprotocol', id); - }) ).append( $('<button>').click(function() - { - if(confirmDelete('Are you sure you want to delete this protocol?') == true) - { - var id = $(this).parent().parent().attr('id').replace('protocol-', ''); - settings.settings.config.protocols.splice(id, 1); - $(this).parent().parent().remove(); - loadSettings(); - } - }).text('delete') ) ); - + { + id = $(this).parent().parent().attr('id').replace('protocol-', ''); + showTab('editprotocol', id); + }) ).append( $('<button>').click(function() + { + if(confirmDelete('Are you sure you want to delete this protocol?') == true) + { + var id = Number($(this).parent().parent().attr('id').replace('protocol-', '')); + var pid = pids.indexOf(id); + settings.settings.config.protocols.splice(pid, 1); + $(this).parent().parent().remove(); + loadSettings(); + } + }).text('delete') ) ); + $tbody.append(tr); } - + $table.append($tbody); $('#page').append($table); - - $('#page').append( + + $('#page').append( $('<button>').attr('class', 'floatright').click(function() { showTab('editprotocol', 'new'); }).text('add new') ); - + + function refreshProtocolStatus() + { + getData(function(data) + { + protocol = data.config.protocols; + for (index in protocol) + { + var row = $('#protocol-' + index); + var status = protocol[index].online; + + $(row.children()[1]).html( formatStatus(status) ); + + if (status == undefined) + { + setTimeout(function() + { + refreshProtocolStatus(); + },1000); + } + } + }); + } + + pinterval = setInterval(function() + { + refreshProtocolStatus(); + },10000); + refreshProtocolStatus(); + break; - - - - - - - + case 'editprotocol': - if (streamname != 'new') { currentdata = settings.settings.config.protocols[streamname]; } - - currentconnectors = []; - for (var index in settings.settings.config.protocols) { //build a list of the current connectors to see if the dependencies are already configured - currentconnectors.push(settings.settings.config.protocols[index].connector); - } - - function buildProtocolFields(selectedProtocol) - { - data = settings.settings.capabilities.connectors[selectedProtocol]; - - $t = $('<p>').text(data.desc); - if ((typeof data.deps != 'undefined') && (data.deps)) - { - $t.append($('<p>').text('Dependencies:')); - $s = $('<ul>'); - deps = data.deps.split(','); - for (var index in deps) - { - t = deps[index]; - if ($.inArray(deps[index],currentconnectors) < 0) { - $u = $('<span>').text(' (Not yet configured!)').addClass('red'); - } - else { - $u = $('<span>').text(' (Configured)').addClass('green'); - } - $s.append($('<li>').text(t).append($u)); - } - $t.append($s); - } - $('#protocoldesc').html( $t ); - - $protocolfields = $('<div>'); - if (typeof data.required != 'undefined') - { - $protocolfields.append( $('<p>').text('Required parameters') ); - for(fieldname in data.required) - { - switch (data.required[fieldname].type) - { - case 'str': - var inputType = 'text' - break; - case 'uint': - var inputType = 'number' - var func = 'uint' - break; - case 'int': - var inputType = 'number' - break; - default: - - break; - } - $i = $('<input>').attr('type',inputType).attr('id','protocol-parameter-'+fieldname).addClass('required'); - if (func == 'uint') { - $i.addClass('uint'); - } - $protocolfields.append( - $('<label>').text(data.required[fieldname].name).attr('title',data.required[fieldname].help).append($i) - ); - } - } - if (typeof data.optional != 'undefined') - { - $protocolfields.append( $('<p>').text('Optional parameters') ); - for(fieldname in data.optional) - { - switch (data.optional[fieldname].type) - { - case 'str': - var inputType = 'text' - break; - case 'uint': - var inputType = 'number' - var func = 'uint' - break; - case 'int': - var inputType = 'number' - break; - default: - - break; - } - $i = $('<input>').attr('type',inputType).attr('id','protocol-parameter-'+fieldname); - if (func == 'uint') { - $i.addClass('uint'); - } - $protocolfields.append( - $('<label>').text(data.optional[fieldname].name).attr('title',data.optional[fieldname].help).append($i) - ); - } - } - $('#protocolfields').html($protocolfields); - if (streamname != 'new') { - for (fieldname in currentdata) { - if ((fieldname != 'connector') && (fieldname != 'online')) { - $('#protocol-parameter-'+fieldname).val(currentdata[fieldname]); - } - } - } - } - - loadSettings(function() - { - if (streamname == 'new') { t = 'add new protocol'; } - else { t = 'edit protocol'; } - - $('#page').append( $('<p>').text(t) ); - - $selectprotocol = $('<select>').attr('id', 'edit-protocol').change(function() - { - buildProtocolFields($(this).children(':selected').val()); - }); - for(protocol in settings.settings.capabilities.connectors) - { - if ((streamname != 'new') && (currentdata.connector == protocol)) { - $selectprotocol.append( - $('<option>').attr('value', protocol).attr('selected','selected').text(protocol) - ); - } - else { - $selectprotocol.append( - $('<option>').attr('value', protocol).text(protocol) - ); - } - } - - $div = $('<div>').attr('id', 'editprotocol'); - $div.append( - $('<label>').attr('for', 'protocol-edit-protocol').text('protocol').append( - $selectprotocol - ) - ); - - - $('#page').append( $div ); - $('#editprotocol').append( $('<div>').attr('id','protocoldesc') ); - $('#editprotocol').append( $('<div>').attr('id', 'protocolfields') ); - $('#editprotocol').append( - $('<button>').text('cancel').addClass('floatright').click(function() - { - showTab('protocols'); - }) - ); - $('#editprotocol').append( - $('<button>').text('save').addClass('floatright').click(function() - { - error = false; - $('input.required').each(function(){ //check if all required fields have contents - if ($(this).val() == '') { - $(this).focus(); - $(this).parent().addClass('red'); - error = true; - } - }); - $('input[type="number"]').each(function(){ //turn all numbers into integers - $(this).val(Math.floor($(this).val())); - }); - $('input.uint').each(function(){ //check if all uints are actually uints - if ($(this).val() < 0) { - $(this).focus(); - $(this).parent().addClass('red'); - error = true; - } - }); - if (error) { return; } + if (streamname != 'new') + { + currentdata = settings.settings.config.protocols[streamname]; + } + + currentconnectors = []; + // build a list of the current connectors to see if the dependencies are already configured + for (var index in settings.settings.config.protocols) + { + currentconnectors.push(settings.settings.config.protocols[index].connector); + } + + function buildProtocolParameterFields(data,required) + { + for (fieldname in data) + { + switch(data[fieldname].type) + { + case 'str': + var inputType = 'text' + break; + case 'uint': + var inputType = 'number' + var func = 'uint' + break; + case 'int': + var inputType = 'number' + break; + } + $i = $('<input>').attr('type',inputType).attr('id','protocol-parameter-'+fieldname); + if (func == 'uint') + { + $i.addClass('uint'); + } + if (required) + { + $i.addClass('required'); + } + $protocolfields.append( + $('<label>').text(data[fieldname].name).attr('title',data[fieldname].help).append($i) + ); + } + } + + function buildProtocolFields(selectedProtocol) + { + data = settings.settings.capabilities.connectors[selectedProtocol]; + + $t = $('<p>').text(data.desc); + if ((typeof data.deps != 'undefined') && (data.deps)) + { + $t.append($('<p>').text('Dependencies:')); + $s = $('<ul>'); + deps = data.deps.split(','); + for (var index in deps) + { + t = deps[index]; + if ($.inArray(deps[index],currentconnectors) < 0) + { + $u = $('<span>').text(' (Not yet configured!)').addClass('red'); + }else{ + $u = $('<span>').text(' (Configured)').addClass('green'); + } + $s.append($('<li>').text(t).append($u)); + } + $t.append($s); + } + $('#protocoldesc').html( $t ); + + $protocolfields = $('<div>'); + if (typeof data.required != 'undefined') + { + $protocolfields.append( $('<p>').text('Required parameters') ); + buildProtocolParameterFields(data.required,true); + } + if (typeof data.optional != 'undefined') + { + $protocolfields.append( $('<p>').text('Optional parameters') ); + buildProtocolParameterFields(data.optional,false); + } + $('#protocolfields').html($protocolfields); + if (streamname != 'new') + { + for (fieldname in currentdata) + { + if ((fieldname != 'connector') && (fieldname != 'online')) + { + $('#protocol-parameter-'+fieldname).val(currentdata[fieldname]); + } + } + } + } + + loadSettings(function() + { + if (streamname == 'new') { t = 'add new protocol'; } + else { t = 'edit protocol'; } + + $('#page').append( $('<p>').text(t) ); + + $selectprotocol = $('<select>').attr('id', 'edit-protocol').change(function() + { + buildProtocolFields($(this).children(':selected').val()); + }); + for(protocol in settings.settings.capabilities.connectors) + { + if ((streamname != 'new') && (currentdata.connector == protocol)) { + $selectprotocol.append( + $('<option>').attr('value', protocol).attr('selected','selected').text(protocol) + ); + }else{ + $selectprotocol.append( + $('<option>').attr('value', protocol).text(protocol) + ); + } + } + + $div = $('<div>').attr('id', 'editprotocol'); + $div.append( + $('<label>').attr('for', 'protocol-edit-protocol').text('protocol').append( + $selectprotocol + ) + ); + + $('#page').append( $div ); + $('#editprotocol').append( $('<div>').attr('id','protocoldesc') ); + $('#editprotocol').append( $('<div>').attr('id', 'protocolfields') ); + $('#editprotocol').append( + $('<button>').text('cancel').addClass('floatright').click(function() + { + showTab('protocols'); + }) + ); + $('#editprotocol').append( + $('<button>').text('save').addClass('floatright').click(function() + { + error = false; + //check if all required fields have contents + $('input.required').each(function() + { + if ($(this).val() == '') + { + $(this).focus(); + $(this).parent().addClass('red'); + error = true; + } + }); + //turn all numbers into integers + $('input[type="number"]').each(function() + { + $(this).val(Math.floor($(this).val())); + }); + //check if all uints are actually uints + $('input.uint').each(function() + { + if ($(this).val() < 0) + { + $(this).focus(); + $(this).parent().addClass('red'); + error = true; + } + }); + if (error) { return; } if(!settings.settings.config.protocols) { settings.settings.config.protocols = []; } - - connectorval = $('#edit-protocol').val() + + connectorval = $('#edit-protocol').val() var newprotocol = { - connector: connectorval + connector: connectorval }; - $('input').each(function(){ - newprotocol[$(this).attr('id').split('-')[2]] = $(this).val();; - }); - if (streamname == 'new') { - settings.settings.config.protocols.push(newprotocol); - } - else { - settings.settings.config.protocols[streamname] = newprotocol; - } - + + $('input').each(function(){ + newprotocol[$(this).attr('id').split('-')[2]] = $(this).val();; + }); + if (streamname == 'new') { + settings.settings.config.protocols.push(newprotocol); + }else{ + settings.settings.config.protocols[streamname] = newprotocol; + } + loadSettings(function() { showTab('protocols'); }); - }) - ); - - buildProtocolFields($('select#edit-protocol :selected').val()); - - }); - - - - - break; - - - - - - - - case 'streams': - + }) + ); + + buildProtocolFields($('select#edit-protocol :selected').val()); + + }); + + break; + + case 'streams': + // the filter element containr $div = $('<div>').attr('id', 'streams-filter'); - - // filters the table. uses the streamsdisplay + + // filters the table. uses the streamsdisplay function filterTable() { $('#streams-list-tbody').children().each(function(k, v) { var type = $($(v).children()[0]).text().toLowerCase(); - + $(v).show(); - + if(type == 'recorded' && streamsdisplay[0] == false) { $(v).hide(); } - + if(type == 'live' && streamsdisplay[1] == false) { $(v).hide(); } }); } - + function filterOn(event, elem) { if(event.target.id == '') { return; // label click goes bubbles on checkbox, so ignore it } - + var what = $(elem).text(); - + if(what == 'recorded') { streamsdisplay[0] = !streamsdisplay[0]; @@ -575,84 +587,84 @@ streamsdisplay[1] = !streamsdisplay[1]; $('#stream-filter-live').attr('checked', streamsdisplay[1]); } - + filterTable(); } - $div.append( $('<label>').attr('for', 'stream-filter-recorded').text('recorded').append( $('<input>').attr('type', 'checkbox').attr('id', 'stream-filter-recorded').attr('checked', streamsdisplay[0]) - ).click(function(event) - { - filterOn(event, this); - }) + ).click(function(event) + { + filterOn(event, this); + }) ); $div.append( $('<label>').attr('for', 'stream-filter-live').text('live').append( $('<input>').attr('type', 'checkbox').attr('id', 'stream-filter-live').attr('checked', streamsdisplay[1]) - ).click(function(event) - { - filterOn(event, this); - }) + ).click(function(event) + { + filterOn(event, this); + }) ); - $('#page').append($div); - - // refresh every streams' data (status and viewer count) - function refreshStreams() - { - getStreamsData(function(streams) - { - for(stream in streams) - { - if( $('stream-' + stream) ) - { - var row = $('#stream-' + stream); - var status = streams[stream][0]; - - $(row.children()[3]).html( formatStatus(status) ); - - $(row.children()[4]).text(streams[stream][1]); - } - } - }); - }; - - sinterval = setInterval(function() - { - refreshStreams(); - }, 10000); - - refreshStreams(); - + + // refresh every streams' data (status and viewer count) + function refreshStreams() + { + getStreamsData(function(streams) + { + for(stream in streams) + { + if( $('stream-' + stream) ) + { + var row = $('#stream-' + stream); + var status = streams[stream][0]; + + $(row.children()[4]).html( formatStatus(status) ); + + $(row.children()[5]).text(streams[stream][1]); + } + } + }); + }; + + sinterval = setInterval(function() + { + refreshStreams(); + }, 10000); + + refreshStreams(); + $table = $('<table>'); - $table.html("<thead><th>Type</th><th>Embed</th><th>Name</th><th>Status</th><th>Viewers</th><th>Edit</th></thead>"); + $table.html("<thead><th class=sort-type-int>Id</th><th class=sort-type-string>Type</th><th>Embed</th><th class='sort-type-string sortdesc'>Name</th><th class=sort-type-string>Status</th><th class=sort-type-int>Viewers</th><th>Edit</th></thead>"); $tbody = $('<tbody>'); - + var stream, cstr, $tr; - + $tbody.html('').attr('id', 'streams-list-tbody'); - + for(stream in settings.settings.streams) { var cstr = settings.settings.streams[stream]; - + $tr = $('<tr>').attr('id', 'stream-' + stream); - + + $tr.append( $('<td>').text( cstr.sid ) ); + $tr.append( $('<td>').text( TypeofResource( cstr.channel.URL ) ) ); - + $tr.append( $('<td>').append( $('<button>').text('embed').click(function() { var sname = $(this).parent().parent().attr('id').replace('stream-', ''); showTab('embed', sname); }) ) ); // end function, end click(), end append(), end append(). Huzzah jQuery. - - $tr.append( $('<td>').text(cstr.name) ); - - $tr.append( $('<td>').html( formatStatus( cstr.online ) ) ); - + + $tr.append( $('<td>').text( cstr.name ) ); + + $tr.append( $('<td>').html( formatStatus( cstr.online ) ) ); + var cviewers = 0; - + if(settings.settings.statistics && settings.settings.statistics[stream]) { if(settings.settings.statistics[stream] && settings.settings.statistics[stream].curr) @@ -665,44 +677,48 @@ }else{ cviewers = 0; } - + $tr.append( $('<td>').text( cviewers ) ); - + $tr.append( $('<td>').append( $('<button>').text('edit').click(function() { var sname = $(this).parent().parent().attr('id').replace('stream-', ''); - + showTab('editstream', sname); }) ) ); // end function, end click, end append, end append. - + $tbody.append($tr); + + //quickly re-check if the streams are online now + if (cstr.online == undefined) + { + setTimeout(function() + { + refreshStreams(); + }, 1000); + } } - - $table.append($tbody); + + $table.append($tbody).addClass('sortable'); + $table.stupidtable(); $('#page').append($table); - + // on page load, also filter with the (users' defined) stream filter filterTable(); - + $('#page').append( $('<button>').attr('class', 'floatright').click(function() { showTab('editstream', 'new'); }).text('add new') ); - + break; - - - - - - - + case 'editstream': - + var sdata, title; - + if(streamname == 'new') { sdata = @@ -723,9 +739,9 @@ sdata = settings.settings.streams[streamname]; title = 'edit stream "' + sdata.name + '"'; } - + $('#page').append( $('<p>').text(title) ); - + $('#page').append( $('<div>').attr('id', 'editserver').append( $('<label>').attr('for', 'stream-edit-name').text('name').append( @@ -734,19 +750,19 @@ ).append( $('<label>').attr('for', 'stream-edit-source').text('source').append( $('<input>').attr('type', 'text').attr('placeholder', 'SOURCE').attr('id', 'stream-edit-source').attr('value', sdata.channel.URL).keyup(function() - { - var text = $(this).val(); - - if(text.charAt(0) == '/' || text.substr(0, 7) == 'push://') - { - $('#stream-edit-preset').val(''); - $('#stream-edit-preset').hide(); - $('#stream-edit-preset-label').hide(); - }else{ - $('#stream-edit-preset').show(); - $('#stream-edit-preset-label').show(); - } - }) + { + var text = $(this).val(); + + if(text.charAt(0) == '/' || text.substr(0, 7) == 'push://') + { + $('#stream-edit-preset').val(''); + $('#stream-edit-preset').hide(); + $('#stream-edit-preset-label').hide(); + }else{ + $('#stream-edit-preset').show(); + $('#stream-edit-preset-label').show(); + } + }) ) ).append( $('<label>').attr('id', 'stream-edit-preset-label').attr('for', 'stream-edit-preset').text('preset').append( @@ -754,20 +770,19 @@ ) ) ); - - // if the source is push or file, don't do a preset - var text = $('#stream-edit-source').val(); - - if(text.charAt(0) == '/' || text.substr(0, 7) == 'push://') - { - $('#stream-edit-preset').hide(); - $('#stream-edit-preset-label').hide(); - }else{ - $('#stream-edit-preset').show(); - $('#stream-edit-preset-label').show(); - } - - + + // if the source is push or file, don't do a preset + var text = $('#stream-edit-source').val(); + + if(text.charAt(0) == '/' || text.substr(0, 7) == 'push://') + { + $('#stream-edit-preset').hide(); + $('#stream-edit-preset-label').hide(); + }else{ + $('#stream-edit-preset').show(); + $('#stream-edit-preset-label').show(); + } + $('#editserver').append( $('<button>').attr('class', 'floatright').click(function() { @@ -786,37 +801,47 @@ } }).text( streamname == 'new' ? 'cancel' : 'delete' ) ); - + $('#editserver').append( $('<button>').attr('class', 'floatright').click(function() { var n = $('#stream-edit-name'); var s = $('#stream-edit-source'); var p = $('#stream-edit-preset'); - + if(n.val() == ''){ n.focus(); return; } if(s.val() == ''){ s.focus(); return; } - - var newname = n.val().replace(/([^a-zA-Z0-9_])/g, '').toLowerCase(); - + + var newname = n.val().replace(/([^a-zA-Z0-9_])/g, '').toLowerCase(); + sdata.name = newname; sdata.channel.URL = s.val(); sdata.preset.cmd = p.val(); - + if(streamname == 'new') { streamname = newname; + sdata.sid = 0; + for (strm in settings.settings.streams) + { + sdata.sid = Math.max(sdata.sid,settings.settings.streams[strm].sid); + } + sdata.sid += 1; } - + else + { + sdata.sid = settings.settings.streams[streamname].sid; + } + if(!settings.settings.streams) { settings.settings.streams = {}; } - - delete settings.settings.streams[streamname]; - + + delete settings.settings.streams[streamname]; + settings.settings.streams[newname] = sdata; - + loadSettings(function() { showTab('streams'); @@ -824,128 +849,117 @@ }).text('save') ); - + break; - - - - - + case 'embed': - - if(isThereAHTTPConnector()) - { - var embedbase = 'http://' + parseURL(settings.server).host + ':' + getHTTPControllerPort() + '/'; - - $('#page').append( $('<p>').attr('class', 'nocapitals').text('The info embed URL is "' + embedbase + 'info_' + streamname + '.js".') ); - $('#page').append( $('<p>').attr('class', 'nocapitals').text('The embed embed URL is "' + embedbase + 'embed_' + streamname + '.js".') ); - - $('#page').append( $('<button>').text('preview').click(function() - { - showTab('preview', streamname); - } ) ); - - }else{ - $('#page').append( $('<p>').attr('class', 'nocapitals').text('Could\'t find a HTTP connector. Please add a HTTP connector on the "protocol" page.') ); - } - + + if(isThereAHTTPConnector()) + { + var embedbase = 'http://' + parseURL(settings.server).host + ':' + getHTTPControllerPort() + '/'; + + $('#page').append( $('<p>').attr('class', 'nocapitals').text('The info embed URL is "' + embedbase + 'info_' + streamname + '.js".') ); + $('#page').append( $('<p>').attr('class', 'nocapitals').text('The embed embed URL is "' + embedbase + 'embed_' + streamname + '.js".') ); + + $('#page').append( $('<button>').text('preview').click(function() + { + showTab('preview', streamname); + } ) ); + + }else{ + $('#page').append( $('<p>').attr('class', 'nocapitals').text('Could\'t find a HTTP connector. Please add a HTTP connector on the "protocol" page.') ); + } + break; - - - + case 'preview': - - var embed = 'http://' + parseURL(settings.server).host + ':' + getHTTPControllerPort() + '/embed_' + streamname + '.js'; - - $('#page').append( $('<div>').attr('id', 'previewcontainer') ); - - // jQuery doesn't work -> use DOM magic - var script = document.createElement('script'); - script.src = embed; - document.getElementById('previewcontainer').appendChild( script ); - + + var embed = 'http://' + parseURL(settings.server).host + ':' + getHTTPControllerPort() + '/embed_' + streamname + '.js'; + + $('#page').append( $('<div>').attr('id', 'previewcontainer') ); + + // jQuery doesn't work -> use DOM magic + var script = document.createElement('script'); + script.src = embed; + document.getElementById('previewcontainer').appendChild( script ); + break; - - - - - + case 'limits': $table = $('<table>'); $table.html("<thead><th>Type</th><th>Hard/soft</th><th>Value</th><th>applies to</th><th>Action</th></thead>"); $tbody = $('<tbody>'); - + var i, tr, limit, stream, clims, alllimits = settings.settings.config.limits; - + for(stream in settings.settings.streams) { clims = settings.settings.streams[stream].limits; - + $.each(clims, function(k, v) { this.appliesto = stream; this.appliesi = k; }); - + alllimits = alllimits.concat(clims); } - + len = alllimits.length; - + // remove old items $tbody.html(''); - + for(i = 0; i < len; i++) { tr = $('<tr>').attr('id', 'limits-' + i); limit = alllimits[i]; - + tr.append( $('<td>').text( shortToLongLimit(limit.name) ) ); tr.append( $('<td>').text( limit.type ) ); tr.append( $('<td>').text( limit.val ) ); - - + if(limit.appliesto) { tr.append( $('<td>').text( settings.settings.streams[limit.appliesto].name ).attr('id', 'limit-at-' + limit.appliesto + '-' + limit.appliesi) ); }else{ tr.append( $('<td>').text( 'server' ) ); } - + delete limit.appliesto; delete limit.appliesi; - + tr.append( $('<td>').attr('class', 'center').append( $('<button>').click(function() - { - if(confirmDelete('Are you sure you want to delete this limit?') == true) - { - var id = $(this).parent().parent().attr('id').replace('limits-', ''); - var at = $($(this).parent().parent().children()[3]).attr('id'); - - if(at == undefined) - { - settings.settings.config.limits.splice(id, 1); - }else{ - var data = at.replace('limit-at-', '').split('-'); - var loc = data.pop(); - data = data.join('-'); - - settings.settings.streams[data].limits.splice(loc, 1); - } - - $(this).parent().parent().remove(); - - loadSettings(); - } - }).text('delete') ) ); - + { + if(confirmDelete('Are you sure you want to delete this limit?') == true) + { + var id = $(this).parent().parent().attr('id').replace('limits-', ''); + var at = $($(this).parent().parent().children()[3]).attr('id'); + + if(at == undefined) + { + settings.settings.config.limits.splice(id, 1); + }else{ + var data = at.replace('limit-at-', '').split('-'); + var loc = data.pop(); + data = data.join('-'); + + settings.settings.streams[data].limits.splice(loc, 1); + } + + $(this).parent().parent().remove(); + + loadSettings(); + } + }).text('delete') ) ); + $tbody.append(tr); } - + // add new limit $nltr = $('<tr>').attr('class', 'outsidetable'); - + // type selector $ltype = $('<select>').attr('id', 'new-limit-type'); for(i = 0; i < ltypes.length; i++) @@ -957,10 +971,10 @@ $nltr.append( $('<td>').append( $('<select>').attr('id', 'new-limit-hs').append( $('<option>').attr('value', 'hard').text('Hard limit') ).append( $('<option>').attr('value', 'soft').text('Soft limit') ) ) ); // value $nltr.append( $('<td>').append( $('<input>').attr('type', 'text').attr('id', 'new-limit-val') ) ); - + // applies to (stream) var $appliesto = $('<select>').attr('id', 'new-limit-appliesto').append( $('<option>').attr('value', 'server').text('Server') ); - + for(var strm in settings.settings.streams) { $appliesto.append( @@ -968,7 +982,7 @@ ); } $nltr.append( $('<td>').append( $appliesto ) ); - + $nltr.append( $('<td>').attr('class', 'center').append( $('<button>').click(function() @@ -979,55 +993,53 @@ type: $('#new-limit-hs :selected').val(), val: $('#new-limit-val').val() }; - + if( $('#new-limit-appliesto').val() == 'server') { settings.settings.config.limits.push(obj); }else{ settings.settings.streams[ $('#new-limit-appliesto').val() ].limits.push(obj); } - + loadSettings(function() { showTab('limits'); }); - + }).text('add new') ) ); $tbody.append($nltr); - + $table.append($tbody); $('#page').append($table); - + break; - - - + case 'logs': $table = $('<table>'); $table.html("<thead><th>Date<span class='theadinfo'>(MM/DD/YYYY)</span></th><th>Type</th><th>Message</th></thead>"); $tbody = $('<tbody>'); - - if(!settings.settings.log) - { - return; // no logs, so just bail - } + + if(!settings.settings.log) + { + return; // no logs, so just bail + } var i, cur, $tr, logs = settings.settings.log, len = logs.length; - + if(len >= 2 && settings.settings.log[0][0] < settings.settings.log[len - 1][0]) { logs.reverse(); } - + $tbody.html(''); - + for(i = 0; i < len; i++) { cur = settings.settings.log[i]; - + $tr = $('<tr>').append( $('<td>').text(formatDate(cur[0])) ).append( @@ -1035,13 +1047,13 @@ ).append( $('<td>').text(cur[2]) ); - + $tbody.append($tr); } - + $table.append($tbody); $('#page').append($table); - + $('#page').append( $('<button>').attr('class', 'floatright').click(function() { @@ -1052,16 +1064,14 @@ }); }).text('Purge logs') ); - + break; - - - + case 'disconnect': showTab('login'); setHeaderState('disconnected'); $('#nav').css('visibility', 'hidden'); - + settings = { server: '', @@ -1074,12 +1084,10 @@ settings: {} }; break; - } // end switch - - + //placeholder for older browsers $('input[placeholder]').placeholder(); - + } diff --git a/lsp/server.html b/lsp/server.html index d087d8fb..b6fe2a3c 100644 --- a/lsp/server.html +++ b/lsp/server.html @@ -13,6 +13,7 @@ <script src='md5.js'></script> <script src='main.js'></script> <script src='functions.js'></script> + <script src='tablesort.js'></script> <link rel='stylesheet' href='style.css' /> diff --git a/lsp/style.css b/lsp/style.css index 32c4d1e7..7aba5a72 100644 --- a/lsp/style.css +++ b/lsp/style.css @@ -117,6 +117,7 @@ button background-color: #505050; color: #fff; border: 0; + margin: 0 2px 0 2px; } @@ -345,6 +346,20 @@ td width: auto; padding: 0; } +.sortable th.sort-type-string, .sortable th.sort-type-int, .sortable th.sort-type-float +{ + background: transparent url('graphics/sort.png') no-repeat 5px 50%; +} + +.sortable th.sortasc +{ + background-image: url('graphics/sort_asc.png'); +} + +.sortable th.sortdesc +{ + background-image: url('graphics/sort_desc.png'); +} #protocoldesc p { font-weight: normal; @@ -352,7 +367,7 @@ td } #protocolfields { - margin-top: 10px; + margin-top: 10px; } #forcesave diff --git a/lsp/tablesort.js b/lsp/tablesort.js new file mode 100644 index 00000000..16551ff0 --- /dev/null +++ b/lsp/tablesort.js @@ -0,0 +1,216 @@ +// Stupid jQuery table plugin. + + + +//http://joequery.github.com/Stupid-Table-Plugin/ + + + +// Call on a table +// sortFns: Sort functions for your datatypes. +(function($){ + $.fn.stupidtable = function(sortFns){ + var table = this; sortFns = sortFns || {}; + + // ==================================================== // + // Utility functions // + // ==================================================== // + + // Merge sort functions with some default sort functions. + sortFns = $.extend({}, { + "int":function(a,b){ return parseInt(a, 10) - parseInt(b, 10); }, + "float":function(a,b){ return parseFloat(a) - parseFloat(b); }, + "string":function(a,b){ if (a<b) return -1; if (a>b) return +1; return 0;} + }, sortFns); + + // Array comparison. See http://stackoverflow.com/a/8618383 + var arrays_equal = function(a,b) { return !!a && !!b && !(a<b || b<a);} + + // Return the resulting indexes of a sort so we can apply + // this result elsewhere. This returns an array of index numbers. + // return[0] = x means "arr's 0th element is now at x" + var sort_map = function(arr, sort_function){ + var sorted = arr.slice(0).sort(sort_function); + var map = []; + var index = 0; + for(var i=0; i<arr.length; i++){ + index = $.inArray(arr[i], sorted); + + // If this index is already in the map, look for the next index. + // This handles the case of duplicate entries. + while($.inArray(index, map) != -1){ + index++; + } + map.push(index); + } + return map; + } + + // Apply a sort map to the array. + var apply_sort_map = function(arr, map){ + var clone = arr.slice(0); + for(var i=0; i<map.length; i++){ + newIndex = map[i]; + clone[newIndex] = arr[i]; + } + return clone; + } + + // Returns true if array is sorted, false otherwise. + // Checks for both ascending and descending + var is_sorted_array = function(arr, sort_function){ + var clone = arr.slice(0); + var reversed = arr.slice(0).reverse(); + var sorted = arr.slice(0).sort(sort_function); + + // Check if the array is sorted in either direction. + return arrays_equal(clone, sorted) || arrays_equal(reversed, sorted); + } + + + var what_order_sorted = function(data, sf, isa) + { + var tmp = [data[0], data[data.length - 1]]; + + tmp.sort(sf); + + if(data[0] == tmp[0] || !isa) + { + return 'desc'; + }else{ + return 'asc'; + } + } + + + + // ==================================================== // + // Begin execution! // + // ==================================================== // + // Do sorting when THs are clicked + + table.delegate("th", "click", function(){ + + if($(this).text().replace(/ /g, '') == '') + { + // empty header, don't allow sorting + return; + } + + + var trs = table.find("tbody tr"); + var i = $(this).index(); + var classes = $(this).attr("class"); + var type = null; + + if (classes){ + classes = classes.split(/\s+/); + + for(var j=0; j<classes.length; j++){ + if(classes[j].search("sort-type-") != -1){ + type = classes[j].replace('sort-', ''); + break; + } + } + if(type){ + type = type.split('-')[1]; + } + else{ + type = "string"; + } + } + + + // Don't attempt to sort if no data type + //if(!type){return false;} + + + var sortMethod = sortFns[type]; + + + // Gather the elements for this column + column = []; + + // Push either the value of the 'data-order-by' attribute if specified + // or just the text() value in this column to column[] for comparison. + trs.each(function(index,tr){ + var e = $(tr).children().eq(i); + var order_by = e.attr('data-order-by') || e.text(); + column.push(order_by); + }); + + + // If the column is already sorted, just reverse the order. The sort + // map is just reversing the indexes. + if(is_sorted_array(column, sortMethod)){ + column.reverse(); + var theMap = []; + for(var i=column.length-1; i>=0; i--){ + theMap.push(i); + } + }else{ + // Get a sort map and apply to all rows + theMap = sort_map(column, sortMethod); + } + + +// remove old sort classes (on this and other columns) +$(this).parent().find('th').each(function() +{ + $(this).removeClass('sortasc sortdesc'); +}); + +// what order are we sorting in? +var whatorder = what_order_sorted(column, sortMethod, is_sorted_array(column, sortMethod)); + +// set new sort class +$(this).addClass(whatorder == 'asc' ? 'sortasc' : 'sortdesc'); + + + + + + var sortedTRs = $(apply_sort_map(trs, theMap)); + + + + // Replace the content of tbody with the sortedTRs. Strangely (and + // conveniently!) enough, .append accomplishes this for us. + table.find("tbody").append(sortedTRs); + + }); + + + +// remove th icon if no header text + $(this).find('th').each(function() + { + var hv = $(this).text().replace(/ /g, ''); + + if(hv == '') + { + $(this).css('background', 'transparent'); + } + }); + + + + + } + })(jQuery); + + + + +$('table.sortable').each(function() +{ + var rows = $(this).find('tbody tr').length; + + if(rows > 1) + { + $(this).stupidtable(); + }else{ + $(this).removeClass('sortable'); + } +}); +