E-Hentai Upload Progress

By HF Last update Mar 10, 2013 — Installed 2,306 times.

There are 7 previous versions of this script.

// ==UserScript==
// @name           E-Hentai Upload Progress
// @description    Show an upload progress bar when uploading files, and allow multiple files selection at once. WARNING: you'll have to disable that script in order to upload files bigger than 96MB on FF3.6.
// @icon           http://g.e-hentai.org/favicon.ico
// @namespace      hf
// @include        http://ul.e-hentai.org/manage.php?act=add&*
// @include        http://ul.exhentai.org/manage.php?act=add&*
// @version        1.4
// ==/UserScript==

/* Helpers */

// Shortcuts
function getel(el) {if(el) return el; return document;}
function gid(id, el) {return getel(el).getElementById(id);}
function gtag(tag, el) {return getel(el).getElementsByTagName(tag);}
function gclass(className, el) {return getel(el).getElementsByClassName(className);}
function qselall(query, el) {return getel(el).querySelectorAll(query);}
function qsel(query, el) {return getel(el).querySelector(query);}
function newel(tag, el) {return getel(el).createElement(tag);}

function insertBefore(newel, el) {
  return el.parentNode.insertBefore(newel, el);
}

function insertAfter(newel, el) {
  var next = el.nextSibling;
  if(!next) return el.parentNode.appendChild(newel);
  return el.parentNode.insertBefore(newel, next);
}

// Convert a JS String from Unicode to UTF-8
function toUtf8(str) {
  var res = '';
  for(var i = 0 ; i < str.length ; ++i) {
    var c = str.charCodeAt(i);
    if(c < 0x80) res += String.fromCharCode(c);
    else if(c < 0x0800) {
      res += String.fromCharCode(0xC0 | (c >> 6), 0x80 | (c & 0x3F));
    } else if(c < 0x10000) {
      res += String.fromCharCode(0xE0 | (c >> 12), 0x80 | ((c >> 6) & 0x3F), 0x80 | (c & 0x3F));
    }
    // The following cannot happen
    else if(c < 0x200000) {
      res += String.fromCharCode(0xF0 | (c >> 18), 0x80 | ((c >> 12) & 0x3F), 0x80 | ((c >> 6) & 0x3F), 0x80 | (c & 0x3F));
    } else if(c < 0x4000000) {
      res += String.fromCharCode(0xF8 | (c >> 24), 0x80 | ((c >> 18) & 0x3F), 0x80 | ((c >> 12) & 0x3F), 0x80 | ((c >> 6) & 0x3F), 0x80 | (c & 0x3F));
    } else /* if(c < 0x80000000) */ {
      res += String.fromCharCode(0xFC | (c >> 30), 0x80 | ((c >> 24) & 0x3F), 0x80 | ((c >> 18) & 0x3F), 0x80 | ((c >> 12) & 0x3F), 0x80 | ((c >> 6) & 0x3F), 0x80 | (c & 0x3F));
    }
  }

  return res;
}

if(window.chrome) {
  /* aeosynth implementations */
  GM_setValue = function(name, value) {
    value = (typeof value)[0] + value;
    localStorage.setItem(name, value);
  };

  GM_getValue = function(name, defaultValue) {
    var value = localStorage.getItem(name);
    if (!value) return defaultValue;
    var type = value[0];
    value = value.substring(1);
    switch (type) {
      case 'b': return value == 'true';
      case 'n': return Number(value);
      default: return value;
    }
  };
}

/* Script core */

var g_cur_idx = undefined;
var g_cur_file_idx = 0;
var g_err_msg = '';
var g_last_status = undefined;
var uploadform = gid('uploadform');
var g_autoscroll = true;
var g_upload_size = 0;    /* The whole number of bytes to upload. */
var g_upload_start = 0;   /* When the upload started in ms (since epoch). */
var g_uploaded_bytes = 0; /* The whole number of bytes uploaded so far. */
var g_upload_info = undefined;  /* A HTML element where the upload info are shown. */

