Greasy Fork

FanfictionQomplete

Loads all following chapters on fanfiction.net and strips off bloat.

目前为 2015-11-06 提交的版本。查看 最新版本

// ==UserScript==
// @name          FanfictionQomplete
// @description   Loads all following chapters on fanfiction.net and strips off bloat.
// @namespace     https://greasyfork.org/en/users/11891-qon
// @author        Qon
// @include       https://www.fanfiction.net/s/*/*
// @include       https://www.fictionpress.com/s/*/*
// @include       https://www.fimfiction.net/story/*/*
// @include       http://www.fimfiction.net/story/*/*
// @compatible    firefox
// @compatible    chrome
// @noframes
// @grant         GM_xmlhttpRequest
// @grant         GM_getValue
// @grant         GM_setValue
// @run-at        document-start
// @license       Simple Public License 2.0 (SimPL) https://tldrlegal.com/license/simple-public-license-2.0-%28simpl%29
// @version 0.0.1.20151106155133
// ==/UserScript==

// javascript:var script=document.createElement("script");var t=new Date(Date());script.src="https://greasyfork.org/en/scripts/10182-fanfictionqomplete/code/fanfictionqomplete.js?"+t.getFullYear()+t.getMonth()+t.getDate();document.body.appendChild(script);window.setTimeout(function(){document.runFFQomplete();},500);

/*
TODO
  !checkForBadJavascripts! (remove addJS_Node ?)
  Add support for other sites
    WIP https://www.fictionpress.com
    WIP fimfiction.com
  Change width by dragging the border? and position?
  Add copies of all links at the end of a fanfic.
  Save scroll, because the browser built in one doesn't work between browser restarts
*/


// Source for checkForBadJavascripts: https://gist.github.com/BrockA/2620135
/*--- checkForBadJavascripts()
    This is a utility function, meant to be used inside a Greasemonkey script that
    has the "@run-at document-start" directive set.
    It Checks for and deletes or replaces specific <script> tags.
*/
function checkForBadJavascripts(controlArray) {
    /*--- Note that this is a self-initializing function.  The controlArray
        parameter is only active for the FIRST call.  After that, it is an
        event listener.

        The control array row is  defines like so:
        [bSearchSrcAttr, identifyingRegex, callbackFunction]
        Where:
            bSearchSrcAttr      True to search the SRC attribute of a script tag
                                false to search the TEXT content of a script tag.
            identifyingRegex    A valid regular expression that should be unique
                                to that particular script tag.
            callbackFunction    An optional function to execute when the script is
                                found.  Use null if not needed.
        Usage example:
            checkForBadJavascripts ( [
                [false, /old, evil init()/, function () {addJS_Node (init);} ],
                [true,  /evilExternalJS/i,  null ]
            ] );
    */
    if (!controlArray.length) return null;
    checkForBadJavascripts = function(zEvent) {
        for (var J = controlArray.length - 1; J >= 0; --J) {
            var bSearchSrcAttr = controlArray[J][0];
            var identifyingRegex = controlArray[J][1];
            if (bSearchSrcAttr) {
                if (identifyingRegex.test(zEvent.target.src)) {
                    stopBadJavascript(J);
                    return false; } }
            else {
                if (identifyingRegex.test(zEvent.target.textContent)) {
                    stopBadJavascript(J);
                    return false; } } }
        function stopBadJavascript(controlIndex) {
            zEvent.stopPropagation();
            zEvent.preventDefault();
            var callbackFunction = controlArray[J][2];
            if (typeof callbackFunction == "function")
                callbackFunction(zEvent.target);
            //--- Remove the node just to clear clutter from Firebug inspection.
            zEvent.target.parentNode.removeChild(zEvent.target);
            //--- Script is intercepted, remove it from the list.
            controlArray.splice(J, 1);
            if (!controlArray.length) {
                //--- All done, remove the listener.
                window.removeEventListener(
                    'beforescriptexecute', checkForBadJavascripts, true ); } } }
    /*--- Use the "beforescriptexecute" event to monitor scipts as they are loaded.
        See https://developer.mozilla.org/en/DOM/element.onbeforescriptexecute
        Note seems to work on scripts that are dynamically created, despite what
        the spec says.
    */
    window.addEventListener('beforescriptexecute', checkForBadJavascripts, true);
    return checkForBadJavascripts;
}

