// ==UserScript== // @name FanfictionQomplete // @description Proper multi-chapter reading mode with less clutter. // @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.20151113203630 // @downloadURL none // ==/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 --Font size Minimize Qontrol panel or something to reduce button amount Add support for other sites fimfiction.com chapter selector 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 */ var live = true polyfill() var siteMatch = /^.*?www\.(.*?)\..*?\//.exec(document.location.href)[1] var hash_settings = document.location.hash.slice(1).split('&') if(hash_settings.map(c=>c.split('=')[0]).includes('Qomplete')) { checkForBadJavascripts ([ [true, /static\.fimfiction\.net\/js\/scripts\.js\?uupAcQPf/, null] ,[false, /\$\(window\)\.scroll\( function\(e\)/ /*)*/, null]]) window.addEventListener('load', runFFQomplete)} else {window.addEventListener('load', injectQompleteButton)} function injectQompleteButton() { switch(siteMatch){ case 'fimfiction': var lc = document.getElementById('chapter_title') lc = lc&&lc.parentNode lc = lc&&lc.getElementsByClassName('button-group')[0] 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!' lc.insertBefore(btn,lc.lastChild.previousSibling) break; case 'fanfiction': case 'fictionpress': var lc = document.getElementsByClassName('lc') 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) break; }} function runFFQomplete() { // Add the loaded settings to url bar so that they don't get changed for this tab if the settings are changed elsewhere. var settings = readSettings() saveSettings(settings) var re = getChapRE() // TODO remove vvv? window.addEventListener('load', ()=>{ var a = document.getElementsByClassName('skiptranslate') for (; a.length; a[0].remove()); document.body.removeAttribute('style')}) // Initialisations var appendedNow = 1 var notAppendedYet = 0 var chapArr = [] var activeChap = parseInt(urlGetChap(document.location)) // Grab the elements we want var title = document.getElementsByTagName('title')[0] var profile_top = getProfileTop() var ficpic = profile_top.getElementsByTagName('img')[0] var statusComplete = !!(/Status: Complete/.exec(profile_top.innerHTML)) var chap_select = document.getElementById('chap_select') var latestChap = getLatestChap() var favicons = getFavicons() var chap = chapFromPage(document.location.href, document) // Get the chapter that is already loaded clean() // Now, add items. The ones we kept and modified and new ones. addStyle() title.innerHTML = fixTitleText(title.innerHTML) document.head.appendChild(title) for (i=0;i(/favicon/.test(c.href)))} function readSettings() { var settings = {Qomplete:1} var parr = ['center','bgcol','edge','width','fontsz'] if(live){ for(var i in parr) { settings[parr[i]] = GM_getValue(parr[i],0)}} var hash_settings = document.location.hash.slice(1).split('&') for(var i=0; i0) { load_image(tries-1) }}})} load_image(5)} function urlGetChap(url) { var arr = re.exec(url) return arr[2]} function urlSetChap(url, n) { var arr = re.exec(url) return arr[1] + n + ((siteMatch==='fimfiction') ? arr[3] : arr[3].replace(/\/[^\/]*$/, ""))} function inc(url) { var arr = re.exec(url) return arr[1] + (parseInt(arr[2]) + 1) + ((siteMatch==='fimfiction') ? arr[3] : arr[3].replace(/\/[^\/]*$/, ""))} function chapFromPage(url, page) { var storytext; if(siteMatch==='fanfiction'||siteMatch==='fictionpress'){ storytext = page.getElementById('storytext')} else if(siteMatch==='fimfiction') { 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, '$1'); 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 (siteMatch==='fanfiction' || siteMatch==='fictionpress') { var newChapTitle = /(.*(| [Cc]hapter [^:]+: .*)), a .*/.exec(title.innerHTML)} else if(siteMatch==='fimfiction') { var newChapTitle = /(.*) - FIMFiction.net/.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} function clean(){ 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')} function addStyle(){ var style = document.createElement('style') style.setAttribute('type', 'text/css') style.innerHTML = `body{background-color:#000;color:#ccc;margin:0;padding:0;font-family:"Verdana";} ul.tags > li{display:inline;} ul.tags > li::before{content:" #"} #loading{position:inherit;width:100%;height:5px;} button, select{border-radius:4px;padding:4px 12px;background: linear-gradient(to bottom, #333, #000);border-width: 1px;color:#ccc;background-color:#000;} button:hover{background-image:none;} .panel{text-align:center;} a.external, option.external{background: transparent url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAFZJREFUeF59z4EJADEIQ1F36k7u5E7ZKXeUQPACJ3wK7UNokVxVk9kHnQH7bY9hbDyDhNXgjpRLqFlo4M2GgfyJHhjq8V4agfrgPQX3JtJQGbofmCHgA/nAKks+JAjFAAAAAElFTkSuQmCC") no-repeat scroll right center;padding-right: 13px;} div.wrap{max-width:1300px;margin:auto;padding:0px 5px 0px 5px;} div.wrap:nth-of-type(2){padding-top:5px;margin-top:50px;} div.wrap:last-child{padding-bottom:5px;margin-bottom:50px;} div.pad{background-color:#222;padding:50px;} .chapter{}#profile_top{}img{float:left;}canvas{float:left;} a:link{color:#555;}a:visited{color:#555;}a:hover{color:#aaa;}a:active{color:#aaa;} option{} option:nth-of-type(6n+1)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #f00, #ff0);} option:nth-of-type(6n+2)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #ff0, #0f0);} option:nth-of-type(6n+3)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #0f0, #0ff);} option:nth-of-type(6n+4)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #0ff, #00f);} option:nth-of-type(6n+5)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #00f, #f0f);} option:nth-of-type(6n+0)::before{content:"";padding-left:3px;margin-left:-3px;margin-right:4px;background:linear-gradient(to bottom, #f0f, #f00);} .col:nth-of-type(6n+${((4-activeChap+60000)%6)}){background-color:#f00;background:linear-gradient(to bottom, #f00, #ff0);} .col:nth-of-type(6n+${((5-activeChap+60000)%6)}){background-color:#ff0;background:linear-gradient(to bottom, #ff0, #0f0);} .col:nth-of-type(6n+${((6-activeChap+60000)%6)}){background-color:#0f0;background:linear-gradient(to bottom, #0f0, #0ff);} .col:nth-of-type(6n+${((7-activeChap+60000)%6)}){background-color:#0ff;background:linear-gradient(to bottom, #0ff, #00f);} .col:nth-of-type(6n+${((8-activeChap+60000)%6)}){background-color:#00f;background:linear-gradient(to bottom, #00f, #f0f);} .col:nth-of-type(6n+${((9-activeChap+60000)%6)}){background-color:#f0f;background:linear-gradient(to bottom, #f0f, #f00);} @media (max-width: 700px) {div.wrap{padding-left:1px;padding-right:1px;} div.pad{padding-left:5px;padding-right:5px;}} ${['.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #f0f);}' ,'.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #f00);}' ,'.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #ff0);}' ,'.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #0f0);}' ,'.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #0ff);}' ,'.profile{background-color:#777;background:linear-gradient(to bottom, #fff, #00f);}'][urlGetChap(document.location.href) % 6]}` document.head.appendChild(style)} function fixTitleText(titleText){ if(siteMatch==='fanfiction') { var settitle = /(.*?)(?:[Cc]hapter .*, a |, a )(.*) fanfic \| FanFiction/ var storyname = settitle.exec(titleText) if(storyname) { return storyname[1] +(statusComplete&&activeChap==1?' - Qomplete':(' - chapter '+activeChap+(latestChap!=activeChap?'-'+latestChap:''))) +' - '+storyname[2]}} if(siteMatch==='fictionpress') { var settitle = /(.*?)(?:[Cc]hapter .*, a |, a )(.*) fiction \| FictionPress/ var storyname = settitle.exec(titleText) if(storyname) { return storyname[1] +(statusComplete&&activeChap==1?' - Qomplete':(' - chapter '+activeChap+(latestChap!=activeChap?'-'+latestChap:''))) +' - '+storyname[2]}} if(siteMatch==='fimfiction') { var settitle = /.* - (.*) - FIMFiction.net/ var storyname = settitle.exec(titleText) if(storyname) { return storyname[1] + (' - chapter '+activeChap+(latestChap!=activeChap?'-'+latestChap:''))}}} 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%)`} function addButtons(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 = 'Black'} else { settings.bgcol = 0 e.remove() document.getElementById('bgcolbtn').innerHTML = '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 = 'White'} saveSettings(settings)} bgcolbtn.setAttribute('onclick', 'bgcolclick()') bgcolbtn.setAttribute('style', 'float:left;') bgcolbtn.innerHTML = '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 = '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 = '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 = '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 = 'Narrow'} saveSettings(settings)} widthbtn.setAttribute('onclick', 'widthclick()') widthbtn.setAttribute('class', 'center') widthbtn.innerHTML = 'Width: Default' var fontszSelect = document.createElement('select') fontszSelect.setAttribute('id', 'fontzsselect'); fontszSelect.setAttribute('style', 'float:left;') function setFontSize(pt) { var settings = readSettings() settings.fontsz = pt saveSettings(settings) var e = document.getElementById('fontsz-style') if(pt!=0){ var css = `body{font-size:${pt}pt}` if (!e) { e = document.createElement('style') e.setAttribute('id', 'fontsz-style') document.head.appendChild(e)} e.innerHTML = css} else { if(e) e.remove()}} for(var i = 0; i<=50; i+=1){ var option = document.createElement('option') if(i==settings.fontsz){option.setAttribute('selected','')} option.value = i.toString() option.innerHTML = `${i||''}pt` fontszSelect.appendChild(option)} fontszSelect.setAttribute('onchange', 'setFontSize(this.options[this.selectedIndex].value)') function setSettings(){ polyfill() var live = false 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='Dark'} if((e=document.getElementById('edge-style'))&&settings.edge!=null) {e.remove(); document.getElementById('edgebtn').innerHTML='Rainbow'} if((e=document.getElementById('width-style'))&&settings.width!=null) {e.remove(); document.getElementById('widthbtn').innerHTML='Width: Default'} for(var i=0; isaveSettings(readSettings())) widthbtn.addEventListener('click',_=>saveSettings(readSettings())) bgcolbtn.addEventListener('click',_=>saveSettings(readSettings())) edgebtn.addEventListener('click',_=>saveSettings(readSettings())) fontszSelect.addEventListener('change',_=>saveSettings(readSettings())) panel.appendChild(posbtn) panel.appendChild(widthbtn) panel.appendChild(bgcolbtn) panel.appendChild(edgebtn) panel.appendChild(fontszSelect) 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)}} function loadQomplete() { var a = document.getElementsByClassName('skiptranslate') for (; a.length; a[0].remove()); a = document.getElementsByClassName('ad_container') for (; a.length; a[0].remove()); document.body.removeAttribute('style') updateLoading(activeChap - 1, latestChap - (activeChap - 1), 0, latestChap)} function appendChapterFromURL(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)} // }, 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) }} } function polyfill(){ if (Element.prototype.remove == undefined) { Element.prototype.remove = function() { this.parentNode.removeChild(this) } } if (!Array.prototype.includes) { Array.prototype.includes = function(searchElement /*, fromIndex*/ ) { 'use strict'; var O = Object(this); var len = parseInt(O.length) || 0; if (len === 0) {return false; } var n = parseInt(arguments[1]) || 0; var k; if (n >= 0) {k = n; } else { k = len + n; if (k < 0) {k = 0;} } var currentElement; while (k < len) { currentElement = O[k]; if (searchElement === currentElement || (searchElement !== searchElement && currentElement !== currentElement)) { return true; } k++; } return false; }; }} // 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