function updateStatusScroll() {
  if(!g_autoscroll) return;

  var status_div = gid('status_div');
  status_div.scrollTop = status_div.scrollHeight;
}

function uploadNextFile() {
  // Find the next file to upload
  var el = uploadform.elements[g_cur_idx];
  if(!(el.files && ++g_cur_file_idx < el.files.length)) {
    g_cur_file_idx = 0;
    do {
      ++g_cur_idx;

      if(g_cur_idx >= uploadform.elements.length) {
//       if(g_err_msg != '') alert(g_err_msg);
        // We're done uploading all the files
        var url, label;
        if(gid('ulact').value == 'ulmore') {
          url = document.location.href;
          label = 'Add More';
        } else {
          url = document.location.href.replace(/act=add/, 'act=preview');
          label = 'Continue';
        }

        if(g_last_status) {
          var button = newel('input');
          button.type = 'button';
          button.value = label;
          button.className = 'stdbtn';
          button.style.width = '150px';
          button.addEventListener('click', function() {
            document.location.href = url;
          }, true);
          insertAfter(button, g_last_status.parentNode);
        } else {
          document.location.href = url;
        }

        return;
      }

      el = uploadform.elements[g_cur_idx];

//     GM_log("Debug: uploadNextFile(" + el.name + ")");

      if(!el.type.match(/^file$/i)) continue;
      if(el.files.length != 0) break;
    } while(true);
  }

  setProgress('ulp' + g_cur_idx, g_cur_file_idx / el.files.length);

  var file = el.files[g_cur_file_idx];
  var xhr = new XMLHttpRequest();

  xhr.upload.addEventListener("progress", function(e) {
    if(e.lengthComputable) {
      setProgress('ulp' + g_cur_idx, (g_cur_file_idx + e.loaded / e.total) / el.files.length);
      setUploadInfo(g_uploaded_bytes + e.loaded);
    }
  }, false);

  xhr.upload.addEventListener("load", function(e){
    // XXX: 'load' is sent when the server answers, not when the upload completes on FF :(
    setProgress('ulp' + g_cur_idx, (g_cur_file_idx + 1) / el.files.length);
    g_last_status.innerHTML = g_last_status.innerHTML.replace(/^\S+/, 'Processing');
    updateStatusScroll();
  }, false);

  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
      if ((xhr.status >= 200 && xhr.status <= 200) || xhr.status == 304) {
        //GM_log(xhr.responseText);
        var html = xhr.responseText;

        // Check for error or processing messages
        var matches;
        matches = html.match(/<div class="ui">(.+)\r?\n/);
        if(matches) {
          // Add the message to the status log
          g_last_status.innerHTML = matches[1];
          var last_child = g_last_status.lastChild

          while(g_last_status.firstChild) {
            var child = g_last_status.removeChild(g_last_status.lastChild);
            insertAfter(child, g_last_status);
          }

          if(last_child) {
            g_last_status.parentNode.removeChild(g_last_status);
            g_last_status = last_child;
          }

          updateStatusScroll();
        } else {
          g_last_status.innerHTML += ' <span style="color: green"><b>OK</b></span>';
        }

//         var re = /<p style="color:red; font-weight:bold; text-align:center">([^<]+)<\/p>/gi;
//         while(matches = re.exec(html)) {
//           g_err_msg += matches[1] + "\n";
//           GM_log("Error: " + matches[1]);
//         }

        g_uploaded_bytes += file.size ? file.size : file.fileSize;

        uploadNextFile();
      } else {
        alert("Error while uploading the file.");
      }
    }
  };

  var status = newel('p');
  status.style.textAlign = "center";
  status.innerHTML = 'Preparing <b>' +
                     (file.fileName ? file.fileName : file.name) + '</b> ...';

  if(g_last_status == undefined) {
    var div = newel('div');
    div.addEventListener('scroll', function(e) {
      g_autoscroll = (e.target.scrollTop + 100) >= e.target.scrollHeight;
    }, true);
    div.style.maxHeight = '100px';
    div.style.overflow = 'auto';
    div.id = 'status_div';

    insertBefore(div, qsel('div.ui').firstChild);
    div.appendChild(status);
  } else {
    insertAfter(status, g_last_status);
  }
  updateStatusScroll();
  g_last_status = status;

  xhr.open("POST", uploadform.action, true);

  if(typeof(FormData) != "undefined") {  /* Need FF4+ or Chrome7+ */
    // Create a custom form and post it.
    var formData = new FormData();
    formData.append(el.name, file);
    // XXX: sending the form fields before the file doesn't always work.
    for(var i = 0 ; i < uploadform.elements.length ; ++i) {
      var field = uploadform.elements[i];

      if(field.type.match(/^(file|submit|button)$/i)) continue;
      formData.append(field.name, (field.name == 'ulact' ? 'uldone' : field.value));
    }

    g_last_status.innerHTML = g_last_status.innerHTML.replace(/^\S+/, 'Uploading');
    xhr.send(formData);
  } else  {
    // Build the HTTP request body, and send it.
    // XXX: the whole file content is stored in memory.
    var boundary = "ehtuplprgs" + Math.round(Math.random() * 1000);

    xhr.overrideMimeType("multipart/form-data, boundary=" + boundary);
    xhr.setRequestHeader("Content-Type", "multipart/form-data, boundary=" + boundary);

    var fields = '';
    for(var i = 0 ; i < uploadform.elements.length ; ++i) {
      var field = uploadform.elements[i];

      if(field.type.match(/^(file|submit|button)$/i)) continue;

      fields += "--" + boundary + "\r\n";
      fields += 'Content-Disposition: form-data; name="' + field.name + '"\r\n\r\n';
      fields += (field.name == 'ulact' ? 'uldone' : field.value) + "\r\n";
    }

    var reader = new FileReader();
    reader.onload = function(evt) {
      var ctype = file.type;
      if(ctype == "") ctype = "application/octet-stream";

      try {
        var body = "--" + boundary + "\r\n";
        var fileName = '';

        // NOTE: the file name should be converted to UTF-8 for xhr.sendAsBinary() to work.
        body += 'Content-Disposition: form-data; name="' + el.name + '"; filename="' + toUtf8(file.name) + '"\r\n';
        body += "Content-Type: " + ctype + "\r\n\r\n";
        body += evt.target.result + "\r\n";
        // XXX: sending the form fields before the file doesn't always work.
        body += fields;
        body += "--" + boundary + "--\r\n";
      } catch(e) {
        // We might reach the memory limit when adding the fields.
        GM_log(e);
        reader.onerror(evt);
        return;
      }

      g_last_status.innerHTML = g_last_status.innerHTML.replace(/^\S+/, 'Uploading');

      xhr.sendAsBinary(body);
    };
    reader.onerror = function(e) {
      // There seems to exist a 96MB limit to the JS string size.
      g_last_status.innerHTML =
        '<b>Error while preparing ' + file.name + '.</b> Loaded '
        + e.loaded + ' bytes on ' + e.total + '.';
      g_last_status.style.color = 'red';
      GM_log("Error while preparing " + file.name + ".\n\nLoaded " + e.loaded + " bytes on " + e.total + ".");

      uploadNextFile();
    }
    reader.readAsBinaryString(file);
  }
}