function addJS_Node(text, s_URL, funcToRun) {
    var D = document;
    var scriptNode = D.createElement('script');
    scriptNode.type = "text/javascript";
    if (text) scriptNode.textContent = text;
    if (s_URL) scriptNode.src = s_URL;
    if (funcToRun) scriptNode.textContent = '(' + funcToRun.toString() + ')()';
    var targ = D.getElementsByTagName('head')[0] || D.body || D.documentElement;
    //--- Don't error check here. if DOM not available, should throw error.
    targ.appendChild(scriptNode);
}


if (Element.prototype.remove == undefined) {
  Element.prototype.remove = function() {
    this.parentNode.removeChild(this) } }

function injectQompleteButton() {
  var lc = document.getElementsByClassName('lc')
  if (!lc.length) {
    lc = document.getElementById('chapter_title')
    lc = lc&&lc.parentNode
    lc = lc&&lc.getElementsByClassName('button-group')[0]
    console.log(lc)
    var btn = document.createElement('a')
    btn.addEventListener('click', runFFQomplete)
    btn.setAttribute('class', 'styled_button styled_button_grey button-icon-only')
    btn.setAttribute('title', 'Append all following chapters and remove unecessary bloat.')
    btn.innerHTML = 'Qomplete!'
    console.log(lc.lastChild.previousSibling)
    lc.insertBefore(btn,lc.lastChild.previousSibling)
  }
  else {
    lc = lc[0]
    var btn = document.createElement('button')
    // btn.setAttribute('onclick', 'runFFQomplete();')
    btn.addEventListener('click', runFFQomplete)
    btn.setAttribute('class', 'btn')
    btn.setAttribute('style', 'margin-left:12px;margin-right:2px;')
    btn.setAttribute('title', 'Append all following chapters and remove unecessary bloat.')
    btn.innerHTML = 'Qomplete!'
    lc.appendChild(btn)}}
window.addEventListener('load', injectQompleteButton)

// document.styleSheets[0].cssText = "";

