User:Phlip/Greasemonkey

From Homestar Runner Wiki

(Difference between revisions)
Jump to: navigation, search
m (That's not where I meant to put that)
(So I learned how JS promises work)
Line 4: Line 4:
If you don't have it already, you'll need to install [https://addons.mozilla.org/en-US/firefox/addon/748 Greasemonkey], then restart Firefox and return to this page.
If you don't have it already, you'll need to install [https://addons.mozilla.org/en-US/firefox/addon/748 Greasemonkey], then restart Firefox and return to this page.
-
Then, just click on <span class="plainlinks">[{{fullurl:{{FULLPAGENAME}}|action=raw&ctype=text/javascript&cachedodge=4.2.77&fakeextension=.user.js}} this link]</span> to install the script.
+
Then, just click on <span class="plainlinks">[{{fullurl:{{FULLPAGENAME}}|action=raw&ctype=text/javascript&cachedodge=4.3.97&fakeextension=.user.js}} this link]</span> to install the script.
To upgrade a new version when it's updated, just click the install link again &ndash; it'll automagically replace the old version. If the option is enabled, the script will automatically check for updates for you.
To upgrade a new version when it's updated, just click the install link again &ndash; it'll automagically replace the old version. If the option is enabled, the script will automatically check for updates for you.
Line 15: Line 15:
// Homestar All-In-One
// Homestar All-In-One
-
// version 4.2
+
// version 4.3
-
// 2017-11-22
+
// 2017-11-24
// Copyright (c) Phillip Bradbury, Loafing
// Copyright (c) Phillip Bradbury, Loafing
//
//
Line 40: Line 40:
// @name          Homestar All-In-One
// @name          Homestar All-In-One
// @namespace    http://www.hrwiki.org/
// @namespace    http://www.hrwiki.org/
-
// @description  Combination of many Homestar Runner scripts. Version 4.2.
+
// @description  Combination of many Homestar Runner scripts. Version 4.3.
-
// @version      4.2.77
+
// @version      4.3.97
// @downloadURL  http://www.hrwiki.org/w/index.php?title=User:Phlip/Greasemonkey&action=raw&ctype=text/javascriptfakeextension=.user.js
// @downloadURL  http://www.hrwiki.org/w/index.php?title=User:Phlip/Greasemonkey&action=raw&ctype=text/javascriptfakeextension=.user.js
// @icon          http://www.hrwiki.org/w/images/thumb/1/1b/logo.png/32px-logo.png
// @icon          http://www.hrwiki.org/w/images/thumb/1/1b/logo.png/32px-logo.png
Line 60: Line 60:
// ==/UserScript==
// ==/UserScript==
-
(function(){
+
(async function(){
function Utils()
function Utils()
{
{
Line 83: Line 83:
// Based on http://userscripts.org/topics/41177
// Based on http://userscripts.org/topics/41177
-
Utils.prototype.useGMFunctions = function useGMFunctions()
+
Utils.prototype.useGMFunctions = async function useGMFunctions()
{
{
// We can't just test if GM_getValue exists, because in Chrome they do exist
// We can't just test if GM_getValue exists, because in Chrome they do exist
// but they don't actually do anything, just report failure to console.log
// but they don't actually do anything, just report failure to console.log
-
// Note that on Firefox Quantum, with Greasemonkey 4, this will not use the
+
// Have to do it like this instead of like "if(window.GM_getValue)"
-
// GM get/setValue, but use localStorage instead, as getValue returns
+
// because apparently this function isn't actually on "window", and I don't
-
// a Promise now, which would require rewriting a lot of things (which maybe
+
// know where it actually lives...
-
// I'll get to eventually).
+
if (typeof(GM) == "object" && GM.getValue && await GM.getValue("this-value-doesn't-exist-I-promise", true))
-
+
return 2; // Use GM4 methods
-
// We don't want it to actually write anything to console.log, though, so
+
else if (typeof(GM_getValue) == "function" && GM_getValue("this-value-doesn't-exist-I-promise", true))
-
// let's stop that
+
return 1; // Use GM3 methods
-
var log = console.log;
+
else
-
console.log = function log(){};
+
return 0; // Use native methods
-
var gmstorage = typeof(GM_getValue) == "function" && GM_getValue("this-value-doesn't-exist-I-promise", true);
+
-
console.log = log;
+
return gmstorage;
return gmstorage;
};
};
// Only really need to do this once...
// Only really need to do this once...
-
Utils.prototype.useGMFunctions = Utils.prototype.useGMFunctions();
+
Utils.prototype.useGMFunctions = await Utils.prototype.useGMFunctions();
-
Utils.prototype.getPref = function getPref(key, def)
+
Utils.prototype.getPref = async function getPref(key, def)
{
{
-
// Have to do it like this instead of like "if(window.GM_getValue)"
+
if (this.useGMFunctions == 2)
-
// because apparently this function isn't actually on "window", and I don't
+
return await GM.getValue(key, def);
-
// know where it actually lives...
+
else if (this.useGMFunctions == 1)
-
if (this.useGMFunctions)
+
return GM_getValue(key, def);
return GM_getValue(key, def);
else if (window.localStorage)
else if (window.localStorage)
Line 133: Line 130:
Utils.prototype.setPref = function setPref(key, value)
Utils.prototype.setPref = function setPref(key, value)
{
{
-
if (this.useGMFunctions)
+
if (this.useGMFunctions == 2)
 +
GM.setValue(key, value);
 +
else if (this.useGMFunctions == 1)
GM_setValue(key, value);
GM_setValue(key, value);
else if (window.localStorage)
else if (window.localStorage)
Line 153: Line 152:
};
};
-
Utils.prototype.downloadPage = function downloadPage(url, loadcb, errorcb, method)
+
Utils.prototype.downloadPage = function downloadPage(url, method)
{
{
if (!method)
if (!method)
method = 'GET';
method = 'GET';
-
if (typeof(GM) == "object" && GM.xmlHttpRequest)
+
return new Promise((resolve, reject) => {
-
{
+
if (typeof(GM) == "object" && GM.xmlHttpRequest) {
-
var opts = {
+
GM.xmlHttpRequest({
-
method: method,
+
method: method,
-
url: url,
+
url: url,
-
onload: function onload(res) {loadcb(res.responseText, res.status, res.statusText, res.responseHeaders);}
+
onload: res => resolve({text: res.responseText, status: res.status, statusText: res.statusText, headers: res.responseHeaders}),
-
};
+
onerror: res => reject(`${res.status} ${res.statusText}`)
-
if (errorcb)
+
});
-
opts.onerror = function onerror(res) {errorcb(res.status, res.statusText, res.responseHeaders);};
+
} else if (typeof(GM_xmlhttpRequest) == "function") {
-
GM.xmlHttpRequest(opts);
+
GM_xmlhttpRequest({
-
}
+
method: method,
-
else if (typeof(GM_xmlhttpRequest) == "function")
+
url: url,
-
{
+
onload: res => resolve({text: res.responseText, status: res.status, statusText: res.statusText, headers: res.responseHeaders}),
-
var opts = {
+
onerror: res => reject(`${res.status} ${res.statusText}`)
-
method: method,
+
});
-
url: url,
+
} else {
-
onload: function onload(res) {loadcb(res.responseText, res.status, res.statusText, res.responseHeaders);}
+
var xhr = new XMLHttpRequest();
-
};
+
xhr.onload = () => resolve({text: xhr.responseText, status: xhr.status, statusText: xhr.statusText, headers: xhr.getAllResponseHeaders()});
-
if (errorcb)
+
xhr.onerror = () => reject(`${xhr.status} ${xhr.statusText}`);
-
opts.onerror = function onerror(res) {errorcb(res.status, res.statusText, res.responseHeaders);};
+
xhr.open(method, url);
-
GM_xmlhttpRequest(opts);
+
xhr.send();
-
}
+
}
-
else
+
});
-
{
+
-
var xhr = new XMLHttpRequest();
+
-
xhr.onload = function onload() {loadcb(xhr.responseText, xhr.status, xhr.statusText, xhr.getAllResponseHeaders());};
+
-
if (errorcb)
+
-
xhr.onerror = function onerror() {errorcb(xhr.status, xhr.statusText, xhr.getAllResponseHeaders());};
+
-
xhr.open(method, url);
+
-
xhr.send();
+
-
}
+
};
};
Utils.prototype.buildWikiUrl = function buildWikiUrl(page)
Utils.prototype.buildWikiUrl = function buildWikiUrl(page)
Line 194: Line 185:
return "http://www.hrwiki.org/w/index.php?title=" + url + "&action=raw&source=allinone&cachedodge=" + this.getPref('cachedodge', 0);
return "http://www.hrwiki.org/w/index.php?title=" + url + "&action=raw&source=allinone&cachedodge=" + this.getPref('cachedodge', 0);
};
};
-
Utils.prototype.downloadWiki = function downloadWiki(page, loadcb, errorcb)
+
Utils.prototype.downloadWiki = async function downloadWiki(page)
{
{
-
this.downloadPage(this.buildWikiUrl(page), this.wikiPageDownloaded.bind(this, loadcb, errorcb, 0), errorcb);
+
for (var timesredirected = 0; timesredirected < 3; timesredirected++) {
-
};
+
var res = await this.downloadPage(this.buildWikiUrl(page));
-
Utils.prototype.wikiPageDownloaded = function wikiPageDownloaded(loadcb, errorcb, timesredirected, text, status, statusText)
+
-
{
+
// check for redirects
-
// check for redirects
+
var matches = res.text.match(/^\s*#\s*REDIRECT\s*\[\[(.*)\]\]/i);
-
var matches = text.match(/^\s*#\s*REDIRECT\s*\[\[(.*)\]\]/i);
+
if (matches)
-
if (matches)
+
-
{
+
-
if (timesredirected >= 3) // follow 3 redirects, but no more
+
{
{
-
errorcb(500, "Too many redirects");
+
// Get the page name out of the redirect text
-
return;
+
var text = matches[1];
 +
if ((matches = text.match(/^(.*)\|/)))
 +
text = matches[1];
 +
if ((matches = text.match(/^(.*)\#/)))
 +
text = matches[1];
 +
page = text.replace(/^\s+|\s+$/g, '');
}
}
-
// Get the page name out of the redirect text
+
else
-
text = matches[1];
+
return res.text;
-
if ((matches = text.match(/^(.*)\|/)))
+
-
text = matches[1];
+
-
if ((matches = text.match(/^(.*)\#/)))
+
-
text = matches[1];
+
-
text = text.replace(/^\s+|\s+$/g, '');
+
-
this.downloadPage(this.buildWikiUrl(text), this.wikiPageDownloaded.bind(this, loadcb, errorcb, timesredirected + 1), errorcb);
+
-
return;
+
}
}
-
loadcb(text, status, statusText);
+
throw "Too many redirects";
};
};
-
Utils.prototype.downloadWikiXML = function downloadWikiXML(page, loadcb, errorcb)
+
Utils.prototype.parseWikiXML = function parseWikiXML(text)
-
{
+
-
this.downloadWiki(page, this.wikiXMLDownloaded.bind(this, loadcb, errorcb), errorcb);
+
-
};
+
-
Utils.prototype.wikiXMLDownloaded = function wikiXMLDownloaded(loadcb, errorcb, text, status, statusText)
+
{
{
// strip various things - templates and <pre> tags for wiki formatting, and <noinclude> sections...
// strip various things - templates and <pre> tags for wiki formatting, and <noinclude> sections...
Line 242: Line 224:
catch (e)
catch (e)
{
{
-
errorcb(500, "Error in XML:\n" + e.toString());
+
throw "Error in XML:\n" + e.toString();
-
return;
+
}
}
// check if returned document is an error message
// check if returned document is an error message
Line 253: Line 234:
// Firefox's errors look like this:
// Firefox's errors look like this:
// <parsererror>Error details<sourcetext>Source text</sourcetext></parsererror>
// <parsererror>Error details<sourcetext>Source text</sourcetext></parsererror>
-
errorcb(500,
+
throw (
error.firstChild.nodeValue.replace(/Location: .*\n/, "") + "\n" +
error.firstChild.nodeValue.replace(/Location: .*\n/, "") + "\n" +
doc.documentElement.lastChild.textContent
doc.documentElement.lastChild.textContent
Line 262: Line 243:
// Chrome's errors look like this:
// Chrome's errors look like this:
// <someRoot><parsererror style="..."><h3>Generic error message</h3><div style="...">Error details</div><h3>Generic footer</h3><attempted parsing of page/></someRoot>
// <someRoot><parsererror style="..."><h3>Generic error message</h3><div style="...">Error details</div><h3>Generic footer</h3><attempted parsing of page/></someRoot>
-
errorcb(500,
+
throw (
"Error in XML:\n" +
"Error in XML:\n" +
error.getElementsByTagName('div')[0].textContent
error.getElementsByTagName('div')[0].textContent
Line 270: Line 251:
{
{
// Try to at least return something
// Try to at least return something
-
errorcb(500,
+
throw (
"Error in XML:\n" +
"Error in XML:\n" +
error.textContent
error.textContent
);
);
}
}
-
return;
 
}
}
-
loadcb(doc, status, statusText);
+
return doc;
};
};
-
Utils.prototype.currentFrame = function currentFrame(callback, flashmovie)
+
Utils.prototype.currentFrame = async function currentFrame(flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback(false);
 
return;
return;
-
}
 
if (flashmovie === globals.flashmovie && globals.is_puppets)
if (flashmovie === globals.flashmovie && globals.is_puppets)
{
{
-
playercomm.targetCurrentFrame(flashmovie, "/videoplayer", (a) => {
+
var a = await playercomm.targetCurrentFrame(flashmovie, "/videoplayer");
-
// Keep track of whether the current frame is changing, for isPlaying()
+
-
// If we stay on the same frame for more than, say, a second, guess
+
-
// that we're paused.
+
-
if (a != this.guessisplaying.lastframe)
+
-
{
+
-
this.guessisplaying.lastframe = a;
+
-
this.guessisplaying.lastframeat = new Date();
+
-
this.guessisplaying.state = true;
+
-
}
+
-
else if (new Date() - this.guessisplaying.lastframeat > 1000)
+
-
{
+
-
this.guessisplaying.state = false;
+
-
}
+
-
if (callback)
+
// Keep track of whether the current frame is changing, for isPlaying()
-
callback(a);
+
// If we stay on the same frame for more than, say, a second, guess
-
});
+
// that we're paused.
 +
if (a != this.guessisplaying.lastframe)
 +
{
 +
this.guessisplaying.lastframe = a;
 +
this.guessisplaying.lastframeat = new Date();
 +
this.guessisplaying.state = true;
 +
}
 +
else if (new Date() - this.guessisplaying.lastframeat > 1000)
 +
{
 +
this.guessisplaying.state = false;
 +
}
 +
 +
return a;
}
}
else
else
{
{
-
playercomm.currentFrame(flashmovie, callback)
+
return await playercomm.currentFrame(flashmovie)
}
}
};
};
-
Utils.prototype.totalFrames = function totalFrames(callback, flashmovie)
+
Utils.prototype.totalFrames = async function totalFrames(flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback(false);
 
return;
return;
-
}
 
var a;
var a;
if (flashmovie === globals.flashmovie && globals.is_puppets)
if (flashmovie === globals.flashmovie && globals.is_puppets)
-
playercomm.targetTotalFrames(flashmovie, "/videoplayer", callback)
+
return await playercomm.targetTotalFrames(flashmovie, "/videoplayer")
else
else
-
playercomm.totalFrames(flashmovie, callback)
+
return await playercomm.totalFrames(flashmovie)
};
};
-
Utils.prototype.isPlaying = function isPlaying(callback, flashmovie)
+
Utils.prototype.isPlaying = async function isPlaying(flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback(false);
 
return;
return;
-
}
 
if (flashmovie === globals.flashmovie && globals.is_puppets)
if (flashmovie === globals.flashmovie && globals.is_puppets)
Line 351: Line 318:
// it's possible I just haven't tried the right thing)...
// it's possible I just haven't tried the right thing)...
// So, for puppet toons, we need to try to track whether it seems to be playing...
// So, for puppet toons, we need to try to track whether it seems to be playing...
-
callback(this.guessisplaying.state);
+
return this.guessisplaying.state;
}
}
else
else
{
{
-
playercomm.isPlaying(flashmovie, callback);
+
return await playercomm.isPlaying(flashmovie);
}
}
};
};
-
Utils.prototype.framesLoaded = function framesLoaded(callback, flashmovie)
+
Utils.prototype.framesLoaded = async function framesLoaded(flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback(false);
 
return;
return;
-
}
 
if (flashmovie === globals.flashmovie && globals.is_puppets)
if (flashmovie === globals.flashmovie && globals.is_puppets)
-
playercomm.targetFramesLoaded(flashmovie, '/videoplayer', callback)
+
return await playercomm.targetFramesLoaded(flashmovie, '/videoplayer')
else
else
-
playercomm.targetFramesLoaded(flashmovie, '/', callback)
+
return await playercomm.targetFramesLoaded(flashmovie, '/')
};
};
-
Utils.prototype.isLoaded = function isLoaded(callback, flashmovie)
+
Utils.prototype.isLoaded = async function isLoaded(flashmovie)
{
{
-
this.currentFrame((frame) => {callback(frame >= 0)}, flashmovie);
+
var frame = await this.currentFrame(flashmovie);
 +
return frame >= 0;
};
};
-
Utils.prototype.whenLoaded = function whenLoaded(callback, flashmovie)
+
Utils.prototype.waitLoaded = function waitLoaded(flashmovie)
{
{
-
if (!flashmovie)
+
var useglobal = false;
 +
if (!flashmovie) {
 +
useglobal = true;
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
 +
}
if (!flashmovie)
if (!flashmovie)
-
return;
+
return new Promise((resolve, reject) => reject());
-
this.currentFrame((frame) => {
+
if (useglobal && this.loadedPromise)
-
if (frame >= 0)
+
return this.loadedPromise;
-
callback();
+
 +
async function poll(resolve) {
 +
if (await this.isLoaded(flashmovie))
 +
resolve();
else
else
-
setTimeout(this.whenLoaded.bind(this, callback, flashmovie), 100);
+
setTimeout(poll.bind(this, resolve), 100)
-
}, flashmovie);
+
}
-
};
+
var promise = new Promise(poll.bind(this));
-
Utils.prototype.stop = function stop(callback, flashmovie)
+
if (useglobal)
 +
this.loadedPromise = promise;
 +
return promise;
 +
}
 +
Utils.prototype.stop = async function stop(flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback();
 
return;
return;
-
}
 
if (flashmovie === globals.flashmovie && globals.is_puppets)
if (flashmovie === globals.flashmovie && globals.is_puppets)
{
{
-
playercomm.targetStop(flashmovie, "/videoplayer", () => {
+
await playercomm.targetStop(flashmovie, "/videoplayer");
-
// make sure this.guessisplaying.lastframe is updated so that it doesn't
+
-
// go back to state=true
+
-
this.currentFrame((frame) => {
+
-
this.guessisplaying.state = false;
+
-
}, flashmovie);
+
-
if (callback)
+
// make sure this.guessisplaying.lastframe is updated so that it doesn't
-
callback();
+
// go back to state=true
-
});
+
await this.currentFrame(flashmovie);
 +
this.guessisplaying.state = false;
}
}
else
else
{
{
-
playercomm.stop(flashmovie, callback);
+
await playercomm.stop(flashmovie);
}
}
};
};
-
Utils.prototype.play = function play(callback, flashmovie)
+
Utils.prototype.play = async function play(flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback();
 
return;
return;
-
}
 
if (flashmovie === globals.flashmovie && globals.is_puppets)
if (flashmovie === globals.flashmovie && globals.is_puppets)
{
{
-
playercomm.targetPlay(flashmovie, "/videoplayer", callback);
+
await playercomm.targetPlay(flashmovie, "/videoplayer");
this.guessisplaying.state = true;
this.guessisplaying.state = true;
this.guessisplaying.lastframeat = new Date();
this.guessisplaying.lastframeat = new Date();
Line 440: Line 402:
else
else
{
{
-
playercomm.play(flashmovie, callback);
+
await playercomm.play(flashmovie);
}
}
};
};
-
Utils.prototype.goto = function goto(frame, callback, flashmovie)
+
Utils.prototype.goto = async function goto(frame, flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback();
 
return;
return;
-
}
 
if (flashmovie === globals.flashmovie && globals.is_puppets)
if (flashmovie === globals.flashmovie && globals.is_puppets)
{
{
-
playercomm.targetGoto(flashmovie, "/videoplayer", frame, () => {
+
await playercomm.targetGoto(flashmovie, "/videoplayer", frame);
-
// make sure this.guessisplaying.lastframe is updated so that it doesn't
+
-
// go back to state=true
+
-
this.currentFrame((frame) => {
+
-
this.guessisplaying.state = false;
+
-
}, flashmovie);
+
-
if (callback)
+
// make sure this.guessisplaying.lastframe is updated so that it doesn't
-
callback();
+
// go back to state=true
-
});
+
await this.currentFrame(flashmovie);
 +
this.guessisplaying.state = false;
}
}
else
else
{
{
-
playercomm.goto(flashmovie, frame, callback);
+
await playercomm.goto(flashmovie, frame);
}
}
};
};
-
Utils.prototype.zoomOut = function zoomOut(factor, callback, flashmovie)
+
Utils.prototype.zoomOut = async function zoomOut(factor, flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback();
 
return;
return;
-
}
 
-
playercomm.zoom(flashmovie, 100 * factor, callback);
+
await playercomm.zoom(flashmovie, 100 * factor);
};
};
-
Utils.prototype.zoomIn = function zoomIn(factor, callback, flashmovie)
+
Utils.prototype.zoomIn = async function zoomIn(factor, flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback();
 
return;
return;
-
}
 
-
playercomm.zoom(flashmovie, 100 / factor, callback);
+
await playercomm.zoom(flashmovie, 100 / factor);
};
};
-
Utils.prototype.zoomReset = function zoomReset(callback, flashmovie)
+
Utils.prototype.zoomReset = async function zoomReset(factor, flashmovie)
{
{
if (!flashmovie)
if (!flashmovie)
flashmovie = globals.flashmovie;
flashmovie = globals.flashmovie;
if (!flashmovie)
if (!flashmovie)
-
{
 
-
if (callback)
 
-
callback();
 
return;
return;
-
}
 
-
playercomm.zoom(flashmovie, 0, callback);
+
await playercomm.zoom(flashmovie, 0);
};
};
Line 769: Line 711:
this.filename = this.filename.substr(0,i);
this.filename = this.filename.substr(0,i);
}
}
-
Globals.prototype.initModules = function initModules()
+
Globals.prototype.initModules = async function initModules()
{
{
this.modules = {};
this.modules = {};
Line 780: Line 722:
this.modules.subtitles = new Subtitles();
this.modules.subtitles = new Subtitles();
this.modules.updates = new Updates();
this.modules.updates = new Updates();
 +
// Can load the preferences in each module in parallel
 +
var start = new Date();
 +
var loadpromises = []
for (var i in this.modules)
for (var i in this.modules)
-
this.modules[i].init();
+
loadpromises.push(this.modules[i].load());
 +
await Promise.all(loadpromises)
 +
var end = new Date();
 +
console.log(`Loaded prefs in ${end - start}ms`);
 +
// Initialise each module in sequence
 +
for (var i in this.modules)
 +
await this.modules[i].init();
this.modules.settingspane.initComplete();
this.modules.settingspane.initComplete();
};
};
Line 1,037: Line 988:
}
}
-
PlayerComm.prototype.currentFrame = function currentFrame(elem, callback)
+
 +
