There are 17 previous versions of this script.
Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)
// ==UserScript==
// @name 4shadow
// @namespace http://img.4chan.org/
// @description One 4chan updater clone to rule them all.
// @include http://*.4chan.org/*/res/*.html*
// @include http://*.4chan.org/*/imgboard.php*
// @version 7.0
// @author Chris Done, Todd Kirby
// ==/UserScript==
// Copyright (c) 2009, Todd Kirby
// Released under the GNU General Public License
// http://www.gnu.org/copyleft/gpl.html
// WTF aren't these defined by firefox?
const PAGE_OK = 200, PAGE_NOT_MODIFIED = 304, PAGE_NOT_FOUND = 404;
XMLHttpRequest.DONE = 4;
const INITIAL_POLL_INTERVAL = 10; // if the user doesn't set something else in prefs.
const SEND_DEBUG_MESSAGES_TO_CONSOLE = false;
// if the user wants debug messages, use firebug console if available, else fall back to GM_log.
if (!SEND_DEBUG_MESSAGES_TO_CONSOLE) { GM_log = function(){} }
const HIGHLIGHT_NEW_POSTS = false; // For debugging but may eventually be a user setting. Right now it colors each grab of posts a different random color to allow to see what got picked up at each interval.
function Updater() {
var me = this;
var running = false;
var lastUpdate = null;
var timer = null;
var prefsPanel = null;
var formPanel = null;
var button = null;
var prefs = null;
this.run = function() {
prefs = new PrefsList();
// prefs.add('name', default_value, onchange_callback);
// TODO: callbacks should pass value back
prefs.add('auto-update', true);
prefs.add('disable-forms', true);
//prefs.add('run-scripts', false);
prefs.add('thread-died-title', '(THREAD DIED)');
prefs.add('thread-died-notice', 'This thread no longer exists');
prefs.add('poll-interval', INITIAL_POLL_INTERVAL, me.changeInterval);
timer = new Timer(poll_server, prefs.get('poll-interval'));
lastUpdate = new Date(document.lastModified);
formPanel = new FormPanel(prefs);
//prefs.add('ajax-form-submit', false);
prefs.add('show-form-instructions', false, formPanel.displayFormGarbage);
prefs.add('popup-reply-form', false);
prefs.add('clear-form-after-submit', true);
prefs.add('hide-form-after-submit', true);
prefs.get('popup-reply-form') && formPanel.init();
prefsPanel = new PrefsPanel(prefs);
button = new UpdaterButton(function(e) {
if (prefs.get('popup-reply-form') && e.shiftKey) {
formPanel.show();
return;
}
if (e.ctrlKey) {
prefsPanel.show();
return;
}
me.toggle();
});
prefs.get('auto-update') ? start() : stop();
}
poll_server = function() {
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == XMLHttpRequest.DONE) {
switch (request.status) {
case PAGE_OK:
GM_log('updating page');
if (request.responseText) { // GET request
updatePageFromText(request.responseText);
setLastUpdated(new Date(request.getResponseHeader('Last-Modified')));
timer.start();
} else { // HEAD request
updatePageFromIframe(request);
// NOTE: The callback function handleIframeLoaded, set up by
// updatePageFromIframe is responsible for starting timer
// up again when it's done fetching and rendering the iframe.
}
break;
case PAGE_NOT_MODIFIED:
GM_log('page not modified');
timer.start();
break;
case PAGE_NOT_FOUND:
GM_log('thread died');
doThreadDied();
break;
default:
GM_log('unhandled server response');
timer.start();
break;
}
}
}
timer.stop(); // ...until request is handled. This stops the update timer until
// the previous page is actually fetched and rendered. This
// insures that no more page requests are fired until the
// previous request is handled completely.
//var request_type = prefs.get('run-scripts') ? 'HEAD' : 'GET';
var request_type = 'GET';
try {
request.open(request_type, window.location.href.replace(/#.*/, ''), true);
request.setRequestHeader('If-Modified-Since', lastUpdate.toUTCString());
request.send(null);
GM_log('polling server...');
} catch (e) {
GM_log('Error connecting to server');
timer.start();
}
}
this.isRunning = function() {
return running;
}
this.changeInterval = function(new_poll_interval) {
timer.setInterval(new_poll_interval);
if (me.isRunning()) {
timer.restart();
}
}
setLastUpdated = function(when) {
lastUpdate = when;
}
start = function() {
button.setStarted();
timer.start();
running = true;
}
stop = function() {
button.setStopped();
timer.stop();
running = false;
}
this.toggle = function() {
this.isRunning() ? stop() : start();
}
updatePageFromText = function (text) {
var container, last_item, add_post, current_reply_count, new_replies, buffer;
// grab just the element containing the replies
delform = text.match(/<form name=\"delform\".*?>[\s\S]*?<\/form>/gm);
current_reply_count =
$X("count(//form[@name='delform']//td[@class='reply' or @class='replyhl'])");
// grab each reply row (<tr></tr>) from the reply container
// FIXME: This doesn't grab inter-table anchor links.
// FIXME: Need to deal with potential deleted posts
replies = delform[0].match(/<tr>[\s\S]*?<\/tr>/gm);
if (replies.length > current_reply_count) {
new_replies = replies.slice(current_reply_count);
new_replies = new_replies.map(
function(item) {
// add a new table element around the reply. This is kind of a hack
// since we need some element to .innerHTML into, we just pull the row
// from the post and then reinsert it into a new <table> element
table = document.createElement('table');
table.innerHTML = item;
if (HIGHLIGHT_NEW_POSTS) {
table.setAttribute('style', 'background-color: ' + random_color());
}
return (table);
});
add_post = function(item) {
// 4chan_ext doesn't get to run on ajax requests so we only have to
// deal with the delform container case.
// FIXME: container and last_item need to be calculated every time.
container = $X("//form[@name='delform']");
last_item = $X("//br[@clear='left']", document);
container.insertBefore(item, last_item);
};
new_replies.forEach(add_post);
}
// run 4chan's script to highlight replies
try { unsafeWindow.init(); } catch(e) {}
}
updatePageFromIframe = function() {
subdomain = getSubdomain(window.location.href);
redirector_subdomain = getSubdomain($X("//form[@name='delform']").action);
// HACK HACK: we can't reload the main page into an iframe due to
// a recursion guard mechanism in Fireflodge so we ask for the page from
// the host that replies get sent to and let it redirect us back to the
// current page.
url = window.location.href.replace(/#.*/, '').replace(subdomain,redirector_subdomain);
// NOTE: handleIframeLoaded callback restarts timer when page rendering is done.
dom_from_hidden_iframe(url, 'fs-updater-iframe', me.handleIframeLoaded);
}
this.handleIframeLoaded = function(iframe_doc) {
var container, last_item, add_post, current_reply_count, new_replies;
// dont try to run on pages we don't recognize (404...)
if (!$X("id('header')")) {
return;
}
// FIXME: We only need to caclulate container once. 4chan extension is either
// going to be on or off for our whole run.
if ( (container = $X("//div[@class='4chan_ext_thread_replies']")) ) {
add_post = function(item) { container.appendChild(item); };
} else {
container = $X("//form[@name='delform']");
last_item = $X("//form[@name='delform']//table[last()-1]");
add_post = function(item) { container.insertBefore(item, last_item.nextSibling); };
}
current_reply_count =
$X("count(//form[@name='delform']//td[@class='reply' or @class='replyhl'])");
// FIXME: This doesn't grab inter-table anchor links.
new_replies = $x("//form[@name='delform']//table[position() > " +
current_reply_count +
"]//td[@class='reply' or @class='replyhl']/ancestor::table",
iframe_doc);
if (HIGHLIGHT_NEW_POSTS) {
new_replies = new_replies.map(function(item) {
item.setAttribute('style', 'background-color: ' + random_color);
return item;
} );
}
new_replies.forEach(add_post);
this.setLastUpdated(new Date(iframe_doc.lastModified));
timer.start();
}
doThreadDied = function() {
stop();
button.setDied();
// add thread died message to page title
document.title = prefs.get('thread-died-title');
// disable forms if the user requested
prefs.get('disable-forms') && disableForms();
}
disableForms = function() {
// change submit button title so the user knows why it's disabled
$X("/html/body//form[@name='post']//input[@type='submit']").value =
prefs.get('thread-died-title');
// disable all inputs but textarea so the user can copy their text still
// if they want to save or use it somewhere else.
inputs = $x("/html/body//form[@name='post']//input").forEach(
function(input) { input.disabled = true });
}
}
function Timer(callback, interval) {
var callback = callback;
var interval = interval;
var id = null;
const ONE_SECOND_IN_MILLISECONDS = 1000; // milliseconds
this.isRunning = function() { return id; }
this.setInterval = function(new_interval) { interval = new_interval; }
this.start = function() {
if (this.isRunning()) {
GM_log('timer already started, skipping');
} else {
id = window.setInterval(callback, ONE_SECOND_IN_MILLISECONDS * interval);
GM_log('starting timer');
}
}
this.stop = function() {
GM_log('stopping timer');
clearInterval(id);
id = null;
}
this.restart = function () {
GM_log('restarting timer');
this.stop();
this.start();
}
}
function PrefsList() {
this.add = function(pref_name, default_value, onchange) {
this[pref_name] = { 'default_value' : default_value };
if (onchange !== undefined) {
this[pref_name].onchange = onchange;
}
}
this.get = function(pref_name) {
return GM_getValue(pref_name, this[pref_name].default_value);
}
this.set = function(pref_name, new_value) {
if (this[pref_name].onchange !== undefined) {
this[pref_name].onchange(new_value);
}
GM_setValue(pref_name, new_value);
}
}
function PrefsPanel(prefs) {
var me = this; // closure magic so we can us this object in event handlers.
var inited = false;
var prefs = prefs;
this.init = function() {
GM_addStyle(panel_css);
GM_addStyle(prefs_panel_css);
prefs_div = document.createElement('div');
prefs_div.id = 'fs-prefs';
prefs_div.innerHTML = prefs_panel_html;
$X('/html/body').appendChild(prefs_div);
$X("id('fs-prefs-submit')").addEventListener('click', this.save, true);
$X("id('fs-prefs-cancel')").addEventListener('click', this.cancel, true);
inited = true;
}
this.save = function() {
// Use 'me' on any functions that may be called by event handlers
me.store();
me.hide();
}
this.cancel = function() {
me.hide();
}
this.store = function() {
poll_interval_select = $X("id('poll-interval-select')");
prefs.set('poll-interval', poll_interval_select[poll_interval_select.selectedIndex].value);
prefs.set('auto-update', $X("id('auto-update-checkbox')").checked);
//prefs.set('ajax-form-submit', $X("id('ajax-form-submit-checkbox')").checked);
prefs.set('disable-forms', $X("id('disable-forms-checkbox')").checked);
//prefs.set('run-scripts', $X("id('run-scripts-checkbox')").checked);
//prefs.set('show-form-instructions', $X("id('show-form-instructions-checkbox')").checked);
prefs.set('popup-reply-form', $X("id('popup-reply-form-checkbox')").checked);
prefs.set('clear-form-after-submit', $X("id('clear-form-after-submit-checkbox')").checked);
prefs.set('hide-form-after-submit', $X("id('hide-form-after-submit-checkbox')").checked);
}
this.load = function() {
$X("id('auto-update-checkbox')").checked = prefs.get('auto-update');
$X("id('popup-reply-form-checkbox')").checked = prefs.get('popup-reply-form');
//$X("id('show-form-instructions-checkbox')").checked = prefs.get('show-form-instructions');
$X("id('disable-forms-checkbox')").checked = prefs.get('disable-forms');
//$X("id('run-scripts-checkbox')").checked = prefs.get('run-scripts');
//$X("id('ajax-form-submit-checkbox')").checked = prefs.get('ajax-form-submit');
$X("id('clear-form-after-submit-checkbox')").checked = prefs.get('clear-form-after-submit');
$X("id('hide-form-after-submit-checkbox')").checked = prefs.get('hide-form-after-submit');
$X("id('poll-interval-select')//option[@value='" +
prefs.get('poll-interval') + "']").selected = true;
}
this.hide = function() {
$X("id('fs-prefs')").style.display = 'none';
}
this.show = function() {
inited || this.init();
this.load();
$X("id('fs-prefs')").style.display = 'block';
}
}
function FormPanel(prefs) {
var me = this; // closure magic so we can us this object in event handlers.
var inited = false;
var prefs = prefs;
this.setStatus = function(text, color) {
status_span = $X("id('fs-form-panel-status-text')");
status_span.innerHTML = text;
status_span.style.color = color;
}
this.submitted = function(doc) {
GM_log('remove getSubmitResponse message handler');
window.removeEventListener("message", me.getSubmitResponse, false);
}
this.getSubmitResponse = function(e) {
GM_log('Form posted. Response was: ' + e.data);
if (e.data == 'fs_success') {
me.setStatus("Reply Posted", "green");
if (prefs.get('clear-form-after-submit')) {
$X("//form[@name='post']").reset();
}
if (prefs.get('hide-form-after-submit')) {
me.hide();
}
} else {
me.setStatus(e.data, "red");
}
}
this.handleSubmit = function() {
dom_from_hidden_iframe("", 'fs-form-submit-iframe', me.submitted);
window.addEventListener("message", me.getSubmitResponse, false);
me.setStatus("Posting Reply...", "green");
return true;
}
this.init = function() {
// TODO: need to check if other panel has added panel_css already
GM_addStyle(panel_css);
GM_addStyle(form_panel_css);
form_div = document.createElement('div');
form_div.id = 'fs-updater-form';
form_div.style.display = 'none';
form_div.innerHTML = form_panel_html;
$X('/html/body').appendChild(form_div);
$X("id('fs-form-panel-closebox')").addEventListener('click', this.hide, false);
this.moveFormToUpdater();
$X("//form[@name='post']").target = 'fs-form-submit-iframe';
$X("//form[@name='post']").addEventListener('submit', me.handleSubmit, false);
this.displayFormGarbage(prefs.get('show-form-instructions'));
inited = true;
}
this.hide = function() {
$X("id('fs-updater-form')").style.display = 'none';
}
this.moveFormToUpdater = function() {
$X("id('fs-form-panel-content')").insertBefore($X("//form[@name='post']"), $X("id('fs-form-panel-status-text')"));
}
this.displayFormGarbage = function(value) {
// FIXME: use better xpath to avoid parentNode crap.
$X("//td[@class='rules']").parentNode.parentNode.parentNode.style.display = value == true ? 'block' : 'none';
}
this.show = function() {
inited || this.init();
this.setStatus("","");
$X("id('fs-updater-form')").style.display = 'block';
}
}
function UpdaterButton(callback) {
const STARTED_LABEL = 'Updater Running';
const STOPPED_LABEL = 'Updater Stopped';
const THREAD_DIED_LABEL = 'Thread Died';
GM_addStyle('#updater-button { font-family:Lucida Grande, Tahoma, Arial, Verdana; font-size: small; position:fixed; bottom:2px; right:0px; color: #FFF; padding: 1px; border: 1px;}');
GM_addStyle('#updater-button.fs-button-running { background-color: #0A0; }');
GM_addStyle('#updater-button.fs-button-died { background-color: #A00; }');
GM_addStyle('#updater-button.fs-button-stopped { background-color: #000; }');
container_div = document.createElement('div');
container_div.innerHTML = '<button id="updater-button" title="Ctrl-Click for Settings"></button>';
$X('/html/body').appendChild(container_div);
$X("id('updater-button')").addEventListener('click', callback, true);
this.setStarted = function() {
butt = $X("id('updater-button')");
butt.blur();
butt.innerHTML = STARTED_LABEL;
butt.className = 'fs-button-running';
}
this.setStopped = function() {
butt = $X("id('updater-button')");
butt.blur();
butt.innerHTML = STOPPED_LABEL;
butt.className = 'fs-button-stopped';
}
this.setDied = function() {
butt = $X("id('updater-button')");
butt.blur();
butt.innerHTML = THREAD_DIED_LABEL;
butt.className = 'fs-button-died';
}
}
// UTILITY FUNCTIONS
// list nodes matching this expression, optionally relative to the node `root'
function $x(xpath, root) {
var doc = root ? root.evaluate ? root : root.ownerDocument : document, next;
// FIXME: ANY_TYPE returns unordered iterators which can, in theory lead to out of
// order posts but I've never seen it happen in practice.
var got = doc.evaluate(xpath, root||doc, null, XPathResult.ANY_TYPE, null), result = [];
switch (got.resultType) {
case got.STRING_TYPE:
return got.stringValue;
case got.NUMBER_TYPE:
return got.numberValue;
case got.BOOLEAN_TYPE:
return got.booleanValue;
default:
while ((next = got.iterateNext()))
result.unshift(next);
return result;
}
}
function $X(xpath, root) {
var got = $x(xpath, root);
return got instanceof Array ? got[0] : got;
}
function getSubdomain(str) {
var re = new RegExp('^(?:f|ht)tp(?:s)?\://([^/]+)', 'im');
return str.match(re)[1].split('.')[0];
}
function dom_from_hidden_iframe(url, iframe_id, callback)
{
function loaded(e) {
try {
e.target.removeEventListener('load', loaded, false);
// avoid racing with GM's DOMContentLoaded callback
setTimeout(function() { callback(e.target.contentDocument); }, 10);
} catch(e) {
GM_log('Error in iframe loaded handler');
}
};
var iframe = $X("id('" + iframe_id + "')");
if (iframe == undefined) {
iframe = document.createElement('iframe');
iframe.style.height = iframe.style.width;
iframe.style.visibility = 'hidden';
iframe.style.position = 'absolute';
iframe.id = iframe_id;
iframe.name = iframe_id;
document.body.appendChild(iframe);
}
iframe.addEventListener('load', loaded, false);
GM_log('loading new content into iframe');
if (url) {
try {
iframe.src = url;
} catch(e) {
GM_log('Error in another script in the iframe');
}
}
}
function pad(number,length) {
var str = '' + number;
while (str.length < length)
str = '0' + str;
return str;
}
function random_color() {
return "#" +
pad(Math.floor(Math.random()*256).toString(16),2) +
pad(Math.floor(Math.random()*256).toString(16),2) +
pad(Math.floor(Math.random()*256).toString(16),2);
}
// RESOURCES
// TODO: define these externally and @resource them in
const prefs_panel_css = (<r><![CDATA[
#fs-prefs { text-align: left; position:fixed; bottom:25px; right:0px; display: block}
select.fs_select { color:#333; font-size:100%; margin:1px 0; padding:1px 0 0; border-bottom:1px solid #ddd; border-left:1px solid #c3c3c3; border-right:1px solid #c3c3c3; border-top:1px solid #7c7c7c; }
input.fs-checkbox { display:block; height:13px; line-height:1.4em; margin:6px 0 0 3px; width:13px; }
label.choice { color:#444; display:block; font-size:100%; line-height:1.4em; margin:-1.55em 0 0 25px; padding:4px 0 5px; width:90%; }
]]></r>).toString();
const form_panel_css = (<r><![CDATA[
#fs-updater-form { text-align: left; position:fixed; bottom:25px; right:0px; display: block }
]]></r>).toString();
const panel_css = (<r><![CDATA[
.fs-panel-title { color: #444; font-weight: bold; margin:0; min-height:0; text-align: center; text-decoration:none;}
.fs-outerpair1 { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNC8yNS8wNk7vxRQAAAAgdEVYdFNvZnR3YXJlAE1hY3JvbWVkaWEgRmlyZXdvcmtzIE1Yu5EqJAAAAGdJREFUeJxNzksKAkEMhOGvHzqKC126c87g/e8kuFIYpt2kJYEfAlVJVcEZA3swd9BwQ0cN5hSMhhVLMpVk0vDECYckjknHA2+84kDqs3fc4wNs+OITkVvFFZdgiah/n4pj0IOWupQfFtoS9rTbP5UAAAAASUVORK5CYII%3D) right top no-repeat;}
.fs-outerpair2 {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNC8yNS8wNk7vxRQAAAAgdEVYdFNvZnR3YXJlAE1hY3JvbWVkaWEgRmlyZXdvcmtzIE1Yu5EqJAAAAGZJREFUeJxNzsEKwkAMhOFvu2tVBI/iRfr+D6il2m69pJDAkIH5maTgihseeGHCE3eMFS10xgUjKlYsFUOo4RS+44dPRUlQwY4NM94HIO2Obwby7AlYMLcUbBH0+GcQ5qjNDWucLH966BpsOjDX/QAAAABJRU5ErkJggg%3D%3D) left bottom no-repeat; padding-top: 8px; padding-left: 8px; }
.fs-shadowbox { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAYAAACadoJwAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNC8yNS8wNk7vxRQAAAAgdEVYdFNvZnR3YXJlAE1hY3JvbWVkaWEgRmlyZXdvcmtzIE1Yu5EqJAAADUxJREFUeJzt3btO41AUQNGTUf6/4xcRBRWCgREPT5EYMkGiIdoRmbUky47SnHbr3mtvlmUZAACAU9hsNlczczszNzNzvb/fzszdzDz+OuNsAADAf0aAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAABkBAgAAJARIAAAQEaAAAAAGQECAACc0vLVJUAAAIBTWr76U4AAAACntK52vB08vxMgAADAKb3MzOvsAuRThAgQAADglP7sr+f5CJH3CNluNpur880GAAD8cOt2q5fZhcfDzPyemaf973VFZJmZZTszt+eZEwAA+OHWrVXrdqs/s4uPu5m5n88R8radmZt+TgAA4AIcB8jz7KLjfnYR8jBH27G2M3PdzwkAAFyIw7devc4uOJ5mFx/rKsgaIIsVEAAA4DsO33L1Oh9nQdYQeZqDcyDOgAAAAN91uBVrjZDn/bU+v81+C9bdOSYEAAAuynGErG/G+uebINuZeTzLeAAAwKU5/gr68X3+AiuZhS0L93K2AAAAAElFTkSuQmCC) bottom right;
}
.fs-innerbox { position: relative; left: -8px; top: -8px; }
.fs-panel-closebox { float:right; margin-left:-18px; padding-right: 5px }
]]></r>).toString();
const form_panel_html = (<r><![CDATA[
<div class="fs-outerpair1">
<div class="fs-outerpair2">
<div class="fs-shadowbox">
<div class="fs-innerbox" style="border:1px solid">
<table cellpadding="0" cellspacing="0" style="margin: 0; padding 0">
<tr>
<td style="border-bottom: 1px solid #000; background-color:#DEDEDE">
<div class="fs-panel-title">Reply Form
</td>
<td width="16" style="border-bottom: 1px solid #000; background-color:#DEDEDE">
<img width="16" height="16" class="fs-panel-closebox" id="fs-form-panel-closebox" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABGdBTUEAALGPC/xhBQAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEgAACxIB0t1+/AAAAAd0SU1FB9EHDRMNKMuHqnkAAAC2SURBVHicjVKxEcQgDFP+sgMNFSPQsELWyzRAQ8MKNKyQEXCRL5wjHM9zqJJtZFl3bNZaLOM4jh3AuuYRADjPc/7UOcd799q6rmvF5FMZERGREIIa1HIgKKVIKQFIKUspXTl2yDkzV0oppZjnnMcObJ1Sai9OKXUnvaFb30mzd9Bat2Ot9Sy0MYZ5jDHGyNwY8zd0CAFACIG3tuUgA3e99+24K1+Bc+438RCPYP3/bfd9Lz5lfAGcP5gicQQKTAAAAABJRU5ErkJggg==">
</td>
</tr>
<tr style="border-bottom: 1px solid #000">
<td style="border-bottom: 1px solid #000; background-color:white" colspan="2" align="center">
<div id="fs-form-panel-content" style="margin:10px">
<span id="fs-form-panel-status-text" style="font-weight: bold; font-size: 1.2em; margin:10px"></span>
</div>
</td>
<tr>
<table>
</div>
</div>
</div>
</div>
]]></r>).toString();
const prefs_panel_html = (<r><![CDATA[
<div class="fs-outerpair1">
<div class="fs-outerpair2">
<div class="fs-shadowbox">
<div class="fs-innerbox">
<table cellpadding="0" cellspacing="0" border="1" style="margin: 0; padding 0">
<tr>
<td align="center">
<div id="fs-form-container" style="background:#fff;border:1px solid #ccc;margin:0 auto;text-align:left;font-family:Lucida Grande, Tahoma, Arial, Verdana, sans-serif; font-size:small;">
<div style="background-color:#dedede" class="fs-panel-title">4Shadow Settings</div>
<div style="margin:10px">
<fieldset><legend>Updater Settings</legend>
<span style="color:#444;margin:-1.55em 0 0 0; ">Check for new replies every </span><select class="element fs_select" id="poll-interval-select" name="poll-interval-select">
<option value="5">5 seconds</option>
<option value="6">6 seconds</option>
<option value="7">7 seconds</option>
<option value="8">8 seconds</option>
<option value="9">9 seconds</option>
<option value="10">10 seconds</option>
<option value="15">15 seconds</option>
<option value="20">20 seconds</option>
<option value="30">30 seconds</option>
<option value="45">45 seconds</option>
<option value="60">minute</option>
<option value="120">2 minutes</option>
<option value="300">5 minutes</option>
<option value="600">10 minutes</option>
<option value="1800">30 minutes</option>
<option value="3600">hour</option>
</select>
<span>
<div style="margin:15px;"></div>
<input id="auto-update-checkbox" name="auto-update-checkbox" class="element fs-checkbox" type="checkbox" value="" />
<label class="choice" for="auto-update-checkbox">Start updating when page loads</label>
</span>
<span>
<input id="disable-forms-checkbox" name="disable-forms-checkbox" class="element fs-checkbox" type="checkbox" value="1" />
<label class="choice" for="disable-forms-checkbox">Disable forms when thread 404s</label>
</span>
<span>
<input disabled="disabled" id="run-scripts-checkbox" name="run-scripts-checkbox" class="element fs-checkbox" type="checkbox" value="1" />
<label class="choice" for="run-scripts-checkbox">Run scripts (broken by changes to 4chan servers)</label>
</span>
</fieldset>
<p/>
<fieldset><legend>Reply Settings</legend>
<!--<span>
<input id="ajax-form-submit-checkbox" name="ajax-form-submit-checkbox" class="element fs-checkbox" type="checkbox" value="1" />
<label class="choice" for="ajax-form-submit-checkbox">Post replies without reloading</label>
</span>-->
<span>
<input id="popup-reply-form-checkbox" name="popup-reply-form-checkbox" class="element fs-checkbox" type="checkbox" value="1" />
<label class="choice" for="popup-reply-form-checkbox">Popup reply form on shift-click</label>
</span>
<span>
<input id="clear-form-after-submit-checkbox" name="clear-form-after-submit-checkbox" class="element fs-checkbox" type="checkbox" value="1" />
<label class="choice" for="clear-form-after-submit-checkbox">Clear form after successful submit</label>
</span>
<span>
<input id="hide-form-after-submit-checkbox" name="hide-form-after-submit-checkbox" class="element fs-checkbox" type="checkbox" value="1" />
<label class="choice" for="hide-form-after-submit-checkbox">Hide form after successful submit</label>
</span>
<!--<span>
<input id="show-form-instructions-checkbox" name="show-form-instructions-checkbox" class="element fs-checkbox" type="checkbox" value="1" />
<label class="choice" for="show-form-instructions-checkbox">Show Form Instructions</label>
</span>-->
</fieldset>
</div>
<div style="margin: 5px" align="center">
<input id="fs-prefs-submit" class="button-text" type="button" name="submit" value="Save" />
<input id="fs-prefs-cancel" class="button-text" type="button" name="cancel" value="Cancel" />
</div>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
]]></r>).toString();
// SCRIPT ENTRY POINT
window.addEventListener("load", run, false);
function run() {
if (window.frameElement) {
try {
if (window.frameElement.id == 'fs-updater-iframe') {
// bail if called from within fs-updater-iframe since it's just us
// loading the page into an iframe to grab new posts.
return;
}
} catch (e) {
// check for error messages and pass them up
var message = null;
var match = document.body.innerHTML.match(/<font .*color="red" size=".*?"><b>(Error:.*?)<br>/);
if (match) {
message = match[1];
} else {
message = 'fs_success';
}
window.parent.postMessage(message, '*');
return;
}
}
if (!document.getElementById('navtop')) return;
updater = new Updater();
updater.run();
}