text: // ==UserScript==
  // @name Bandcamp Supporters You Follow
  // @version 1.0.3
  // @description Show supporters of an album/track that you follow
  // @namespace 289690-squeek502
  // @license 0BSD
  // @match http*://*.bandcamp.com/*
  // @include http*://*.bandcamp.com/*
  // @grant GM_xmlhttpRequest
  // @grant GM_setValue
  // @grant GM_getValue
  // ==/UserScript==
  
  // messy code ahead
  
  var OPTION_AUTOLOAD = 'autoload';
  var SMALL_SIZE = '28px';
  var LARGE_SIZE = 'auto';
  var DEFAULT_SIZE = SMALL_SIZE;
  var OPTION_SIZE = 'size';
  var DEFAULT_AUTOLOAD = false;
  
  var collectedByContainer = document.querySelector('.collected-by.tralbum.collectors');
  if (!collectedByContainer)
    return;
  
  var get; var post;
  var loadedimgs = [];
  
  var existingMessage = collectedByContainer.querySelector('.message');
  
  var optionsMenu = document.createElement('div');
  optionsMenu.style.display = 'none';
  optionsMenu.style.marginTop = '10px';
  optionsMenu.style.marginBottom = '10px';
  optionsMenu.style.position = 'relative';
  
  var toggleAutoload = document.createElement('input');
  toggleAutoload.setAttribute('type', 'checkbox');
  toggleAutoload.checked = GM_getValue("autoload", false);
  toggleAutoload.id = 'supporters-you-follow-autoload';
  var toggleAutoloadLabel = document.createElement('label');
  toggleAutoloadLabel.setAttribute('for', 'supporters-you-follow-autoload');
  toggleAutoloadLabel.textContent = 'autoload';
  toggleAutoloadLabel.style.fontWeight = 'bold';
  var toggleAutoloadContainer = document.createElement('div');
  toggleAutoloadContainer.style.display = 'inline-block';
  toggleAutoloadContainer.style.marginRight = '30px';
  toggleAutoloadContainer.appendChild(toggleAutoload);
  toggleAutoloadContainer.appendChild(toggleAutoloadLabel);
  
  toggleAutoload.addEventListener('change', function() {
    GM_setValue(OPTION_AUTOLOAD, this.checked);
  });
  
  var onSizeChanged = function() {
    if (this.checked) {
      GM_setValue(OPTION_SIZE, this.value);
      loadedimgs.forEach(function(img) {
        img.style.width = this.value;
        img.style.height = this.value;
      }.bind(this));
    }
  };
  
  var curSizeSetting = GM_getValue(OPTION_SIZE, DEFAULT_SIZE);
  var sizeSmall = document.createElement('input');
  sizeSmall.setAttribute('type', 'radio');
  sizeSmall.name = 'supporters-you-follow-size';
  sizeSmall.value = SMALL_SIZE;
  sizeSmall.id = 'supporters-you-follow-size-small';
  sizeSmall.style.marginLeft = '10px';
  sizeSmall.checked = curSizeSetting == SMALL_SIZE;
  sizeSmall.addEventListener('change', onSizeChanged);
  var sizeSmallLabel = document.createElement('label');
  sizeSmallLabel.setAttribute('for', 'supporters-you-follow-size-small');
  sizeSmallLabel.textContent = 'small';
  sizeSmallLabel.style.marginRight = '10px';
  sizeSmallLabel.style.fontWeight = 'bold';
  var sizeLarge = document.createElement('input');
  sizeLarge.setAttribute('type', 'radio');
  sizeLarge.name = 'supporters-you-follow-size';
  sizeLarge.value = LARGE_SIZE;
  sizeLarge.id = 'supporters-you-follow-size-large';
  sizeLarge.checked = curSizeSetting == LARGE_SIZE;
  sizeLarge.addEventListener('change', onSizeChanged);
  var sizeLargeLabel = document.createElement('label');
  sizeLargeLabel.setAttribute('for', 'supporters-you-follow-size-large');
  sizeLargeLabel.textContent = 'large';
  sizeLargeLabel.style.fontWeight = 'bold';
  var sizeContainer = document.createElement('div');
  sizeContainer.style.display = 'inline-block';
  sizeContainer.appendChild(sizeSmall);
  sizeContainer.appendChild(sizeSmallLabel);
  sizeContainer.appendChild(sizeLarge);
  sizeContainer.appendChild(sizeLargeLabel);
  
  optionsMenu.appendChild(toggleAutoloadContainer);
  optionsMenu.appendChild(document.createTextNode("thumb size: "));
  optionsMenu.appendChild(sizeContainer);
  optionsMenu.appendChild(document.createElement('hr'));
  
  var optionsLink = document.createElement('a');
  optionsLink.textContent = 'options';
  optionsLink.style.float = 'right';
  collectedByContainer.insertBefore(optionsLink, existingMessage);
  optionsLink.addEventListener('click', function() {
    var wasHidden = optionsMenu.style.display == 'none';
    optionsMenu.style.display = wasHidden ? 'block' : 'none';
    optionsLink.style.fontWeight = wasHidden ? 'bold' : 'normal';
  });
  
  var message = existingMessage.cloneNode(true);
  message.textContent = 'supporters you follow';
  collectedByContainer.insertBefore(message, existingMessage);
  collectedByContainer.insertBefore(optionsMenu, existingMessage);
  
  var existingDeets = collectedByContainer.querySelector('.deets');
  var deets = document.createElement('div');
  deets.style.marginBottom = '16px';
  deets.style.marginTop = '10px';
  deets.style.lineHeight = '0';
  var statusElement = document.createElement('div');
  statusElement.style.lineHeight = '100%';
  statusElement.style.opacity = '0.5';
  statusElement.textContent = 'loading';
  statusElement.style.display = 'none';
  deets.appendChild(statusElement);
  collectedByContainer.insertBefore(deets, existingMessage);
  
  var makeThumb = function(fan) {
    var a = document.createElement('a');
    a.href = fan.url;
    a.title = fan.name;
    a.style.margin = '0 7px 7px 0px';
    a.style.display = 'inline-block';
    var img = document.createElement('img');
    img.src = "https://f4.bcbits.com/img/" + fan.image_id.toString().padStart(10, '0') + "_42.jpg";
    img.setAttribute('alt', fan.name + " thumbnail");
    img.style.display = 'block';
    var size = GM_getValue("size", DEFAULT_SIZE);
    img.style.width = size;
    img.style.height = size;
    a.appendChild(img);
    loadedimgs.push(img);
    return a;
  };
  
  var handleSupporter = function(supporter) {
    if (this[supporter.fan_id]) {
      if (statusElement.parentNode) {
        deets.removeChild(statusElement);
      }
      deets.appendChild(makeThumb(supporter));
    }
  };
  
  var onEnd = function() {
    if (loadedimgs.length === 0) {
      statusElement.textContent = 'none found';
    } else if (statusElement.parentNode) {
      deets.removeChild(statusElement);
    }
  };
  
  var onError = function(err) {
    statusElement.textContent = err;
  };
  
  var getNext = function(tralbum_type, tralbum_id, token, lookup) {
    var data;
    if (token !== undefined) {
      data = '{"tralbum_type":"'+tralbum_type+'","tralbum_id":'+tralbum_id+',"token":"'+token+'","count":80}';
    } else {
      data = '{"tralbum_type":"'+tralbum_type+'","tralbum_id":'+tralbum_id+',"count":80}';
    }
    var url = (new URL("/api/tralbumcollectors/2/thumbs", document.location)).href;
    post(url, data, function(status, res, url) {
      if (status != 200) {
        console.warn("failed to get more supporters", status, res, url);
        onError("unexpected http status: " + status);
        return;
      }
      var parsed = JSON.parse(res);
      if (parsed.error) {
        console.warn("error when getting more supporters", parsed.error_message, parsed);
        onError(parsed.error_message);
        return;
      }
      parsed.results.forEach(handleSupporter.bind(lookup));
      if (parsed.more_available) {
        var lastToken = parsed.results[parsed.results.length - 1].token;
        getNext(tralbum_type, tralbum_id, lastToken, lookup);
      } else {
        onEnd();
      }
    });
  };
  
  var onSummary = function(err, summary) {
    if (err) {
      onError(err);
      return;
    }
    var collectorsData = JSON.parse(document.querySelector('#collectors-data').getAttribute('data-blob'));
    var supporters = collectorsData.thumbs;
    var lookup = summary.follows.following;
  
    var pageData = JSON.parse(document.querySelector('#pagedata').getAttribute('data-blob'));
    var tralbum = pageData.fan_tralbum_data;
    // collectorsData.thumbs has only minimal data now, so we have to re-get the first page using no token
    getNext(tralbum.tralbum_type, tralbum.tralbum_id, undefined, lookup);
  };
  
  var go = function() {
    statusElement.style.display = 'block';
    var url = (new URL("/api/fan/2/collection_summary", document.location)).href;
    get(url, function(status, res, url) {
      if (status != 200) {
        onSummary("unexpected http status code: " + status);
        return;
      }
      var parsedRes = JSON.parse(res);
      onSummary(parsedRes.error_message, parsedRes.collection_summary);
    });
  };
  
  get = function(url, cb) {
    var opts = {
      method: 'GET',
      url: url,
      onload: function (res) {
        cb(res.status, res.responseText, res.finalUrl || url);
      }
    };
    GM_xmlhttpRequest(opts);
  };
  
  post = function(url, data, cb) {
    var opts = {
      method: 'POST',
      url: url,
      data: data,
      headers: {
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
      },
      onload: function (res) {
        cb(res.status, res.responseText, res.finalUrl || url);
      }
    };
    GM_xmlhttpRequest(opts);
  };
  
  if (GM_getValue("autoload", false)) {
    go();
  } else {
    var loadLink = document.createElement('a');
    loadLink.textContent = 'load...';
    loadLink.style.lineHeight = '100%';
    var handleAutoloadChecked;
    var load = function() {
      go();
      deets.removeChild(loadLink);
      toggleAutoload.removeEventListener('change', handleAutoloadChecked);
    };
    loadLink.addEventListener('click', function(e) {
      load();
      e.preventDefault();
      e.stopImmediatePropagation();
    }, true);
    deets.appendChild(loadLink);
  
    handleAutoloadChecked = function() {
      if(this.checked && loadLink.parentNode) {
        load();
      }
    };
    toggleAutoload.addEventListener('change', handleAutoloadChecked);
  }
  