PlayerComm.prototype.currentFrame = function currentFrame(elem)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_currentFrame",
message: "aio_req_currentFrame",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem)
id: this.getId(elem)
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.targetCurrentFrame = function currentFrame(elem, target, callback)
+
PlayerComm.prototype.targetCurrentFrame = function currentFrame(elem, target)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_targetCurrentFrame",
message: "aio_req_targetCurrentFrame",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem),
id: this.getId(elem),
target: target
target: target
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.totalFrames = function totalFrames(elem, callback)
+
PlayerComm.prototype.totalFrames = function totalFrames(elem)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_totalFrames",
message: "aio_req_totalFrames",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem)
id: this.getId(elem)
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.targetTotalFrames = function targetTotalFrames(elem, target, callback)
+
PlayerComm.prototype.targetTotalFrames = function targetTotalFrames(elem, target)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_targetTotalFrames",
message: "aio_req_targetTotalFrames",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem),
id: this.getId(elem),
target: target
target: target
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.isPlaying = function isPlaying(elem, callback)
+
PlayerComm.prototype.isPlaying = function isPlaying(elem)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_isPlaying",
message: "aio_req_isPlaying",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem)
id: this.getId(elem)
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.targetFramesLoaded = function targetFramesLoaded(elem, target, callback)
+
PlayerComm.prototype.targetFramesLoaded = function targetFramesLoaded(elem, target)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_targetFramesLoaded",
message: "aio_req_targetFramesLoaded",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem),
id: this.getId(elem),
target: target
target: target
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.stop = function stop(elem, callback)
+
PlayerComm.prototype.stop = function stop(elem)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_stop",
message: "aio_req_stop",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem)
id: this.getId(elem)
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.targetStop = function targetStop(elem, target, callback)
+
PlayerComm.prototype.targetStop = function targetStop(elem, target)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_targetStop",
message: "aio_req_targetStop",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem),
id: this.getId(elem),
target: target
target: target
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.play = function play(elem, callback)
+
PlayerComm.prototype.play = function play(elem)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_play",
message: "aio_req_play",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem)
id: this.getId(elem)
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.targetPlay = function targetPlay(elem, target, callback)
+
PlayerComm.prototype.targetPlay = function targetPlay(elem, target)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_targetPlay",
message: "aio_req_targetPlay",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem),
id: this.getId(elem),
target: target
target: target
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.goto = function goto(elem, frame, callback)
+
PlayerComm.prototype.goto = function goto(elem, frame)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_goto",
message: "aio_req_goto",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem),
id: this.getId(elem),
frame: frame
frame: frame
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.targetGoto = function targetGoto(elem, target, frame, callback)
+
PlayerComm.prototype.targetGoto = function targetGoto(elem, target, frame)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_targetGoto",
message: "aio_req_targetGoto",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem),
id: this.getId(elem),
target: target,
target: target,
frame: frame
frame: frame
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.zoom = function zoom(elem, zoom, callback)
+
PlayerComm.prototype.zoom = function zoom(elem, zoom)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_zoom",
message: "aio_req_zoom",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem),
id: this.getId(elem),
zoom: zoom
zoom: zoom
-
}, this.origin)
+
}, this.origin));
}
}
-
PlayerComm.prototype.setScaleMode = function setScaleMode(elem, scaleMode, callback)
+
PlayerComm.prototype.setScaleMode = function setScaleMode(elem, scaleMode)
{
{
-
window.postMessage({
+
return new Promise(resolve => window.postMessage({
message: "aio_req_setScaleMode",
message: "aio_req_setScaleMode",
-
callback: this.storeCallback(callback),
+
callback: this.storeCallback(resolve),
id: this.getId(elem),
id: this.getId(elem),
scaleMode: scaleMode
scaleMode: scaleMode
-
}, this.origin)
+
}, this.origin));
}
}
Line 1,229: Line 1,181:
function SettingsPane()
function SettingsPane()
 +
{
 +
}
 +
SettingsPane.prototype.load = function load()
{
{
}
}
Line 1,393: Line 1,348:
this.settingslist.style.maxHeight = a + 'px';
this.settingslist.style.maxHeight = a + 'px';
};
};
-
SettingsPane.prototype.cacheDodge = function cacheDodge()
+
SettingsPane.prototype.cacheDodge = function cacheDodge(e)
{
{
-
utils.setPref("cachedodge", Math.random().toString());
+
if (e && e.preventDefault)
 +
e.preventDefault();
 +
utils.setPref("cachedodge", Math.random().toString());
 +
globals.modules.updates.cacheDodge();
};
};
Line 1,459: Line 1,417:
function Fullscreen()
function Fullscreen()
{
{
-
this.shouldresize = utils.getPref('resize', true);
 
-
this.noscale = utils.getPref('noscale', false);
 
}
}
-
Fullscreen.prototype.init = function init()
+
Fullscreen.prototype.load = async function load()
 +
{
 +
this.shouldresize = await utils.getPref('resize', true);
 +
this.noscale = await utils.getPref('noscale', false);
 +
}
 +