function onUpload(e) {
//   GM_log("Debug: onUpload(" + e + ")");

  if(g_cur_idx == undefined) {
    for(var i = 0 ; i < uploadform.elements.length ; ++i) {
      uploadform.elements[i].disabled = true;
    }

    if(!gid('ulp_check').checked) {
      // XXX: onUpload() can't be called anymore if ulp is disabled.
      GM_log("Legacy upload.");
      uploadform.removeEventListener('submit', onUpload, true);
      //document.location.href = 'javascript:document.getElementById("uploadform").submit();';
      //uploadform.submit();
      return true;
    }

    g_cur_idx = 0;
    g_cur_file_idx = -1;

    /* Compute the whole upload size. */
    g_upload_size = 0;
    for(var i = uploadform.elements.length  - 1 ; i >= 0 ; --i) {
      var el = uploadform.elements[i];
      if(!el.files) continue;

      for(var j = el.files.length - 1 ; j >= 0 ; --j) {
        var file = el.files[j];
        if(file.fileSize) g_upload_size += el.files[j].fileSize; // obsolete
        else g_upload_size += el.files[j].size;
      }
    }
    g_upload_start = Date.now();
    setUploadInfo(0);

    uploadNextFile();
  }

  if(e) {
    e.stopPropagation();
    e.preventDefault();
  }

  return false;
}