var hash_settings = document.location.hash.slice(1).split('&')
for(var i=0; i<hash_settings.length; ++i){
  var kv = hash_settings[i].split('=')
  if(kv[0]==='Qomplete') {
    checkForBadJavascripts ([
      [true, /static\.fimfiction\.net\/js\/scripts\.js\?uupAcQPf/, null],
      [false, /\$\(window\)\.scroll\( function\(e\)/ /*)*/, null]
    ])
    window.addEventListener('load', runFFQomplete)}}

var live = true
function readSettings() {
  var settings = {Qomplete:1}
  if(live){
    settings.center = GM_getValue('center',0)
    settings.bgcol = GM_getValue('bgcol',0)
    settings.edge = GM_getValue('edge',0)
    settings.width = GM_getValue('width',0)
  }
  var hash_settings = document.location.hash.slice(1).split('&')
  for(var i=0; i<hash_settings.length; ++i){
    var kv = hash_settings[i].split('=')
    switch(kv[0]){
    case 'center':
      settings.center = parseInt(kv[1])
      break;
    case 'bgcol':
      settings.bgcol = parseInt(kv[1])
      break;
    case 'edge':
      settings.edge = parseInt(kv[1])
      break;
    case 'width':
      settings.width = parseInt(kv[1])
      break;
    }
  }
  return settings }
function saveSettings(o) {
  var q = []
  for(var k in o) {
    if(o.hasOwnProperty(k)) {
      if(live) {GM_setValue(k,o[k])}
      q.push(k+'='+o[k])}}
  window.location = '#'+q.join('&')}
var settings = readSettings()
console.log(settings)

function runFFQomplete() {
  saveSettings(settings)
  window.onload = function() {
    var a = document.getElementsByClassName('skiptranslate')
    for (; a.length;) {
      a[0].remove()}
    document.body.removeAttribute('style')}

  var re = /(^.*?(?:fan|fim)?fiction(?:press\.com|\.net)\/s(?:tory)?\/\d+\/)(\d+)(\/?[^#]*)/

  function urlGetChap(url) {
    var arr = re.exec(url)
    return arr[2]}

  function urlSetChap(url, n) {
    var arr = re.exec(url)
    return arr[1] + n + ((/fimfiction\.net/.exec(document.location)==null) ? arr[3] : arr[3].replace(/\/[^\/]*$/, ""))}

  function inc(url) {
    var arr = re.exec(url)
    return arr[1] + (parseInt(arr[2]) + 1) + ((/fimfiction\.net/.exec(document.location)==null) ? arr[3] : arr[3].replace(/\/[^\/]*$/, ""))}

  function chapFromPage(url, page) {
    var storytext = page.getElementById('storytext') // fanfiction.net
    if(!storytext) {
      //  fimfiction.net
      if(latestChap==1) {
        try {
          latestChap = page.getElementById('chapter_format').getElementsByClassName('title')[0].getElementsByTagName('ul')[0].getElementsByTagName('li').length
        } catch (e) {}}

      storytext = page.getElementsByClassName('chapter_content')[0]
      storytext.style = ''
      var styles = storytext.getElementsByTagName('style')
      for(style of styles) {
        style.remove()}}

    if (storytext) {
      // var ps = storytext.getElementsByTagName('p'), d = 0
      // for (q of ps) {
      //     q.style.color = 'hsl(' + d + ' ,20%, 80%)'
      //     // q.innerHTML = q.innerHTML.replace(/([\.,?!])/g, '<span style="color:hsl(' + d + ' ,100%, 50%);">$1</span>')
      //     d = (d + 1 / ps.length * 360) % 360}
      var wrap = page.createElement('div')
      wrap.setAttribute('class', 'wrap col')
      wrap.setAttribute('id', urlGetChap(url))
      var pad = page.createElement('div')
      pad.setAttribute('class', 'pad')
      var chapdiv = page.createElement('div')
      chapdiv.setAttribute('class', 'chapter')
      var chapspan = page.createElement('span')
      chapspan.innerHTML = urlGetChap(url) + '. '
      var title = page.getElementsByTagName('title')[0]
      var chaptitle = page.createElement('a')
      chaptitle.setAttribute('href', url.replace(/#.*$/, ""))
      chaptitle.setAttribute('class', 'external')
      if(/fimfiction\.net/.exec(document.location)==null) {
        var cut = /(.*(| [Cc]hapter [^:]+: .*)), a .* fanfic \| FanFiction/
        var newChapTitle = cut.exec(title.innerHTML)}
      else {
        var cut = /(.*) - FIMFiction.net/
        var newChapTitle = cut.exec(title.innerHTML)}
      chaptitle.innerHTML = newChapTitle ? newChapTitle[1] : title.innerHTML
      chapdiv.appendChild(chapspan)
      chapdiv.appendChild(chaptitle)
      chapdiv.appendChild(document.createElement('hr'))
      chapdiv.appendChild(storytext)
      pad.appendChild(chapdiv)
      wrap.appendChild(pad)
      return wrap
    }
    else return null}

  document.body.setAttribute('style', '')
  var title = document.getElementsByTagName('title')[0]

  var profile_top = document.getElementById('profile_top')
  if(!profile_top) profile_top = document.getElementsByClassName('inner')[0] //  fimfiction, TODO make better
  var statusCompleteRE = /Status: Complete/
  var statusComplete = statusCompleteRE.exec(profile_top.innerHTML) ? true : false
  var chap_select = document.getElementById('chap_select')
  var latestChap = chap_select ? chap_select.children.length : 1
  var activeChap = parseInt(urlGetChap(document.location))
  var appendedNow = 1
  var notAppendedYet = 0
  var chapArr = []
  var aaa=0
  var bbb=1
  if(/fimfiction\.net/.exec(document.location)==null) {aaa=2; bbb=5;}
  // else {aaa=0; bbb=1;}
  var favicon = [].slice.call(document.head.getElementsByTagName('link')).slice(aaa,bbb)

  var chap = chapFromPage(document.location.href, document)

  var ptbuttons = profile_top.getElementsByTagName('button')
  if (ptbuttons.length) {ptbuttons[0].remove()}

  for (; document.head.firstElementChild;) document.head.firstElementChild.remove();
  for (; document.body.firstElementChild;) document.body.firstElementChild.remove();
  document.body.removeAttribute('style')

  var style = document.createElement('style')
  style.setAttribute('type', 'text/css')
  /*
  def gencss():
    css = ''
    wps = 5
    wpe = 1
    pps = 50
    ppe = 5
    mwe = 650
    wpr = wpl = wpe
    ppr = ppl = ppe
    mw = mwe
    while ppl+ppr<=pps*2 or wpl+wpr<=wps*2:
      if ppl+ppr<=pps*2:
        cs = r'@media (max-width: '+str( mw) +r'px) {div.pad{padding-left:'+str(ppl)+r'px;padding-right:'+str(ppr)+'px;}}\\n\\\n' #'
        if ppl<=ppr:
          ppl += 1
        else:
          ppr += 1
      elif wpl+wpr<=wps*2:
        cs = r'@media (max-width: '+str( mw) +r'px) {div.wrap{padding-left:'+str(wpl)+r'px;padding-right:'+str(wpr)+'px;}}\\n\\\n' #'
        if wpl<=wpr:
          wpl += 1
        else:
          wpr += 1
      css = cs+css
      mw = mw+1
    return css
  */
  console.log(activeChap)
  style.innerHTML =
    'body{background-color:#000;color:#ccc;margin:0;padding:0;font-family:"Verdana";}\n\
    ul.tags > li{display:inline;}\n\
    ul.tags > li::before{content:" #"}\n\
    #loading{position:inherit;width:100%;height:5px;}\n\
    button, select{border-radius:4px;padding:4px 12px;background: linear-gradient(to bottom, #333, #000);border-width: 1px;color:#ccc;background-color:#000;}\n\
    button:hover{background-image:none;}\n\
    .panel{text-align:center;}\n\
    a.external, option.external{background: transparent url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZ\
G9iZSBJbWFnZVJlYWR5ccllPAAAAFZJREFUeF59z4EJADEIQ1F36k7u5E7ZKXeUQPACJ3wK7UNokVxVk9kHnQH7bY9hbDyDhNXgjpRLqFlo4M2GgfyJHhjq8V4agfrgPQX3JtJQGbofmCHgA/nAKks+JAjFA\
AAAAElFTkSuQmCC") no-repeat scroll right center;padding-right: 13px;}\n\
    div.wrap{max-width:1300px;margin:auto;padding:0px 5px 0px 5px;}\n\
    div.wrap:nth-of-type(2){padding-top:5px;margin-top:50px;}\n\
    div.wrap:last-child{padding-bottom:5px;margin-bottom:50px;}\n\
    div.pad{background-color:#222;padding:50px;}\n\
    .chapter{}#profile_top{}img{float:left;}canvas{float:left;}\n\
    a:link{color:#555;}a:visited{color:#555;}a:hover{color:#aaa;}a:active{color:#aaa;}\n\
    option{}\n\
    option:nth-of-type(6n+1)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #f00, #ff0);}\n\
    option:nth-of-type(6n+2)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #ff0, #0f0);}\n\
    option:nth-of-type(6n+3)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #0f0, #0ff);}\n\
    option:nth-of-type(6n+4)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #0ff, #00f);}\n\
    option:nth-of-type(6n+5)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #00f, #f0f);}\n\
    option:nth-of-type(6n+0)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #f0f, #f00);}\n\
    .col:nth-of-type(6n+'+ ((4- activeChap+60000)%6) +'){background-color:#f00;background:linear-gradient(to bottom, #f00, #ff0);}\n\
    .col:nth-of-type(6n+'+ ((5- activeChap+60000)%6) +'){background-color:#ff0;background:linear-gradient(to bottom, #ff0, #0f0);}\n\
    .col:nth-of-type(6n+'+ ((6- activeChap+60000)%6) +'){background-color:#0f0;background:linear-gradient(to bottom, #0f0, #0ff);}\n\
    .col:nth-of-type(6n+'+ ((7- activeChap+60000)%6) +'){background-color:#0ff;background:linear-gradient(to bottom, #0ff, #00f);}\n\
    .col:nth-of-type(6n+'+ ((8- activeChap+60000)%6) +'){background-color:#00f;background:linear-gradient(to bottom, #00f, #f0f);}\n\
    .col:nth-of-type(6n+'+ ((9- activeChap+60000)%6) +'){background-color:#f0f;background:linear-gradient(to bottom, #f0f, #f00);}\n'
    +'@media (max-width: 700px) {div.wrap{padding-left:1px;padding-right:1px;} div.pad{padding-left:5px;padding-right:5px;}}\n'
  switch ((urlGetChap(document.location.href) % 6)) {
    case 0:
      style.innerHTML += '.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #f0f);}\n'
      break;
    case 1:
      style.innerHTML += '.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #f00);}\n'
      break;
    case 2:
      style.innerHTML += '.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #ff0);}\n'
      break;
    case 3:
      style.innerHTML += '.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #0f0);}\n'
      break;
    case 4:
      style.innerHTML += '.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #0ff);}\n'
      break;
    case 5:
      style.innerHTML += '.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #00f);}\n'
      break;
  }
  document.head.appendChild(style)
  if(/fimfiction\.net/.exec(document.location)==null) {
    var settitle = /(.*)[Cc]hapter .*, a (.*) fanfic \| FanFiction/
    var storyname = settitle.exec(title.innerHTML)
    if(!storyname) {
      var settitle = /(.*), a (.*) fanfic \| FanFiction/
      var storyname = settitle.exec(title.innerHTML) }
    if(storyname) {
      title.innerHTML = storyname[1]+' - '+storyname[2]+(statusComplete && activeChap==1 ? ' - Qomplete' : (' - chapter '+activeChap+(latestChap!=activeChap?'-'+latestChap:'')))}
  } else {
    var settitle = /.* - (.*) - FIMFiction.net/
    var storyname = settitle.exec(title.innerHTML)
    // console.log(storyname)
    if(storyname) {
      title.innerHTML = storyname[1] + (' - chapter '+activeChap+(latestChap!=activeChap?'-'+latestChap:'')) } }

  document.head.appendChild(title)
  for (i=0;i<favicon.length;i+=1) {document.head.appendChild(favicon[i])}

  var ficpic = profile_top.getElementsByTagName('img')[0]
  if(ficpic){
    // http://pastebin.ca/1425789
    function mime_from_data(data) // Simple function that checks for JPG, GIF and PNG from image data. Otherwise returns false.
    {
      if('GIF'==data.substr(0,3))return 'image/gif';
      else if('PNG'==data.substr(1,3))return 'image/png';
      else if('JFIF'==data.substr(6,4))return 'image/jpg';
      return false;
    };
    function data_string(data) // Generates the binary data string from character / multibyte data
    {
      var data_string='';
      for(var i=0,il=data.length;i<il;i++)data_string+=String.fromCharCode(data[i].charCodeAt(0)&0xff);
      return data_string;
    };

    function load_image(tries) {
      GM_xmlhttpRequest({
        method: 'GET',
        url: ficpic.src,
        overrideMimeType: 'text/plain; charset=x-user-defined',
        onload: function(xhr) {
          var data = data_string(xhr.responseText);
          if(mime_from_data(data)) {
            var base64_data = btoa(data); // Encode to base64 string
            var data_url = 'data:' + mime_from_data(data) + ';base64,' + base64_data; // Make the data url
            ficpic.src = data_url
          } else if (tries>0) {
            load_image(tries-1)
          }
        }
      });
      return;
    };
    load_image(5)
  }

  // window.addEventListener('load_image',load_image,false); // Make sure image has properly loaded

  var loadwrap = document.createElement('div')
  loadwrap.setAttribute('class', 'wrap')
  loadwrap.style.position = 'sticky'
  loadwrap.style.top = '0px'
  var loading = document.createElement('div')
  loading.style.float = 'left'
  loading.setAttribute('id', 'loading')
  // loading.style.margin = '0px'
  // loading.innerHTML = String.fromCharCode(160)
  loadwrap.appendChild(loading)
  document.body.appendChild(loadwrap)

  function updateLoading(ignore, appended, downloaded, total) {
    // var loading = document.getElementById('loading')
    var p0 = parseInt(ignore / total * 100)
    var p1 = parseInt((appended + ignore) / total * 100)
    if (p1 == 100) {
      setTimeout(function() {
        loading.style.display = 'none'
      }, (activeChap != latestChap) * 100)
    }
    var p2 = parseInt((downloaded + appended + ignore) / total * 100)
    loading.style.background = 'linear-gradient(to right' +
      ', white 0%' +
      ', #555 ' + p0/2 +
      '%, white ' + p0 +
      '%, lime ' + p0 +
      '%, lime ' + p1 +
      '%, blue ' + p1 +
      '%, blue ' + p2 +
      '%, white ' + p2 +
      '%, white 100%)'
  }

  updateLoading(activeChap - 1, appendedNow, notAppendedYet, latestChap)

  var profile = document.createElement('div')
  profile.setAttribute('class', 'wrap profile')
  profile.setAttribute('id', 'profile')
  var pad = document.createElement('div')
  pad.setAttribute('class', 'pad')
  pad.appendChild(profile_top)
  profile.appendChild(pad)
  document.body.appendChild(profile)

  var panel = document.createElement('div')
  panel.setAttribute('class', 'panel')

  {
    var posbtn = document.createElement('button')
    posbtn.setAttribute('id', 'posbtn')
    function centerclick() {
      var settings = readSettings()
      var e = document.getElementById('position-style')
      if (e) {
        if (e.innerHTML == 'div.wrap{margin-left:0px;}') {
          settings.center = 2
          e.innerHTML = 'div.wrap{margin-right:0px;}'
          document.getElementById('posbtn').innerHTML = 'Right'}
        else {
          settings.center = 0
          e.remove()
          document.getElementById('posbtn').innerHTML = 'Centered'}}
      else {
        settings.center = 1
        var s = document.createElement('style')
        s.setAttribute('id', 'position-style')
        s.innerHTML = 'div.wrap{margin-left:0px;}'
        document.head.appendChild(s)
        document.getElementById('posbtn').innerHTML = 'Left'}
      saveSettings(settings)}
    posbtn.setAttribute('onclick', 'centerclick()')
    posbtn.setAttribute('class', 'center;')
    posbtn.innerHTML = 'Centered'

    var bgcolbtn = document.createElement('button')
    bgcolbtn.setAttribute('id', 'bgcolbtn')
    function bgcolclick() {
      var settings = readSettings()
      var e = document.getElementById('bgcol-style')
      if (e) {
        if (e.innerHTML == 'body{color:#000;}a:hover{color:#000;}div.pad{background-color:#fff;}') {
          settings.bgcol = 2
          e.innerHTML = 'body{color:#fff;}div.pad{background-color:#000;}'
          document.getElementById('bgcolbtn').innerHTML = 'Background: Black'}
        else {
          settings.bgcol = 0
          e.remove()
          document.getElementById('bgcolbtn').innerHTML = 'Background: Dark'}}
      else {
        settings.bgcol = 1
        var s = document.createElement('style')
        s.setAttribute('id', 'bgcol-style')
        s.innerHTML = 'body{color:#000;}a:hover{color:#000;}div.pad{background-color:#fff;}'
        document.head.appendChild(s)
        document.getElementById('bgcolbtn').innerHTML = 'Background: White'}
      saveSettings(settings)}
    bgcolbtn.setAttribute('onclick', 'bgcolclick()')
    bgcolbtn.setAttribute('style', 'float:left;')
    bgcolbtn.innerHTML = 'Background: Dark'

    var edgebtn = document.createElement('button')
    edgebtn.setAttribute('id', 'edgebtn')
    function edgeclick() {
      var settings = readSettings()
      var e = document.getElementById('edge-style')
      if (e) {
        settings.edge = 0
        e.remove()
        document.getElementById('edgebtn').innerHTML = 'Edge: Rainbow'}
      else {
        settings.edge = 1
        var s = document.createElement('style')
        s.setAttribute('id', 'edge-style')
        s.innerHTML = '.col:nth-of-type(n){background-color:#333;background:#333;}.profile{background-color:#333;background:linear-gradient(to bottom, #fff, #333);}'
        document.head.appendChild(s)
        document.getElementById('edgebtn').innerHTML = 'Edge: Gray'}
      saveSettings(settings)}
    edgebtn.setAttribute('onclick', 'edgeclick()')
    edgebtn.setAttribute('style', 'float:left;')
    edgebtn.innerHTML = 'Edge: Rainbow'

    var widthbtn = document.createElement('button')
    widthbtn.setAttribute('id', 'widthbtn')
    function widthclick() {
      var settings = readSettings()
      var e = document.getElementById('width-style')
      if (e) {
        if (e.innerHTML == 'div.wrap{max-width:777px;}') {
          settings.width = 2
          e.innerHTML = 'div.wrap{max-width:100%;}'
          document.getElementById('widthbtn').innerHTML = 'Width: Wide'}
        else {
          settings.width = 0
          e.remove()
          document.getElementById('widthbtn').innerHTML = 'Width: Default'}}
      else {
        settings.width = 1
        var s = document.createElement('style')
        s.setAttribute('id', 'width-style')
        s.innerHTML = 'div.wrap{max-width:777px;}'
        document.head.appendChild(s)
        document.getElementById('widthbtn').innerHTML = 'Width: Narrow'}
      saveSettings(settings)}
    widthbtn.setAttribute('onclick', 'widthclick()')
    widthbtn.setAttribute('class', 'center')
    widthbtn.innerHTML = 'Width: Default'

    function setSettings(){
      var live = false
      if (Element.prototype.remove == undefined) {
        Element.prototype.remove = function() {
          this.parentNode.removeChild(this)}}
      var e,settings = readSettings()
      if((e=document.getElementById('position-style'))&&settings.center!=null) {e.remove(); document.getElementById('posbtn').innerHTML='Centered'}
      if((e=document.getElementById('bgcol-style'))&&settings.bgcol!=null) {e.remove(); document.getElementById('bgcolbtn').innerHTML='Background: Dark'}
      if((e=document.getElementById('edge-style'))&&settings.edge!=null) {e.remove(); document.getElementById('edgebtn').innerHTML='Edge: Rainbow'}
      if((e=document.getElementById('width-style'))&&settings.width!=null) {e.remove(); document.getElementById('widthbtn').innerHTML='Width: Default'}
      for(var i=0; i<settings.center; ++i) {setTimeout(centerclick,0)}
      for(var i=0; i<settings.bgcol; ++i) {setTimeout(bgcolclick,0)}
      for(var i=0; i<settings.edge; ++i) {setTimeout(edgeclick,0)}
      for(var i=0; i<settings.width; ++i) {setTimeout(widthclick,0)}}
    var scripttag = document.createElement('script')
    scripttag.setAttribute('id','button-helper')
    scripttag.innerHTML = [
      ,setSettings.toString().slice(23,-1)
      ,readSettings.toString()
      ,saveSettings.toString()
      ,centerclick.toString()
      ,bgcolclick.toString()
      ,edgeclick.toString()
      ,widthclick.toString()
      ].join('\n')

    posbtn.addEventListener('click',_=>{saveSettings(readSettings())})
    widthbtn.addEventListener('click',_=>{saveSettings(readSettings())})
    bgcolbtn.addEventListener('click',_=>{saveSettings(readSettings())})
    edgebtn.addEventListener('click',_=>{saveSettings(readSettings())})

    panel.appendChild(posbtn)
    panel.appendChild(widthbtn)
    panel.appendChild(bgcolbtn)
    panel.appendChild(edgebtn)
    document.body.appendChild(scripttag)
  }

  if (chap_select) {
    chap_select.setAttribute('onchange', 'if(this.options[this.selectedIndex].value < ' + urlGetChap(document.location.href) + '){' +
      chap_select.getAttribute('onchange') +
      '}' + ' else {document.getElementById(\'\'+this.options[this.selectedIndex].value).scrollIntoView();}')
    chap_select.setAttribute('style', 'float:right;')
    var os = chap_select.getElementsByTagName('option')
    for (i = 0; i < urlGetChap(document.location) - 1; i += 1) {os[i].setAttribute('class', 'external')}
    panel.appendChild(chap_select)}

  document.getElementById('profile').firstChild.appendChild(panel)
  // document.body.insertBefore(panel, document.body.firstChild)
  document.body.appendChild(chap)

  function loadQomplete() {
    var a = document.getElementsByClassName('skiptranslate')
    for (; a.length;) {a[0].remove()}
    a = document.getElementsByClassName('ad_container')
    for (; a.length;) {a[0].remove()}

    element.next = function() {
      if(this.firstChild) return this.firstChild
      if(this.nextSibling) return this.nextSibling
      return this.parentNode.nextSibling}
    // window.parentNode.replaceChild(window.cloneNode(false), window)
    // var el = document
    // while(el) {
    //   e.parentNode.replaceChild(el.cloneNode(false), el)
    //   el = el.next()}
    // document.head.parentNode.replaceChild(document.head.cloneNode(true), document.head)
    // document.body.parentNode.replaceChild(document.body.cloneNode(true), document.body)
    // var el = document.body
    // elClone = el.cloneNode(true);
    // el.parentNode.replaceChild(elClone, el);
    document.body.removeAttribute('style')
    updateLoading(activeChap - 1, latestChap - (activeChap - 1), 0, latestChap)}

  function appendChapterFromURL(url) {
    var oReq = new XMLHttpRequest();
    oReq.onload = function() {
      var xmlDoc = new DOMParser().parseFromString(this.responseText, "text/html")
      var url = this.responseURL ? this.responseURL : this.responseURLfallback
      var chap = chapFromPage(url, xmlDoc)
      if (chap) {
        document.body.appendChild(chap)
        appendedNow += 1
        updateLoading(activeChap - 1, appendedNow, notAppendedYet, latestChap)
        window.setTimeout(function() {
          appendChapterFromURL(inc(url))
        }, 0)}
      else loadQomplete()}
    oReq.responseURLfallback = url
    oReq.open("get", url, true)
    oReq.send()}

  function appendChapterFromURL2(url) {
    var oReq = new XMLHttpRequest();
    oReq.onload = function() {
      // setTimeout(function(this2){
      var this2 = this // for debug purposes, use comment above as code to simulate delays (and its matching closing part)
      var xmlDoc = new DOMParser().parseFromString(this2.responseText, "text/html")
      var url = this2.responseURL ? this2.responseURL : this2.responseURLfallback
      var chap = chapFromPage(url, xmlDoc)
      if (chap) {
        chapArr[parseInt(urlGetChap(url))] = chap
        notAppendedYet += 1
        updateLoading(activeChap - 1, appendedNow, notAppendedYet, latestChap)
        if (appendedNow + notAppendedYet + activeChap - 1 == latestChap) {
          // consolog("all downloaded", performance.now())
        }
      }
      // }, Math.random()*8000+50*urlGetChap(this.responseURL), this)
    }
    oReq.responseURLfallback = url
    oReq.open("get", url, true)
    oReq.send()}

  function appendNextChap(n) {
    if (chapArr[n]) {
      document.body.appendChild(chapArr[n])
      appendedNow += 1
      notAppendedYet -= 1
      updateLoading(activeChap - 1, appendedNow, notAppendedYet, latestChap)
      if (n < latestChap) {
        appendNextChap(n + 1)}
      else {
        loadQomplete() }}
    else {
      window.setTimeout(function() {
        appendNextChap(n)
      }, 50) }}

  if (true) {
    // Asynchronous chapter load. Very fast for big fanfics.
    for (i = activeChap; i < latestChap; i += 1)
      appendChapterFromURL2(urlSetChap(document.location.href, i + 1));
    if (activeChap == latestChap) {
      loadQomplete()}
    else {
      appendNextChap(activeChap + 1)}}
  else {
    // Synchronous chapter load. Slow for big fanfics but doesn't hit the server as hard.
    appendChapterFromURL(inc(document.location.href))}
}