Fullscreen.prototype.init = async function init()
{
{
this.setting_main = globals.modules.settingspane.addCheckbox('resize', "Resize flash to full-screen", "Resizes the toon so it fills the entire window", this.shouldresize);
this.setting_main = globals.modules.settingspane.addCheckbox('resize', "Resize flash to full-screen", "Resizes the toon so it fills the entire window", this.shouldresize);
Line 1,485: Line 1,446:
this.doResize();
this.doResize();
if (this.noscale)
if (this.noscale)
-
this.setScaleMode("noScale");
+
await this.setScaleMode("noScale");
};
};
Fullscreen.prototype.doResize = function doResize()
Fullscreen.prototype.doResize = function doResize()
Line 1,558: Line 1,519:
globals.modules.seekbar.seekbar.style.width = Math.max(dw, 450) + "px";
globals.modules.seekbar.seekbar.style.width = Math.max(dw, 450) + "px";
};
};
-
Fullscreen.prototype.setScaleMode = function setScaleMode(scaleMode)
+
Fullscreen.prototype.setScaleMode = async function setScaleMode(scaleMode)
{
{
-
utils.whenLoaded(() => {
+
await utils.waitLoaded();
-
playercomm.setScaleMode(globals.flashmovie, scaleMode);
+
await playercomm.setScaleMode(globals.flashmovie, scaleMode);
-
});
+
};
};
Fullscreen.prototype.updateSettings = function updateSettings()
Fullscreen.prototype.updateSettings = function updateSettings()
Line 1,580: Line 1,540:
function Seekbar()
function Seekbar()
{
{
-
this.enabled = utils.getPref('seekbar', true);
 
-
this.framecounter = utils.getPref('frames', false);
 
-
this.zoom = utils.getPref('zoom', false);
 
}
}
-
Seekbar.prototype.init = function init() {
+
Seekbar.prototype.load = async function load() {
 +
this.enabled = await utils.getPref('seekbar', true);
 +
this.framecounter = await utils.getPref('frames', false);
 +
this.zoom = await utils.getPref('zoom', false);
 +
}
 +
Seekbar.prototype.init = async function init() {
this.setting_enabled = globals.modules.settingspane.addCheckbox('seekbar', "Show seek bar", "Lets you fast forward and rewind", this.enabled);
this.setting_enabled = globals.modules.settingspane.addCheckbox('seekbar', "Show seek bar", "Lets you fast forward and rewind", this.enabled);
this.setting_framecounter = globals.modules.settingspane.addCheckbox('framecounter', "Show frame counter on seek bar", "Shows you exactly where you are", this.framecounter, this.setting_enabled);
this.setting_framecounter = globals.modules.settingspane.addCheckbox('framecounter', "Show frame counter on seek bar", "Shows you exactly where you are", this.framecounter, this.setting_enabled);
Line 1,593: Line 1,555:
if (this.enabled)
if (this.enabled)
-
this.addSeekbar();
+
await this.addSeekbar();
this.dragging = false;
this.dragging = false;
-
utils.isPlaying((playing) => {this.paused = !playing;});
+
this.paused = !await utils.isPlaying();
document.addEventListener("mousemove", this.dragMousemove.bind(this), false);
document.addEventListener("mousemove", this.dragMousemove.bind(this), false);
document.addEventListener("mouseup", this.release.bind(this), false);
document.addEventListener("mouseup", this.release.bind(this), false);
Line 1,615: Line 1,577:
this.addSeekbar();
this.addSeekbar();
};
};
-
Seekbar.prototype.addSeekbar = function addSeekbar()
+
Seekbar.prototype.addSeekbar = async function addSeekbar()
{
{
this.dragging = false;
this.dragging = false;
-
utils.isPlaying((playing) => {this.paused = !playing;});
+
this.paused = !await utils.isPlaying();
this.seekbar = document.createElement("div");
this.seekbar = document.createElement("div");
Line 1,752: Line 1,714:
};
};
-
Seekbar.prototype.update = function update()
+
Seekbar.prototype.update = async function update()
{
{
if (!this.seekbar)
if (!this.seekbar)
Line 1,759: Line 1,721:
var fullSliderWidth = parseInt(document.defaultView.getComputedStyle(this.slider, null).width, 10);
var fullSliderWidth = parseInt(document.defaultView.getComputedStyle(this.slider, null).width, 10);
var sliderWidth = fullSliderWidth - parseInt(document.defaultView.getComputedStyle(this.thumb, null).width, 10);
var sliderWidth = fullSliderWidth - parseInt(document.defaultView.getComputedStyle(this.thumb, null).width, 10);
-
utils.totalFrames((tot) => {
+
var tot = await utils.totalFrames();
-
if (tot > 0)
+
if (tot > 0)
 +
{
 +
var frame = await utils.currentFrame();
 +
if (frame < 0)
 +
frame = 0;
 +
if (this.framecountertext)
{
{
-
utils.currentFrame((frame) => {
+
var a = tot.toString();
-
if (frame < 0)
+
var b = (frame+1).toString();
-
frame = 0;
+
while (b.length < a.length)
-
if (this.framecountertext)
+
b = "\u2007" + b; // U+2007 FIGURE SPACE
-
{
+
this.framecountertext.nodeValue = b+"/"+a;
-
var a = tot.toString();
+
-
var b = (frame+1).toString();
+
-
while (b.length < a.length)
+
-
b = "\u2007" + b; // U+2007 FIGURE SPACE
+
-
this.framecountertext.nodeValue = b+"/"+a;
+
-
}
+
-
if(!this.dragging)
+
-
{
+
-
if (tot > 1)
+
-
this.thumb.style.left = (frame/(tot - 1)*sliderWidth)+"px";
+
-
else
+
-
this.thumb.style.left = "0";
+
-
utils.isPlaying((playing) => {
+
-
this.paused = !playing;
+
-
this.pauseButtonImg.src = this.paused ? globals.images.play : globals.images.pause;
+
-
});
+
-
}
+
-
utils.framesLoaded((frame) => {
+
-
this.loadmeter.style.width = (frame/tot*fullSliderWidth)+"px";
+
-
});
+
-
});
+
}
}
-
else if (this.framecountertext)
+
if(!this.dragging)
{
{
-
this.framecountertext.nodeValue = "Loading...";
+
if (tot > 1)
 +
this.thumb.style.left = (frame/(tot - 1)*sliderWidth)+"px";
 +
else
 +
this.thumb.style.left = "0";
 +
this.paused = !await utils.isPlaying();
 +
this.pauseButtonImg.src = this.paused ? globals.images.play : globals.images.pause;
}
}
-
});
+
var loaded = await utils.framesLoaded();
 +
this.loadmeter.style.width = (loaded/tot*fullSliderWidth)+"px";
 +
}
 +
else if (this.framecountertext)
 +
{
 +
this.framecountertext.nodeValue = "Loading...";
 +
}
};
};
-
Seekbar.prototype.pauseUnpause = function pauseUnpause()
+
Seekbar.prototype.pauseUnpause = async function pauseUnpause()
{
{
-
utils.isPlaying((playing) => {
+
this.paused = await utils.isPlaying();
-
this.paused = playing;
+
this.pauseButtonImg.src = this.paused ? globals.images.play : globals.images.pause;
-
this.pauseButtonImg.src = this.paused ? globals.images.play : globals.images.pause;
+
if (this.paused)
-
if (this.paused)
+
await utils.stop();
-
utils.stop();
+
else
-
else
+
await utils.play();
-
utils.play();
+
-
});
+
};
};
-
Seekbar.prototype.rewind = function rewind()
+
Seekbar.prototype.rewind = async function rewind()
{
{
-
utils.goto(0, () => {
+
await utils.goto(0);
-
utils.play();
+
await utils.play();
-
});
+
};
};
-
Seekbar.prototype.fastforward = function fastforward()
+
Seekbar.prototype.fastforward = async function fastforward()
{
{
-
utils.totalFrames((tot) => {
+
var tot = await utils.totalFrames();
-
utils.goto(tot - 1);
+
await utils.goto(tot - 1);
-
})
+
};
};
-
Seekbar.prototype.prevFrame = function prevFrame()
+
Seekbar.prototype.prevFrame = async function prevFrame()
{
{
-
utils.currentFrame((frame) => {
+
var frame = await utils.currentFrame();
-
utils.goto(frame - 1);
+
await utils.goto(frame - 1);
-
})
+
};
};
-
Seekbar.prototype.nextFrame = function nextFrame()
+
Seekbar.prototype.nextFrame = async function nextFrame()
{
{
-
utils.currentFrame((frame) => {
+
var frame = await utils.currentFrame();
-
utils.goto(frame + 1);
+
await utils.goto(frame + 1);
-
})
+
};
};
-
Seekbar.prototype.zoomIn = function zoomIn()
+
Seekbar.prototype.zoomIn = async function zoomIn()
{
{
-
utils.zoomIn(1.5);
+
await utils.zoomIn(1.5);
};
};
-
Seekbar.prototype.zoomOut = function zoomOut()
+
Seekbar.prototype.zoomOut = async function zoomOut()
{
{
-
utils.zoomOut(1.5);
+
await utils.zoomOut(1.5);
};
};
-
Seekbar.prototype.zoomNormal = function zoomNormal()
+
Seekbar.prototype.zoomNormal = async function zoomNormal()
{
{
-
utils.zoomReset();
+
await utils.zoomReset();
};
};
Line 1,851: Line 1,802:
return false;
return false;
};
};
-
Seekbar.prototype.dragMousemove = function dragMousemove(e)
+
Seekbar.prototype.dragMousemove = async function dragMousemove(e)
{
{
if (!this.dragging) return;
if (!this.dragging) return;
Line 1,863: Line 1,814:
if (pos > 1)
if (pos > 1)
pos = 1;
pos = 1;
-
utils.totalFrames((t) => {
+
var t = await utils.totalFrames();
-
if (t > 1)
+
if (t > 1)
-
{
+
{
-
var frame = Math.round(t * pos);
+
var frame = Math.round(t * pos);
-
utils.goto(frame);
+
await utils.goto(frame);
-
}
+
}
-
});
+
this.thumb.style.left = (pos * width) + "px";
this.thumb.style.left = (pos * width) + "px";
};
};
Line 1,882: Line 1,832:
function WikiLink()
function WikiLink()
{
{
-
this.enabled = utils.getPref('hrwiki', true);
+
}
 +
WikiLink.prototype.load = async function load() {
 +
this.enabled = await utils.getPref('hrwiki', true);
}
}
WikiLink.prototype.init = function init()
WikiLink.prototype.init = function init()
Line 2,027: Line 1,979:
function NextPrev()
function NextPrev()
{
{
-
this.enabled = utils.getPref('prevnext', true);
+
}
-
this.docheck = utils.getPref('checknext', true);
+
NextPrev.prototype.load = async function load() {
 +
this.enabled = await utils.getPref('prevnext', true);
 +
this.docheck = await utils.getPref('checknext', true);
}
}
NextPrev.prototype.init = function init()
NextPrev.prototype.init = function init()
Line 2,129: Line 2,083:
this.prevlink.style.display = "block";
this.prevlink.style.display = "block";
if (this.docheck && !this.checkedNext && this.nextlink)
if (this.docheck && !this.checkedNext && this.nextlink)
-
utils.downloadPage(this.nextlink.href + "?cachedodge=" + utils.getPref('cachedodge', 0), this.onCheckLoad.bind(this), this.onCheckError.bind(this), "HEAD");
+
/*no await*/ this.doCheckNext();
else if (this.nextlink)
else if (this.nextlink)
this.nextlink.style.display = "block";
this.nextlink.style.display = "block";
Line 2,141: Line 2,095:
}
}
};
};
-
NextPrev.prototype.onCheckLoad = function onCheckLoad(text, status, statustext, headers)
+
NextPrev.prototype.doCheckNext = async function doCheckNext()
{
{
-
if (status == 200 && headers.indexOf("404error.html") < 0)
+
try {
 +
var res = await utils.downloadPage(this.nextlink.href + "?cachedodge=" + (await utils.getPref('cachedodge', 0)), "HEAD");
 +
} catch (e) {
 +
this.nextlink.parentNode.removeChild(this.nextlink);
 +
this.nextlink = undefined;
 +
return;
 +
}
 +
 +
if (res.status == 200 && res.headers.indexOf("404error.html") < 0)
{
{
this.checkedNext = true;
this.checkedNext = true;
Line 2,156: Line 2,118:
NextPrev.prototype.onCheckError = function onCheckError()
NextPrev.prototype.onCheckError = function onCheckError()
{
{
-
this.nextlink.parentNode.removeChild(this.nextlink);
 
-
this.nextlink = undefined;
 
};
};
function Navbar()
function Navbar()
{
{
-
this.enabled = utils.getPref('navbar', false);
 
-
this.rando = {};
 
-
for (var i in this.SECTIONS)
 
-
this.rando[i] = utils.getPref('rando' + i, true);
 
}
}
Navbar.prototype.SECTIONS = {
Navbar.prototype.SECTIONS = {
Line 2,178: Line 2,134:
};
};
Navbar.prototype.MAIN_COUNT = 26;
Navbar.prototype.MAIN_COUNT = 26;
 +
Navbar.prototype.load = async function load() {
 +
this.enabled = await utils.getPref('navbar', false);
 +
this.rando = {};
 +
for (var i in this.SECTIONS)
 +
this.rando[i] = await utils.getPref('rando' + i, true);
 +
}
Navbar.prototype.init = function init() {
Navbar.prototype.init = function init() {
utils.addGlobalStyle(
utils.addGlobalStyle(
Line 2,254: Line 2,216:
this.newnavbar.style.marginTop = (globals.modules.seekbar.enabled ? "0" : "10px");
this.newnavbar.style.marginTop = (globals.modules.seekbar.enabled ? "0" : "10px");
globals.navbar = this.newnavbar;
globals.navbar = this.newnavbar;
-
this.loadRandoXML();
+
/*no await*/ this.loadRandoXML();
}
}
else
else
Line 2,279: Line 2,241:
document.body.appendChild(newnavbar);
document.body.appendChild(newnavbar);
-
this.mainlink = this.addnavbarlink(newnavbar, "http://www.homestarrunner.com/main" + Math.floor(Math.random() * this.MAIN_COUNT + 1) + ".html", "Main");
+
this.mainlink = this.addnavbarlink(newnavbar, "http://homestarrunner.com/main" + Math.floor(Math.random() * this.MAIN_COUNT + 1) + ".html", "Main");
// just for fun, re-randomise on each mouse-over (for the status bar)
// just for fun, re-randomise on each mouse-over (for the status bar)
this.mainlink.addEventListener("mouseout", this.newMainLink.bind(this), false);
this.mainlink.addEventListener("mouseout", this.newMainLink.bind(this), false);
-
this.addnavbarlink(newnavbar, "http://www.homestarrunner.com/toons.html", "Toons");
+
this.addnavbarlink(newnavbar, "http://homestarrunner.com/toons.html", "Toons");
-
this.addnavbarlink(newnavbar, "http://www.homestarrunner.com/games.html", "Games");
+
this.addnavbarlink(newnavbar, "http://homestarrunner.com/games.html", "Games");
-
this.addnavbarlink(newnavbar, "http://www.homestarrunner.com/characters2.html", "Characters");
+
this.addnavbarlink(newnavbar, "http://homestarrunner.com/characters2.html", "Characters");
-
this.addnavbarlink(newnavbar, "http://www.homestarrunner.com/homester.html", "Downloads");
+
this.addnavbarlink(newnavbar, "http://homestarrunner.com/homester.html", "Downloads");
-
this.addnavbarlink(newnavbar, "http://www.homestarrunner.com/store.html", "Store", "storelink");
+
this.addnavbarlink(newnavbar, "http://homestarrunner.com/store.html", "Store", "storelink");
-
this.addnavbarlink(newnavbar, "http://www.homestarrunner.com/sbemail.html", "SB Emails");
+
this.addnavbarlink(newnavbar, "http://homestarrunner.com/sbemail.html", "SB Emails");
//this.addnavbarlink(newnavbar, "http://feeds.feedburner.com/HomestarRunner", "Subscribe");
//this.addnavbarlink(newnavbar, "http://feeds.feedburner.com/HomestarRunner", "Subscribe");
this.addnavbarlink(newnavbar, "https://www.youtube.com/user/homestarrunnerdotcom", "YouTube");
this.addnavbarlink(newnavbar, "https://www.youtube.com/user/homestarrunnerdotcom", "YouTube");
-
this.addnavbarlink(newnavbar, "http://www.homestarrunner.com/email.html", "Contact");
+
this.addnavbarlink(newnavbar, "http://homestarrunner.com/email.html", "Contact");
//this.addnavbarlink(newnavbar, "http://podstar.homestarrunner.com/", "Podcast");
//this.addnavbarlink(newnavbar, "http://podstar.homestarrunner.com/", "Podcast");
-
this.addnavbarlink(newnavbar, "http://www.homestarrunner.com/legal.html", "Legal");
+
this.addnavbarlink(newnavbar, "http://homestarrunner.com/legal.html", "Legal");
this.randolink = this.addnavbarlink(newnavbar, "javascript:void(alert('rando.xml not loaded yet... be patient'))", "Rando");
this.randolink = this.addnavbarlink(newnavbar, "javascript:void(alert('rando.xml not loaded yet... be patient'))", "Rando");
this.randolink.addEventListener("mouseout", this.newRandoLink.bind(this), false);
this.randolink.addEventListener("mouseout", this.newRandoLink.bind(this), false);
Line 2,313: Line 2,275:
Navbar.prototype.newMainLink = function newMainLink()
Navbar.prototype.newMainLink = function newMainLink()
{
{
-
this.mainlink.href="http://www.homestarrunner.com/main" + Math.floor(Math.random() * this.MAIN_COUNT + 1) + ".html";
+
this.mainlink.href="http://homestarrunner.com/main" + Math.floor(Math.random() * this.MAIN_COUNT + 1) + ".html";
};
};
Navbar.prototype.newRandoLink = function newRandoLink()
Navbar.prototype.newRandoLink = function newRandoLink()
Line 2,333: Line 2,295:
};
};
-
Navbar.prototype.loadRandoXML = function loadRandoXML()
+
Navbar.prototype.loadRandoXML = async function loadRandoXML()
{
{
// Only run this once
// Only run this once
Line 2,340: Line 2,302:
this.haveLoadedXML = true;
this.haveLoadedXML = true;
-
utils.downloadPage(
+
try {
-
"http://www.homestarrunner.com/rando.xml?cachedodge=" + utils.getPref('cachedodge', 0),
+
var res = await utils.downloadPage(
-
this.randoXMLLoaded.bind(this),
+
"http://homestarrunner.com/rando.xml?cachedodge=" + (await utils.getPref('cachedodge', 0))
-
this.randoXMLError.bind(this)
+
);
-
);
+
-
};
+
var parser = new DOMParser();
-
Navbar.prototype.randoXMLLoaded = function randoXMLLoaded(responseText)
+
// fix invalid XML...
-
{
+
// add missing root element
-
var parser = new DOMParser();
+
var doc = res.text.replace(/<\?xml.*?\?>/g, ""); // strip <?xml ?> tag
-
// fix invalid XML...
+
doc = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n<rando>" + doc + "</rando>";
-
// add missing root element
+
// fix bad ampersands
-
var doc = responseText.replace(/<\?xml.*?\?>/g, ""); // strip <?xml ?> tag
+
doc = doc.replace(/&(?!\w*;)/g, "&amp;");
-
doc = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n<rando>" + doc + "</rando>";
+
doc = parser.parseFromString(doc, "application/xml");
-
// fix bad ampersands
+
var sbemailcounter = 0;
-
doc = doc.replace(/&(?!\w*;)/g, "&amp;");
+
this.allrandourls = [];
-
doc = parser.parseFromString(doc, "application/xml");
+
for (var i = 0; i < doc.documentElement.childNodes.length; i++)
-
var sbemailcounter = 0;
+
-
this.allrandourls = [];
+
-
for (var i = 0; i < doc.documentElement.childNodes.length; i++)
+
-
{
+
-
var node = doc.documentElement.childNodes[i];
+
-
if (node.nodeType == 1)
+
{
{
-
var type = node.nodeName.toLowerCase();
+
var node = doc.documentElement.childNodes[i];
-
var u = node.getAttribute('u');
+
if (node.nodeType == 1)
-
var n = node.getAttribute('n');
+
-
if (!n) n = "Untitled";
+
-
if (type == "sb")
+
{
{
-
sbemailcounter++;
+
var type = node.nodeName.toLowerCase();
-
n = "SBEmail: " + n;
+
var u = node.getAttribute('u');
 +
var n = node.getAttribute('n');
 +
if (!n) n = "Untitled";
 +
if (type == "sb")
 +
{
 +
sbemailcounter++;
 +
n = "SBEmail: " + n;
 +
}
 +
if (u)
 +
this.allrandourls.push({u: "http://homestarrunner.com/" + u, n: n, type: type});
 +
else
 +
this.allrandourls.push({u: "http://homestarrunner.com/sbemail" + sbemailcounter + ".html", n: n, type: type});
}
}
-
if (u)
 
-
this.allrandourls.push({u: "http://www.homestarrunner.com/" + u, n: n, type: type});
 
-
else
 
-
this.allrandourls.push({u: "http://www.homestarrunner.com/sbemail" + sbemailcounter + ".html", n: n, type: type});
 
}
}
 +
this.filterRando();
 +
} catch (e) {
 +
this.randolink.href = "javascript:void(alert('Error loading rando.xml... try refreshing'))";
}
}
-
this.filterRando();
 
-
};
 
-
Navbar.prototype.randoXMLError = function randoXMLError()
 
-
{
 
-
this.randolink.href = "javascript:void(alert('Error loading rando.xml... try refreshing'))";
 
};
};
Navbar.prototype.filterRando = function filterRando()
Navbar.prototype.filterRando = function filterRando()
Line 2,401: Line 2,359:
function Subtitles()
function Subtitles()
{
{
-
this.enabled = utils.getPref('subtitles', false);
 
-
this.captions = utils.getPref('captions', true);
 
-
this.colours = utils.getPref('colours', true);
 
-
this.testsubs = utils.getPref('testsubs', false);
 
-
this.language = utils.getPref('language', "en");
 
-
this.testsubsdata = unescape(utils.getPref('testsubsdata', this.DEFAULTXML));
 
-
this.names = utils.getPref('names', 0);
 
}
}
Subtitles.prototype.DEFAULTXML = escape('<?xml version="1.0" encoding="utf-8"?>\n<transcript xml:lang="en-us">\n<line start="" end="" speaker=""></line>\n</transcript>');
Subtitles.prototype.DEFAULTXML = escape('<?xml version="1.0" encoding="utf-8"?>\n<transcript xml:lang="en-us">\n<line start="" end="" speaker=""></line>\n</transcript>');
Subtitles.prototype.NAMES_OPTS = ["Never", "Voiceovers", "Always"];
Subtitles.prototype.NAMES_OPTS = ["Never", "Voiceovers", "Always"];
Subtitles.prototype.NO_SUBTITLES = document.createComment("");
Subtitles.prototype.NO_SUBTITLES = document.createComment("");
 +
Subtitles.prototype.load = async function load() {
 +
this.enabled = await utils.getPref('subtitles', false);
 +
this.captions = await utils.getPref('captions', true);
 +
this.colours = await utils.getPref('colours', true);
 +
this.testsubs = await utils.getPref('testsubs', false);
 +
this.language = await utils.getPref('language', "en");
 +
this.testsubsdata = unescape(await utils.getPref('testsubsdata', this.DEFAULTXML));
 +
this.names = await utils.getPref('names', 0);
 +
}
Subtitles.prototype.init = function init()
Subtitles.prototype.init = function init()
{
{
Line 2,459: Line 2,419:
this.language_populated = false;
this.language_populated = false;
-
this.populateLanguage();
+
/*no await*/ this.populateLanguage();
this.setting_captions = globals.modules.settingspane.addCheckbox('captions', "Show captions", "Include sound effects in the subtitles", this.captions, this.setting_enabled);
this.setting_captions = globals.modules.settingspane.addCheckbox('captions', "Show captions", "Include sound effects in the subtitles", this.captions, this.setting_enabled);
Line 2,500: Line 2,460:
this.subsready = false;
this.subsready = false;
-
this.setupSubtitles();
+
/*no await*/ this.setupSubtitles();
window.setInterval(this.update.bind(this), 50);
window.setInterval(this.update.bind(this), 50);
Line 2,524: Line 2,484:
utils.setPref('testsubsdata', escape(this.testsubsdata));
utils.setPref('testsubsdata', escape(this.testsubsdata));
-
this.setupSubtitles();
+
/*no await*/ this.setupSubtitles();
};
};
-
Subtitles.prototype.populateLanguage = function populateLanguage()
+
Subtitles.prototype.populateLanguage = async function populateLanguage()
{
{
var option = document.createElement('option');
var option = document.createElement('option');
Line 2,533: Line 2,493:
option.selected = true;
option.selected = true;
this.setting_language.appendChild(option);
this.setting_language.appendChild(option);
-
utils.downloadWikiXML("Subtitles:Languages", this.languageListDownloaded.bind(this), this.languageListError.bind(this));
+
-
};
+
try {
-
Subtitles.prototype.languageListDownloaded = function languageListDownloaded(xml)
+
var xml = await utils.downloadWiki("Subtitles:Languages");
-
{
+
xml = utils.parseWikiXML(xml);
 +
} catch (e) {
 +
while (this.setting_language.firstChild)
 +
this.setting_language.removeChild(this.setting_language.firstChild);
 +
var option = document.createElement('option');
 +
option.appendChild(document.createTextNode("Error loading languages"));
 +
option.selected = true;
 +
this.setting_language.appendChild(option);
 +
return;
 +
}
 +
while (this.setting_language.firstChild)
while (this.setting_language.firstChild)
this.setting_language.removeChild(this.setting_language.firstChild);
this.setting_language.removeChild(this.setting_language.firstChild);
Line 2,561: Line 2,531:
this.setting_language.disabled = false;
this.setting_language.disabled = false;
this.language_populated = true;
this.language_populated = true;
-
};
 
-
Subtitles.prototype.languageListError = function languageListError()
 
-
{
 
-
while (this.setting_language.firstChild)
 
-
this.setting_language.removeChild(this.setting_language.firstChild);
 
-
var option = document.createElement('option');
 
-
option.appendChild(document.createTextNode("Error loading languages"));
 
-
option.selected = true;
 
-
this.setting_language.appendChild(option);
 
};
};
Line 2,619: Line 2,580:
if (!this.errorsholder)
if (!this.errorsholder)
this.createErrorsHolder();
this.createErrorsHolder();
-
var div = document.createElement("div");
+
var pre = document.createElement("pre");
-
div.appendChild(document.createTextNode(message));
+
pre.appendChild(document.createTextNode(message));
-
this.errorsholder.appendChild(div);
+
this.errorsholder.appendChild(pre);
globals.modules.fullscreen.doResize();
globals.modules.fullscreen.doResize();
};
};
-
Subtitles.prototype.setupSubtitles = function setupSubtitles()
+
Subtitles.prototype.setupSubtitles = async function setupSubtitles()
{
{
this.removeSubtitles();
this.removeSubtitles();
Line 2,636: Line 2,597:
this.setSubtitles(document.createTextNode("Loading subtitles..."));
this.setSubtitles(document.createTextNode("Loading subtitles..."));
-
if (!this.charsready)
+
try {
-
utils.downloadWikiXML('Subtitles:Characters', this.charactersLoaded.bind(this), this.downloadSubsError.bind(this));
+
await this.loadCharacters();
-
else
+
await this.reloadSubs();
-
this.reloadSubs();
+
} catch (e) {
 +
this.removeSubtitles();
 +
if (this.testsubs)
 +
this.transcriptError(e.toString());
 +
}
};
};
-
Subtitles.prototype.charactersLoaded = function charactersLoaded(xml)
+
Subtitles.prototype.loadCharacters = async function loadCharacters() {
-
{
+
if (this.charsready)
-
var speakers = xml.getElementsByTagName("speaker");
+
return;
 +
 +
var xml = await utils.downloadWiki('Subtitles:Characters');
 +
xml = utils.parseWikiXML(xml);
 +
this.characters = {
this.characters = {
sfx: {
sfx: {
Line 2,651: Line 2,620:
}
}
};
};
 +
var speakers = xml.getElementsByTagName("speaker");
for (var i = 0; i < speakers.length; i++)
for (var i = 0; i < speakers.length; i++)
{
{
Line 2,664: Line 2,634:
}
}
this.charsready = true;
this.charsready = true;
-
this.reloadSubs();
+
}
-
};
+
Subtitles.prototype.reloadSubs = async function reloadSubs()
-
Subtitles.prototype.downloadSubsError = function downloadSubsError(status, statusText)
+
-
{
+
-
this.removeSubtitles();
+
-
if (this.testsubs)
+
-
this.transcriptError(statusText);
+
-
};
+
-
Subtitles.prototype.reloadSubs = function reloadSubs()
+
{
{
if (!this.charsready)
if (!this.charsready)
Line 2,682: Line 2,645:
this.setSubtitles(document.createTextNode("Loading subtitles..."));
this.setSubtitles(document.createTextNode("Loading subtitles..."));
 +
var xml;
if (!this.testsubs)
if (!this.testsubs)
-
utils.downloadWikiXML('Subtitles:' + globals.filename + '/' + this.language, this.transcriptLoaded.bind(this), this.downloadSubsError.bind(this));
+
xml = await utils.downloadWiki('Subtitles:' + globals.filename + '/' + this.language);
else
else
-
utils.wikiXMLDownloaded(this.transcriptLoaded.bind(this), this.downloadSubsError.bind(this), this.testsubsdata, 200, "OK");
+
xml = this.testsubsdata;
 +
xml = utils.parseWikiXML(xml);
 +
this.parseTranscript(xml);
 +
 +
this.subsready = true;
};
};
-
Subtitles.prototype.transcriptLoaded = function transcriptLoaded(xml)
+
Subtitles.prototype.parseTranscript = function parseTranscript(xml)
{
{
// set some defaults
// set some defaults
Line 2,723: Line 2,691:
this.transcript.push(line);
this.transcript.push(line);
}
}
-
this.subsready = true;
 
};
};
Subtitles.prototype.inheritLanguages = function inheritLanguages(node)
Subtitles.prototype.inheritLanguages = function inheritLanguages(node)
Line 2,880: Line 2,847:
};
};
-
Subtitles.prototype.update = function update()
+
Subtitles.prototype.update = async function update()
{
{
if (!this.enabled || !this.charsready || !this.subsready || !this.subtitleholder)
if (!this.enabled || !this.charsready || !this.subsready || !this.subtitleholder)
return;
return;
-
utils.currentFrame((frame) => {
+
var frame = await utils.currentFrame();
-
if (frame < 0)
+
if (frame < 0)
-
return;
+
return;
-
frame++; // Make 1-based
+
frame++; // Make 1-based
-
// binary search to find the right transcript line
+
// binary search to find the right transcript line
-
var first = 0;
+
var first = 0;
-
var last = this.transcript.length;
+
var last = this.transcript.length;
-
while(first < (last - 1))
+
while(first < (last - 1))
 +
{
 +
var mid = (first + last) >> 1;
 +
if (frame >= this.transcript[mid].start)
{
{
-
var mid = (first + last) >> 1;
+
first = mid;
-
if (frame >= this.transcript[mid].start)
+
if (frame <= this.transcript[mid].end)
-
{
+
break;
-
first = mid;
+
-
if (frame <= this.transcript[mid].end)
+
-
break;
+
-
}
+
-
else
+
-
last = mid;
+
}
}
-
// should we actually show the line?
 
-
if(this.transcript[first] && this.transcript[first].start <= frame && this.transcript[first].end >= frame)
 
-
this.setSubtitles(this.transcript[first].text);
 
else
else
-
this.setSubtitles(false);
+
last = mid;
-
});
+
}
 +
// should we actually show the line?
 +
if(this.transcript[first] && this.transcript[first].start <= frame && this.transcript[first].end >= frame)
 +
this.setSubtitles(this.transcript[first].text);
 +
else
 +
this.setSubtitles(false);
};
};
Line 2,926: Line 2,892:
// Returned by Special:Getversion
// Returned by Special:Getversion
-
// <versionstring>4.2.77=http://www.hrwiki.org/w/index.php?title=User:Phlip/Greasemonkey&action=raw&ctype=text/javascript&fakeextension=.user.js</versionstring>
+
// <versionstring>4.3.97=http://www.hrwiki.org/w/index.php?title=User:Phlip/Greasemonkey&action=raw&ctype=text/javascript&fakeextension=.user.js</versionstring>
function Updates()
function Updates()
{
{
-
this.enabled = utils.getPref('updates', true);
 
}
}
-
Updates.CURRENT_VERSION = [4, 2, 77];
+
Updates.CURRENT_VERSION = [4, 3, 97];
 +
Updates.CHECK_INTERVAL = 24*60*60*1000; // once per day
 +
Updates.prototype.load = async function load() {
 +
this.enabled = await utils.getPref('updates', true);
 +
}
Updates.prototype.init = function init()
Updates.prototype.init = function init()
{
{
Line 2,945: Line 2,914:
this.setting_enabled = globals.modules.settingspane.addCheckbox('updates', "Check for updates", "Regularly check for updates to the All-in-one script", this.enabled);
this.setting_enabled = globals.modules.settingspane.addCheckbox('updates', "Check for updates", "Regularly check for updates to the All-in-one script", this.enabled);
-
this.havechecked = false;
+
/*no await*/ this.doCheck();
-
this.doCheck();
+
};
};
Updates.prototype.updateSettings = function updateSettings()
Updates.prototype.updateSettings = function updateSettings()
Line 2,955: Line 2,923:
};
};
-
Updates.prototype.doCheck = function doCheck()
+
Updates.prototype.doCheck = async function doCheck()
{
{
-
if (this.havechecked || !this.enabled)
+
if (this.updatelink) {
 +
this.updatelink.parentNode.removeChild(this.updatelink);
 +
this.updatelink = null;
 +
}
 +
 +
if (!this.enabled)
return;
return;
-
this.havechecked = true;
 
-
if (Date.now() - utils.getPref("lastchecktime", 0) > 86400000)
+
var str;
 +
if (Date.now() - (await utils.getPref("lastchecktime", 0)) > Updates.CHECK_INTERVAL)
{
{
-
utils.downloadPage("http://www.hrwiki.org/wiki/Special:Getversion/User:Phlip/Greasemonkey?cachedodge=" + Math.random(), this.onLoad.bind(this));
+
str = await utils.downloadPage("http://www.hrwiki.org/wiki/Special:Getversion/User:Phlip/Greasemonkey?cachedodge=" + Math.random());
 +
str = str.text;
 +
utils.setPref("lastchecktime", Date.now());
 +
utils.setPref("lastcheckstring", str);
}
}
else
else
-
this.handleUpdateString(utils.getPref("lastcheckstring", ""));
+
str = await utils.getPref("lastcheckstring", "");
-
};
+
-
Updates.prototype.onLoad = function onLoad(textcontent)
+
-
{
+
-
utils.setPref("lastchecktime", Date.now());
+
-
utils.setPref("lastcheckstring", textcontent);
+
-
this.handleUpdateString(textcontent);
+
-
};
+
-
Updates.prototype.handleUpdateString = function handleUpdateString(str)
+
-
{
+
var parts = str.split("@@");
var parts = str.split("@@");
for (var i = 0; i < parts.length; i++)
for (var i = 0; i < parts.length; i++)
Line 3,002: Line 2,970:
updatelink.appendChild(updatelinkimage);
updatelink.appendChild(updatelinkimage);
document.body.appendChild(updatelink);
document.body.appendChild(updatelink);
 +
this.updatelink = updatelink;
return;
return;
}
}
}
}
};
};
 +
 +
Updates.prototype.cacheDodge = function cacheDodge()
 +
{
 +
utils.setPref("lastchecktime", 0);
 +
/*no await*/ this.doCheck();
 +
}
// Podstar/Videlectrix (stock IIS), HRWiki and stock Apache error pages, respectively. Don't do anything on those pages.
// Podstar/Videlectrix (stock IIS), HRWiki and stock Apache error pages, respectively. Don't do anything on those pages.
Line 3,015: Line 2,990:
var playercomm = new PlayerComm();
var playercomm = new PlayerComm();
playercomm.init();
playercomm.init();
-
globals.initModules();
+
await globals.initModules();
})();
})();
/*</pre>*/
/*</pre>*/

Revision as of 12:18, 24 November 2017

/*

Contents

Installation instructions

Firefox

If you don't have it already, you'll need to install Greasemonkey, then restart Firefox and return to this page.

Then, just click on this link to install the script.

To upgrade a new version when it's updated, just click the install link again – it'll automagically replace the old version. If the option is enabled, the script will automatically check for updates for you.

Chrome

This script can be installed as an extension from the Chrome Web Store. Chrome will then automatically keep it up-to-date for you via the normal update process.

Script code

*/