function setProgress(base, val) {
  var border = gid(base + '_border');
  var label = gid(base + '_label');
  var progress = gid(base + '_progress');

  border.style.visibility = 'visible';

  val = Math.floor(val * 100);
  label.textContent = val + '%';
  progress.style.width = val + '%';
}

function setUploadInfo(size) {
  if(size <= 0) {
    // Init
//    g_upload_info.innerHTML = 'Upload speed: n/a (0/'
//      + g_upload_size + ') | ETA: n/a';
    g_upload_info.innerHTML = 'Upload speed: n/a | ETA: n/a';
    return;
  }

  var now = Date.now();
  if(now <= g_upload_start + 5000) return;

  var rate = size * 1000 / (now - g_upload_start);  // Bps
  var eta = (g_upload_size - size) / rate;

  var rate_unit = 'Bps';
  if(rate >= 1024*1024*1024) {
    rate_unit = 'GBps';
    rate /= 1024*1024*1024;
  } else if(rate >= 1024*1024) {
    rate_unit = 'MBps';
    rate /= 1024*1024;
  } else if(rate >= 1024) {
    rate_unit = 'KBps';
    rate /= 1024;
  }
  rate = new String(Math.round(rate * 10) / 10);
  if(rate.indexOf('.') < 0) rate += '.0';

  var eta_str = '';
  if(eta >= 3600) {
    var h = Math.floor(eta / 3600);
    eta = eta - (h * 3600);
    if(h < 10) eta_str += '0';
    eta_str += h + 'h';
  }
  if(eta >= 60) {
    var m = Math.floor(eta / 60);
    eta = eta - (m * 60);
    if(m < 10) eta_str += '0';
    eta_str += m + 'min';
  }
  eta = Math.round(eta);
  if(eta < 10) eta_str += '0';
  eta_str += eta + 's';

//  g_upload_info.innerHTML = 'Upload speed: ' + rate + rate_unit + ' ('
//    + size + '/' + g_upload_size + ') | ETA: ' + eta_str;
  g_upload_info.innerHTML = 'Upload speed: ' + rate + rate_unit
    + ' | ETA: ' + eta_str;
}

function onPreUpload(e) {
  /* This function does the same things as unsafeWindow.submit_upload(),
   * but doesn't submit the form.
   */
  gid("ulmore").disabled = "disabled";
  gid("ulmore").value = "Uploading...";
  gid("uldone").disabled = "disabled";
  gid("uldone").value = "Uploading...";
  gid("ulskip").disabled = "disabled";
  gid("ulskip").value = "Uploading...";
  gid("ulact").value = e.target.name;

  return onUpload(e);
}

function setSubmitHook(enabled) {
  /* NOTE: even if the "click" event is canceled, the "onclick" code is still
   *       executed in Chrome ...
   */
  /* NOTE: submiting the uploadform from the user script doesn't seem to work
   *       properly with Chrome, that's why we play with the onclick value here.
   */
  if(enabled) {
    gid('ulmore').addEventListener('click', onPreUpload, true);
    gid('uldone').addEventListener('click', onPreUpload, true);

    gid('ulmore').setAttribute('onclick',  'return false;' + gid('ulmore').getAttribute('onclick'));
    gid('uldone').setAttribute('onclick',  'return false;' + gid('uldone').getAttribute('onclick'));
  } else {
    gid('ulmore').removeEventListener('click', onPreUpload, true);
    gid('uldone').removeEventListener('click', onPreUpload, true);

    gid('ulmore').setAttribute('onclick',  gid('ulmore').getAttribute('onclick').replace(/^return false;/, ''));
    gid('uldone').setAttribute('onclick',  gid('uldone').getAttribute('onclick').replace(/^return false;/, ''));
  }
}

function init() {
  if(!uploadform) {
    GM_log("Error: cannot find the upload form.");
    return;
  }

  var input_div = undefined;

  // Create the upload progress elements
  for(var i = 0 ; i < uploadform.elements.length ; ++i) {
    var el = uploadform.elements[i];
    if(!el.type || !el.type.match(/^file$/i)) continue;

    el.multiple = GM_getValue('enabled', true);

    var border = newel('div');
    border.id = 'ulp' + i + '_border';
    border.style.height = '14px';
    border.style.padding = '0px';
    border.style.margin = '0px';
    border.style.marginRight = '5px';
    border.style.display = 'inline-block';
    border.style.border = '1px solid #5C0D12';
    border.style.visibility = 'hidden';
    insertBefore(border, el.parentNode.firstChild);
    // Adapt the progress bar width to the page layout
    border.style.width = (border.offsetLeft - 2) + 'px';

    var label = newel('span');
    label.id = 'ulp' + i + '_label';
    label.textContent = '0%';
    label.style.height = '100%';
    label.style.width = '100%';
    label.style.textAlign = 'center';
    label.style.cssFloat = 'right'; // the text isn't centered without that ...
    border.appendChild(label);

    var progress = newel('div');
    progress.id = 'ulp' + i + '_progress';
    progress.style.height = '100%';
    progress.style.width = '0%';
    progress.style.background = '#A3A091';
    progress.style.position = 'relative';
    progress.style.zIndex = '-1';
    border.appendChild(progress);

//     setProgress('ulp' + i, Math.random()); // DEBUG

    if(!input_div) {
      /* Find the DIV that contains the file inputs. */
      for(input_div = el.parentNode ;
          input_div && input_div.tagName.toLowerCase() != 'div' ;
          input_div = input_div.parentNode);
    }
  }

  var ulpcheck = newel('input');
  ulpcheck.id = 'ulp_check';
  ulpcheck.type = 'checkbox';
  ulpcheck.checked = GM_getValue('enabled', true);
  ulpcheck.style.marginLeft = '10px';
  insertAfter(ulpcheck, gid('ulskip'));

  var label = newel('label');
  label.htmlFor = 'ulp_check';
  label.innerHTML = 'Use Upload Progress Bars';
  insertAfter(label, ulpcheck);

  ulpcheck.addEventListener('change', function(e) {
    GM_setValue('enabled', ulpcheck.checked);
    setSubmitHook(ulpcheck.checked);

    for(var i = 0 ; i < uploadform.elements.length ; ++i) {
      var el = uploadform.elements[i];
      if(!el.type || !el.type.match(/^file$/i)) continue;

      if((el.multiple = ulpcheck.checked) == false && el.files.length > 1) {
        // Reset
        el.value = '';
      }
    }
  }, true);

  setSubmitHook(ulpcheck.checked);

  if(input_div) {
    g_upload_info = document.createElement('div');
    //g_upload_info.innerHTML = 'Upload speed: XXX.XXBps | ETA: XXhXXminXXs';
    g_upload_info.style.position = 'absolute';
    g_upload_info.style.top = '0';
    g_upload_info.style.left = '0';
    g_upload_info.style.backgroundColor = 'rgb(237, 235, 223)';
    g_upload_info.style.paddingRight = '5px';
    insertBefore(g_upload_info, input_div.firstChild);
  }
}

init();