// Homestar All-In-One
// version 4.3
// 2017-11-24
// Copyright (c) Phillip Bradbury, Loafing
//
// --------------------------------------------------------------------
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// --------------------------------------------------------------------
//
// ==UserScript==
// @name          Homestar All-In-One
// @namespace     http://www.hrwiki.org/
// @description   Combination of many Homestar Runner scripts. Version 4.3.
// @version       4.3.97
// @downloadURL   http://www.hrwiki.org/w/index.php?title=User:Phlip/Greasemonkey&action=raw&ctype=text/javascriptfakeextension=.user.js
// @icon          http://www.hrwiki.org/w/images/thumb/1/1b/logo.png/32px-logo.png
// @match         http://homestarrunner.com/*
// @match         http://www.homestarrunner.com/*
// @match         http://podstar.homestarrunner.com/*
// @match         http://videlectrix.com/*
// @match         http://www.videlectrix.com/*
// @match         http://hrwiki.org/mirror/*
// @match         http://www.hrwiki.org/mirror/*
// @match         https://secure.homestarrunner.com/heythanks.html*
// @grant         GM.getValue
// @grant         GM.setValue
// @grant         GM.xmlHttpRequest
// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_xmlhttpRequest
// ==/UserScript==

(async function(){
	function Utils()
	{
		this.guessisplaying = {
			lastframe: -1,
			lastframeat: new Date(),
			state: true
		};
	}
	
	// Taken from http://diveintogreasemonkey.org/patterns/add-css.html
	Utils.prototype.addGlobalStyle = function addGlobalStyle(css)
	{
		var head, style;
		head = document.getElementsByTagName('head')[0];
		if (!head) return;
		style = document.createElement('style');
		style.type = 'text/css';
		style.appendChild(document.createTextNode(css));
		head.appendChild(style);
	};
	
	// Based on http://userscripts.org/topics/41177
	Utils.prototype.useGMFunctions = async function useGMFunctions()
	{
		// We can't just test if GM_getValue exists, because in Chrome they do exist
		// but they don't actually do anything, just report failure to console.log
	
		// Have to do it like this instead of like "if(window.GM_getValue)"
		// because apparently this function isn't actually on "window", and I don't
		// know where it actually lives...
		if (typeof(GM) == "object" && GM.getValue && await GM.getValue("this-value-doesn't-exist-I-promise", true))
			return 2; // Use GM4 methods
		else if (typeof(GM_getValue) == "function" && GM_getValue("this-value-doesn't-exist-I-promise", true))
			return 1; // Use GM3 methods
		else
			return 0; // Use native methods
	
		return gmstorage;
	};
	// Only really need to do this once...
	Utils.prototype.useGMFunctions = await Utils.prototype.useGMFunctions();
	Utils.prototype.getPref = async function getPref(key, def)
	{
		if (this.useGMFunctions == 2)
			return await GM.getValue(key, def);
		else if (this.useGMFunctions == 1)
			return GM_getValue(key, def);
		else if (window.localStorage)
		{
			var value = localStorage.getItem("hr-allinone-" + key);
			if (value === null)
				return def;
			var type = value[0];
			value = value.substring(1);
			if (type == 'b')
				return Number(value) != 0;
			else if (type == 'n')
				return Number(value);
			else
				return value;
		}
		else
		{
			alert("Homestar Runner All-in-one is not supported on this platform");
			throw "Couldn't find a local storage provider";
		}
	};
	Utils.prototype.setPref = function setPref(key, value)
	{
		if (this.useGMFunctions == 2)
			GM.setValue(key, value);
		else if (this.useGMFunctions == 1)
			GM_setValue(key, value);
		else if (window.localStorage)
		{
			if (typeof(value) == "string")
				localStorage.setItem("hr-allinone-" + key, "s" + value);
			else if (typeof(value) == "number")
				localStorage.setItem("hr-allinone-" + key, "n" + value);
			else if (typeof(value) == "boolean")
				localStorage.setItem("hr-allinone-" + key, "b" + (value ? 1 : 0));
			else
				throw "Unexpected type for storage: " + typeof(value);
		}
		else
		{
			alert("Homestar Runner All-in-one is not supported on this platform");
			throw "Couldn't find a local storage provider";
		}
	};
	
	Utils.prototype.downloadPage = function downloadPage(url, method)
	{
		if (!method)
			method = 'GET';
		return new Promise((resolve, reject) => {
			if (typeof(GM) == "object" && GM.xmlHttpRequest) {
				GM.xmlHttpRequest({
					method: method,
					url: url,
					onload: res => resolve({text: res.responseText, status: res.status, statusText: res.statusText, headers: res.responseHeaders}),
					onerror: res => reject(`${res.status} ${res.statusText}`)
				});
			} else if (typeof(GM_xmlhttpRequest) == "function") {
				GM_xmlhttpRequest({
					method: method,
					url: url,
					onload: res => resolve({text: res.responseText, status: res.status, statusText: res.statusText, headers: res.responseHeaders}),
					onerror: res => reject(`${res.status} ${res.statusText}`)
				});
			} else {
				var xhr = new XMLHttpRequest();
				xhr.onload = () => resolve({text: xhr.responseText, status: xhr.status, statusText: xhr.statusText, headers: xhr.getAllResponseHeaders()});
				xhr.onerror = () => reject(`${xhr.status} ${xhr.statusText}`);
				xhr.open(method, url);
				xhr.send();
			}
		});
	};
	Utils.prototype.buildWikiUrl = function buildWikiUrl(page)
	{
		var url = escape(page.replace(/ /g, '_'));
		return "http://www.hrwiki.org/w/index.php?title=" + url + "&action=raw&source=allinone&cachedodge=" + this.getPref('cachedodge', 0);
	};
	Utils.prototype.downloadWiki = async function downloadWiki(page)
	{
		for (var timesredirected = 0; timesredirected < 3; timesredirected++) {
			var res = await this.downloadPage(this.buildWikiUrl(page));
	
			// check for redirects
			var matches = res.text.match(/^\s*#\s*REDIRECT\s*\[\[(.*)\]\]/i);
			if (matches)
			{
				// Get the page name out of the redirect text
				var text = matches[1];
				if ((matches = text.match(/^(.*)\|/)))
					text = matches[1];
				if ((matches = text.match(/^(.*)\#/)))
					text = matches[1];
				page = text.replace(/^\s+|\s+$/g, '');
			}
			else
				return res.text;
		}
		throw "Too many redirects";
	};
	Utils.prototype.parseWikiXML = function parseWikiXML(text)
	{
		// strip various things - templates and <pre> tags for wiki formatting, and <noinclude> sections...
		// <includeonly> tags are stripped (but their contents kept) for consistency.
		text = text.replace(/{{.*?}}/g, "");
		text = text.replace(/<\/?pre[^>]*>/g, "");
		text = text.replace(/<noinclude[^>]*>.*?<\/noinclude[^>]*>/g, "");
		text = text.replace(/<includeonly[^>]*>(.*?)<\/includeonly[^>]*>/g, "$1");
		text = text.replace(/^\s+/g, "");
	
		var parser = new DOMParser();
		try
		{
			var doc = parser.parseFromString(text, "application/xml");
		}
		catch (e)
		{
			throw "Error in XML:\n" + e.toString();
		}
		// check if returned document is an error message
		if (doc.getElementsByTagName('parsererror').length > 0)
		{
			var error = doc.getElementsByTagName('parsererror')[0];
			if (error.firstChild.nodeType == doc.TEXT_NODE && error.lastChild.nodeType == doc.ELEMENT_NODE && error.lastChild.nodeName == "sourcetext")
			{
				// Firefox's errors look like this:
				// <parsererror>Error details<sourcetext>Source text</sourcetext></parsererror>
				throw (
					error.firstChild.nodeValue.replace(/Location: .*\n/, "") + "\n" +
					doc.documentElement.lastChild.textContent
				);
			}
			else if (error.getElementsByTagName('div').length > 0)
			{
				// Chrome's errors look like this:
				// <someRoot><parsererror style="..."><h3>Generic error message</h3><div style="...">Error details</div><h3>Generic footer</h3><attempted parsing of page/></someRoot>
				throw (
					"Error in XML:\n" +
					error.getElementsByTagName('div')[0].textContent
				);
			}
			else
			{
				// Try to at least return something
				throw (
					"Error in XML:\n" +
					error.textContent
				);
			}
		}
		return doc;
	};
	
	Utils.prototype.currentFrame = async function currentFrame(flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		if (flashmovie === globals.flashmovie && globals.is_puppets)
		{
			var a = await playercomm.targetCurrentFrame(flashmovie, "/videoplayer");
	
			// Keep track of whether the current frame is changing, for isPlaying()
			// If we stay on the same frame for more than, say, a second, guess
			// that we're paused.
			if (a != this.guessisplaying.lastframe)
			{
				this.guessisplaying.lastframe = a;
				this.guessisplaying.lastframeat = new Date();
				this.guessisplaying.state = true;
			}
			else if (new Date() - this.guessisplaying.lastframeat > 1000)
			{
				this.guessisplaying.state = false;
			}
	
			return a;
		}
		else
		{
			return await playercomm.currentFrame(flashmovie)
		}
	};
	Utils.prototype.totalFrames = async function totalFrames(flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		var a;
		if (flashmovie === globals.flashmovie && globals.is_puppets)
			return await playercomm.targetTotalFrames(flashmovie, "/videoplayer")
		else
			return await playercomm.totalFrames(flashmovie)
	};
	Utils.prototype.isPlaying = async function isPlaying(flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		if (flashmovie === globals.flashmovie && globals.is_puppets)
		{
			// There isn't a telltarget version of IsPlaying, there's no flag for it in
			// TGetProperty, and it doesn't seem to be gettable via GetVariable (though
			// it's possible I just haven't tried the right thing)...
			// So, for puppet toons, we need to try to track whether it seems to be playing...
			return this.guessisplaying.state;
		}
		else
		{
			return await playercomm.isPlaying(flashmovie);
		}
	};
	Utils.prototype.framesLoaded = async function framesLoaded(flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		if (flashmovie === globals.flashmovie && globals.is_puppets)
			return await playercomm.targetFramesLoaded(flashmovie, '/videoplayer')
		else
			return await playercomm.targetFramesLoaded(flashmovie, '/')
	};
	Utils.prototype.isLoaded = async function isLoaded(flashmovie)
	{
		var frame = await this.currentFrame(flashmovie);
		return frame >= 0;
	};
	Utils.prototype.waitLoaded = function waitLoaded(flashmovie)
	{
		var useglobal = false;
		if (!flashmovie) {
			useglobal = true;
			flashmovie = globals.flashmovie;
		}
		if (!flashmovie)
			return new Promise((resolve, reject) => reject());
	
		if (useglobal && this.loadedPromise)
			return this.loadedPromise;
	
		async function poll(resolve) {
			if (await this.isLoaded(flashmovie))
				resolve();
			else
				setTimeout(poll.bind(this, resolve), 100)
		}
		var promise = new Promise(poll.bind(this));
		if (useglobal)
			this.loadedPromise = promise;
		return promise;
	}
	Utils.prototype.stop = async function stop(flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		if (flashmovie === globals.flashmovie && globals.is_puppets)
		{
			await playercomm.targetStop(flashmovie, "/videoplayer");
	
			// make sure this.guessisplaying.lastframe is updated so that it doesn't
			// go back to state=true
			await this.currentFrame(flashmovie);
			this.guessisplaying.state = false;
		}
		else
		{
			await playercomm.stop(flashmovie);
		}
	};
	Utils.prototype.play = async function play(flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		if (flashmovie === globals.flashmovie && globals.is_puppets)
		{
			await playercomm.targetPlay(flashmovie, "/videoplayer");
			this.guessisplaying.state = true;
			this.guessisplaying.lastframeat = new Date();
		}
		else
		{
			await playercomm.play(flashmovie);
		}
	};
	Utils.prototype.goto = async function goto(frame, flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		if (flashmovie === globals.flashmovie && globals.is_puppets)
		{
			await playercomm.targetGoto(flashmovie, "/videoplayer", frame);
	
			// make sure this.guessisplaying.lastframe is updated so that it doesn't
			// go back to state=true
			await this.currentFrame(flashmovie);
			this.guessisplaying.state = false;
		}
		else
		{
			await playercomm.goto(flashmovie, frame);
		}
	};
	Utils.prototype.zoomOut = async function zoomOut(factor, flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		await playercomm.zoom(flashmovie, 100 * factor);
	};
	Utils.prototype.zoomIn = async function zoomIn(factor, flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		await playercomm.zoom(flashmovie, 100 / factor);
	};
	Utils.prototype.zoomReset = async function zoomReset(factor, flashmovie)
	{
		if (!flashmovie)
			flashmovie = globals.flashmovie;
		if (!flashmovie)
			return;
	
		await playercomm.zoom(flashmovie, 0);
	};
	
	Utils.prototype.insertAfter = function insertAfter(newElement, referenceElement)
	{
		if(referenceElement.nextSibling)
			referenceElement.parentNode.insertBefore(newElement, referenceElement.nextSibling);
		else
			referenceElement.parentNode.appendChild(newElement);
	};

	function Globals()
	{
		this.whichsite = 0;
		if (location.hostname.indexOf("podstar") >= 0) this.whichsite = 1;
		if (location.hostname.indexOf("videlectrix") >= 0) this.whichsite = 2;
		if (location.pathname.indexOf("/mirror/") >= 0) this.whichsite = 3;
	
		// icons, as Base64-encoded PNG files.
		this.images = {
			close:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAAm' +
				'JLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfWBRkTNhxuPxLkAAAAHX' +
				'RFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBUaGUgR0lNUO9kJW4AAAEKSURBVCjPhdGxSgNBFA' +
				'XQMzpgYWwsLEQUDBJBQgqFIChZEPR7/DA/QCGQTgQtJE1ENoWohYUgbGKQyFjErNv52nObe1' +
				'9wqGWg7z0l5YVgVdOu+wUt507tqIVQ4Zodp861ooELe15M5KFI6Zfr9u25MIj6Jl4cmSIPBW' +
				'rq2o5cufO4aOJDYSozNTa2pK4t03PtwUdMKRRykAmW0dTRcyNXpBQpI8GJDTR050zkNzK0bM' +
				'MZLvUNZ8yCfy6Wvbc1NVyi4dloXjqWvds6uvp41pFmpVOKJWd6bgwxkmTMIotWKpwrfBkZl7' +
				'uMonUHf5wSlV2+fUZrjnXdzrmyy7djD8GWTW9e51z557o1Tz85FH/WkOkaHQAAAABJRU5Erk' +
				'Jggg==',
			ffwd:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABl' +
				'BMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAA' +
				'sTAQCanBgAAAAHdElNRQfeCgQNLh+v5c+DAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aC' +
				'BHSU1QV4EOFwAAAC9JREFUCNcVisENAEAIwjo6ozmKI/j0YfS4hAeUIhFBJlV0M8Mudz8uno' +
				'a+LFiTHqCuHAU1qtJ6AAAAAElFTkSuQmCC',
			hrwiki:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGX' +
				'RFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAm1QTFRF////2wAzgZDJAiGNAB' +
				'6Lenp6ABCEABKFAAyDjp3O8gAAipjLlaPPFUixAB6OAA6C/f//fY3JABaIhJXK///50gAn//' +
				'/4CymXyQAaAA+DOFCm1QAmDiuX//zvnV2IfI3IQ0h7ABSFN0+qZXm9ABSG9PTxABiK2wAkuQ' +
				'AdSWW5WGu4cILCgYy209PZGRdjABeH0AALDiyYASGOhJTL2bi8k5OTzgAj1QAdLkilAAiDAB' +
				'iQIiCBzwAbyAAk//31ABSO0gAXDB95c5nZDAxeoRhHOVCp7u3lfx1W1LrCxQYtwwApQVitwA' +
				'QpJj2bAAyFKSODfI3GYna86urqysfL9fT0NUyXMDGGNk6cxgASy9rkAB2OQ1qzTmOzu8Pa4d' +
				'/b+v//58zRFEqw09XR25yrIjyh9P//g5PLAAN+foy/uRY92treh6neAASDXXC9jJvKjJvL6e' +
				'npiJfKDzejNk6r2wE1N0+rABKEAB6KxAAn0tPWyQAZRFuvXXfB/f392AAgKiib2QAyABaJhJ' +
				'TH2XeNEy+ZzgAwBiSRKUOlgI27urrP7t/iCghS0AAfk3SoyBc+iChf3vH1VWq426GvgI/Hiz' +
				'duboDCPEOXABCDSmu/DyeD///6P2K4OUJ/HByRlKHOAB+O8AAA2QI1hZPHg5TI9PT0ABuJiZ' +
				'jM1tbdf43CzgApAB2We4vD7e3rwgAseInHAyGWi5rOUme3hIuqFTGaWG25dojDd5LQ5Ki1AA' +
				'yMASCNcYHEAyKOABqLACSWHDeR+vr6uwAiIyBjipnJ1AIyjZvMmJyaITylAByMAB2L5wAlHD' +
				'eeCCaUcHCjWGy4wBQy/7AMAgAAARFJREFUeNpiONFeuLWjfL4RM1/R4tXyx5kZdrNHZ8za5D' +
				'jT3n/KimUtLDsZhPbFJilY14d5cC3o1dRnaWPILaucozf3DAND3DmnxqWnmRjyd+046NzJwG' +
				'B6dMJ6xVNHJjPIrGvq1mVgYGBlZQg9xjlxD4MKh2+wKgMILLET00mPYmCqFpctYIAAEamz3A' +
				'zaG4TdgmohAllpgsoMbBw5y9fshwiEW0qyM7jGbIlMjWcQCOlKMKnR8rZgyDOe3e95oErCJq' +
				'LOPDGQ8xBDCv8qF9tWRkb1SStPTvNTU2JgK83OrDjMaKbB0Gwgt23zdIap83h9vBZKJ4MMdZ' +
				'/Bs5EhwHBvz9qSBoftDAx9olbFiwACDABkK1N43Z86KwAAAABJRU5ErkJggg==',
			next:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABl' +
				'BMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAA' +
				'sTAQCanBgAAAAHdElNRQfeCgQNLSOrp+DHAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aC' +
				'BHSU1QV4EOFwAAACtJREFUCNdjULBhMLBhsLBhsLFhsLNhsAeiPQz2f8BoD4hrB5ayACtTsA' +
				'EA6J8JvyvoxNYAAAAASUVORK5CYII=',
			pause:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABl' +
				'BMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAA' +
				'sTAQCanBgAAAAHdElNRQfeCgQNLS1MH83AAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aC' +
				'BHSU1QV4EOFwAAAA5JREFUCNdjsLFhIAUBALQwB4FBHjsqAAAAAElFTkSuQmCC',
			play:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABl' +
				'BMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAA' +
				'sTAQCanBgAAAAHdElNRQfeCgQNLjLqOpP2AAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aC' +
				'BHSU1QV4EOFwAAACdJREFUCNcdirEJAAAMg/z/qpzUAwJpG3ARRTBgyCEyxCTFVX1yN7Ejqh' +
				'alykITkQAAAABJRU5ErkJggg==',
			prefs:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAll' +
				'BMVEUAGQASEhIfHx8fJy8pKSk2NjZBQUFJR0ZQUE9RUVFSUlJNX3NoaGhsaWdramlycG1meY' +
				'98fHx+fn5wgpV0iqKKh4R4jaR9jJx8kad9kad/mbONmaWEnrmEnrqkoZy3t7fIx8bKyMHT0c' +
				'3S0dDU09DV1NPP1t3W1dXY2Njb2tfe29bf3tzj4uHr6+js6+r39/f5+PgAAABrL3yvAAAAAX' +
				'RSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfWBR' +
				'oFKh31UQ8DAAAAgUlEQVQY022OxxLCMAwFRSc4BEIPJZQQ08v+/8+RsTExDDpIe3ijfSJ/hx' +
				'9g62Dt4GaAI+8YT0t27+BxxvvE/no5pYT10lGFrE34Ja40W3g1oMGmW7YZ6hnCYexKTPVkXi' +
				'vuvWe1Cz1aKqPNI3N0slI2TNYZiARJX30qERc7wBPKC4WRDzWdWHfmAAAAAElFTkSuQmCC',
			prev:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABl' +
				'BMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAA' +
				'sTAQCanBgAAAAHdElNRQfeCgQNLgFV6vLgAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aC' +
				'BHSU1QV4EOFwAAACxJREFUCNdjsGFhsOFhsJFhsLFhsKlhsPnDYPuHwR6MgAwgFyRoA1YAVM' +
				'YCABGLC3k4wQ8QAAAAAElFTkSuQmCC',
			rewind:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABl' +
				'BMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAA' +
				'sTAQCanBgAAAAHdElNRQfeCgQNLhgxgVogAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aC' +
				'BHSU1QV4EOFwAAAC9JREFUCNdjYGRkYGZmYGdn4OdnkJdnsLdnqK9n+P8fhIAMIBcoCJQCKg' +
				'AqY2QEALxwB9ke+WHMAAAAAElFTkSuQmCC',
			stop:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1' +
				'BMVEUAAACnej3aAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9' +
				'4KBA0uOX3oSn4AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAADElEQV' +
				'QI12NgIA0AAAAwAAHHqoWOAAAAAElFTkSuQmCC',
			update:
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAABGCAMAAABG8BK2AAAC8V' +
				'BMVEUAAAD/AAD+AQH/AQH/AgL+AwP/AwP+BAT/BAT/BQX+Bgb/Bgb/Bwf+CAj/CAj/CQn/Cg' +
				'r+Cwv/Cwv+DAz/DAz/DQ3/Dg7+Dw//Dw//EBD+ERH/ERH/EhL/ExP+FBT/FRX/Fhb/Fxf+GB' +
				'j/GBj/GRn/Ghr/Gxv/HBz/HR3/Hh7/Hx//ICD+ISH/ISH/IiL/IyP/JCT/JSX/Jib/Jyf/KS' +
				'n/Kyv/LCz/LS3/Ly//MDD/MTH+MjL/MjL/MzP/NDT/NTX/Njb+Nzf/Nzf/ODj+OTn/OTn/Oj' +
				'r/PDz/Pj7/Pz//QUH/QkL+Q0P/RUX/Rkb/R0f/SEj/SUn/Skr/S0v/TEz/TU3/Tk7/T0//UF' +
				'D/UVH/UlL/VFT/VVX/Vlb/WFj/WVn/Wlr/W1v/XFz/XV3/Xl7/X1//YGD/YWH/YmL/Y2P/ZW' +
				'X/Zmb/Z2f/aGj/aWn/amr/a2v/bGz/bW3/bm7/b2//cHD/cXH/cnL/dHT/dnb/d3f/eHj/eX' +
				'n/e3v/fX3/fn7/f3//gID/gYH/goL/g4P/hIT/hob/h4f/iIj/iYn/ior/i4v/jIz/jY3/jo' +
				'7+kJD/kJD/kZH/kpL/lJT/lpb/l5f/mJj/mZn/mpr/m5v/nJz/nZ3/n5//oKD/oaH/oqL/o6' +
				'P/pqb/p6f/qKj/qan/qqr/q6v/rKz/ra3/r6//sLD/sbH/srL/s7P/tLT/tbX/trb/t7f/uL' +
				'j/urr/u7v/vLz/vb3/vr7/v7//wMD/wcH/wsL/w8P/xMT/xcX/xsb+x8f/x8f/yMj/ycn/ys' +
				'r/y8v/zMz/zc3/zs7/z8//0ND/0dH/0tL/09P+1NT/1NT/1tb/19f+2Nj/2Nj/2dn/29v/3N' +
				'z/3d3/39//4OD/4eH/4uL/4+P/5OT/5eX/5ub/5+f/6Oj/6en/6ur/6+v/7Oz/7e3/7u7/7+' +
				'/+8PD/8fH/8vL/8/P/9PT/9fX/9vb/9/f/+Pj/+fn/+vr/+/v//Pz//f3+/v7//v7////+AA' +
				'A5GkRyAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAADzoAAA+IAUHKF/gAAA' +
				'AHdElNRQfXCRYICgxGxxkcAAAEL0lEQVRYw63Xe1wURRwA8Pm1G0KcHdGBkKAYjxC0yLJITU' +
				'l7cr7RUjAos4AuraCH2pWCVlZaRpD5AEXDwAemQRFdmgQeCgWUPKTk4JJHomAq5PBXu/fC2z' +
				't2Z7fdf+Y38/nc9zPz+83M7iEQ9VBDjCNxStKGG5xJSBSjWPV+c3m0nxNFDEP/XBf3ZkPLuv' +
				'GOigiG2oLrhyvVJX26abdzFXKGWtrUPRXA5aasRjyD5ijkzJjd/2aMNkXqhCiKoxAzU9bg3n' +
				'mDdXe1V4iZJIzTBnvhH9xrpxAzKbj1cYDY2Ww8AMuOL7NTiBg6koZX2rruhFhjLJsVP5iv8b' +
				'FTSBj6xxo/CHqxXftwYxFTKwhY/aj9iogYOgfrRwCM/vr0qXOmpUQ0pXAVYYZa19tuymc8xq' +
				'vY1u0nnOXCUQQZ6vnf/p5jiibpqgOYxqcctwRwFUEmqrD/1VvMYWppjGrUE7/ghkAHRYhxy8' +
				'QdG6x79u2DBbru/mLHuQgyr+H9HYatCkvv2U3Hdmv9nSgCzKyW/MnBpW1HvSz9gRHsMUAiGe' +
				'/1OA5A9XlX/TQv7pkmZtzB/Y1UNvBMP2NIDOVTeJjpT49lJNOjXHHq/Mb7eRQe5pnavAm2W3' +
				'jRt33Fjw2t8C3qG3z8AWvsOnFba6YbNZTCw9yYYsg2qkfabqpZPkPOhXc2ET2bk3FpAvDXSJ' +
				'BxbSsZ29O1fz2BwrtvVlzSNb60vX5ruEJI4WVUxxoTISSp46hWJaA4MtSw2dlVRXlq5jy6H6' +
				'5hRzw+XasSUBwYOu2rC4YO/bmWM0EesPRQsGnsZiGFy9AlVbmRzG9dQrMr1NSEE1OEs+uEoX' +
				'bivUGW+EBrIGh3KYkUDuP7bu3JPZ7mOKSsgFr4ggeRwmE87/FfW9Pqbb74vqgOg3Ay5XqGmp' +
				'Re9+U7vsvL/0oybZRE9rIhU65j6Az9tZL0ffn3jdtyadNdzEAaTiZVBhn6O9y+YBxAUw64fn' +
				'R+hxoUVXg5qWJjqBzcFsbutYrDwwBWHvr9rUrc5E+q2JjQExceYduHruQqBgAe3NhvLBhDrN' +
				'iYyD79agXzTtXg98xs9CIvcsXGRPzQc7F68R23NlxZQtk+pZEohnoyBuDuqI9P99Y244rhJP' +
				'eLMyZQ90exJgyUU/dgfPEpKYp5UeHak83fT2Tf0pXX8hMlKMj6Znu57HIMcwjmZmCcI15BVI' +
				'CvWfLK7ExmKnzbPH3fJ6IV9NzZLG/LKo4Y49kmOHKUaAVB2T8h1pzGGMeLrrSVmX71iPUzaO' +
				'afMyRk15Lios4EixONl0hU2ErldW82O5rOORIVU8ELDZ8xDq2sPRsmUTHvm8LuyvjFr/+Kc3' +
				'0kKpbtt6OuC+OefSOlKrYTHqf5MNVPsoLs/2QjGZj/oSB5FCSPguRRkDwKkkdB8ihIHgXJoy' +
				'B5FCSPguRRkDzKf7Z6NUd33kmjAAAAAElFTkSuQmCC'
		};
	
		// find flash objects
		var objs;
		switch (this.whichsite)
		{
			case 0: // www.homestarrunner.com
				objs = document.getElementsByTagName("EMBED");
				if (objs && objs.length >= 2)
				{
					this.flashmovie = objs[0];
					this.navbar = objs[1];
				}
				else if (objs && objs.length >= 1)
				{
					this.flashmovie = objs[0];
					this.navbar = false;
				}
				else
				{
					this.flashmovie = false;
					this.navbar = false;
				}
				if (!this.flashmovie)
				{
					objs = document.getElementsByTagName("OBJECT");
					if (objs && objs.length >= 1)
						this.flashmovie = objs[0];
				}
				break;
			case 1: // podstar.homestarrunner.com
				objs = document.getElementsByTagName("EMBED");
				this.flashmovie = false;
				if (objs && objs.length >= 1)
					this.navbar = objs[0];
				else
					this.navbar = false;
				break;
			case 2: // videlectrix
				objs = document.getElementsByTagName("EMBED");
				this.navbar = false;
				if (objs && objs.length >= 1)
					this.flashmovie = objs[0];
				else
					this.flashmovie = false;
				/*settings.navbar = false;*/
				break;
			case 3: // mirror
				objs = document.getElementsByTagName("EMBED");
				this.flashmovie = false;
				if (objs && objs.length >= 1)
					this.flashmovie = objs[0];
				if (!this.flashmovie)
				{
					objs = document.getElementsByTagName("OBJECT");
					if (objs && objs.length >= 1)
						this.flashmovie = objs[0];
				}
				this.navbar = document.getElementById('navbar');
				/*if (!this.navbar)
					settings.navbar = false;*/
				var flashcontainer = document.getElementById('flash');
				if (flashcontainer)
					flashcontainer.style.width = "auto";
				break;
		}
		if (this.flashmovie)
		{
			//expose Flash plugin-added methods
			if (this.flashmovie.wrappedJSObject)
				this.flashmovie = this.flashmovie.wrappedJSObject;
			
			// confirm that this is really a flash file
			// and not (for example) the embedded background sound on SB's website
			var src = this.flashmovie.getAttribute('src');
			if (this.flashmovie.nodeName.toLowerCase() == "object")
			{
				if (src)
				{
					if (src.substring(src.length - 4).toLowerCase() != ".swf")
						this.flashmovie = false;
				}
				else
				{
					var a = this.flashmovie.getElementsByTagName('param').namedItem("movie");
					if (!a || a.value.substring(a.value.length - 4).toLowerCase() != ".swf")
						this.flashmovie = false;
					else
						src = a.value;
				}
			}
			else if (this.flashmovie.nodeName.toLowerCase() == "embed")
			{
				if (!src || src.substring(src.length - 4).toLowerCase() != ".swf")
					this.flashmovie = false;
			}
	
			// puppet_background.swf is a wrapper around the puppet stuff popup toons
			// This flag tells things like seekbar to control the wrapped movie clip
			if (src)
				this.is_puppets = src == "puppet_background.swf" || src.substring(src.length - 22) == "/puppet_background.swf";
		}
		// Don't run large flash objects inline (gets rid of some extra padding from
		// having the movie sitting on the baseline)
		if (this.flashmovie)
		{
			this.flashmovie.style.display = "block";
			this.flashmovie.style.margin = "0 auto";
		}
		if (this.navbar)
		{
			this.navbar.style.display = "block";
			this.navbar.style.margin = "0 auto";
		}
	
		this.filename = window.location.pathname.toLowerCase();
		var i = this.filename.lastIndexOf('/');
		if (i >= 0)
			this.filename = this.filename.substr(i + 1);
		i = this.filename.lastIndexOf('.');
		if (i >= 0)
			this.filename = this.filename.substr(0,i);
	}
	Globals.prototype.initModules = async function initModules()
	{
		this.modules = {};
		this.modules.settingspane = new SettingsPane();
		this.modules.fullscreen = new Fullscreen();
		this.modules.seekbar = new Seekbar();
		this.modules.wikilink = new WikiLink();
		this.modules.nextprev = new NextPrev();
		this.modules.navbar = new Navbar();
		this.modules.subtitles = new Subtitles();
		this.modules.updates = new Updates();
		// Can load the preferences in each module in parallel
		var start = new Date();
		var loadpromises = []
		for (var i in this.modules)
			loadpromises.push(this.modules[i].load());
		await Promise.all(loadpromises)
		var end = new Date();
		console.log(`Loaded prefs in ${end - start}ms`);
		// Initialise each module in sequence
		for (var i in this.modules)
			await this.modules[i].init();
		this.modules.settingspane.initComplete();
	};

	function PlayerComm()
	{
	}
	PlayerComm.handlers = {};
	PlayerComm.prototype.init = function init()
	{
		var script = document.createElement("script");
		script.appendChild(document.createTextNode("(" + this.inPageContext + ")();"));
		document.body.appendChild(script);
	
		this.origin = document.location.protocol + "//" + document.location.hostname;
		var defaultport = '';
		if (document.location.protocol == 'http:')
			defaultport = '80';
		else if (document.location.protocol == 'https:')
			defaultport = '443';
		if (document.location.port && document.location.port != defaultport)
			this.origin += ":" + document.location.port;
	
		this.callbacks = [];
		this.id_count = 0;
	
		window.addEventListener("message", this.receiveMessage.bind(this), false);
	}
	PlayerComm.prototype.inPageContext = function inPageContext()
	{
		// This code is run in the page context (which in Chrome is the only one
		// allowed to communicate with the Flash object) to communicate with the script
	
		var origin = document.location.protocol + "//" + document.location.hostname;
		var defaultport = '';
		if (document.location.protocol == 'http:')
			defaultport = '80';
		else if (document.location.protocol == 'https:')
			defaultport = '443';
		if (document.location.port && document.location.port != defaultport)
			origin += ":" + document.location.port;
	
		var handlers = {}
	
		function receiveMessage(event)
		{
			if (event.origin !== origin)
				return;
			if (event.source !== window)
				return;
			if (event.data.message.substring(0, 8) !== 'aio_req_')
				return;
	
			var message = event.data.message.substring(8);
			handlers[message](event.data);
		}
		window.addEventListener("message", receiveMessage, false);
	
		// Documentation for the Flash interface is really lacking...
		// Adobe removed the docs from their website.
		// Luckily, the Wayback Machine captures all
		// http://web.archive.org/web/20100710000820/http://www.adobe.com/support/flash/publishexport/scriptingwithflash/scriptingwithflash_03.html
		// http://web.archive.org/web/20090210205955/http://www.adobe.com/support/flash/publishexport/scriptingwithflash/scriptingwithflash_04.html
	
		handlers.currentFrame = function currentFrame(data)
		{
			var elem = document.getElementById(data.id);
			var a = elem.CurrentFrame;
			if (typeof(a) == 'function')
				a = elem.CurrentFrame();
			if (typeof(a) !== 'number' || a < 0)
				a = -1;
	
			window.postMessage({
				message: "aio_resp_paramCallback",
				callback: data.callback,
				val: a
			}, origin);
		}
	
		handlers.targetCurrentFrame = function targetCurrentFrame(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.TCurrentFrame) == 'function')
				a = elem.TCurrentFrame(data.target);
			else
				a = -1;
	
			window.postMessage({
				message: "aio_resp_paramCallback",
				callback: data.callback,
				val: a
			}, origin);
		}
	
		handlers.totalFrames = function totalFrames(data)
		{
			var elem = document.getElementById(data.id);
			var a = elem.TotalFrames;
			if (typeof(a) == 'function')
				a = elem.TotalFrames();
			if (typeof(a) !== 'number' || a < 0)
				a = -1;
	
			window.postMessage({
				message: "aio_resp_paramCallback",
				callback: data.callback,
				val: a
			}, origin);
		}
	
		handlers.targetTotalFrames = function targetTotalFrames(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.TGetPropertyAsNumber) == 'function')
				a = elem.TGetPropertyAsNumber(data.target, 5);  // TOTAL_FRAMES
			else
				a = -1;
	
			window.postMessage({
				message: "aio_resp_paramCallback",
				callback: data.callback,
				val: a
			}, origin);
		}
	
		handlers.isPlaying = function isPlaying(data)
		{
			var elem = document.getElementById(data.id);
			var a = elem.IsPlaying;
			if (typeof(a) == 'function')
				a = elem.IsPlaying();
			if (typeof(a) == 'number')
				a = (a != 0);
			else if (typeof(a) != 'boolean')
				a = false;
	
			window.postMessage({
				message: "aio_resp_paramCallback",
				callback: data.callback,
				val: a
			}, origin);
		}
	
		handlers.targetFramesLoaded = function targetFramesLoaded(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.TGetPropertyAsNumber) == 'function')
				a = elem.TGetPropertyAsNumber(data.target, 12);  // FRAMES_LOADED
			else
				a = -1;
	
			window.postMessage({
				message: "aio_resp_paramCallback",
				callback: data.callback,
				val: a
			}, origin);
		}
	
		handlers.stop = function stop(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.StopPlay) == 'function')
				elem.StopPlay();
	
			window.postMessage({
				message: "aio_resp_basicCallback",
				callback: data.callback
			}, origin);
		}
	
		handlers.targetStop = function targetStop(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.TStopPlay) == 'function')
				elem.TStopPlay(data.target);
	
			window.postMessage({
				message: "aio_resp_basicCallback",
				callback: data.callback
			}, origin);
		}
	
		handlers.play = function play(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.Play) == 'function')
				elem.Play();
	
			window.postMessage({
				message: "aio_resp_basicCallback",
				callback: data.callback
			}, origin);
		}
	
		handlers.targetPlay = function targetPlay(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.TPlay) == 'function')
				elem.TPlay(data.target);
	
			window.postMessage({
				message: "aio_resp_basicCallback",
				callback: data.callback
			}, origin);
		}
	
		handlers.goto = function goto(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.GotoFrame) == 'function')
				elem.GotoFrame(data.frame);
	
			window.postMessage({
				message: "aio_resp_basicCallback",
				callback: data.callback
			}, origin);
		}
	
		handlers.targetGoto = function targetGoto(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.TGotoFrame) == 'function')
				elem.TGotoFrame(data.target, data.frame);
	
			window.postMessage({
				message: "aio_resp_basicCallback",
				callback: data.callback
			}, origin);
		}
	
		handlers.zoom = function zoom(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.Zoom) == 'function')
				elem.Zoom(data.zoom);
	
			window.postMessage({
				message: "aio_resp_basicCallback",
				callback: data.callback
			}, origin);
		}
	
		handlers.setScaleMode = function setScaleMode(data)
		{
			var elem = document.getElementById(data.id);
			if (typeof(elem.SetVariable) == 'function')
				elem.SetVariable("Stage.scaleMode", data.scaleMode);
	
			window.postMessage({
				message: "aio_resp_basicCallback",
				callback: data.callback
			}, origin);
		}
	}
	
	
	PlayerComm.prototype.currentFrame = function currentFrame(elem)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_currentFrame",
			callback: this.storeCallback(resolve),
			id: this.getId(elem)
		}, this.origin));
	}
	
	PlayerComm.prototype.targetCurrentFrame = function currentFrame(elem, target)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_targetCurrentFrame",
			callback: this.storeCallback(resolve),
			id: this.getId(elem),
			target: target
		}, this.origin));
	}
	
	PlayerComm.prototype.totalFrames = function totalFrames(elem)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_totalFrames",
			callback: this.storeCallback(resolve),
			id: this.getId(elem)
		}, this.origin));
	}
	
	PlayerComm.prototype.targetTotalFrames = function targetTotalFrames(elem, target)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_targetTotalFrames",
			callback: this.storeCallback(resolve),
			id: this.getId(elem),
			target: target
		}, this.origin));
	}
	
	PlayerComm.prototype.isPlaying = function isPlaying(elem)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_isPlaying",
			callback: this.storeCallback(resolve),
			id: this.getId(elem)
		}, this.origin));
	}
	
	PlayerComm.prototype.targetFramesLoaded = function targetFramesLoaded(elem, target)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_targetFramesLoaded",
			callback: this.storeCallback(resolve),
			id: this.getId(elem),
			target: target
		}, this.origin));
	}
	
	PlayerComm.prototype.stop = function stop(elem)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_stop",
			callback: this.storeCallback(resolve),
			id: this.getId(elem)
		}, this.origin));
	}
	
	PlayerComm.prototype.targetStop = function targetStop(elem, target)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_targetStop",
			callback: this.storeCallback(resolve),
			id: this.getId(elem),
			target: target
		}, this.origin));
	}
	
	PlayerComm.prototype.play = function play(elem)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_play",
			callback: this.storeCallback(resolve),
			id: this.getId(elem)
		}, this.origin));
	}
	
	PlayerComm.prototype.targetPlay = function targetPlay(elem, target)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_targetPlay",
			callback: this.storeCallback(resolve),
			id: this.getId(elem),
			target: target
		}, this.origin));
	}
	
	PlayerComm.prototype.goto = function goto(elem, frame)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_goto",
			callback: this.storeCallback(resolve),
			id: this.getId(elem),
			frame: frame
		}, this.origin));
	}
	
	PlayerComm.prototype.targetGoto = function targetGoto(elem, target, frame)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_targetGoto",
			callback: this.storeCallback(resolve),
			id: this.getId(elem),
			target: target,
			frame: frame
		}, this.origin));
	}
	
	PlayerComm.prototype.zoom = function zoom(elem, zoom)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_zoom",
			callback: this.storeCallback(resolve),
			id: this.getId(elem),
			zoom: zoom
		}, this.origin));
	}
	
	PlayerComm.prototype.setScaleMode = function setScaleMode(elem, scaleMode)
	{
		return new Promise(resolve => window.postMessage({
			message: "aio_req_setScaleMode",
			callback: this.storeCallback(resolve),
			id: this.getId(elem),
			scaleMode: scaleMode
		}, this.origin));
	}
	
	PlayerComm.prototype.receiveMessage = function receiveMessage(event)
	{
		if (event.origin !== this.origin)
			return;
		if (event.source !== window)
			return;
		if (event.data.message.substring(0, 9) !== 'aio_resp_')
			return;
	
		var message = event.data.message.substring(9);
		PlayerComm.handlers[message].call(this, event.data);
	}
	
	PlayerComm.handlers.basicCallback = function basicCallback(data)
	{
		var callback = this.getCallback(data.callback);
		if (callback)
			callback();
	}
	
	PlayerComm.handlers.paramCallback = function paramCallback(data)
	{
		var callback = this.getCallback(data.callback);
		if (callback)
			callback(data.val);
	}
	
	PlayerComm.prototype.storeCallback = function storeCallback(callback)
	{
		if (!callback)
			return -1;
		var ix = 0;
		while (this.callbacks[ix] !== undefined)
			ix++;
		this.callbacks[ix] = callback;
		return ix;
	}
	PlayerComm.prototype.getCallback = function getCallback(ix)
	{
		if (ix < 0)
			return undefined;
		var callback = this.callbacks[ix];
		this.callbacks[ix] = undefined;
		return callback;
	}
	PlayerComm.prototype.getId = function getId(elem)
	{
		if (!elem.id)
		{
			this.id_count++;
			elem.id = "aio_id_" + this.id_count;
		}
		return elem.id;
	}

	function SettingsPane()
	{
	}
	SettingsPane.prototype.load = function load()
	{
	}
	SettingsPane.prototype.init = function init()
	{
		utils.addGlobalStyle(
			'#settingsbox, #settingslink\n' +
			'{\n' +
			'\tborder-right: 1px solid #666;\n' +
			'\tborder-bottom: 1px solid #666;\n' +
			'\tbackground: #EEE;\n' +
			'\tcolor: #000;\n' +
			'\tposition: fixed;\n' +
			'\toverflow: auto;\n' +
			'\tleft: 0;\n' +
			'\ttop: 0;\n' +
			'\tfont: 12px sans-serif;\n' +
			'\ttext-align: left;\n' +
			'\tz-index: 2;\n' +
			'}\n' +
			'#settingsbox\n' +
			'{\n' +
			'\twidth: 350px;\n' +
			'}\n' +
			'#settingstitlebar\n' +
			'{\n' +
			'\tfont-weight: bolder;\n' +
			'\tbackground: #CCC;\n' +
			'\tborder-bottom: 1px solid #666;\n' +
			'\tpadding: 3px;\n' +
			'}\n' +
			'#settingstitlebar img\n' +
			'{\n' +
			'\tvertical-align: text-bottom;\n' +
			'}\n' +
			'#settingstitlebar .prefsicon\n' +
			'{\n' +
			'\tfloat: left;\n' +
			'\tmargin-right: 0.5em;\n' +
			'}\n' +
			'#settingstitlebar .buttonimage, #settingslink .buttonimage\n' +
			'{\n' +
			'\tcursor: pointer;\n' +
			'\tdisplay: block;\n' +
			'}\n' +
			'#settingstitlebar .buttonimage\n' +
			'{\n' +
			'\tfloat: right;\n' +
			'}\n' +
			'#settingsbox form\n' +
			'{\n' +
			'\tmargin: 0;\n' +
			'\tpadding: 3px;\n' +
			'}\n' +
			'#settingsbox ul, #settingsbox li\n' +
			'{\n' +
			'\tlist-style: none;\n' +
			'\tmargin: 0;\n' +
			'\tpadding: 0;\n' +
			'}\n' +
			'#settingsbox ul ul\n' +
			'{\n' +
			'\tmargin-left: 2em;\n' +
			'}\n' +
			'#settingsbox input[type="checkbox"]\n' +
			'{\n' +
			'\tmargin-right: 0.25em;\n' +
			'}\n' +
			'#settingsbuttons\n' +
			'{\n' +
			'\ttext-align: center;\n' +
			'}\n' +
			'#settingslink\n' +
			'{\n' +
			'\tpadding: 3px;\n' +
			'}\n' +
			""
		);
		
		var settingsbox = document.createElement('div');
		this.settingsbox = settingsbox;
		settingsbox.id = 'settingsbox';
		settingsbox.style.display = 'none';
		document.body.appendChild(settingsbox);
		var titlebar = document.createElement('div');
		titlebar.id = 'settingstitlebar';
		settingsbox.appendChild(titlebar);
		var closebutton = document.createElement('img');
		closebutton.src = globals.images.close;
		closebutton.title = "Click to hide preferences";
		closebutton.className = 'buttonimage';
		closebutton.addEventListener('click', this.hidePane.bind(this), false);
		titlebar.appendChild(closebutton);
		var prefslogo = document.createElement('img');
		prefslogo.src = globals.images.prefs;
		prefslogo.className = 'prefsicon';
		titlebar.appendChild(prefslogo);
		titlebar.appendChild(document.createTextNode("Preferences"));
		var settingsform = document.createElement('form');
		settingsbox.appendChild(settingsform);
		var settingslist = document.createElement('ul');
		this.settingslist = settingslist;
		var a = window.innerHeight - 75;
		if (a < 40) a = 40;
		settingslist.style.maxHeight = a + 'px';
		settingslist.style.overflow = 'auto'; // vertical scrollbar if needed
		window.addEventListener('resize', this.resizeWindow.bind(this), true);
		settingsform.appendChild(settingslist);
	
		var div = document.createElement('div');
		div.id = 'settingsbuttons';
		settingsform.appendChild(div);
		var savebutton = document.createElement('input');
		savebutton.type = "submit";
		savebutton.value = "Save and Apply";
		div.appendChild(savebutton);
		var nocachebutton = document.createElement('input');
		nocachebutton.type = "submit";
		nocachebutton.value = "Clear subtitles cache";
		nocachebutton.addEventListener("click", this.cacheDodge.bind(this), false);
		div.appendChild(document.createTextNode(" "));
		div.appendChild(nocachebutton);
		settingsform.addEventListener("submit", this.saveSettings.bind(this), false);
		
		var settingslink = document.createElement('div');
		this.settingslink = settingslink;
		settingslink.id = 'settingslink';
		var settingslinkimage = document.createElement('img');
		settingslinkimage.src = globals.images.prefs;
		settingslinkimage.title = "Click to show preferences";
		settingslinkimage.className = 'prefsicon buttonimage';
		settingslinkimage.addEventListener('click', this.showPane.bind(this), false);
		settingslink.appendChild(settingslinkimage);
		document.body.appendChild(settingslink);
		
		this.hidePanels = [];
	};
	SettingsPane.prototype.saveSettings = function saveSettings(e)
	{
		// stop the form from actually being submitted
		if (e && e.preventDefault)
			e.preventDefault();
		
		for (var i in globals.modules)
			globals.modules[i].updateSettings();
		
		return false;
	};
	SettingsPane.prototype.updateSettings = function updateSettings(){};
	SettingsPane.prototype.showPane = function showPane()
	{
		this.settingsbox.style.display = "block";
		this.settingslink.style.display = "none";
	};
	SettingsPane.prototype.hidePane = function hidePane()
	{
		this.settingsbox.style.display = "none";
		this.settingslink.style.display = "block";
	};
	SettingsPane.prototype.resizeWindow = function resizeWindow()
	{
		var a = window.innerHeight - 75;
		if (a < 40) a = 40;
		this.settingslist.style.maxHeight = a + 'px';
	};
	SettingsPane.prototype.cacheDodge = function cacheDodge(e)
	{
		if (e && e.preventDefault)
			e.preventDefault();
		utils.setPref("cachedodge", Math.random().toString());
		globals.modules.updates.cacheDodge();
	};
	
	SettingsPane.prototype.addSettingRow = function addSettingRow(parent)
	{
		if (!parent)
			parent = this.settingslist;
		else
		{
			var checkbox = undefined;
			if (parent.tagName.toLowerCase() == "input")
			{
				checkbox = parent;
				parent = parent.parentNode;
			}
			var ul = parent.getElementsByTagName("ul");
			if (ul.length)
				parent = ul[ul.length - 1];
			else
			{
				ul = document.createElement("ul");
				parent.appendChild(ul);
				parent = ul;
	
				if (checkbox)
				{
					this.hidePanels.push({checkbox: checkbox, panel: ul});
					checkbox.addEventListener("click", this.showHidePanel.bind(this, checkbox, ul), false);
				}
			}
		}
		var settingrow = document.createElement('li');
		parent.appendChild(settingrow);
		return settingrow;
	};
	SettingsPane.prototype.addCheckbox = function addCheckbox(id, label, title, checked, parent)
	{
		var settingrow = this.addSettingRow(parent);
		var settingcheckbox = document.createElement('input');
		settingcheckbox.type = 'checkbox';
		settingcheckbox.checked = checked;
		settingcheckbox.title = title;
		settingcheckbox.id = 'setting_' + id;
		settingrow.appendChild(settingcheckbox);
		var settinglabel = document.createElement('label');
		settinglabel.htmlFor = 'setting_' + id;
		settinglabel.appendChild(document.createTextNode(label));
		settinglabel.title = settingcheckbox.title;
		settingrow.appendChild(settinglabel);
		return settingcheckbox;
	};
	
	SettingsPane.prototype.showHidePanel = function showHidePanel(checkbox, panel)
	{
		panel.style.display = checkbox.checked ? "" : "none";
	};
	SettingsPane.prototype.initComplete = function initComplete()
	{
		for (var i = 0; i < this.hidePanels.length; i++)
			this.showHidePanel(this.hidePanels[i].checkbox, this.hidePanels[i].panel);
	};

	function Fullscreen()
	{
	}
	Fullscreen.prototype.load = async function load()
	{
		this.shouldresize = await utils.getPref('resize', true);
		this.noscale = await utils.getPref('noscale', false);
	}
	Fullscreen.prototype.init = async function init()
	{
		this.setting_main = globals.modules.settingspane.addCheckbox('resize', "Resize flash to full-screen", "Resizes the toon so it fills the entire window", this.shouldresize);
		this.setting_noscale = globals.modules.settingspane.addCheckbox('noscale', "Show behind the black", "Lets you see what's happening beyond the frames", this.noscale, this.setting_main);
		
		if (!globals.flashmovie)
			return;
	
		this.initwidth = globals.flashmovie.width;
		this.initheight = globals.flashmovie.height;
		if (this.initwidth.toString().indexOf('%') >= 0 || this.initwidth.toString().indexOf('%') >= 0)
		{
			this.isPercentage = true;
			this.aspect = 1.0;
		}
		else
		{
			this.isPercentage = false;
			this.aspect = this.initwidth / this.initheight;
		}
		window.addEventListener('resize', this.doResize.bind(this), true);
		this.doResize();
		if (this.noscale)
			await this.setScaleMode("noScale");
	};
	Fullscreen.prototype.doResize = function doResize()
	{
		if (!globals.flashmovie)
			return;
		
		if (!this.shouldresize)
		{
			globals.flashmovie.style.width = this.initwidth + "px";
			globals.flashmovie.style.height = this.initheight + "px";
			if (globals.modules.seekbar.seekbar)
				globals.modules.seekbar.seekbar.style.width = Math.max(this.initwidth, 450) + "px";
			return;
		}
		
		var dw = window.innerWidth;
		var dh = window.innerHeight;
	
		var a = document.defaultView.getComputedStyle(document.body, null);
		// parseInt will take the number part at the start, turning eg "10px" into 10
		dw -= parseInt(a.marginLeft,10);
		dw -= parseInt(a.marginRight,10);
		dh -= parseInt(a.marginTop,10);
		dh -= parseInt(a.marginBottom,10);
	
		if (globals.navbar)
		{
			a = document.defaultView.getComputedStyle(globals.navbar, null);
			dh -= parseInt(a.height,10);
			dh -= parseInt(a.marginTop,10);
			dh -= parseInt(a.marginBottom,10);
		}
		if (globals.modules.seekbar.seekbar)
		{
			a = document.defaultView.getComputedStyle(globals.modules.seekbar.seekbar, null);
			dh -= parseInt(a.height,10);
			dh -= parseInt(a.marginTop,10);
			dh -= parseInt(a.marginBottom,10);
		}
		if (globals.modules.subtitles.subtitleholder)
		{
			a = document.defaultView.getComputedStyle(globals.modules.subtitles.subtitleholder, null);
			dh -= parseInt(a.height,10);
			dh -= parseInt(a.marginTop,10);
			dh -= parseInt(a.marginBottom,10);
		}
		if (globals.modules.subtitles.errorsholder)
		{
			a = document.defaultView.getComputedStyle(globals.modules.subtitles.errorsholder, null);
			dh -= parseInt(a.height,10);
			dh -= parseInt(a.marginTop,10);
			dh -= parseInt(a.marginBottom,10);
		}
		// enforce a (rather small) minimum size, regardless of how much crap is squeezed below the frame
		if (dw < 100) dw = 100;
		if (dh < 100) dh = 100;
		// if it was a percentage size, or we're looking outside the frame, just fill the whole window.
		// otherwise, keep the aspect ratio correct... "touch inside" style.
		if (!this.isPercentage && !this.noscale)
		{
			if(dw <= dh * this.aspect)
				dh = Math.floor(dw / this.aspect);
			else
				dw = Math.floor(dh * this.aspect);
		}
	
		// set embed's size
		globals.flashmovie.style.width = dw + "px";
		globals.flashmovie.style.height = dh + "px";
		if (globals.modules.seekbar.seekbar)
			globals.modules.seekbar.seekbar.style.width = Math.max(dw, 450) + "px";
	};
	Fullscreen.prototype.setScaleMode = async function setScaleMode(scaleMode)
	{
		await utils.waitLoaded();
		await playercomm.setScaleMode(globals.flashmovie, scaleMode);
	};
	Fullscreen.prototype.updateSettings = function updateSettings()
	{
		this.shouldresize = this.setting_main.checked;
		utils.setPref("resize", this.shouldresize);
		var old_noscale = this.noscale;
		this.noscale = this.setting_noscale.checked;
		utils.setPref("noscale", this.noscale);
		this.doResize();
		if (this.noscale && !old_noscale)
			this.setScaleMode("noScale");
		else if (!this.noscale && old_noscale)
			this.setScaleMode("showAll");
	};

	function Seekbar()
	{
	}
	Seekbar.prototype.load = async function load() {
		this.enabled = await utils.getPref('seekbar', true);
		this.framecounter = await utils.getPref('frames', false);
		this.zoom = await utils.getPref('zoom', false);
	}
	Seekbar.prototype.init = async function init() {
		this.setting_enabled = globals.modules.settingspane.addCheckbox('seekbar', "Show seek bar", "Lets you fast forward and rewind", this.enabled);
		this.setting_framecounter = globals.modules.settingspane.addCheckbox('framecounter', "Show frame counter on seek bar", "Shows you exactly where you are", this.framecounter, this.setting_enabled);
		this.setting_zoom = globals.modules.settingspane.addCheckbox('zoom', "Show zooming controls", "Allows zooming in on the toon", this.zoom, this.setting_enabled);
		
		if (!globals.flashmovie)
			return;
	
		if (this.enabled)
			await this.addSeekbar();
	
		this.dragging = false;
		this.paused = !await utils.isPlaying();
		document.addEventListener("mousemove", this.dragMousemove.bind(this), false);
		document.addEventListener("mouseup", this.release.bind(this), false);
	
		window.setInterval(this.update.bind(this), 50);
	};
	Seekbar.prototype.updateSettings = function updateSettings()
	{
		if (this.enabled)
			this.removeSeekbar();
		this.enabled = this.setting_enabled.checked;
		utils.setPref("seekbar", this.enabled);
		this.framecounter = this.setting_framecounter.checked;
		utils.setPref("frames", this.framecounter);
		this.zoom = this.setting_zoom.checked;
		utils.setPref("zoom", this.zoom);
		if (this.enabled && globals.flashmovie)
			this.addSeekbar();
	};
	Seekbar.prototype.addSeekbar = async function addSeekbar()
	{
		this.dragging = false;
		this.paused = !await utils.isPlaying();
	
		this.seekbar = document.createElement("div");
		var where = globals.flashmovie;
		while(where.parentNode.tagName.toLowerCase()=="object" || where.parentNode.tagName.toLowerCase()=="embed")
			where=where.parentNode;
		utils.insertAfter(this.seekbar, where);
		this.seekbar.style.width = globals.flashmovie.width;
		this.seekbar.style.margin = "0 auto";
	
		var table=document.createElement("table");
		table.style.width="100%";
		this.seekbar.appendChild(table);
		var row=table.insertRow();
		this.pauseButton=document.createElement("button");
		this.pauseButtonImg = document.createElement("img");
		this.pauseButtonImg.src = globals.images.pause;
		this.pauseButton.appendChild(this.pauseButtonImg);
		var buttonCell=row.insertCell();
		buttonCell.appendChild(this.pauseButton);
		var rewindCell=row.insertCell();
		this.rewindButton=document.createElement("button");
		var img = document.createElement("img");
		img.src = globals.images.rewind;
		this.rewindButton.appendChild(img);
		rewindCell.appendChild(this.rewindButton);
		var prevCell=row.insertCell();
		this.prevButton=document.createElement("button");
		img = document.createElement("img");
		img.src = globals.images.prev;
		this.prevButton.appendChild(img);
		prevCell.appendChild(this.prevButton);
	
		this.slider=row.insertCell();
		this.slider.width="100%";
		var visibleSlider=document.createElement("div");
		visibleSlider.style.position="relative";
		visibleSlider.style.height="0.5em";
		visibleSlider.style.width="100%";
		visibleSlider.style.borderRadius="0.25em";
		visibleSlider.style.background="#333";
		this.slider.appendChild(visibleSlider);
		this.loadmeter=document.createElement("div");
		this.loadmeter.style.position="absolute";
		this.loadmeter.style.top=this.loadmeter.style.left = "0";
		this.loadmeter.style.height="0.5em";
		this.loadmeter.style.width="0";
		this.loadmeter.style.borderRadius="0.25em";
		this.loadmeter.style.background="#aaa";
		visibleSlider.appendChild(this.loadmeter);
		this.thumb=document.createElement("div");
		this.thumb.style.position="absolute";
		this.thumb.style.height="1em";
		this.thumb.style.width="0.5em";
		this.thumb.style.top="-0.25em";
		this.thumb.style.borderRadius="0.25em";
		this.thumb.style.background="#666";
		visibleSlider.appendChild(this.thumb);
	
		var nextCell=row.insertCell();
		this.nextButton=document.createElement("button");
		img = document.createElement("img");
		img.src = globals.images.next;
		this.nextButton.appendChild(img);
		nextCell.appendChild(this.nextButton);
		var ffCell=row.insertCell();
		this.ffButton=document.createElement("button");
		img = document.createElement("img");
		img.src = globals.images.ffwd;
		this.ffButton.appendChild(img);
		ffCell.appendChild(this.ffButton);
	
		if (this.framecounter)
		{
			var frameCell=row.insertCell();
			var framediv=document.createElement("div");
			framediv.style.background="#ccc";
			framediv.style.color="#000";
			framediv.style.fontWeight="bold";
			framediv.style.padding = "0 5px";
			frameCell.appendChild(framediv);
			this.framecountertext=document.createTextNode("");
			framediv.appendChild(this.framecountertext);
		}
		else
			this.framecountertext = false;
	
		if (this.zoom && !globals.modules.fullscreen.noscale)
		{
			var zoomOutCell=row.insertCell();
			this.zoomOutButton=document.createElement("button");
			// \u2212 is −
			this.zoomOutButton.appendChild(document.createTextNode("\u2212"));
			zoomOutCell.appendChild(this.zoomOutButton);
			var zoomNormalCell=row.insertCell();
			this.zoomNormalButton=document.createElement("button");
			this.zoomNormalButton.appendChild(document.createTextNode("0"));
			zoomNormalCell.appendChild(this.zoomNormalButton);
			var zoomInCell=row.insertCell();
			this.zoomInButton=document.createElement("button");
			this.zoomInButton.appendChild(document.createTextNode("+"));
			zoomInCell.appendChild(this.zoomInButton);
		}
		else
		{
			this.zoomOutButton = false;
			this.zoomNormalButton = false;
			this.zoomInButton = false;
		}
	
		this.slider.addEventListener("mousedown", this.drag.bind(this), false);
		this.pauseButton.addEventListener("click",this.pauseUnpause.bind(this),false);
		this.rewindButton.addEventListener("click",this.rewind.bind(this),false);
		this.prevButton.addEventListener("click",this.prevFrame.bind(this),false);
		this.nextButton.addEventListener("click",this.nextFrame.bind(this),false);
		this.ffButton.addEventListener("click",this.fastforward.bind(this),false);
		if (this.zoomOutButton)
		{
			this.zoomOutButton.addEventListener("click",this.zoomOut.bind(this),false);
			this.zoomNormalButton.addEventListener("click",this.zoomNormal.bind(this),false);
			this.zoomInButton.addEventListener("click",this.zoomIn.bind(this),false);
		}
	
		globals.modules.fullscreen.doResize();
	};
	Seekbar.prototype.removeSeekbar = function removeSeekbar()
	{
		if (!this.seekbar)
			return;
		this.seekbar.parentNode.removeChild(this.seekbar);
		this.seekbar = undefined;
		globals.modules.fullscreen.doResize();
	};
	
	Seekbar.prototype.update = async function update()
	{
		if (!this.seekbar)
			return;
	
		var fullSliderWidth = parseInt(document.defaultView.getComputedStyle(this.slider, null).width, 10);
		var sliderWidth = fullSliderWidth - parseInt(document.defaultView.getComputedStyle(this.thumb, null).width, 10);
		var tot = await utils.totalFrames();
		if (tot > 0)
		{
			var frame = await utils.currentFrame();
			if (frame < 0)
				frame = 0;
			if (this.framecountertext)
			{
				var a = tot.toString();
				var b = (frame+1).toString();
				while (b.length < a.length)
					b = "\u2007" + b; // U+2007 FIGURE SPACE
				this.framecountertext.nodeValue = b+"/"+a;
			}
			if(!this.dragging)
			{
				if (tot > 1)
					this.thumb.style.left = (frame/(tot - 1)*sliderWidth)+"px";
				else
					this.thumb.style.left = "0";
				this.paused = !await utils.isPlaying();
				this.pauseButtonImg.src = this.paused ? globals.images.play : globals.images.pause;
			}
			var loaded = await utils.framesLoaded();
			this.loadmeter.style.width = (loaded/tot*fullSliderWidth)+"px";
		}
		else if (this.framecountertext)
		{
			this.framecountertext.nodeValue = "Loading...";
		}
	};
	
	Seekbar.prototype.pauseUnpause = async function pauseUnpause()
	{
		this.paused = await utils.isPlaying();
		this.pauseButtonImg.src = this.paused ? globals.images.play : globals.images.pause;
		if (this.paused)
			await utils.stop();
		else
			await utils.play();
	};
	Seekbar.prototype.rewind = async function rewind()
	{
		await utils.goto(0);
		await utils.play();
	};
	Seekbar.prototype.fastforward = async function fastforward()
	{
		var tot = await utils.totalFrames();
		await utils.goto(tot - 1);
	};
	Seekbar.prototype.prevFrame = async function prevFrame()
	{
		var frame = await utils.currentFrame();
		await utils.goto(frame - 1);
	};
	Seekbar.prototype.nextFrame = async function nextFrame()
	{
		var frame = await utils.currentFrame();
		await utils.goto(frame + 1);
	};
	Seekbar.prototype.zoomIn = async function zoomIn()
	{
		await utils.zoomIn(1.5);
	};
	Seekbar.prototype.zoomOut = async function zoomOut()
	{
		await utils.zoomOut(1.5);
	};
	Seekbar.prototype.zoomNormal = async function zoomNormal()
	{
		await utils.zoomReset();
	};
	
	Seekbar.prototype.drag = function drag(e)
	{
		this.dragging=true;
		this.dragMousemove(e);
		e.preventDefault();
		return false;
	};
	Seekbar.prototype.dragMousemove = async function dragMousemove(e)
	{
		if (!this.dragging) return;
		var pageX = e.clientX + document.body.scrollLeft;
		var rect = this.slider.getBoundingClientRect();
		var thumbWidth = parseInt(document.defaultView.getComputedStyle(this.thumb, null).width, 10);
		var width = rect.right - rect.left - thumbWidth;
		var pos = (pageX - rect.left - thumbWidth/2) / width;
		if (pos < 0)
			pos = 0;
		if (pos > 1)
			pos = 1;
		var t = await utils.totalFrames();
		if (t > 1)
		{
			var frame = Math.round(t * pos);
			await utils.goto(frame);
		}
		this.thumb.style.left = (pos * width) + "px";
	};
	Seekbar.prototype.release = function release()
	{
		if (!this.dragging) return;
		if (!this.paused)
			utils.play();
		this.dragging = false;
	};

	function WikiLink()
	{
	}
	WikiLink.prototype.load = async function load() {
		this.enabled = await utils.getPref('hrwiki', true);
	}
	WikiLink.prototype.init = function init()
	{
		this.setting_enabled = globals.modules.settingspane.addCheckbox('hrwiki', "Add HRWiki link", "Adds a link to the appropriate page on the Homestar Runner Wiki", this.enabled);
	
		this.buildWikiLink();
		this.showWikiLink();
	};
	WikiLink.prototype.updateSettings = function updateSettings()
	{
		this.enabled = this.setting_enabled.checked;
		utils.setPref("hrwiki", this.enabled);
		// This is called before Subtitles.updateSettings, so delay until after that happens
		// so we can update the subtitles link as appropriate
		window.setTimeout(this.showWikiLink.bind(this), 0);
	};
	
	WikiLink.prototype.buildWikiLink = function buildWikiLink()
	{
		// many pages on the mirror have an "info" link in the navbar (thanks Tom!)... use that
		if (globals.whichsite === 3)
		{
			var navbar;
			if (globals.modules.navbar && globals.modules.navbar.originalnavbar)
				navbar = globals.modules.navbar.originalnavbar;
			else
				navbar = globals.navbar;
			if (navbar)
			{
				var a = navbar.getElementsByTagName("a");
				for (var i = 0; i < a.length; i++)
				{
					if (a[i].firstChild.nodeType === 3 && a[i].firstChild.nodeValue === "info")
					{
						this.addHRWikiLink(a[i].href, true);
						return;
					}
				}
			}
		}
		
		// pull the filename from the url, use it as a link to HRWiki
		// all the filenames except a couple of special-cases are
		//  redirects to their articles
		// don't link to certain pages, they aren't redirects, but already existing pages
		// also detect a 404 error and special-case Strong Sad's Lament
		     if (document.title === "Oops! You bwoke it.")
			this.addHRWikiLink("404'd");
		else if (globals.filename === "interview")
			this.addHRWikiLink("The_Interview");
		else if (globals.filename === "fhqwhgads")
			this.addHRWikiLink("Everybody_to_the_Limit");
		else if (globals.filename === "trogdor")
			this.addHRWikiLink("TROGDOR!");
		else if (globals.filename === "marshie")
			this.addHRWikiLink("Meet_Marshie");
		else if (globals.filename === "eggs")
			this.addHRWikiLink("Eggs_(toon)");
		else if (globals.filename === "fireworks")
			this.addHRWikiLink("Happy_Fireworks");
		else if (globals.filename === "sbemail100")
			this.addHRWikiLink("Not_the_100th_Email!!!");
		else if (globals.filename === "sbemail200")
			this.addHRWikiLink("Page_Load_Error");
		else if (globals.filename === "sbcg4ap")
			this.addHRWikiLink("Strong_Bad's_Cool_Game_for_Attractive_People_Advertisement");
		else if (globals.filename === "dangeresque")
			this.addHRWikiLink("Dangeresque_Roomisode_1:_Behind_the_Dangerdesque");
		else if (location.pathname.substr(0, 12) === "/sadjournal/" && globals.filename != "wonderyears" && globals.filename != "super8")
			this.addHRWikiLink("Strong_Sad's_Lament");
		else if (location.pathname.substr(0,5) === "/vii/" && (globals.filename === "" || globals.filename === "index"))
			this.addHRWikiLink("Viidelectrix");
		else if (globals.filename === "" || globals.filename === "index")
		{
			if (document.location.pathname === "/slash/slash/")
				this.addHRWikiLink("Screenland_-_24_Apr_2017");
			else if (globals.whichsite === 0)
				this.addHRWikiLink("Index_Page");
			else if (globals.whichsite === 1)
				this.addHRWikiLink("Podstar_Runner");
			else if (globals.whichsite === 2)
				this.addHRWikiLink("Videlectrix");
			//else if (globals.whichsite === 3)
			//	; // this will be a 403 page - do nothing.
		}
		else
			this.addHRWikiLink(globals.filename);
	};
	
	WikiLink.prototype.addHRWikiLink = function addHRWikiLink(pagename, isurl)
	{
		this.linkdiv = document.createElement("div");
		this.linkdiv.style.borderLeft = this.linkdiv.style.borderBottom = '1px solid #666';
		this.linkdiv.style.background = '#EEE';
		this.linkdiv.style.position = "fixed";
		this.linkdiv.style.overflow = 'auto';
		this.linkdiv.style.right = "0px";
		this.linkdiv.style.top = "0px";
		this.linkdiv.style.padding = "3px";
		var link = document.createElement("a");
		if (isurl)
			link.href = pagename;
		else
			link.href = "http://www.hrwiki.org/wiki/" + escape(pagename.replace(/ /g, '_'));
		link.title = "See the HRWiki article for this page";
		link.style.display = "block";
		link.style.textDecoration = "none";
		this.linkdiv.appendChild(link);
		var img=document.createElement("img");
		img.style.border="0px";
		img.style.display="block";
		img.src=globals.images.hrwiki;
		link.appendChild(img);
		this.sublink = document.createElement("a");
		this.sublink.title = "See the HRWiki article for this page's subtitles";
		this.sublink.style.display = "block";
		this.sublink.style.textDecoration = "none";
		this.sublink.style.textAlign = "center";
		this.sublink.style.fontSize = this.sublink.style.lineHeight = "16px";
		this.sublink.style.marginTop = "3px";
		this.linkdiv.appendChild(this.sublink);
		this.sublink.appendChild(document.createTextNode('S'));
		document.body.appendChild(this.linkdiv);
	};
	
	WikiLink.prototype.showWikiLink = function showWikiLink()
	{
		if (this.enabled)
		{
			this.linkdiv.style.display = "block";
			if (globals.modules.subtitles && globals.modules.subtitles.enabled)
			{
				this.sublink.style.display = "block";
				this.sublink.href = "http://www.hrwiki.org/wiki/Subtitles:" + escape(globals.filename.replace(/ /g, '_')) + "/" + escape(globals.modules.subtitles.language);
			}
			else
				this.sublink.style.display = "none";
		}
		else
			this.linkdiv.style.display = "none";
	};

	function NextPrev()
	{
	}
	NextPrev.prototype.load = async function load() {
		this.enabled = await utils.getPref('prevnext', true);
		this.docheck = await utils.getPref('checknext', true);
	}
	NextPrev.prototype.init = function init()
	{
		this.setting_enabled = globals.modules.settingspane.addCheckbox('prevnext', "Show previous/next buttons", "Lets you easily move through SBEmails, TGS, etc", this.enabled);
		this.setting_docheck = globals.modules.settingspane.addCheckbox('checknext', "Check if next exists", 'Doesn\'t add a "next" link on the latest SBEmail, etc', this.docheck, this.setting_enabled);
	
		this.createPrevNext();
		this.showPrevNext();
	};
	NextPrev.prototype.updateSettings = function updateSettings()
	{
		this.enabled = this.setting_enabled.checked;
		utils.setPref("prevnext", this.enabled);
		this.docheck = this.setting_docheck.checked;
		utils.setPref("checknext", this.docheck);
		this.showPrevNext();
	};
	
	NextPrev.prototype.createPrevNext = function createPrevNext()
	{
		// this is coded like this instead of just looking for /(\d+)/ so that it
		// doesn't find pages like commandos3 or xmas04
		var result;
		if ((result = globals.filename.match(/^(sbemail|tgs|answer|bizcasfri|puppetjam|main)(\d+)$/)))
		{
			// sbemail100 and sbemail200 aren't actually sbemails
			if (!(result[1] == "sbemail" && (result[2] == "100" || result[2] == "200")))
				this.addPrevNextlinks(result[1],parseInt(result[2],10));
		}
		else if (globals.filename == "sbemailahundred")
			this.addPrevNextlinks("sbemail", 100);
		else if (globals.filename == "kotpoptoon")
			this.addPrevNextlinks("sbemail", 151);
		else if (globals.filename == "sbemailtwohundred")
			this.addPrevNextlinks("sbemail", 200);
		else if (globals.filename == "hremail3184")
			this.addPrevNextlinks("sbemail", 201);
		else if (globals.filename == "dween_tgs")
			this.addPrevNextlinks("tgs", 6);
	};
	NextPrev.prototype.addPrevNextlinks = function addPrevNextlinks(series, num)
	{
		if (num > 1)
		{
			this.prevlink = document.createElement("a");
			this.prevlink.href = this.makeLink(series, num - 1);
			this.prevlink.style.position="fixed";
			this.prevlink.style.left="0px";
			this.prevlink.style.bottom="0px";
			this.prevlink.style.padding="3px";
			this.prevlink.style.background="white";
			this.prevlink.style.border="1px solid black";
			this.prevlink.style.textDecoration="none";
			this.prevlink.style.display = "none";
			var img = document.createElement("img");
			img.style.border = "none";
			img.src = globals.images.prev;
			this.prevlink.appendChild(img);
			document.body.appendChild(this.prevlink);
		}
	
		this.nextlink = document.createElement("a");
		this.nextlink.href = this.makeLink(series, num + 1);
		this.nextlink.style.position="fixed";
		this.nextlink.style.right="0px";
		this.nextlink.style.bottom="0px";
		this.nextlink.style.padding="3px";
		this.nextlink.style.background="white";
		this.nextlink.style.border="1px solid black";
		this.nextlink.style.textDecoration="none";
		this.nextlink.style.display = "none";
		img = document.createElement("img");
		img.style.border = "none";
		img.src = globals.images.next;
		this.nextlink.appendChild(img);
		document.body.appendChild(this.nextlink);
	
		this.checkedNext = false;
	};
	NextPrev.prototype.makeLink = function makeLink(series, num)
	{
		if (series == "sbemail" && num == 100)
			return "sbemailahundred.html";
		else if (series == "sbemail" && num == 151)
			return "kotpoptoon.html";
		else if (series == "sbemail" && num == 200)
			return "sbemailtwohundred.html";
		else if (series == "sbemail" && num == 201)
			return "hremail3184.html";
		else
			return series + num + ".html";
	};
	
	NextPrev.prototype.showPrevNext = function showPrevNext()
	{
		if (this.enabled)
		{
			if (this.prevlink)
				this.prevlink.style.display = "block";
			if (this.docheck && !this.checkedNext && this.nextlink)
				/*no await*/ this.doCheckNext();
			else if (this.nextlink)
				this.nextlink.style.display = "block";
		}
		else
		{
			if (this.prevlink)
				this.prevlink.style.display = "none";
			if (this.nextlink)
				this.nextlink.style.display = "none";
		}
	};
	NextPrev.prototype.doCheckNext = async function doCheckNext()
	{
		try {
			var res = await utils.downloadPage(this.nextlink.href + "?cachedodge=" + (await utils.getPref('cachedodge', 0)), "HEAD");
		} catch (e) {
			this.nextlink.parentNode.removeChild(this.nextlink);
			this.nextlink = undefined;
			return;
		}
	
		if (res.status == 200 && res.headers.indexOf("404error.html") < 0)
		{
			this.checkedNext = true;
			this.showPrevNext();
		}
		else if (this.nextlink)
		{
			this.nextlink.parentNode.removeChild(this.nextlink);
			this.nextlink = undefined;
		}
	};
	NextPrev.prototype.onCheckError = function onCheckError()
	{
	};

	function Navbar()
	{
	}
	Navbar.prototype.SECTIONS = {
		t: "Big Toons",
		sh: "Shorts",
		ho: "Holday Toons",
		p: "Puppet Stuff",
		teh: "Powered by The Cheat",
		sb: "Strong Bad Emails",
		am: "Marzipan's Answering Machine",
		tgs: "Teen Girl Squad"
	};
	Navbar.prototype.MAIN_COUNT = 26;
	Navbar.prototype.load = async function load() {
		this.enabled = await utils.getPref('navbar', false);
		this.rando = {};
		for (var i in this.SECTIONS)
			this.rando[i] = await utils.getPref('rando' + i, true);
	}
	Navbar.prototype.init = function init() {
		utils.addGlobalStyle(
			'#newnavbar\n' +
			'{\n' +
			'\tmargin: 0;\n' +
			'\tpadding: 0;\n' +
			'\ttext-align: center;\n' +
			'\ttext-transform: lowercase;\n' +
			'\theight: 10px;\n' +
			'\tfont: 10px/10px sans-serif;\n' +
			'}\n' +
			'#newnavbar li\n' +
			'{\n' +
			'\tmargin: 0;\n' +
			'\tpadding: 0;\n' +
			'\tdisplay: inline;\n' +
			'}\n' +
			'#newnavbar :link, #newnavbar :visited\n' +
			'{\n' +
			'\tcolor: #666;\n' +
			'\tfont-family: sans-serif;\n' +
			'\ttext-decoration: none;\n' +
			'\tpadding: 0 1em;\n' +
			'}\n' +
			'#newnavbar :link:hover, #newnavbar :visited:hover\n' +
			'{\n' +
			'\tcolor: #999;\n' +
			'}\n' +
			'\n' +
			"/* for overriding podstar's settings: */\n" +
			'#newnavbar :link, #newnavbar :visited\n' +
			'{\n' +
			'\tfont-weight: normal;\n' +
			'}\n' +
			'#newnavbar :link:hover, #newnavbar :visited:hover\n' +
			'{\n' +
			'\tbackground: transparent;\n' +
			'\tfont-weight: normal;\n' +
			'}\n' +
			""
		);
	
		this.setting_enabled = globals.modules.settingspane.addCheckbox('navbar', "Plain HTML navbar", "Replaces the flash navbar with normal links, so you can open in tabs, etc", this.enabled);
		this.setting_rando = {};
		for (var i in this.SECTIONS)
			this.setting_rando[i] = globals.modules.settingspane.addCheckbox('rando' + i, "Include " + this.SECTIONS[i] + " in rando", 'Limit the "rando" function to what you like to watch', this.rando[i], this.setting_enabled);
		
		this.allrandourls = false;
		this.randourls = false;
	
		this.originalnavbar = globals.navbar;
		this.newnavbar = this.buildNavbar(this.originalnavbar);
		this.showNavbar();
	};
	Navbar.prototype.updateSettings = function updateSettings()
	{
		this.enabled = this.setting_enabled.checked;
		utils.setPref("navbar", this.enabled);
		for (var i in this.SECTIONS)
		{
			this.rando[i] = this.setting_rando[i].checked;
			utils.setPref("rando" + i, this.rando[i]);
		}
		this.filterRando();
		this.showNavbar();
	};
	
	Navbar.prototype.showNavbar = function showNavbar()
	{
		if (this.enabled)
		{
			if (this.originalnavbar)
				this.originalnavbar.style.display = "none";
			this.newnavbar.style.display = "";
			this.newnavbar.style.marginTop = (globals.modules.seekbar.enabled ? "0" : "10px");
			globals.navbar = this.newnavbar;
			/*no await*/ this.loadRandoXML();
		}
		else
		{
			if (this.originalnavbar)
				this.originalnavbar.style.display = "";
			this.newnavbar.style.display = "none";
			globals.navbar = this.originalnavbar;
		}
		globals.modules.fullscreen.doResize();
	};
	
	Navbar.prototype.buildNavbar = function buildNavbar(where)
	{
		var newnavbar = document.createElement("ul");
		newnavbar.id = "newnavbar";
		if (where)
		{
			while(where.parentNode.tagName.toLowerCase() == "object")
				where = where.parentNode;
			utils.insertAfter(newnavbar, where);
		}
		else
			document.body.appendChild(newnavbar);
	
		this.mainlink = this.addnavbarlink(newnavbar, "http://homestarrunner.com/main" + Math.floor(Math.random() * this.MAIN_COUNT + 1) + ".html", "Main");
		// just for fun, re-randomise on each mouse-over (for the status bar)
		this.mainlink.addEventListener("mouseout", this.newMainLink.bind(this), false);
		this.addnavbarlink(newnavbar, "http://homestarrunner.com/toons.html", "Toons");
		this.addnavbarlink(newnavbar, "http://homestarrunner.com/games.html", "Games");
		this.addnavbarlink(newnavbar, "http://homestarrunner.com/characters2.html", "Characters");
		this.addnavbarlink(newnavbar, "http://homestarrunner.com/homester.html", "Downloads");
		this.addnavbarlink(newnavbar, "http://homestarrunner.com/store.html", "Store", "storelink");
		this.addnavbarlink(newnavbar, "http://homestarrunner.com/sbemail.html", "SB Emails");
		//this.addnavbarlink(newnavbar, "http://feeds.feedburner.com/HomestarRunner", "Subscribe");
		this.addnavbarlink(newnavbar, "https://www.youtube.com/user/homestarrunnerdotcom", "YouTube");
		this.addnavbarlink(newnavbar, "http://homestarrunner.com/email.html", "Contact");
		//this.addnavbarlink(newnavbar, "http://podstar.homestarrunner.com/", "Podcast");
		this.addnavbarlink(newnavbar, "http://homestarrunner.com/legal.html", "Legal");
		this.randolink = this.addnavbarlink(newnavbar, "javascript:void(alert('rando.xml not loaded yet... be patient'))", "Rando");
		this.randolink.addEventListener("mouseout", this.newRandoLink.bind(this), false);
	
		return newnavbar;
	};
	Navbar.prototype.addnavbarlink = function addnavbarlink(ul, href, title, extraclass)
	{
		var li = document.createElement("li");
		var link = document.createElement("a");
		link.href = href;
		link.appendChild(document.createTextNode(title));
		if (extraclass)
			link.className = extraclass;
		li.appendChild(link);
		ul.appendChild(li);
		return link;
	};
	
	Navbar.prototype.newMainLink = function newMainLink()
	{
		this.mainlink.href="http://homestarrunner.com/main" + Math.floor(Math.random() * this.MAIN_COUNT + 1) + ".html";
	};
	Navbar.prototype.newRandoLink = function newRandoLink()
	{
		if (!this.randourls)
			return;
	
		if (this.randourls.length > 0)
		{
			var r = this.randourls[Math.floor(Math.random() * this.randourls.length)];
			this.randolink.href = r.u;
			this.randolink.title = r.n;
		}
		else
		{
			this.randolink.href = "javascript:void(alert('Nothing to choose from'))";
			this.randolink.title = "Nothing to choose from";
		}
	};
	
	Navbar.prototype.loadRandoXML = async function loadRandoXML()
	{
		// Only run this once
		if (this.haveLoadedXML)
			return;
		this.haveLoadedXML = true;
	
		try {
			var res = await utils.downloadPage(
				"http://homestarrunner.com/rando.xml?cachedodge=" + (await utils.getPref('cachedodge', 0))
			);
	
			var parser = new DOMParser();
			// fix invalid XML...
			// add missing root element
			var doc = res.text.replace(/<\?xml.*?\?>/g, ""); // strip <?xml ?> tag
			doc = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n<rando>" + doc + "</rando>";
			// fix bad ampersands
			doc = doc.replace(/&(?!\w*;)/g, "&");
			doc = parser.parseFromString(doc, "application/xml");
			var sbemailcounter = 0;
			this.allrandourls = [];
			for (var i = 0; i < doc.documentElement.childNodes.length; i++)
			{
				var node = doc.documentElement.childNodes[i];
				if (node.nodeType == 1)
				{
					var type = node.nodeName.toLowerCase();
					var u = node.getAttribute('u');
					var n = node.getAttribute('n');
					if (!n) n = "Untitled";
					if (type == "sb")
					{
						sbemailcounter++;
						n = "SBEmail: " + n;
					}
					if (u)
						this.allrandourls.push({u: "http://homestarrunner.com/" + u, n: n, type: type});
					else
						this.allrandourls.push({u: "http://homestarrunner.com/sbemail" + sbemailcounter + ".html", n: n, type: type});
				}
			}
			this.filterRando();
		} catch (e) {
			this.randolink.href = "javascript:void(alert('Error loading rando.xml... try refreshing'))";
		}
	};
	Navbar.prototype.filterRando = function filterRando()
	{
		if (!this.allrandourls)
			return;
		this.randourls = [];
		for (var i in this.allrandourls)
		{
			var r = this.allrandourls[i];
			if (this.rando[r.type] === false) // === false so that it's considered "true" for undefined... if they add a new toon type
				continue;
			this.randourls.push(r);
		}
		this.newRandoLink();
	};

	function Subtitles()
	{
	}
	Subtitles.prototype.DEFAULTXML = escape('<?xml version="1.0" encoding="utf-8"?>\n<transcript xml:lang="en-us">\n<line start="" end="" speaker=""></line>\n</transcript>');
	Subtitles.prototype.NAMES_OPTS = ["Never", "Voiceovers", "Always"];
	Subtitles.prototype.NO_SUBTITLES = document.createComment("");
	Subtitles.prototype.load = async function load() {
		this.enabled = await utils.getPref('subtitles', false);
		this.captions = await utils.getPref('captions', true);
		this.colours = await utils.getPref('colours', true);
		this.testsubs = await utils.getPref('testsubs', false);
		this.language = await utils.getPref('language', "en");
		this.testsubsdata = unescape(await utils.getPref('testsubsdata', this.DEFAULTXML));
		this.names = await utils.getPref('names', 0);
	}
	Subtitles.prototype.init = function init()
	{
		utils.addGlobalStyle(
			'.subtitles\n' +
			'{\n' +
			'\tbackground: black;\n' +
			'\tcolor: white;\n' +
			'\tfont: 20px/25px sans-serif;\n' +
			'\theight: 100px;\n' +
			'\ttext-align: center;\n' +
			'}\n' +
			'\n' +
			'.subtitle_errors\n' +
			'{\n' +
			'\tbackground: black;\n' +
			'\tcolor: red;\n' +
			'\tfont: 12pt sans-serif;\n' +
			'\ttext-align: left;\n' +
			'\tmargin: 0.5em;\n' +
			'}\n' +
			'\n' +
			'.subtitles .italic\n' +
			'{\n' +
			'\tfont-style: italic;\n' +
			'}\n' +
			'.subtitles .italic em, .subtitles .italic cite, .subtitles .italic i\n' +
			'{\n' +
			'\tfont-style: normal;\n' +
			'}\n' +
			""
		);
		
		this.setting_enabled = globals.modules.settingspane.addCheckbox('subtitles', "Show subtitles", "Shows subtitles or captions below the toon, if any are available", this.enabled);
	
		var settingrow = globals.modules.settingspane.addSettingRow(this.setting_enabled);
		var settinglabel = document.createElement('label');
		settinglabel.htmlFor = "setting_language";
		settinglabel.appendChild(document.createTextNode('Subtitle Language: '));
		settinglabel.title = 'Display subtitles in this language, if any';
		settingrow.appendChild(settinglabel);
		this.setting_language = document.createElement('select');
		this.setting_language.title = 'Display subtitles in this language, if any';
		this.setting_language.id = "setting_language";
		this.setting_language.disabled = true;
		settingrow.appendChild(this.setting_language);
	
		this.language_populated = false;
		/*no await*/ this.populateLanguage();
	
		this.setting_captions = globals.modules.settingspane.addCheckbox('captions', "Show captions", "Include sound effects in the subtitles", this.captions, this.setting_enabled);
		this.setting_colours = globals.modules.settingspane.addCheckbox('colours', "Use colours", "Distinguish characters by colour effects (turn off if colourblind)", this.colours, this.setting_enabled);
	
		settingrow = globals.modules.settingspane.addSettingRow(this.setting_enabled);
		settinglabel = document.createElement('label');
		settinglabel.htmlFor = "setting_names";
		settinglabel.appendChild(document.createTextNode('Show speakers\' names: '));
		settinglabel.title = 'Show the speakers\' names before their lines';
		settingrow.appendChild(settinglabel);
		this.setting_names = document.createElement('select');
		this.setting_names.title = 'Show the speakers\' names before their lines';
		this.setting_names.id = "setting_names";
		settingrow.appendChild(this.setting_names);
		for (var i = 0; i < this.NAMES_OPTS.length; i++)
		{
			var option = document.createElement('option');
			option.value = i;
			option.appendChild(document.createTextNode(this.NAMES_OPTS[i]));
			if (this.names == i)
				option.selected = true;
			this.setting_names.appendChild(option);
		}
	
		this.setting_testsubs = globals.modules.settingspane.addCheckbox('testsubs', "Test subtitles script", "Use this to test a subtitles script (copy/paste into a text box)", this.testsubs, this.setting_enabled);
	
		settingrow = globals.modules.settingspane.addSettingRow(this.setting_testsubs);
		this.setting_testsubsdata = document.createElement('textarea');
		this.setting_testsubsdata.title = 'Paste your XML data here';
		this.setting_testsubsdata.id = "setting_testsubsdata";
		this.setting_testsubsdata.style.width = "100%";
		this.setting_testsubsdata.style.height = "10em";
		this.setting_testsubsdata.style.fontSize = "8px";
		this.setting_testsubsdata.style.textAlign = "left";
		this.setting_testsubsdata.appendChild(document.createTextNode(this.testsubsdata));
		settingrow.appendChild(this.setting_testsubsdata);
	
		this.charsready = false;
		this.subsready = false;
	
		/*no await*/ this.setupSubtitles();
	
		window.setInterval(this.update.bind(this), 50);
	};
	Subtitles.prototype.updateSettings = function updateSettings()
	{
		this.enabled = this.setting_enabled.checked;
		utils.setPref('subtitles', this.enabled);
		if (this.language_populated)
		{
			this.language = this.setting_language.value;
			utils.setPref('language', this.language);
		}
		this.captions = this.setting_captions.checked;
		utils.setPref('captions', this.captions);
		this.colours = this.setting_colours.checked;
		utils.setPref('colours', this.colours);
		this.names = this.setting_names.value;
		utils.setPref('names', this.names);
		this.testsubs = this.setting_testsubs.checked;
		utils.setPref('testsubs', this.testsubs);
		this.testsubsdata = this.setting_testsubsdata.value;
		utils.setPref('testsubsdata', escape(this.testsubsdata));
	
		/*no await*/ this.setupSubtitles();
	};
	
	Subtitles.prototype.populateLanguage = async function populateLanguage()
	{
		var option = document.createElement('option');
		option.appendChild(document.createTextNode("Loading..."));
		option.selected = true;
		this.setting_language.appendChild(option);
	
		try {
			var xml = await utils.downloadWiki("Subtitles:Languages");
			xml = utils.parseWikiXML(xml);
		} catch (e) {
			while (this.setting_language.firstChild)
				this.setting_language.removeChild(this.setting_language.firstChild);
			var option = document.createElement('option');
			option.appendChild(document.createTextNode("Error loading languages"));
			option.selected = true;
			this.setting_language.appendChild(option);
			return;
		}
	
		while (this.setting_language.firstChild)
			this.setting_language.removeChild(this.setting_language.firstChild);
	
		var languages = xml.getElementsByTagName('language');
		for (var i = 0; i < languages.length; i++)
		{
			var node = languages[i];
			// sanity-check the node
			if (node.hasAttribute('xml:lang') && node.firstChild && (node.firstChild.nodeType == xml.TEXT_NODE || node.firstChild.nodeType == xml.CDATA_SECTION_NODE))
			{
				var option = document.createElement('option');
				option.appendChild(document.createTextNode(node.firstChild.nodeValue));
				option.lang = option.value = node.getAttribute('xml:lang');
				if (option.lang == this.language)
					option.selected = true;
				option.dir = "ltr";
				if (node.hasAttribute('dir'))
					option.dir = node.getAttribute('dir');
				this.setting_language.appendChild(option);
			}
		}
		
		this.setting_language.disabled = false;
		this.language_populated = true;
	};
	
	Subtitles.prototype.removeSubtitles = function removeSubtitles()
	{
		if (this.subtitleholder)
		{
			this.subtitleholder.parentNode.removeChild(this.subtitleholder);
			this.subtitleholder = undefined;
		}
		if (this.errorsholder)
		{
			this.errorsholder.parentNode.removeChild(this.errorsholder);
			this.errorsholder = undefined;
		}
	
		globals.modules.fullscreen.doResize();
	};
	Subtitles.prototype.createSubtitleHolder = function createSubtitleHolder()
	{
		this.subtitleholder = document.createElement('div');
		this.subtitleholder.className = "subtitles";
		var where = globals.flashmovie;
		if (globals.modules.seekbar && globals.modules.seekbar.seekbar)
			where = globals.modules.seekbar.seekbar;
		while(where.parentNode.tagName.toLowerCase() == "object")
			where = where.parentNode;
		utils.insertAfter(this.subtitleholder, where);
		this.subtitleholder.appendChild(this.NO_SUBTITLES);
		this.currentsubtitles = this.NO_SUBTITLES;
	
		globals.modules.fullscreen.doResize();
	};
	Subtitles.prototype.createErrorsHolder = function createErrorsHolder()
	{
		this.errorsholder = document.createElement('div');
		this.errorsholder.className = "subtitle_errors";
		var where = globals.flashmovie;
		if (globals.modules.seekbar && globals.modules.seekbar.seekbar)
			where = globals.modules.seekbar.seekbar;
		while(where.parentNode.tagName.toLowerCase() == "object")
			where = where.parentNode;
		utils.insertAfter(this.errorsholder, where);
	
		globals.modules.fullscreen.doResize();
	};
	Subtitles.prototype.transcriptError = function transcriptError(message)
	{
		if (!this.errorsholder)
			this.createErrorsHolder();
		var pre = document.createElement("pre");
		pre.appendChild(document.createTextNode(message));
		this.errorsholder.appendChild(pre);
	
		globals.modules.fullscreen.doResize();
	};
	
	Subtitles.prototype.setupSubtitles = async function setupSubtitles()
	{
		this.removeSubtitles();
	
		if (!this.enabled)
			return;
	
		this.createSubtitleHolder();
		this.setSubtitles(document.createTextNode("Loading subtitles..."));
		
		try {
			await this.loadCharacters();
			await this.reloadSubs();
		} catch (e) {
			this.removeSubtitles();
			if (this.testsubs)
				this.transcriptError(e.toString());
		}
	};
	Subtitles.prototype.loadCharacters = async function loadCharacters() {
		if (this.charsready)
			return;
	
		var xml = await utils.downloadWiki('Subtitles:Characters');
		xml = utils.parseWikiXML(xml);
	
		this.characters = {
			sfx: {
				color: "#FFF",
				sfx: true,
				name: {en: ""}
			}
		};
		var speakers = xml.getElementsByTagName("speaker");
		for (var i = 0; i < speakers.length; i++)
		{
			var speakername = speakers[i].getAttribute("id");
			this.characters[speakername] = {color: speakers[i].getAttribute("color"), sfx: speakers[i].hasAttribute("sfx"), name: {en: ""}};
			var names = speakers[i].getElementsByTagName("name");
			for (var j = 0; j < names.length; j++)
			{
				var lang = names[j].getAttribute("xml:lang");
				if (names[j].firstChild && (names[j].firstChild.nodeType == xml.TEXT_NODE || names[j].firstChild.nodeType == xml.CDATA_SECTION_NODE))
					this.characters[speakername].name[lang] = names[j].firstChild.nodeValue;
			}
		}
		this.charsready = true;
	}
	Subtitles.prototype.reloadSubs = async function reloadSubs()
	{
		if (!this.charsready)
			return;
		this.subsready = false;
	
		this.removeSubtitles();
		this.createSubtitleHolder();
		this.setSubtitles(document.createTextNode("Loading subtitles..."));
	
		var xml;
		if (!this.testsubs)
			xml = await utils.downloadWiki('Subtitles:' + globals.filename + '/' + this.language);
		else
			xml = this.testsubsdata;
		xml = utils.parseWikiXML(xml);
		this.parseTranscript(xml);
	
		this.subsready = true;
	};
	
	Subtitles.prototype.parseTranscript = function parseTranscript(xml)
	{
		// set some defaults
		if (!xml.documentElement.getAttribute("xml:lang")) xml.documentElement.setAttribute("xml:lang", this.language);
		if (!xml.documentElement.getAttribute("dir"))      xml.documentElement.setAttribute("dir",      "ltr");
		// inherit languages to all subnodes
		this.inheritLanguages(xml.documentElement);
		// now parse the lines into divs and get start and end frames
		var lines = xml.getElementsByTagName("line");
		var previousEnd = NaN;
		this.transcript = [];
		for (var i = 0; i < lines.length; i++)
		{
			var line = {};
			// ignore lines with missing start/end values
			// so you can add all the lines and not worry about timing them until later
			if (!lines[i].getAttribute("start") || !lines[i].getAttribute("end"))
				continue;
			line.start = parseInt(lines[i].getAttribute("start"), 10);
			line.end = parseInt(lines[i].getAttribute("end"), 10);
			if (this.testsubs)
			{
				if (isNaN(line.start))
					this.transcriptError("Start value \"" + lines[i].getAttribute("start") + "\" is not a number");
				if (isNaN(line.end))
					this.transcriptError("End value \"" + lines[i].getAttribute("end") + "\" is not a number");
				if (line.end < line.start)
					this.transcriptError("Line beginning frame " + line.start + " ends before it begins.");
				if (line.start < previousEnd)
					this.transcriptError("Line beginning frame " + line.start + " starts before the previous frame ends.");
				previousEnd = line.end;
			}
			line.text = this.importNodes(lines[i]);
			this.transcript.push(line);
		}
	};
	Subtitles.prototype.inheritLanguages = function inheritLanguages(node)
	{
		for (var i = node.firstChild; i; i = i.nextSibling)
		{
			if (i.nodeType == i.ELEMENT_NODE)
			{
				if (!i.hasAttribute("xml:lang")) i.setAttribute("xml:lang", node.getAttribute("xml:lang"));
				if (!i.hasAttribute("dir"))      i.setAttribute("dir",      node.getAttribute("dir"));
				this.inheritLanguages(i);
			}
		}
	};
	Subtitles.prototype.importNodes = function importNodes(node)
	{
		var name = node.nodeName.toLowerCase();
		if (this.characters[name])
		{
			node.setAttribute("speaker", name);
			name = "speaker";
		}
		if (name == "line" || name == "speaker")
		{
			// format the speaker appropriately as a div
			var speaker = node.getAttribute("speaker");
			if (!this.captions && (speaker == "sfx" || node.hasAttribute("sfx")))
				return document.createComment(""); // return nothing
			newNode = document.createElement("div");
			var char = this.characters[speaker];
			if (!char)
			{
				if (this.testsubs && speaker)
				{
					var line = node;
					while (line && line.nodeName != "line")
						line = line.parentNode;
					if (line)
						this.transcriptError("Line beginning frame " + line.getAttribute("start") + " has an unrecognised speaker name \"" + speaker + '"');
				}
				char = {color: "#FFF", name: {en: ""}};
			}
			if (this.colours)
				newNode.style.color = char.color;
			if (node.hasAttribute("voiceover"))
				newNode.className = "italic";
			if (node.hasAttribute("volume"))
			{
				newNode.style.fontSize = (node.getAttribute("volume") * 100) + "%";
				newNode.style.lineHeight = "1.25em";
			}
			newNode.lang = node.getAttribute("xml:lang");
			newNode.dir = node.getAttribute("dir");
			var hasSpeakerChildren = false;
			for (var i = node.firstChild; i; i = i.nextSibling)
			{
				if (i.nodeType == i.ELEMENT_NODE)
				{
					newNode.appendChild(this.importNodes(i));
					var a = i.nodeName.toLowerCase();
					if (a == "line" || a == "speaker" || this.characters[a])
						hasSpeakerChildren = true;
				}
				else if (i.nodeType == i.TEXT_NODE || i.nodeType == i.CDATA_SECTION_NODE)
					newNode.appendChild(document.importNode(i, true));
			}
			if (!hasSpeakerChildren)
			{
				// this is a normal text node - do some extra text stuff
				if (char.sfx || node.hasAttribute("sfx"))
				{
					newNode.insertBefore(document.createTextNode('('), newNode.firstChild);
					newNode.appendChild(document.createTextNode(')'));
					newNode.className = "italic";
				}
				if (this.names == 2 || (node.hasAttribute("voiceover") && this.names == 1))
				{
					// find the language with the longest prefix match
					// fall back to "en" if none found
					var bestmatch = "en";
					var langbits = node.getAttribute("xml:lang").split("-");
					for (i = langbits.length; i >= 1; i--)
					{
						var lang = langbits.slice(0, i).join("-");
						if (char.name[lang])
						{
							bestmatch = lang;
							break;
						}
					}
					if (char.name[bestmatch] != '')
						newNode.insertBefore(document.createTextNode(char.name[bestmatch] + ": "), newNode.firstChild);
				}
			}
			return newNode;
		}
		else
		{
			// check element blacklist
			if (name == "script" ||
			    name == "style"  ||
			    name == "object" ||
			    name == "param"  ||
			    name == "embed"  ||
			    name == "a"      ||
			    name == "img"    ||
			    name == "applet" ||
			    name == "map"    ||
			    name == "frame"  ||
			    name == "iframe" ||
			    name == "meta"   ||
			    name == "link"   ||
			    name == "form"   ||
			    name == "input")
			{
				if (this.testsubs)
					this.transcriptError("Blacklisted element \"" + name + "\" stripped.");
				return document.createComment(""); // return nothing
			}
			var newNode = document.createElement(name);
			// copy across attributes
			for (i = 0; i < node.attributes.length; i++)
			{
				name = node.attributes[i].nodeName.toLowerCase();
				// check attribute blacklist
				// javascript, and anything that might load stuff from offsite
				if (name != "href" && name != "src" && name.substring(0, 2) != "on")
				{
					if (name == "style")
					{
						// regex taken from MediaWiki Sanitizer.php
						if (!node.attributes[i].value.match(/(expression|tps*:\/\/|url\\s*\()/i))
							newNode.setAttribute("style", node.attributes[i].value);
					}
					else if (name == "xml:lang")
					{
						newNode.lang = node.attributes[i].value;
					}
					else
						newNode.setAttribute(node.attributes[i].nodeName, node.attributes[i].value);
				}
				else if (this.testsubs)
					this.transcriptError("Blacklisted attribute \"" + name + "\" stripped.");
			}
			// copy across children
			for (i = node.firstChild; i; i = i.nextSibling)
			{
				if (i.nodeType == i.ELEMENT_NODE)
					newNode.appendChild(this.importNodes(i));
				else if (i.nodeType == i.TEXT_NODE || i.nodeType == i.CDATA_SECTION_NODE)
					newNode.appendChild(document.importNode(i, true));
			}
			return newNode;
		}
		return document.createComment(""); // fallthrough
	};
	
	Subtitles.prototype.update = async function update()
	{
		if (!this.enabled || !this.charsready || !this.subsready || !this.subtitleholder)
			return;
	
		var frame = await utils.currentFrame();
		if (frame < 0)
			return;
		frame++; // Make 1-based
		// binary search to find the right transcript line
		var first = 0;
		var last = this.transcript.length;
		while(first < (last - 1))
		{
			var mid = (first + last) >> 1;
			if (frame >= this.transcript[mid].start)
			{
				first = mid;
				if (frame <= this.transcript[mid].end)
					break;
			}
			else
				last = mid;
		}
		// should we actually show the line?
		if(this.transcript[first] && this.transcript[first].start <= frame && this.transcript[first].end >= frame)
			this.setSubtitles(this.transcript[first].text);
		else
			this.setSubtitles(false);
	};
	
	Subtitles.prototype.setSubtitles = function setSubtitles(node)
	{
		if (!this.subtitleholder)
			return;
		if (!node)
			node = this.NO_SUBTITLES;
		if (this.currentsubtitles != node)
		{
			this.subtitleholder.replaceChild(node, this.subtitleholder.firstChild);
			this.currentsubtitles = node;
		}
	};

	// Returned by Special:Getversion
	// <versionstring>4.3.97=http://www.hrwiki.org/w/index.php?title=User:Phlip/Greasemonkey&action=raw&ctype=text/javascript&fakeextension=.user.js</versionstring>
	
	function Updates()
	{
	}
	Updates.CURRENT_VERSION = [4, 3, 97];
	Updates.CHECK_INTERVAL = 24*60*60*1000; // once per day
	Updates.prototype.load = async function load() {
		this.enabled = await utils.getPref('updates', true);
	}
	Updates.prototype.init = function init()
	{
		// We don't need to do this update checking on Chrome - the Chrome Web Store
		// will handle that for us
		if (!utils.useGMFunctions)
		{
			delete globals.modules.updates;
			return;
		}
	
		this.setting_enabled = globals.modules.settingspane.addCheckbox('updates', "Check for updates", "Regularly check for updates to the All-in-one script", this.enabled);
	
		/*no await*/ this.doCheck();
	};
	Updates.prototype.updateSettings = function updateSettings()
	{
		this.enabled = this.setting_enabled.checked;
		utils.setPref("updates", this.enabled);
		this.doCheck();
	};
	
	Updates.prototype.doCheck = async function doCheck()
	{
		if (this.updatelink) {
			this.updatelink.parentNode.removeChild(this.updatelink);
			this.updatelink = null;
		}
	
		if (!this.enabled)
			return;
	
		var str;
		if (Date.now() - (await utils.getPref("lastchecktime", 0)) > Updates.CHECK_INTERVAL)
		{
			str = await utils.downloadPage("http://www.hrwiki.org/wiki/Special:Getversion/User:Phlip/Greasemonkey?cachedodge=" + Math.random());
			str = str.text;
			utils.setPref("lastchecktime", Date.now());
			utils.setPref("lastcheckstring", str);
		}
		else
			str = await utils.getPref("lastcheckstring", "");
	
		var parts = str.split("@@");
		for (var i = 0; i < parts.length; i++)
		{
			var matches = parts[i].match(/^(\d+)\.(\d+)\.(\d+)=(.*)$/);
			if (!matches) continue;
			if (matches[1] > Updates.CURRENT_VERSION[0] ||
			    (matches[1] == Updates.CURRENT_VERSION[0] && matches[2] > Updates.CURRENT_VERSION[1]) ||
			    (matches[1] == Updates.CURRENT_VERSION[0] && matches[2] == Updates.CURRENT_VERSION[1] && matches[3] > Updates.CURRENT_VERSION[2]))
			{
				var updatelink = document.createElement('a');
				updatelink.href=matches[4];
				updatelink.style.display = "block";
				updatelink.style.position = 'fixed';
				updatelink.style.left = '0px';
				updatelink.style.top = '0px';
				updatelink.style.border = 'none';
				updatelink.style.zIndex = 1;
				var updatelinkimage = document.createElement('img');
				updatelinkimage.src = globals.images.update;
				var oldversionstr = Updates.CURRENT_VERSION[0] + "." + Updates.CURRENT_VERSION[1] + "." + Updates.CURRENT_VERSION[2];
				var newversionstr = matches[1] + "." + matches[2] + "." + matches[3];
				updatelinkimage.title = "Click here to update from script version " + oldversionstr + " to " + newversionstr;
				updatelinkimage.style.display = "block";
				updatelinkimage.style.border = 'none';
				updatelink.appendChild(updatelinkimage);
				document.body.appendChild(updatelink);
				this.updatelink = updatelink;
				return;
			}
		}
	};
	
	Updates.prototype.cacheDodge = function cacheDodge()
	{
		utils.setPref("lastchecktime", 0);
		/*no await*/ this.doCheck();
	}

	// Podstar/Videlectrix (stock IIS), HRWiki and stock Apache error pages, respectively. Don't do anything on those pages.
	if (document.title == "The page cannot be found" || document.title == "Homestar Runner Wiki - 404 Not Found" || document.title == "404 Not Found")
		return;
	
	var utils = new Utils();
	var globals = new Globals();
	var playercomm = new PlayerComm();
	playercomm.init();
	await globals.initModules();
})();

/*
*/
Personal tools