From 6a63650fa3cfaf3df96bae6f176df7eaa0534690 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sun, 29 Jul 2012 00:46:30 +0200 Subject: [PATCH] Start 4chan X rewrite. Fix Chrome's install warning. --- 4chan_x.user.js | 5177 +---------------------------------------------- Cakefile | 54 +- changelog | 4 +- script.coffee | 4140 +------------------------------------ 4 files changed, 50 insertions(+), 9325 deletions(-) diff --git a/4chan_x.user.js b/4chan_x.user.js index a58ef7e37..9377dabf2 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -1,21 +1,17 @@ // ==UserScript== -// @name 4chan x -// @version 2.34.3 -// @namespace aeosynth -// @description Adds various features. -// @copyright 2009-2011 James Campos -// @copyright 2012 Nicolas Stepien -// @license MIT; http://en.wikipedia.org/wiki/Mit_license -// @include http://boards.4chan.org/* -// @include https://boards.4chan.org/* -// @include http://images.4chan.org/* -// @include https://images.4chan.org/* -// @include http://sys.4chan.org/* -// @include https://sys.4chan.org/* -// @run-at document-start -// @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js -// @downloadURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js -// @icon http://mayhemydg.github.com/4chan-x/favicon.gif +// @name 4chan X alpha +// @version 3.0.0 +// @description Adds various features. +// @copyright 2009-2011 James Campos +// @copyright 2012 Nicolas Stepien +// @license MIT; http://en.wikipedia.org/wiki/Mit_license +// @match *://boards.4chan.org/* +// @match *://images.4chan.org/* +// @match *://sys.4chan.org/* +// @run-at document-start +// @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js +// @downloadURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js +// @icon http://mayhemydg.github.com/4chan-x/favicon.gif // ==/UserScript== /* LICENSE @@ -23,7 +19,7 @@ * Copyright (c) 2009-2011 James Campos * Copyright (c) 2012 Nicolas Stepien * http://mayhemydg.github.com/4chan-x/ - * 4chan X 2.34.3 + * 4chan X 3.0.0 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -77,7 +73,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, AutoGif, Conf, Config, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, ImageExpand, ImageHover, Keybinds, Main, Menu, Nav, Options, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, StrikethroughQuotes, ThreadHiding, ThreadStats, Time, TitlePost, UI, Unread, Updater, Watcher, d, g, _base; + var $, $$, Conf, Config, UI, d, g, _base; Config = { main: { @@ -203,6 +199,10 @@ } }; + if (!/^(boards|images|sys)\.4chan\.org$/.test(location.hostname)) { + return; + } + Conf = {}; d = document; @@ -544,5143 +544,4 @@ return Array.prototype.slice.call(root.querySelectorAll(selector)); }; - Filter = { - filters: {}, - init: function() { - var boards, filter, hl, key, op, regexp, stub, top, _i, _len, _ref, _ref1, _ref2, _ref3, _ref4; - for (key in Config.filter) { - this.filters[key] = []; - _ref = Conf[key].split('\n'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - filter = _ref[_i]; - if (filter[0] === '#') { - continue; - } - if (!(regexp = filter.match(/\/(.+)\/(\w*)/))) { - continue; - } - filter = filter.replace(regexp[0], ''); - boards = ((_ref1 = filter.match(/boards:([^;]+)/)) != null ? _ref1[1].toLowerCase() : void 0) || 'global'; - if (boards !== 'global' && boards.split(',').indexOf(g.BOARD) === -1) { - continue; - } - try { - if (key === 'md5') { - regexp = regexp[1]; - } else { - regexp = RegExp(regexp[1], regexp[2]); - } - } catch (e) { - alert(e.message); - continue; - } - op = ((_ref2 = filter.match(/[^t]op:(yes|no|only)/)) != null ? _ref2[1] : void 0) || 'no'; - stub = (function() { - var _ref3; - switch ((_ref3 = filter.match(/stub:(yes|no)/)) != null ? _ref3[1] : void 0) { - case 'yes': - return true; - case 'no': - return false; - default: - return Conf['Show Stubs']; - } - })(); - if (hl = /highlight/.test(filter)) { - hl = ((_ref3 = filter.match(/highlight:(\w+)/)) != null ? _ref3[1] : void 0) || 'filter_highlight'; - top = ((_ref4 = filter.match(/top:(yes|no)/)) != null ? _ref4[1] : void 0) || 'yes'; - top = top === 'yes'; - } - this.filters[key].push(this.createFilter(regexp, op, stub, hl, top)); - } - if (!this.filters[key].length) { - delete this.filters[key]; - } - } - if (Object.keys(this.filters).length) { - return Main.callbacks.push(this.node); - } - }, - createFilter: function(regexp, op, stub, hl, top) { - var settings, test; - test = typeof regexp === 'string' ? function(value) { - return regexp === value; - } : function(value) { - return regexp.test(value); - }; - settings = { - hide: !hl, - stub: stub, - "class": hl, - top: top - }; - return function(value, isOP) { - if (isOP && op === 'no' || !isOP && op === 'only') { - return false; - } - if (!test(value)) { - return false; - } - return settings; - }; - }, - node: function(post) { - var filter, firstThread, isOP, key, result, root, thisThread, value, _i, _len, _ref; - if (post.isInlined) { - return; - } - isOP = post.ID === post.threadID; - root = post.root; - for (key in Filter.filters) { - value = Filter[key](post); - if (value === false) { - continue; - } - _ref = Filter.filters[key]; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - filter = _ref[_i]; - if (!(result = filter(value, isOP))) { - continue; - } - if (result.hide) { - if (isOP) { - if (!g.REPLY) { - ThreadHiding.hide(root.parentNode, result.stub); - } else { - continue; - } - } else { - ReplyHiding.hide(root, result.stub); - } - return; - } - $.addClass(root, result["class"]); - if (isOP && result.top && !g.REPLY) { - thisThread = root.parentNode; - if (firstThread = $('div[class="postContainer opContainer"]').parentNode) { - $.before(firstThread, [thisThread, thisThread.nextElementSibling]); - } - } - } - } - }, - name: function(post) { - return $('.name', post.el).textContent; - }, - uniqueid: function(post) { - var uid; - if (uid = $('.posteruid', post.el)) { - return uid.textContent.slice(5, -1); - } - return false; - }, - tripcode: function(post) { - var trip; - if (trip = $('.postertrip', post.el)) { - return trip.textContent; - } - return false; - }, - mod: function(post) { - var mod; - if (mod = $('.capcode', post.el)) { - return mod.textContent; - } - return false; - }, - email: function(post) { - var mail; - if (mail = $('.useremail', post.el)) { - return decodeURIComponent(mail.href.slice(7)); - } - return false; - }, - subject: function(post) { - return $('.postInfo .subject', post.el).textContent || false; - }, - comment: function(post) { - var data, i, nodes, text, _i, _ref; - text = []; - nodes = d.evaluate('.//br|.//text()', post.blockquote, null, 7, null); - for (i = _i = 0, _ref = nodes.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { - text.push((data = nodes.snapshotItem(i).data) ? data : '\n'); - } - return text.join(''); - }, - country: function(post) { - var flag; - if (flag = $('.countryFlag', post.el)) { - return flag.title; - } - return false; - }, - filename: function(post) { - var file, fileInfo; - fileInfo = post.fileInfo; - if (fileInfo) { - if (file = $('.fileText > span', fileInfo)) { - return file.title; - } else { - return fileInfo.firstElementChild.dataset.filename; - } - } - return false; - }, - dimensions: function(post) { - var fileInfo, match; - fileInfo = post.fileInfo; - if (fileInfo && (match = fileInfo.textContent.match(/\d+x\d+/))) { - return match[0]; - } - return false; - }, - filesize: function(post) { - var img; - img = post.img; - if (img) { - return img.alt; - } - return false; - }, - md5: function(post) { - var img; - img = post.img; - if (img) { - return img.dataset.md5; - } - return false; - }, - menuInit: function() { - var div, entry, type, _i, _len, _ref; - div = $.el('div', { - textContent: 'Filter' - }); - entry = { - el: div, - open: function() { - return true; - }, - children: [] - }; - _ref = [['Name', 'name'], ['Unique ID', 'uniqueid'], ['Tripcode', 'tripcode'], ['Admin/Mod', 'mod'], ['E-mail', 'email'], ['Subject', 'subject'], ['Comment', 'comment'], ['Country', 'country'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'md5']]; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - type = _ref[_i]; - entry.children.push(Filter.createSubEntry(type[0], type[1])); - } - return Menu.addEntry(entry); - }, - createSubEntry: function(text, type) { - var el, onclick, open; - el = $.el('a', { - href: 'javascript:;', - textContent: text - }); - onclick = null; - open = function(post) { - var value; - value = Filter[type](post); - if (value === false) { - return false; - } - $.off(el, 'click', onclick); - onclick = function() { - var re, save, select, ta, tl; - re = type === 'md5' ? value : value.replace(/\/|\\|\^|\$|\n|\.|\(|\)|\{|\}|\[|\]|\?|\*|\+|\|/g, function(c) { - if (c === '\n') { - return '\\n'; - } else if (c === '\\') { - return '\\\\'; - } else { - return "\\" + c; - } - }); - re = type === 'md5' ? "/" + value + "/" : "/^" + re + "$/"; - if (/\bop\b/.test(post["class"])) { - re += ';op:yes'; - } - save = (save = $.get(type, '')) ? "" + save + "\n" + re : re; - $.set(type, save); - Options.dialog(); - select = $('select[name=filter]', $.id('options')); - select.value = type; - $.event(select, new Event('change')); - $.id('filter_tab').checked = true; - ta = select.nextElementSibling; - tl = ta.textLength; - ta.setSelectionRange(tl, tl); - return ta.focus(); - }; - $.on(el, 'click', onclick); - return true; - }; - return { - el: el, - open: open - }; - } - }; - - StrikethroughQuotes = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - var el, quote, show_stub, _i, _len, _ref; - if (post.isInlined) { - return; - } - _ref = post.quotes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if ((el = $.id(quote.hash.slice(1))) && el.hidden) { - $.addClass(quote, 'filtered'); - if (Conf['Recursive Filtering']) { - show_stub = !!$.x('preceding-sibling::div[contains(@class,"stub")]', el); - ReplyHiding.hide(post.root, show_stub); - } - } - } - } - }; - - ExpandComment = { - init: function() { - var a, _i, _len, _ref; - _ref = $$('.abbr'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - a = _ref[_i]; - $.on(a.firstElementChild, 'click', ExpandComment.expand); - } - }, - expand: function(e) { - var a, replyID, threadID, _, _ref; - e.preventDefault(); - _ref = this.href.match(/(\d+)#p(\d+)/), _ = _ref[0], threadID = _ref[1], replyID = _ref[2]; - this.textContent = "Loading " + replyID + "..."; - a = this; - return $.cache(this.pathname, function() { - return ExpandComment.parse(this, a, threadID, replyID); - }); - }, - parse: function(req, a, threadID, replyID) { - var doc, href, node, post, quote, quotes, _i, _len; - if (req.status !== 200) { - a.textContent = "" + req.status + " " + req.statusText; - return; - } - doc = d.implementation.createHTMLDocument(''); - doc.documentElement.innerHTML = req.response; - node = d.importNode(doc.getElementById("m" + replyID), true); - quotes = node.getElementsByClassName('quotelink'); - for (_i = 0, _len = quotes.length; _i < _len; _i++) { - quote = quotes[_i]; - href = quote.getAttribute('href'); - if (href[0] === '/') { - continue; - } - quote.href = "res/" + href; - } - post = { - blockquote: node, - threadID: threadID, - quotes: quotes, - backlinks: [] - }; - if (Conf['Resurrect Quotes']) { - Quotify.node(post); - } - if (Conf['Quote Preview']) { - QuotePreview.node(post); - } - if (Conf['Quote Inline']) { - QuoteInline.node(post); - } - if (Conf['Indicate OP quote']) { - QuoteOP.node(post); - } - if (Conf['Indicate Cross-thread Quotes']) { - QuoteCT.node(post); - } - $.replace(a.parentNode.parentNode, node); - return Main.prettify(node); - } - }; - - ExpandThread = { - init: function() { - var a, span, _i, _len, _ref, _results; - _ref = $$('.summary'); - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - span = _ref[_i]; - a = $.el('a', { - textContent: "+ " + span.textContent, - className: 'summary desktop', - href: 'javascript:;' - }); - $.on(a, 'click', function() { - return ExpandThread.toggle(this.parentNode); - }); - _results.push($.replace(span, a)); - } - return _results; - }, - toggle: function(thread) { - var a, num, pathname, replies, reply, _i, _len; - pathname = "/" + g.BOARD + "/res/" + thread.id.slice(1); - a = $('.summary', thread); - switch (a.textContent[0]) { - case '+': - a.textContent = a.textContent.replace('+', '× Loading...'); - $.cache(pathname, function() { - return ExpandThread.parse(this, thread, a); - }); - break; - case '×': - a.textContent = a.textContent.replace('× Loading...', '+'); - $.cache.requests[pathname].abort(); - break; - case '-': - a.textContent = a.textContent.replace('-', '+'); - num = (function() { - switch (g.BOARD) { - case 'b': - case 'vg': - return 3; - case 't': - return 1; - default: - return 5; - } - })(); - replies = $$('.replyContainer', thread); - replies.splice(replies.length - num, num); - for (_i = 0, _len = replies.length; _i < _len; _i++) { - reply = replies[_i]; - $.rm(reply); - } - } - }, - parse: function(req, thread, a) { - var backlink, doc, href, id, link, nodes, post, quote, reply, threadID, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3; - if (req.status !== 200) { - a.textContent = "" + req.status + " " + req.statusText; - $.off(a, 'click', ExpandThread.cb.toggle); - return; - } - a.textContent = a.textContent.replace('× Loading...', '-'); - doc = d.implementation.createHTMLDocument(''); - doc.documentElement.innerHTML = req.response; - threadID = thread.id.slice(1); - nodes = []; - _ref = $$('.replyContainer', doc); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - reply = _ref[_i]; - reply = d.importNode(reply, true); - _ref1 = $$('.quotelink', reply); - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - quote = _ref1[_j]; - href = quote.getAttribute('href'); - if (href[0] === '/') { - continue; - } - quote.href = "res/" + href; - } - id = reply.id.slice(2); - link = $('a[title="Highlight this post"]', reply); - link.href = "res/" + threadID + "#p" + id; - link.nextSibling.href = "res/" + threadID + "#q" + id; - nodes.push(reply); - } - _ref2 = $$('.summary ~ .replyContainer', a.parentNode); - for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { - post = _ref2[_k]; - $.rm(post); - } - _ref3 = $$('.backlink', a.previousElementSibling); - for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { - backlink = _ref3[_l]; - if (!$.id(backlink.hash.slice(1))) { - $.rm(backlink); - } - } - return $.after(a, nodes); - } - }; - - ThreadHiding = { - init: function() { - var a, hiddenThreads, thread, _i, _len, _ref; - hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {}); - _ref = $$('.thread'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - thread = _ref[_i]; - a = $.el('a', { - className: 'hide_thread_button', - innerHTML: '[ - ]', - href: 'javascript:;' - }); - $.on(a, 'click', ThreadHiding.cb); - $.prepend(thread, a); - if (thread.id.slice(1) in hiddenThreads) { - ThreadHiding.hide(thread); - } - } - }, - cb: function() { - return ThreadHiding.toggle($.x('ancestor::div[parent::div[@class="board"]]', this)); - }, - toggle: function(thread) { - var hiddenThreads, id; - hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {}); - id = thread.id.slice(1); - if (thread.hidden || /\bhidden_thread\b/.test(thread.firstChild.className)) { - ThreadHiding.show(thread); - delete hiddenThreads[id]; - } else { - ThreadHiding.hide(thread); - hiddenThreads[id] = Date.now(); - } - return $.set("hiddenThreads/" + g.BOARD + "/", hiddenThreads); - }, - hide: function(thread, show_stub) { - var a, menuButton, num, opInfo, span, stub, text; - if (show_stub == null) { - show_stub = Conf['Show Stubs']; - } - if (!show_stub) { - thread.hidden = true; - thread.nextElementSibling.hidden = true; - return; - } - if (/\bhidden_thread\b/.test(thread.firstChild.className)) { - return; - } - num = 0; - if (span = $('.summary', thread)) { - num = Number(span.textContent.match(/\d+/)); - } - num += $$('.opContainer ~ .replyContainer', thread).length; - text = num === 1 ? '1 reply' : "" + num + " replies"; - opInfo = $('.desktop > .nameBlock', thread).textContent; - stub = $.el('div', { - className: 'hide_thread_button hidden_thread', - innerHTML: '[ + ] ' - }); - a = stub.firstChild; - $.on(a, 'click', ThreadHiding.cb); - $.add(a, $.tn("" + opInfo + " (" + text + ")")); - if (Conf['Menu']) { - menuButton = Menu.a.cloneNode(true); - $.on(menuButton, 'click', Menu.toggle); - $.add(stub, [$.tn(' '), menuButton]); - } - return $.prepend(thread, stub); - }, - show: function(thread) { - var stub; - if (stub = $('.hidden_thread', thread)) { - $.rm(stub); - } - thread.hidden = false; - return thread.nextElementSibling.hidden = false; - } - }; - - ReplyHiding = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - var side; - if (post.isInlined || post.ID === post.threadID) { - return; - } - side = $('.sideArrows', post.root); - $.addClass(side, 'hide_reply_button'); - side.innerHTML = '[ - ]'; - $.on(side.firstChild, 'click', ReplyHiding.toggle); - if (post.ID in g.hiddenReplies) { - return ReplyHiding.hide(post.root); - } - }, - toggle: function() { - var button, id, quote, quotes, root, _i, _j, _len, _len1; - button = this.parentNode; - root = button.parentNode; - id = root.id.slice(2); - quotes = $$(".quotelink[href$='#p" + id + "'], .backlink[href$='#p" + id + "']"); - if (/\bstub\b/.test(button.className)) { - ReplyHiding.show(root); - for (_i = 0, _len = quotes.length; _i < _len; _i++) { - quote = quotes[_i]; - $.rmClass(quote, 'filtered'); - } - delete g.hiddenReplies[id]; - } else { - ReplyHiding.hide(root); - for (_j = 0, _len1 = quotes.length; _j < _len1; _j++) { - quote = quotes[_j]; - $.addClass(quote, 'filtered'); - } - g.hiddenReplies[id] = Date.now(); - } - return $.set("hiddenReplies/" + g.BOARD + "/", g.hiddenReplies); - }, - hide: function(root, show_stub) { - var a, el, menuButton, side, stub; - if (show_stub == null) { - show_stub = Conf['Show Stubs']; - } - side = $('.sideArrows', root); - if (side.hidden) { - return; - } - side.hidden = true; - el = side.nextElementSibling; - el.hidden = true; - if (!show_stub) { - return; - } - stub = $.el('div', { - className: 'hide_reply_button stub', - innerHTML: '[ + ] ' - }); - a = stub.firstChild; - $.on(a, 'click', ReplyHiding.toggle); - $.add(a, $.tn($('.desktop > .nameBlock', el).textContent)); - if (Conf['Menu']) { - menuButton = Menu.a.cloneNode(true); - $.on(menuButton, 'click', Menu.toggle); - $.add(stub, [$.tn(' '), menuButton]); - } - return $.prepend(root, stub); - }, - show: function(root) { - var stub; - if (stub = $('.stub', root)) { - $.rm(stub); - } - $('.sideArrows', root).hidden = false; - return $('.post', root).hidden = false; - } - }; - - Menu = { - entries: [], - init: function() { - this.a = $.el('a', { - className: 'menu_button', - href: 'javascript:;', - innerHTML: '[]' - }); - this.el = $.el('div', { - className: 'reply dialog', - id: 'menu', - tabIndex: 0 - }); - $.on(this.el, 'click', function(e) { - return e.stopPropagation(); - }); - $.on(this.el, 'keydown', this.keybinds); - $.on(d, 'AddMenuEntry', function(e) { - return Menu.addEntry(e.detail); - }); - return Main.callbacks.push(this.node); - }, - node: function(post) { - var a; - if (post.isInlined && !post.isCrosspost) { - a = $('.menu_button', post.el); - } else { - a = Menu.a.cloneNode(true); - $.add($('.postInfo', post.el), [$.tn('\u00A0'), a]); - } - return $.on(a, 'click', Menu.toggle); - }, - toggle: function(e) { - var lastOpener, post; - e.preventDefault(); - e.stopPropagation(); - if (Menu.el.parentNode) { - lastOpener = Menu.lastOpener; - Menu.close(); - if (lastOpener === this) { - return; - } - } - Menu.lastOpener = this; - post = /\bhidden_thread\b/.test(this.parentNode.className) ? $.x('ancestor::div[parent::div[@class="board"]]/child::div[contains(@class,"opContainer")]', this) : $.x('ancestor::div[contains(@class,"postContainer")][1]', this); - return Menu.open(this, Main.preParse(post)); - }, - open: function(button, post) { - var bLeft, bRect, bTop, el, entry, funk, mRect, _i, _len, _ref; - el = Menu.el; - el.setAttribute('data-id', post.ID); - el.setAttribute('data-rootid', post.root.id); - funk = function(entry, parent) { - var child, children, subMenu, _i, _len; - children = entry.children; - if (!entry.open(post)) { - return; - } - $.add(parent, entry.el); - if (!children) { - return; - } - if (subMenu = $('.subMenu', entry.el)) { - $.rm(subMenu); - } - subMenu = $.el('div', { - className: 'reply dialog subMenu' - }); - $.add(entry.el, subMenu); - for (_i = 0, _len = children.length; _i < _len; _i++) { - child = children[_i]; - funk(child, subMenu); - } - }; - _ref = Menu.entries; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - entry = _ref[_i]; - funk(entry, el); - } - Menu.focus($('.entry', Menu.el)); - $.on(d, 'click', Menu.close); - $.add(d.body, el); - mRect = el.getBoundingClientRect(); - bRect = button.getBoundingClientRect(); - bTop = d.documentElement.scrollTop + d.body.scrollTop + bRect.top; - bLeft = d.documentElement.scrollLeft + d.body.scrollLeft + bRect.left; - el.style.top = bRect.top + bRect.height + mRect.height < d.documentElement.clientHeight ? bTop + bRect.height + 2 + 'px' : bTop - mRect.height - 2 + 'px'; - el.style.left = bRect.left + mRect.width < d.documentElement.clientWidth ? bLeft + 'px' : bLeft + bRect.width - mRect.width + 'px'; - return el.focus(); - }, - close: function() { - var el, focused, _i, _len, _ref; - el = Menu.el; - $.rm(el); - _ref = $$('.focused.entry', el); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - focused = _ref[_i]; - $.rmClass(focused, 'focused'); - } - el.innerHTML = null; - el.removeAttribute('style'); - delete Menu.lastOpener; - delete Menu.focusedEntry; - return $.off(d, 'click', Menu.close); - }, - keybinds: function(e) { - var el, next, subMenu; - el = Menu.focusedEntry; - switch (Keybinds.keyCode(e) || e.keyCode) { - case 'Esc': - Menu.lastOpener.focus(); - Menu.close(); - break; - case 13: - case 32: - el.click(); - break; - case 'Up': - if (next = el.previousElementSibling) { - Menu.focus(next); - } - break; - case 'Down': - if (next = el.nextElementSibling) { - Menu.focus(next); - } - break; - case 'Right': - if ((subMenu = $('.subMenu', el)) && (next = subMenu.firstElementChild)) { - Menu.focus(next); - } - break; - case 'Left': - if (next = $.x('parent::*[contains(@class,"subMenu")]/parent::*', el)) { - Menu.focus(next); - } - break; - default: - return; - } - e.preventDefault(); - return e.stopPropagation(); - }, - focus: function(el) { - var focused, _i, _len, _ref; - if (focused = $.x('parent::*/child::*[contains(@class,"focused")]', el)) { - $.rmClass(focused, 'focused'); - } - _ref = $$('.focused', el); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - focused = _ref[_i]; - $.rmClass(focused, 'focused'); - } - Menu.focusedEntry = el; - return $.addClass(el, 'focused'); - }, - addEntry: function(entry) { - var funk; - funk = function(entry) { - var child, children, el, _i, _len; - el = entry.el, children = entry.children; - $.addClass(el, 'entry'); - $.on(el, 'focus mouseover', function(e) { - e.stopPropagation(); - return Menu.focus(this); - }); - if (!children) { - return; - } - $.addClass(el, 'hasSubMenu'); - for (_i = 0, _len = children.length; _i < _len; _i++) { - child = children[_i]; - funk(child); - } - }; - funk(entry); - return Menu.entries.push(entry); - } - }; - - Keybinds = { - init: function() { - var node, _i, _len, _ref; - _ref = $$('[accesskey]'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - node = _ref[_i]; - node.removeAttribute('accesskey'); - } - return $.on(d, 'keydown', Keybinds.keydown); - }, - keydown: function(e) { - var key, link, o, target, thread; - if (!(key = Keybinds.keyCode(e))) { - return; - } - target = e.target; - if (/TEXTAREA|INPUT/.test(target.nodeName)) { - if (!((key === 'Esc') || (/\+/.test(key)))) { - return; - } - } - thread = Nav.getThread(); - switch (key) { - case Conf.openQR: - Keybinds.qr(thread, true); - break; - case Conf.openEmptyQR: - Keybinds.qr(thread); - break; - case Conf.openOptions: - if (!$.id('overlay')) { - Options.dialog(); - } - break; - case Conf.close: - if (o = $.id('overlay')) { - Options.close.call(o); - } else if (QR.el) { - QR.close(); - } - break; - case Conf.submit: - if (QR.el && !QR.status()) { - QR.submit(); - } - break; - case Conf.spoiler: - if (target.nodeName !== 'TEXTAREA') { - return; - } - Keybinds.tags('spoiler', target); - break; - case Conf.code: - if (target.nodeName !== 'TEXTAREA') { - return; - } - Keybinds.tags('code', target); - break; - case Conf.watch: - Watcher.toggle(thread); - break; - case Conf.update: - Updater.update(); - break; - case Conf.unreadCountTo0: - Unread.replies = []; - Unread.update(true); - break; - case Conf.expandImage: - Keybinds.img(thread); - break; - case Conf.expandAllImages: - Keybinds.img(thread, true); - break; - case Conf.zero: - window.location = "/" + g.BOARD + "/0#delform"; - break; - case Conf.nextPage: - if (link = $('link[rel=next]', d.head)) { - window.location = link.href; - } - break; - case Conf.previousPage: - if (link = $('link[rel=prev]', d.head)) { - window.location.href = link.href; - } - break; - case Conf.nextThread: - if (g.REPLY) { - return; - } - Nav.scroll(+1); - break; - case Conf.previousThread: - if (g.REPLY) { - return; - } - Nav.scroll(-1); - break; - case Conf.expandThread: - ExpandThread.toggle(thread); - break; - case Conf.openThread: - Keybinds.open(thread); - break; - case Conf.openThreadTab: - Keybinds.open(thread, true); - break; - case Conf.nextReply: - Keybinds.hl(+1, thread); - break; - case Conf.previousReply: - Keybinds.hl(-1, thread); - break; - case Conf.hide: - if (/\bthread\b/.test(thread.className)) { - ThreadHiding.toggle(thread); - } - break; - default: - return; - } - return e.preventDefault(); - }, - keyCode: function(e) { - var c, kc, key; - key = (function() { - switch (kc = e.keyCode) { - case 8: - return ''; - case 27: - return 'Esc'; - case 37: - return 'Left'; - case 38: - return 'Up'; - case 39: - return 'Right'; - case 40: - return 'Down'; - case 48: - case 49: - case 50: - case 51: - case 52: - case 53: - case 54: - case 55: - case 56: - case 57: - case 65: - case 66: - case 67: - case 68: - case 69: - case 70: - case 71: - case 72: - case 73: - case 74: - case 75: - case 76: - case 77: - case 78: - case 79: - case 80: - case 81: - case 82: - case 83: - case 84: - case 85: - case 86: - case 87: - case 88: - case 89: - case 90: - c = String.fromCharCode(kc); - if (e.shiftKey) { - return c; - } else { - return c.toLowerCase(); - } - break; - default: - return null; - } - })(); - if (key) { - if (e.altKey) { - key = 'alt+' + key; - } - if (e.ctrlKey) { - key = 'ctrl+' + key; - } - if (e.metaKey) { - key = 'meta+' + key; - } - } - return key; - }, - tags: function(tag, ta) { - var range, selEnd, selStart, value; - value = ta.value; - selStart = ta.selectionStart; - selEnd = ta.selectionEnd; - ta.value = value.slice(0, selStart) + ("[" + tag + "]") + value.slice(selStart, selEnd) + ("[/" + tag + "]") + value.slice(selEnd); - range = ("[" + tag + "]").length + selEnd; - ta.setSelectionRange(range, range); - return $.event(ta, new Event('input')); - }, - img: function(thread, all) { - var thumb; - if (all) { - return $.id('imageExpand').click(); - } else { - thumb = $('img[data-md5]', $('.post.highlight', thread) || thread); - return ImageExpand.toggle(thumb.parentNode); - } - }, - qr: function(thread, quote) { - if (quote) { - QR.quote.call($('a[title="Quote this post"]', $('.post.highlight', thread) || thread)); - } else { - QR.open(); - } - return $('textarea', QR.el).focus(); - }, - open: function(thread, tab) { - var id, url; - id = thread.id.slice(1); - url = "//boards.4chan.org/" + g.BOARD + "/res/" + id; - if (tab) { - return $.open(url); - } else { - return location.href = url; - } - }, - hl: function(delta, thread) { - var next, post, rect, replies, reply, _i, _len; - if (post = $('.reply.highlight', thread)) { - $.rmClass(post, 'highlight'); - post.removeAttribute('tabindex'); - rect = post.getBoundingClientRect(); - if (rect.bottom >= 0 && rect.top <= d.documentElement.clientHeight) { - next = $.x('child::div[contains(@class,"post reply")]', delta === +1 ? post.parentNode.nextElementSibling : post.parentNode.previousElementSibling); - if (!next) { - this.focus(post); - return; - } - if (!(g.REPLY || $.x('ancestor::div[parent::div[@class="board"]]', next) === thread)) { - return; - } - rect = next.getBoundingClientRect(); - if (rect.top < 0 || rect.bottom > d.documentElement.clientHeight) { - next.scrollIntoView(delta === -1); - } - this.focus(next); - return; - } - } - replies = $$('.reply', thread); - if (delta === -1) { - replies.reverse(); - } - for (_i = 0, _len = replies.length; _i < _len; _i++) { - reply = replies[_i]; - rect = reply.getBoundingClientRect(); - if (delta === +1 && rect.top >= 0 || delta === -1 && rect.bottom <= d.documentElement.clientHeight) { - this.focus(reply); - return; - } - } - }, - focus: function(post) { - $.addClass(post, 'highlight'); - post.tabIndex = 0; - return post.focus(); - } - }; - - Nav = { - init: function() { - var next, prev, span; - span = $.el('span', { - id: 'navlinks' - }); - prev = $.el('a', { - textContent: '▲', - href: 'javascript:;' - }); - next = $.el('a', { - textContent: '▼', - href: 'javascript:;' - }); - $.on(prev, 'click', this.prev); - $.on(next, 'click', this.next); - $.add(span, [prev, $.tn(' '), next]); - return $.add(d.body, span); - }, - prev: function() { - if (g.REPLY) { - return window.scrollTo(0, 0); - } else { - return Nav.scroll(-1); - } - }, - next: function() { - if (g.REPLY) { - return window.scrollTo(0, d.body.scrollHeight); - } else { - return Nav.scroll(+1); - } - }, - getThread: function(full) { - var bottom, i, rect, thread, _i, _len, _ref; - Nav.threads = $$('.thread:not([hidden])'); - _ref = Nav.threads; - for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { - thread = _ref[i]; - rect = thread.getBoundingClientRect(); - bottom = rect.bottom; - if (bottom > 0) { - if (full) { - return [thread, i, rect]; - } - return thread; - } - } - return $('.board'); - }, - scroll: function(delta) { - var i, rect, thread, top, _ref, _ref1; - _ref = Nav.getThread(true), thread = _ref[0], i = _ref[1], rect = _ref[2]; - top = rect.top; - if (!((delta === -1 && Math.ceil(top) < 0) || (delta === +1 && top > 1))) { - i += delta; - } - top = (_ref1 = Nav.threads[i]) != null ? _ref1.getBoundingClientRect().top : void 0; - return window.scrollBy(0, top); - } - }; - - QR = { - init: function() { - if (!$.id('postForm')) { - return; - } - Main.callbacks.push(this.node); - return setTimeout(this.asyncInit); - }, - asyncInit: function() { - var link; - if (Conf['Hide Original Post Form']) { - link = $.el('h1', { - innerHTML: "" + (g.REPLY ? 'Reply to Thread' : 'Start a Thread') + "" - }); - $.on(link.firstChild, 'click', function() { - QR.open(); - if (!g.REPLY) { - $('select', QR.el).value = 'new'; - } - return $('textarea', QR.el).focus(); - }); - $.before($.id('postForm'), link); - } - if (Conf['Persistent QR']) { - QR.dialog(); - if (Conf['Auto Hide QR']) { - QR.hide(); - } - } - $.on(d, 'dragover', QR.dragOver); - $.on(d, 'drop', QR.dropFile); - return $.on(d, 'dragstart dragend', QR.drag); - }, - node: function(post) { - return $.on($('a[title="Quote this post"]', post.el), 'click', QR.quote); - }, - open: function() { - if (QR.el) { - QR.el.hidden = false; - return QR.unhide(); - } else { - return QR.dialog(); - } - }, - close: function() { - var i, spoiler, _i, _len, _ref; - QR.el.hidden = true; - QR.abort(); - d.activeElement.blur(); - $.rmClass(QR.el, 'dump'); - _ref = QR.replies; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - i = _ref[_i]; - QR.replies[0].rm(); - } - QR.cooldown.auto = false; - QR.status(); - QR.resetFileInput(); - if (!Conf['Remember Spoiler'] && (spoiler = $.id('spoiler')).checked) { - spoiler.click(); - } - return QR.cleanError(); - }, - hide: function() { - d.activeElement.blur(); - $.addClass(QR.el, 'autohide'); - return $.id('autohide').checked = true; - }, - unhide: function() { - $.rmClass(QR.el, 'autohide'); - return $.id('autohide').checked = false; - }, - toggleHide: function() { - return this.checked && QR.hide() || QR.unhide(); - }, - error: function(err) { - var el; - el = $('.warning', QR.el); - if (typeof err === 'string') { - el.textContent = err; - } else { - el.innerHTML = null; - $.add(el, err); - } - QR.open(); - if (/captcha|verification/i.test(el.textContent)) { - $('[autocomplete]', QR.el).focus(); - } - if (d.hidden || d.oHidden || d.mozHidden || d.webkitHidden) { - return alert(el.textContent); - } - }, - cleanError: function() { - return $('.warning', QR.el).textContent = null; - }, - status: function(data) { - var disabled, input, value; - if (data == null) { - data = {}; - } - if (!QR.el) { - return; - } - if (g.dead) { - value = 404; - disabled = true; - QR.cooldown.auto = false; - } - value = QR.cooldown.seconds || data.progress || value; - input = QR.status.input; - input.value = QR.cooldown.auto && Conf['Cooldown'] ? value ? "Auto " + value : 'Auto' : value || 'Submit'; - return input.disabled = disabled || false; - }, - cooldown: { - init: function() { - if (!Conf['Cooldown']) { - return; - } - QR.cooldown.start($.get("/" + g.BOARD + "/cooldown", 0)); - return $.sync("/" + g.BOARD + "/cooldown", QR.cooldown.start); - }, - start: function(timeout) { - var seconds; - seconds = Math.floor((timeout - Date.now()) / 1000); - return QR.cooldown.count(seconds); - }, - set: function(seconds) { - if (!Conf['Cooldown']) { - return; - } - QR.cooldown.count(seconds); - return $.set("/" + g.BOARD + "/cooldown", Date.now() + seconds * $.SECOND); - }, - count: function(seconds) { - if (!((0 <= seconds && seconds <= 60))) { - return; - } - setTimeout(QR.cooldown.count, 1000, seconds - 1); - QR.cooldown.seconds = seconds; - if (seconds === 0) { - $["delete"]("/" + g.BOARD + "/cooldown"); - if (QR.cooldown.auto) { - QR.submit(); - } - } - return QR.status(); - } - }, - quote: function(e) { - var caretPos, id, range, s, sel, ta, text, _ref; - if (e != null) { - e.preventDefault(); - } - QR.open(); - if (!g.REPLY) { - $('select', QR.el).value = $.x('ancestor::div[parent::div[@class="board"]]', this).id.slice(1); - } - id = this.previousSibling.hash.slice(2); - text = ">>" + id + "\n"; - sel = window.getSelection(); - if ((s = sel.toString()) && id === ((_ref = $.x('ancestor-or-self::blockquote', sel.anchorNode)) != null ? _ref.id.match(/\d+$/)[0] : void 0)) { - if ($.engine === 'presto') { - s = d.getSelection(); - } - s = s.replace(/\n/g, '\n>'); - text += ">" + s + "\n"; - } - ta = $('textarea', QR.el); - caretPos = ta.selectionStart; - ta.value = ta.value.slice(0, caretPos) + text + ta.value.slice(ta.selectionEnd); - ta.focus(); - range = caretPos + text.length; - if ($.engine === 'presto') { - range += text.match(/\n/g).length; - } - ta.setSelectionRange(range, range); - return $.event(ta, new Event('input')); - }, - characterCount: function() { - var count, counter; - counter = QR.charaCounter; - count = this.textLength; - counter.textContent = count; - counter.hidden = count < 1000; - return (count > 1500 ? $.addClass : $.rmClass)(counter, 'warning'); - }, - drag: function(e) { - var i; - i = e.type === 'dragstart' ? 'off' : 'on'; - $[i](d, 'dragover', QR.dragOver); - return $[i](d, 'drop', QR.dropFile); - }, - dragOver: function(e) { - e.preventDefault(); - return e.dataTransfer.dropEffect = 'copy'; - }, - dropFile: function(e) { - if (!e.dataTransfer.files.length) { - return; - } - e.preventDefault(); - QR.open(); - QR.fileInput.call(e.dataTransfer); - return $.addClass(QR.el, 'dump'); - }, - fileInput: function() { - var file, _i, _len, _ref; - QR.cleanError(); - if (this.files.length === 1) { - file = this.files[0]; - if (file.size > this.max) { - QR.error('File too large.'); - QR.resetFileInput(); - } else if (-1 === QR.mimeTypes.indexOf(file.type)) { - QR.error('Unsupported file type.'); - QR.resetFileInput(); - } else { - QR.selected.setFile(file); - } - return; - } - _ref = this.files; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - file = _ref[_i]; - if (file.size > this.max) { - QR.error("File " + file.name + " is too large."); - break; - } else if (-1 === QR.mimeTypes.indexOf(file.type)) { - QR.error("" + file.name + ": Unsupported file type."); - break; - } - if (!QR.replies[QR.replies.length - 1].file) { - QR.replies[QR.replies.length - 1].setFile(file); - } else { - new QR.reply().setFile(file); - } - } - $.addClass(QR.el, 'dump'); - return QR.resetFileInput(); - }, - resetFileInput: function() { - var clone, input; - input = $('[type=file]', QR.el); - input.value = null; - if ($.engine !== 'presto') { - return; - } - clone = $.el('input', { - type: 'file', - accept: input.accept, - max: input.max, - multiple: input.multiple, - size: input.size, - title: input.title - }); - $.on(clone, 'change', QR.fileInput); - $.on(clone, 'click', function(e) { - if (e.shiftKey) { - return QR.selected.rmFile() || e.preventDefault(); - } - }); - return $.replace(input, clone); - }, - replies: [], - reply: (function() { - - function _Class() { - var persona, prev, - _this = this; - prev = QR.replies[QR.replies.length - 1]; - persona = $.get('QR.persona', {}); - this.name = prev ? prev.name : persona.name || null; - this.email = prev && !/^sage$/.test(prev.email) ? prev.email : persona.email || null; - this.sub = prev && Conf['Remember Subject'] ? prev.sub : Conf['Remember Subject'] ? persona.sub : null; - this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false; - this.com = null; - this.el = $.el('a', { - className: 'thumbnail', - draggable: true, - href: 'javascript:;', - innerHTML: '×' - }); - $('input', this.el).checked = this.spoiler; - $.on(this.el, 'click', function() { - return _this.select(); - }); - $.on($('.remove', this.el), 'click', function(e) { - e.stopPropagation(); - return _this.rm(); - }); - $.on($('label', this.el), 'click', function(e) { - return e.stopPropagation(); - }); - $.on($('input', this.el), 'change', function(e) { - _this.spoiler = e.target.checked; - if (_this.el.id === 'selected') { - return $.id('spoiler').checked = _this.spoiler; - } - }); - $.before($('#addReply', QR.el), this.el); - $.on(this.el, 'dragstart', this.dragStart); - $.on(this.el, 'dragenter', this.dragEnter); - $.on(this.el, 'dragleave', this.dragLeave); - $.on(this.el, 'dragover', this.dragOver); - $.on(this.el, 'dragend', this.dragEnd); - $.on(this.el, 'drop', this.drop); - QR.replies.push(this); - } - - _Class.prototype.setFile = function(file) { - var fileUrl, img, url, - _this = this; - this.file = file; - this.el.title = "" + file.name + " (" + ($.bytesToString(file.size)) + ")"; - if (QR.spoiler) { - $('label', this.el).hidden = false; - } - if (!/^image/.test(file.type)) { - this.el.style.backgroundImage = null; - return; - } - url = window.URL || window.webkitURL; - if (typeof url.revokeObjectURL === "function") { - url.revokeObjectURL(this.url); - } - fileUrl = url.createObjectURL(file); - img = $.el('img'); - $.on(img, 'load', function() { - var c, data, i, l, s, ui8a, _i; - s = 90 * 3; - if (img.height < s || img.width < s) { - _this.url = fileUrl; - _this.el.style.backgroundImage = "url(" + _this.url + ")"; - return; - } - if (img.height <= img.width) { - img.width = s / img.height * img.width; - img.height = s; - } else { - img.height = s / img.width * img.height; - img.width = s; - } - c = $.el('canvas'); - c.height = img.height; - c.width = img.width; - c.getContext('2d').drawImage(img, 0, 0, img.width, img.height); - data = atob(c.toDataURL().split(',')[1]); - l = data.length; - ui8a = new Uint8Array(l); - for (i = _i = 0; 0 <= l ? _i < l : _i > l; i = 0 <= l ? ++_i : --_i) { - ui8a[i] = data.charCodeAt(i); - } - _this.url = url.createObjectURL(new Blob([ui8a.buffer], { - type: 'image/png' - })); - _this.el.style.backgroundImage = "url(" + _this.url + ")"; - return typeof url.revokeObjectURL === "function" ? url.revokeObjectURL(fileUrl) : void 0; - }); - return img.src = fileUrl; - }; - - _Class.prototype.rmFile = function() { - var _base1; - QR.resetFileInput(); - delete this.file; - this.el.title = null; - this.el.style.backgroundImage = null; - if (QR.spoiler) { - $('label', this.el).hidden = true; - } - return typeof (_base1 = window.URL || window.webkitURL).revokeObjectURL === "function" ? _base1.revokeObjectURL(this.url) : void 0; - }; - - _Class.prototype.select = function() { - var data, rectEl, rectList, _i, _len, _ref, _ref1; - if ((_ref = QR.selected) != null) { - _ref.el.id = null; - } - QR.selected = this; - this.el.id = 'selected'; - rectEl = this.el.getBoundingClientRect(); - rectList = this.el.parentNode.getBoundingClientRect(); - this.el.parentNode.scrollLeft += rectEl.left + rectEl.width / 2 - rectList.left - rectList.width / 2; - _ref1 = ['name', 'email', 'sub', 'com']; - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - data = _ref1[_i]; - $("[name=" + data + "]", QR.el).value = this[data]; - } - QR.characterCount.call($('textarea', QR.el)); - return $('#spoiler', QR.el).checked = this.spoiler; - }; - - _Class.prototype.dragStart = function() { - return $.addClass(this, 'drag'); - }; - - _Class.prototype.dragEnter = function() { - return $.addClass(this, 'over'); - }; - - _Class.prototype.dragLeave = function() { - return $.rmClass(this, 'over'); - }; - - _Class.prototype.dragOver = function(e) { - e.preventDefault(); - return e.dataTransfer.dropEffect = 'move'; - }; - - _Class.prototype.drop = function() { - var el, index, newIndex, oldIndex, reply; - el = $('.drag', this.parentNode); - index = function(el) { - return Array.prototype.slice.call(el.parentNode.children).indexOf(el); - }; - oldIndex = index(el); - newIndex = index(this); - if (oldIndex < newIndex) { - $.after(this, el); - } else { - $.before(this, el); - } - reply = QR.replies.splice(oldIndex, 1)[0]; - return QR.replies.splice(newIndex, 0, reply); - }; - - _Class.prototype.dragEnd = function() { - var el; - $.rmClass(this, 'drag'); - if (el = $('.over', this.parentNode)) { - return $.rmClass(el, 'over'); - } - }; - - _Class.prototype.rm = function() { - var index, _base1; - QR.resetFileInput(); - $.rm(this.el); - index = QR.replies.indexOf(this); - if (QR.replies.length === 1) { - new QR.reply().select(); - } else if (this.el.id === 'selected') { - (QR.replies[index - 1] || QR.replies[index + 1]).select(); - } - QR.replies.splice(index, 1); - if (typeof (_base1 = window.URL || window.webkitURL).revokeObjectURL === "function") { - _base1.revokeObjectURL(this.url); - } - return delete this; - }; - - return _Class; - - })(), - captcha: { - init: function() { - var _this = this; - if (!(QR.captchaIsEnabled = !!$.id('captchaFormPart'))) { - return; - } - if ($.id('recaptcha_challenge_field_holder')) { - return this.ready(); - } else { - this.onready = function() { - return _this.ready(); - }; - return $.on($.id('recaptcha_widget_div'), 'DOMNodeInserted', this.onready); - } - }, - ready: function() { - var _this = this; - if (this.challenge = $.id('recaptcha_challenge_field_holder')) { - $.off($.id('recaptcha_widget_div'), 'DOMNodeInserted', this.onready); - delete this.onready; - } else { - return; - } - $.after($('.textarea', QR.el), $.el('div', { - className: 'captchaimg', - title: 'Reload', - innerHTML: '' - })); - $.after($('.captchaimg', QR.el), $.el('div', { - className: 'captchainput', - innerHTML: '' - })); - this.img = $('.captchaimg > img', QR.el); - this.input = $('.captchainput > input', QR.el); - $.on(this.img.parentNode, 'click', this.reload); - $.on(this.input, 'keydown', this.keydown); - $.on(this.challenge, 'DOMNodeInserted', function() { - return _this.load(); - }); - $.sync('captchas', function(arr) { - return _this.count(arr.length); - }); - this.count($.get('captchas', []).length); - return this.reload(); - }, - save: function() { - var captcha, captchas, response; - if (!(response = this.input.value)) { - return; - } - captchas = $.get('captchas', []); - while ((captcha = captchas[0]) && captcha.time < Date.now()) { - captchas.shift(); - } - captchas.push({ - challenge: this.challenge.firstChild.value, - response: response, - time: this.timeout - }); - $.set('captchas', captchas); - this.count(captchas.length); - return this.reload(); - }, - load: function() { - var challenge; - this.timeout = Date.now() + 4 * $.MINUTE; - challenge = this.challenge.firstChild.value; - this.img.alt = challenge; - this.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge; - return this.input.value = null; - }, - count: function(count) { - this.input.placeholder = (function() { - switch (count) { - case 0: - return 'Verification (Shift + Enter to cache)'; - case 1: - return 'Verification (1 cached captcha)'; - default: - return "Verification (" + count + " cached captchas)"; - } - })(); - return this.input.alt = count; - }, - reload: function(focus) { - window.location = 'javascript:Recaptcha.reload("t")'; - if (focus) { - return QR.captcha.input.focus(); - } - }, - keydown: function(e) { - var c; - c = QR.captcha; - if (e.keyCode === 8 && !c.input.value) { - c.reload(); - } else if (e.keyCode === 13 && e.shiftKey) { - c.save(); - } else { - return; - } - return e.preventDefault(); - } - }, - dialog: function() { - var fileInput, id, mimeTypes, name, spoiler, ta, thread, threads, _i, _j, _len, _len1, _ref, _ref1; - QR.el = UI.dialog('qr', 'top:0;right:0;', '\ -
\ - Quick Reply \ - ×\ -
\ -
\ -
\ - \ -
\ -
\ - \ -
\ -
'); - if (Conf['Remember QR size'] && $.engine === 'gecko') { - $.on(ta = $('textarea', QR.el), 'mouseup', function() { - return $.set('QR.size', this.style.cssText); - }); - ta.style.cssText = $.get('QR.size', ''); - } - mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace(/\w+/g, function(type) { - switch (type) { - case 'jpg': - return 'image/jpeg'; - case 'pdf': - return 'application/pdf'; - case 'swf': - return 'application/x-shockwave-flash'; - default: - return "image/" + type; - } - }); - QR.mimeTypes = mimeTypes.split(', '); - QR.mimeTypes.push(''); - fileInput = $('input[type=file]', QR.el); - fileInput.max = $('input[name=MAX_FILE_SIZE]').value; - if ($.engine !== 'presto') { - fileInput.accept = mimeTypes; - } - QR.spoiler = !!$('input[name=spoiler]'); - spoiler = $('#spoilerLabel', QR.el); - spoiler.hidden = !QR.spoiler; - QR.charaCounter = $('#charCount', QR.el); - ta = $('textarea', QR.el); - if (!g.REPLY) { - threads = ''; - _ref = $$('.thread'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - thread = _ref[_i]; - id = thread.id.slice(1); - threads += ""; - } - $.prepend($('.move > span', QR.el), $.el('select', { - innerHTML: threads, - title: 'Create a new thread / Reply to a thread' - })); - $.on($('select', QR.el), 'mousedown', function(e) { - return e.stopPropagation(); - }); - } - $.on($('#autohide', QR.el), 'change', QR.toggleHide); - $.on($('.close', QR.el), 'click', QR.close); - $.on($('#dump', QR.el), 'click', function() { - return QR.el.classList.toggle('dump'); - }); - $.on($('#addReply', QR.el), 'click', function() { - return new QR.reply().select(); - }); - $.on($('form', QR.el), 'submit', QR.submit); - $.on(ta, 'input', function() { - return QR.selected.el.lastChild.textContent = this.value; - }); - $.on(ta, 'input', QR.characterCount); - $.on(fileInput, 'change', QR.fileInput); - $.on(fileInput, 'click', function(e) { - if (e.shiftKey) { - return QR.selected.rmFile() || e.preventDefault(); - } - }); - $.on(spoiler.firstChild, 'change', function() { - return $('input', QR.selected.el).click(); - }); - $.on($('.warning', QR.el), 'click', QR.cleanError); - new QR.reply().select(); - _ref1 = ['name', 'email', 'sub', 'com']; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - name = _ref1[_j]; - $.on($("[name=" + name + "]", QR.el), 'input', function() { - var _ref2; - QR.selected[this.name] = this.value; - if (QR.cooldown.auto && QR.selected === QR.replies[0] && (0 < (_ref2 = QR.cooldown.seconds) && _ref2 < 6)) { - return QR.cooldown.auto = false; - } - }); - } - QR.status.input = $('input[type=submit]', QR.el); - QR.status(); - QR.cooldown.init(); - QR.captcha.init(); - $.add(d.body, QR.el); - return $.event(QR.el, new CustomEvent('QRDialogCreation', { - bubbles: true - })); - }, - submit: function(e) { - var callbacks, captcha, captchas, challenge, err, m, opts, post, reply, response, threadID; - if (e != null) { - e.preventDefault(); - } - if (QR.cooldown.seconds) { - QR.cooldown.auto = !QR.cooldown.auto; - QR.status(); - return; - } - QR.abort(); - reply = QR.replies[0]; - threadID = g.THREAD_ID || $('select', QR.el).value; - if (!(threadID === 'new' && reply.file || threadID !== 'new' && (reply.com || reply.file))) { - err = 'No file selected.'; - } else if (QR.captchaIsEnabled) { - captchas = $.get('captchas', []); - while ((captcha = captchas[0]) && captcha.time < Date.now()) { - captchas.shift(); - } - if (captcha = captchas.shift()) { - challenge = captcha.challenge; - response = captcha.response; - } else { - challenge = QR.captcha.img.alt; - if (response = QR.captcha.input.value) { - QR.captcha.reload(); - } - } - $.set('captchas', captchas); - QR.captcha.count(captchas.length); - if (!response) { - err = 'No valid captcha.'; - } - } - if (err) { - QR.cooldown.auto = false; - QR.status(); - QR.error(err); - return; - } - QR.cleanError(); - QR.cooldown.auto = QR.replies.length > 1; - if (Conf['Auto Hide QR'] && !QR.cooldown.auto) { - QR.hide(); - } - if (!QR.cooldown.auto && $.x('ancestor::div[@id="qr"]', d.activeElement)) { - d.activeElement.blur(); - } - QR.status({ - progress: '...' - }); - post = { - resto: threadID, - name: reply.name, - email: reply.email, - sub: reply.sub, - com: reply.com, - upfile: reply.file, - spoiler: reply.spoiler, - mode: 'regist', - pwd: (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value, - recaptcha_challenge_field: challenge, - recaptcha_response_field: response + ' ' - }; - callbacks = { - onload: function() { - return QR.response(this.response); - }, - onerror: function() { - QR.status(); - return QR.error($.el('a', { - href: '//www.4chan.org/banned', - target: '_blank', - textContent: 'Connection error, or you are banned.' - })); - } - }; - opts = { - form: $.formData(post), - upCallbacks: { - onload: function() { - return QR.status({ - progress: '...' - }); - }, - onprogress: function(e) { - return QR.status({ - progress: "" + (Math.round(e.loaded / e.total * 100)) + "%" - }); - } - } - }; - return QR.ajax = $.ajax($.id('postForm').parentNode.action, callbacks, opts); - }, - response: function(html) { - var bs, doc, err, msg, persona, postID, reply, threadID, _, _ref; - doc = d.implementation.createHTMLDocument(''); - doc.documentElement.innerHTML = html; - if (doc.title === '4chan - Banned') { - bs = $$('b', doc); - err = $.el('span', { - innerHTML: /^You were issued a warning/.test($('.boxcontent', doc).textContent.trim()) ? "You were issued a warning on " + bs[0].innerHTML + " as " + bs[3].innerHTML + ".
Warning reason: " + bs[1].innerHTML : "You are banned! ;_;
Please click HERE to see the reason." - }); - } else if (msg = doc.getElementById('errmsg')) { - err = msg.textContent; - if (msg.firstChild.tagName) { - err = msg.firstChild; - err.target = '_blank'; - } - } else if (!(msg = $('b', doc))) { - err = 'Connection error with sys.4chan.org.'; - } - if (err) { - if (/captcha|verification/i.test(err) || err === 'Connection error with sys.4chan.org.') { - QR.cooldown.auto = !!$.get('captchas', []).length; - QR.cooldown.set(2); - } else { - QR.cooldown.auto = false; - } - QR.status(); - QR.error(err); - return; - } - reply = QR.replies[0]; - persona = $.get('QR.persona', {}); - persona = { - name: reply.name, - email: /^sage$/.test(reply.email) ? persona.email : reply.email, - sub: Conf['Remember Subject'] ? reply.sub : null - }; - $.set('QR.persona', persona); - _ref = msg.lastChild.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref[0], threadID = _ref[1], postID = _ref[2]; - $.event(QR.el, new CustomEvent('QRPostSuccessful', { - bubbles: true, - detail: { - threadID: threadID, - postID: postID - } - })); - if (threadID === '0') { - location.pathname = "/" + g.BOARD + "/res/" + postID; - } else { - QR.cooldown.auto = QR.replies.length > 1; - QR.cooldown.set(/sage/i.test(reply.email) ? 60 : 30); - if (Conf['Open Reply in New Tab'] && !g.REPLY && !QR.cooldown.auto) { - $.open("//boards.4chan.org/" + g.BOARD + "/res/" + threadID + "#p" + postID); - } - } - if (Conf['Persistent QR'] || QR.cooldown.auto) { - reply.rm(); - } else { - QR.close(); - } - QR.status(); - return QR.resetFileInput(); - }, - abort: function() { - var _ref; - if ((_ref = QR.ajax) != null) { - _ref.abort(); - } - delete QR.ajax; - return QR.status(); - } - }; - - Options = { - init: function() { - var a, el, settings, _i, _len, _ref; - _ref = ['navtopr', 'navbotr']; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - settings = _ref[_i]; - a = $.el('a', { - href: 'javascript:;', - className: 'settingsWindowLink', - textContent: '4chan X Settings' - }); - $.on(a, 'click', Options.dialog); - el = $.id(settings).firstElementChild; - el.hidden = true; - $.before(el, a); - } - if (!$.get('firstrun')) { - if (!Favicon.el) { - Favicon.init(); - } - $.set('firstrun', true); - return Options.dialog(); - } - }, - dialog: function() { - var arr, back, checked, description, dialog, favicon, fileInfo, filter, hiddenNum, hiddenThreads, indicator, indicators, input, key, li, obj, overlay, sauce, time, tr, ul, _i, _len, _ref, _ref1, _ref2; - dialog = $.el('div', { - id: 'options', - className: 'reply dialog', - innerHTML: '
\ - \ -
\ - \ - | \ - | \ - | \ - | \ -
\ -
\ -
\ -
\ - \ -
\ - \ -
\ -
Sauce is disabled.
\ - Lines starting with a # will be ignored.
\ - You can specify a certain display text by appending ;text:[text] to the url.\ -
    These parameters will be replaced by their corresponding values:\ -
  • $1: Thumbnail url.
  • \ -
  • $2: Full image url.
  • \ -
  • $3: MD5 hash.
  • \ -
  • $4: Current board.
  • \ -
\ - \ -
\ - \ -
\ -
Filter is disabled.
\ - \ -
\ - \ -
\ -
Quote Backlinks are disabled.
\ -
    \ - Backlink formatting\ -
  • :
  • \ -
\ -
Time Formatting is disabled.
\ -
    \ - Time formatting\ -
  • :
  • \ -
  • Supported format specifiers:
  • \ -
  • Day: %a, %A, %d, %e
  • \ -
  • Month: %m, %b, %B
  • \ -
  • Year: %y
  • \ -
  • Hour: %k, %H, %l (lowercase L), %I (uppercase i), %p, %P
  • \ -
  • Minutes: %M
  • \ -
  • Seconds: %S
  • \ -
\ -
File Info Formatting is disabled.
\ -
    \ - File Info Formatting\ -
  • :
  • \ -
  • Link (with original file name): %l (lowercase L, truncated), %L (untruncated)
  • \ -
  • Original file name: %n (Truncated), %N (Untruncated)
  • \ -
  • Spoiler indicator: %p
  • \ -
  • Size: %B (Bytes), %K (KB), %M (MB), %s (4chan default)
  • \ -
  • Resolution: %r (Displays PDF on /po/, for PDFs)
  • \ -
\ -
Unread Favicon is disabled.
\ - Unread favicons
\ - \ - \ -
\ - \ -
\ -
Keybinds are disabled.
\ -
Allowed keys: Ctrl, Alt, Meta, a-z, A-Z, 0-9, Up, Down, Right, Left.
\ - \ - \ -
ActionsKeybinds
\ -
\ -
' - }); - _ref = Config.main; - for (key in _ref) { - obj = _ref[key]; - ul = $.el('ul', { - textContent: key - }); - for (key in obj) { - arr = obj[key]; - checked = $.get(key, Conf[key]) ? 'checked' : ''; - description = arr[1]; - li = $.el('li', { - innerHTML: ": " + description + "" - }); - $.on($('input', li), 'click', $.cb.checked); - $.add(ul, li); - } - $.add($('#main_tab + div', dialog), ul); - } - hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {}); - hiddenNum = Object.keys(g.hiddenReplies).length + Object.keys(hiddenThreads).length; - li = $.el('li', { - innerHTML: " : Forget all hidden posts. Useful if you accidentally hide a post and have \"Show Stubs\" disabled." - }); - $.on($('button', li), 'click', Options.clearHidden); - $.add($('ul:nth-child(2)', dialog), li); - filter = $('select[name=filter]', dialog); - $.on(filter, 'change', Options.filter); - sauce = $('#sauces', dialog); - sauce.value = $.get(sauce.name, Conf[sauce.name]); - $.on(sauce, 'change', $.cb.value); - (back = $('[name=backlink]', dialog)).value = $.get('backlink', Conf['backlink']); - (time = $('[name=time]', dialog)).value = $.get('time', Conf['time']); - (fileInfo = $('[name=fileInfo]', dialog)).value = $.get('fileInfo', Conf['fileInfo']); - $.on(back, 'input', $.cb.value); - $.on(back, 'input', Options.backlink); - $.on(time, 'input', $.cb.value); - $.on(time, 'input', Options.time); - $.on(fileInfo, 'input', $.cb.value); - $.on(fileInfo, 'input', Options.fileInfo); - favicon = $('select[name=favicon]', dialog); - favicon.value = $.get('favicon', Conf['favicon']); - $.on(favicon, 'change', $.cb.value); - $.on(favicon, 'change', Options.favicon); - _ref1 = Config.hotkeys; - for (key in _ref1) { - arr = _ref1[key]; - tr = $.el('tr', { - innerHTML: "" + arr[1] + "" - }); - input = $('input', tr); - input.value = $.get(key, Conf[key]); - $.on(input, 'keydown', Options.keybind); - $.add($('#keybinds_tab + div tbody', dialog), tr); - } - indicators = {}; - _ref2 = $$('.warning', dialog); - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - indicator = _ref2[_i]; - key = indicator.firstChild.textContent; - indicator.hidden = $.get(key, Conf[key]); - indicators[key] = indicator; - $.on($("[name='" + key + "']", dialog), 'click', function() { - return indicators[this.name].hidden = this.checked; - }); - } - overlay = $.el('div', { - id: 'overlay' - }); - $.on(overlay, 'click', Options.close); - $.on(dialog, 'click', function(e) { - return e.stopPropagation(); - }); - $.add(overlay, dialog); - $.add(d.body, overlay); - d.body.style.setProperty('width', "" + d.body.clientWidth + "px", null); - $.addClass(d.body, 'unscroll'); - Options.filter.call(filter); - Options.backlink.call(back); - Options.time.call(time); - Options.fileInfo.call(fileInfo); - return Options.favicon.call(favicon); - }, - close: function() { - $.rm(this); - d.body.style.removeProperty('width'); - return $.rmClass(d.body, 'unscroll'); - }, - clearHidden: function() { - $["delete"]("hiddenReplies/" + g.BOARD + "/"); - $["delete"]("hiddenThreads/" + g.BOARD + "/"); - this.textContent = "hidden: 0"; - return g.hiddenReplies = {}; - }, - keybind: function(e) { - var key; - if (e.keyCode === 9) { - return; - } - e.preventDefault(); - e.stopPropagation(); - if ((key = Keybinds.keyCode(e)) == null) { - return; - } - this.value = key; - return $.cb.value.call(this); - }, - filter: function() { - var el, name, ta; - el = this.nextSibling; - if ((name = this.value) !== 'guide') { - ta = $.el('textarea', { - name: name, - className: 'field', - value: $.get(name, Conf[name]) - }); - $.on(ta, 'change', $.cb.value); - $.replace(el, ta); - return; - } - if (el) { - $.rm(el); - } - return $.after(this, $.el('article', { - innerHTML: '

Use regular expressions, one per line.
\ - Lines starting with a # will be ignored.
\ - For example, /weeaboo/i will filter posts containing the string `weeaboo`, case-insensitive.

\ -
    You can use these settings with each regular expression, separate them with semicolons:\ -
  • \ - Per boards, separate them with commas. It is global if not specified.
    \ - For example: boards:a,jp;.\ -
  • \ -
  • \ - Filter OPs only along with their threads (`only`), replies only (`no`, this is default), or both (`yes`).
    \ - For example: op:only;, op:no; or op:yes;.\ -
  • \ -
  • \ - Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).
    \ - For example: stub:yes; or stub:no;.\ -
  • \ -
  • \ - Highlight instead of hiding. You can specify a class name to use with a userstyle.
    \ - For example: highlight; or highlight:wallpaper;.\ -
  • \ -
  • \ - Highlighted OPs will have their threads put on top of board pages by default.
    \ - For example: top:yes; or top:no;.\ -
  • \ -
' - })); - }, - time: function() { - Time.foo(); - Time.date = new Date(); - return $.id('timePreview').textContent = Time.funk(Time); - }, - backlink: function() { - return $.id('backlinkPreview').textContent = Conf['backlink'].replace(/%id/, '123456789'); - }, - fileInfo: function() { - FileInfo.data = { - link: 'javascript:;', - spoiler: true, - size: '276', - unit: 'KB', - resolution: '1280x720', - fullname: 'd9bb2efc98dd0df141a94399ff5880b7.jpg', - shortname: 'd9bb2efc98dd0df141a94399ff5880(...).jpg' - }; - FileInfo.setFormats(); - return $.id('fileInfoPreview').innerHTML = FileInfo.funk(FileInfo); - }, - favicon: function() { - Favicon["switch"](); - Unread.update(true); - return this.nextElementSibling.innerHTML = " "; - } - }; - - Updater = { - init: function() { - var checkbox, checked, dialog, html, input, name, title, _i, _len, _ref; - html = '
'; - checkbox = Config.updater.checkbox; - for (name in checkbox) { - title = checkbox[name][1]; - checked = Conf[name] ? 'checked' : ''; - html += "
"; - } - checked = Conf['Auto Update'] ? 'checked' : ''; - html += "
"; - dialog = UI.dialog('updater', 'bottom: 0; right: 0;', html); - this.count = $('#count', dialog); - this.timer = $('#timer', dialog); - this.thread = $.id("t" + g.THREAD_ID); - this.unsuccessfulFetchCount = 0; - this.lastModified = '0'; - _ref = $$('input', dialog); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - input = _ref[_i]; - if (input.type === 'checkbox') { - $.on(input, 'click', $.cb.checked); - } - switch (input.name) { - case 'Scroll BG': - $.on(input, 'click', this.cb.scrollBG); - this.cb.scrollBG.call(input); - break; - case 'Verbose': - $.on(input, 'click', this.cb.verbose); - this.cb.verbose.call(input); - break; - case 'Auto Update This': - $.on(input, 'click', this.cb.autoUpdate); - this.cb.autoUpdate.call(input); - break; - case 'Interval': - input.value = Conf['Interval']; - $.on(input, 'change', this.cb.interval); - this.cb.interval.call(input); - break; - case 'Update Now': - $.on(input, 'click', this.update); - } - } - $.add(d.body, dialog); - $.on(d, 'QRPostSuccessful', this.cb.post); - return $.on(d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', this.cb.visibility); - }, - cb: { - post: function() { - if (!Conf['Auto Update This']) { - return; - } - Updater.unsuccessfulFetchCount = 0; - return setTimeout(Updater.update, 500); - }, - visibility: function() { - var state; - state = d.visibilityState || d.oVisibilityState || d.mozVisibilityState || d.webkitVisibilityState; - if (state !== 'visible') { - return; - } - Updater.unsuccessfulFetchCount = 0; - if (Updater.timer.textContent < -Conf['Interval']) { - return Updater.set('timer', -Updater.getInterval()); - } - }, - interval: function() { - var val; - val = parseInt(this.value, 10); - this.value = val > 5 ? val : 5; - $.cb.value.call(this); - return Updater.set('timer', -Updater.getInterval()); - }, - verbose: function() { - if (Conf['Verbose']) { - Updater.set('count', '+0'); - return Updater.timer.hidden = false; - } else { - Updater.set('count', 'Thread Updater'); - Updater.count.className = ''; - return Updater.timer.hidden = true; - } - }, - autoUpdate: function() { - if (Conf['Auto Update This'] = this.checked) { - return Updater.timeoutID = setTimeout(Updater.timeout, 1000); - } else { - return clearTimeout(Updater.timeoutID); - } - }, - scrollBG: function() { - return Updater.scrollBG = this.checked ? function() { - return true; - } : function() { - return !(d.hidden || d.oHidden || d.mozHidden || d.webkitHidden); - }; - }, - update: function() { - var count, doc, id, lastPost, nodes, reply, scroll, _i, _len, _ref, _ref1, _ref2; - if (this.status === 404) { - Updater.set('timer', ''); - Updater.set('count', 404); - Updater.count.className = 'warning'; - clearTimeout(Updater.timeoutID); - g.dead = true; - if (Conf['Unread Count']) { - Unread.title = Unread.title.match(/^.+-/)[0] + ' 404'; - } else { - d.title = d.title.match(/^.+-/)[0] + ' 404'; - } - Unread.update(true); - QR.abort(); - return; - } - if ((_ref = this.status) !== 0 && _ref !== 200 && _ref !== 304) { - if (Conf['Verbose']) { - Updater.set('count', this.statusText); - Updater.count.className = 'warning'; - } - Updater.unsuccessfulFetchCount++; - return; - } - Updater.unsuccessfulFetchCount++; - Updater.set('timer', -Updater.getInterval()); - /* - Status Code 304: Not modified - By sending the `If-Modified-Since` header we get a proper status code, and no response. - This saves bandwidth for both the user and the servers, avoid unnecessary computation, - and won't load images and scripts when parsing the response. - */ - - if ((_ref1 = this.status) === 0 || _ref1 === 304) { - if (Conf['Verbose']) { - Updater.set('count', '+0'); - Updater.count.className = null; - } - return; - } - Updater.lastModified = this.getResponseHeader('Last-Modified'); - doc = d.implementation.createHTMLDocument(''); - doc.documentElement.innerHTML = this.response; - lastPost = Updater.thread.lastElementChild; - id = lastPost.id.slice(2); - nodes = []; - _ref2 = $$('.replyContainer', doc).reverse(); - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - reply = _ref2[_i]; - if (reply.id.slice(2) <= id) { - break; - } - nodes.push(reply); - } - count = nodes.length; - if (Conf['Verbose']) { - Updater.set('count', "+" + count); - Updater.count.className = count ? 'new' : null; - } - if (!count) { - return; - } - Updater.unsuccessfulFetchCount = 0; - Updater.set('timer', -Updater.getInterval()); - scroll = Conf['Scrolling'] && Updater.scrollBG() && lastPost.getBoundingClientRect().bottom - d.documentElement.clientHeight < 25; - $.add(Updater.thread, nodes.reverse()); - if (scroll) { - return nodes[0].scrollIntoView(); - } - } - }, - set: function(name, text) { - var el, node; - el = Updater[name]; - if (node = el.firstChild) { - return node.data = text; - } else { - return el.textContent = text; - } - }, - getInterval: function() { - var i, j; - i = +Conf['Interval']; - j = Math.min(this.unsuccessfulFetchCount, 9); - if (!(d.hidden || d.oHidden || d.mozHidden || d.webkitHidden)) { - j = Math.min(j, 6); - } - return Math.max(i, [5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]); - }, - timeout: function() { - var n; - Updater.timeoutID = setTimeout(Updater.timeout, 1000); - n = 1 + Number(Updater.timer.firstChild.data); - if (n === 0) { - return Updater.update(); - } else if (n >= Updater.getInterval()) { - Updater.unsuccessfulFetchCount++; - Updater.set('count', 'Retry'); - Updater.count.className = null; - return Updater.update(); - } else { - return Updater.set('timer', n); - } - }, - update: function() { - var url, _ref; - Updater.set('timer', 0); - if ((_ref = Updater.request) != null) { - _ref.abort(); - } - url = location.pathname + '?' + Date.now(); - return Updater.request = $.ajax(url, { - onload: Updater.cb.update - }, { - headers: { - 'If-Modified-Since': Updater.lastModified - } - }); - } - }; - - Watcher = { - init: function() { - var favicon, html, input, _i, _len, _ref; - html = '
Thread Watcher
'; - this.dialog = UI.dialog('watcher', 'top: 50px; left: 0px;', html); - $.add(d.body, this.dialog); - _ref = $$('.op input'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - input = _ref[_i]; - favicon = $.el('img', { - className: 'favicon' - }); - $.on(favicon, 'click', this.cb.toggle); - $.before(input, favicon); - } - if (g.THREAD_ID === $.get('autoWatch', 0)) { - this.watch(g.THREAD_ID); - $["delete"]('autoWatch'); - } else { - this.refresh(); - } - $.on(d, 'QRPostSuccessful', this.cb.post); - return $.sync('watched', this.refresh); - }, - refresh: function(watched) { - var board, div, favicon, id, link, nodes, props, watchedBoard, x, _i, _j, _len, _len1, _ref, _ref1, _ref2; - watched || (watched = $.get('watched', {})); - nodes = []; - for (board in watched) { - _ref = watched[board]; - for (id in _ref) { - props = _ref[id]; - x = $.el('a', { - textContent: '×', - href: 'javascript:;' - }); - $.on(x, 'click', Watcher.cb.x); - link = $.el('a', props); - link.title = link.textContent; - div = $.el('div'); - $.add(div, [x, $.tn(' '), link]); - nodes.push(div); - } - } - _ref1 = $$('div:not(.move)', Watcher.dialog); - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - div = _ref1[_i]; - $.rm(div); - } - $.add(Watcher.dialog, nodes); - watchedBoard = watched[g.BOARD] || {}; - _ref2 = $$('.favicon'); - for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { - favicon = _ref2[_j]; - id = favicon.nextSibling.name; - if (id in watchedBoard) { - favicon.src = Favicon["default"]; - } else { - favicon.src = Favicon.empty; - } - } - }, - cb: { - toggle: function() { - return Watcher.toggle(this.parentNode); - }, - x: function() { - var thread; - thread = this.nextElementSibling.pathname.split('/'); - return Watcher.unwatch(thread[3], thread[1]); - }, - post: function(e) { - var postID, threadID, _ref; - _ref = e.detail, postID = _ref.postID, threadID = _ref.threadID; - if (threadID === '0') { - if (Conf['Auto Watch']) { - return $.set('autoWatch', postID); - } - } else if (Conf['Auto Watch Reply']) { - return Watcher.watch(threadID); - } - } - }, - toggle: function(thread) { - var id; - id = $('.favicon + input', thread).name; - return Watcher.watch(id) || Watcher.unwatch(id, g.BOARD); - }, - unwatch: function(id, board) { - var watched; - watched = $.get('watched', {}); - delete watched[board][id]; - $.set('watched', watched); - return Watcher.refresh(); - }, - watch: function(id) { - var thread, watched, _name; - thread = $.id("t" + id); - if ($('.favicon', thread).src === Favicon["default"]) { - return false; - } - watched = $.get('watched', {}); - watched[_name = g.BOARD] || (watched[_name] = {}); - watched[g.BOARD][id] = { - href: "/" + g.BOARD + "/res/" + id, - textContent: Get.title(thread) - }; - $.set('watched', watched); - Watcher.refresh(); - return true; - } - }; - - Anonymize = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - var name, parent, trip; - if (post.isInlined && !post.isCrosspost) { - return; - } - name = $('.postInfo .name', post.el); - name.textContent = 'Anonymous'; - if ((trip = name.nextElementSibling) && trip.className === 'postertrip') { - $.rm(trip); - } - if ((parent = name.parentNode).className === 'useremail' && !/^mailto:sage$/i.test(parent.href)) { - return $.replace(parent, name); - } - } - }; - - Sauce = { - init: function() { - var link, _i, _len, _ref; - if (g.BOARD === 'f') { - return; - } - this.links = []; - _ref = Conf['sauces'].split('\n'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - link = _ref[_i]; - if (link[0] === '#') { - continue; - } - this.links.push(this.createSauceLink(link.trim())); - } - if (!this.links.length) { - return; - } - return Main.callbacks.push(this.node); - }, - createSauceLink: function(link) { - var domain, el, href, m; - link = link.replace(/(\$\d)/g, function(parameter) { - switch (parameter) { - case '$1': - return "' + (isArchived ? img.firstChild.src : 'http://thumbs.4chan.org' + img.pathname.replace(/src(\\/\\d+).+$/, 'thumb$1s.jpg')) + '"; - case '$2': - return "' + img.href + '"; - case '$3': - return "' + encodeURIComponent(img.firstChild.dataset.md5) + '"; - case '$4': - return g.BOARD; - default: - return parameter; - } - }); - domain = (m = link.match(/;text:(.+)$/)) ? m[1] : link.match(/(\w+)\.\w+\//)[1]; - href = link.replace(/;text:.+$/, ''); - href = Function('img', 'isArchived', "return '" + href + "'"); - el = $.el('a', { - target: '_blank', - textContent: domain - }); - return function(img, isArchived) { - var a; - a = el.cloneNode(true); - a.href = href(img, isArchived); - return a; - }; - }, - node: function(post) { - var img, link, nodes, _i, _len, _ref; - img = post.img; - if (post.isInlined && !post.isCrosspost || !img) { - return; - } - img = img.parentNode; - nodes = []; - _ref = Sauce.links; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - link = _ref[_i]; - nodes.push($.tn('\u00A0'), link(img, post.isArchived)); - } - return $.add(post.fileInfo, nodes); - } - }; - - RevealSpoilers = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - var img, s; - img = post.img; - if (!(img && /^Spoiler/.test(img.alt)) || post.isInlined && !post.isCrosspost || post.isArchived) { - return; - } - img.removeAttribute('style'); - s = img.style; - s.maxHeight = s.maxWidth = /\bop\b/.test(post["class"]) ? '250px' : '125px'; - return img.src = "//thumbs.4chan.org" + (img.parentNode.pathname.replace(/src(\/\d+).+$/, 'thumb$1s.jpg')); - } - }; - - Time = { - init: function() { - Time.foo(); - return Main.callbacks.push(this.node); - }, - node: function(post) { - var node; - if (post.isInlined && !post.isCrosspost) { - return; - } - node = $('.postInfo > .dateTime', post.el); - Time.date = new Date(node.dataset.utc * 1000); - return node.textContent = Time.funk(Time); - }, - foo: function() { - var code; - code = Conf['time'].replace(/%([A-Za-z])/g, function(s, c) { - if (c in Time.formatters) { - return "' + Time.formatters." + c + "() + '"; - } else { - return s; - } - }); - return Time.funk = Function('Time', "return '" + code + "'"); - }, - day: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], - month: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], - zeroPad: function(n) { - if (n < 10) { - return '0' + n; - } else { - return n; - } - }, - formatters: { - a: function() { - return Time.day[Time.date.getDay()].slice(0, 3); - }, - A: function() { - return Time.day[Time.date.getDay()]; - }, - b: function() { - return Time.month[Time.date.getMonth()].slice(0, 3); - }, - B: function() { - return Time.month[Time.date.getMonth()]; - }, - d: function() { - return Time.zeroPad(Time.date.getDate()); - }, - e: function() { - return Time.date.getDate(); - }, - H: function() { - return Time.zeroPad(Time.date.getHours()); - }, - I: function() { - return Time.zeroPad(Time.date.getHours() % 12 || 12); - }, - k: function() { - return Time.date.getHours(); - }, - l: function() { - return Time.date.getHours() % 12 || 12; - }, - m: function() { - return Time.zeroPad(Time.date.getMonth() + 1); - }, - M: function() { - return Time.zeroPad(Time.date.getMinutes()); - }, - p: function() { - if (Time.date.getHours() < 12) { - return 'AM'; - } else { - return 'PM'; - } - }, - P: function() { - if (Time.date.getHours() < 12) { - return 'am'; - } else { - return 'pm'; - } - }, - S: function() { - return Time.zeroPad(Time.date.getSeconds()); - }, - y: function() { - return Time.date.getFullYear() - 2000; - } - } - }; - - FileInfo = { - init: function() { - if (g.BOARD === 'f') { - return; - } - this.setFormats(); - return Main.callbacks.push(this.node); - }, - node: function(post) { - var alt, node, span; - if (post.isInlined && !post.isCrosspost || !post.fileInfo) { - return; - } - node = post.fileInfo.firstElementChild; - alt = post.img.alt; - span = $('span', node); - FileInfo.data = { - link: post.img.parentNode.href, - spoiler: /^Spoiler/.test(alt), - size: alt.match(/\d+\.?\d*/)[0], - unit: alt.match(/\w+$/)[0], - resolution: span.previousSibling.textContent.match(/\d+x\d+|PDF/)[0], - fullname: span.title, - shortname: span.textContent - }; - node.setAttribute('data-filename', span.title); - return node.innerHTML = FileInfo.funk(FileInfo); - }, - setFormats: function() { - var code; - code = Conf['fileInfo'].replace(/%([BKlLMnNprs])/g, function(s, c) { - if (c in FileInfo.formatters) { - return "' + f.formatters." + c + "() + '"; - } else { - return s; - } - }); - return this.funk = Function('f', "return '" + code + "'"); - }, - convertUnit: function(unitT) { - var i, size, unitF, units; - size = this.data.size; - unitF = this.data.unit; - if (unitF !== unitT) { - units = ['B', 'KB', 'MB']; - i = units.indexOf(unitF) - units.indexOf(unitT); - if (unitT === 'B') { - unitT = 'Bytes'; - } - if (i > 0) { - while (i-- > 0) { - size *= 1024; - } - } else if (i < 0) { - while (i++ < 0) { - size /= 1024; - } - } - if (size < 1 && size.toString().length > size.toFixed(2).length) { - size = size.toFixed(2); - } - } - return "" + size + " " + unitT; - }, - formatters: { - l: function() { - return "" + (this.n()) + ""; - }, - L: function() { - return "" + (this.N()) + ""; - }, - n: function() { - if (FileInfo.data.fullname === FileInfo.data.shortname) { - return FileInfo.data.fullname; - } else { - return "" + FileInfo.data.shortname + "" + FileInfo.data.fullname + ""; - } - }, - N: function() { - return FileInfo.data.fullname; - }, - p: function() { - if (FileInfo.data.spoiler) { - return 'Spoiler, '; - } else { - return ''; - } - }, - s: function() { - return "" + FileInfo.data.size + " " + FileInfo.data.unit; - }, - B: function() { - return FileInfo.convertUnit('B'); - }, - K: function() { - return FileInfo.convertUnit('KB'); - }, - M: function() { - return FileInfo.convertUnit('MB'); - }, - r: function() { - return FileInfo.data.resolution; - } - } - }; - - Get = { - post: function(board, threadID, postID, root, cb) { - var post, url; - if (board === g.BOARD && (post = $.id("pc" + postID))) { - $.add(root, Get.cleanPost(post.cloneNode(true))); - return; - } - root.textContent = "Loading post No." + postID + "..."; - if (threadID) { - return $.cache("/" + board + "/res/" + threadID, function() { - return Get.parsePost(this, board, threadID, postID, root, cb); - }); - } else if (url = Redirect.post(board, postID)) { - return $.cache(url, function() { - return Get.parseArchivedPost(this, board, postID, root, cb); - }); - } - }, - parsePost: function(req, board, threadID, postID, root, cb) { - var doc, href, link, pc, quote, status, url, _i, _len, _ref; - status = req.status; - if (status !== 200) { - if (url = Redirect.post(board, postID)) { - $.cache(url, function() { - return Get.parseArchivedPost(this, board, postID, root, cb); - }); - } else { - root.textContent = status === 404 ? "Thread No." + threadID + " has not been found." : "Error " + req.status + ": " + req.statusText + "."; - } - return; - } - doc = d.implementation.createHTMLDocument(''); - doc.documentElement.innerHTML = req.response; - if (!(pc = doc.getElementById("pc" + postID))) { - if (url = Redirect.post(board, postID)) { - $.cache(url, function() { - return Get.parseArchivedPost(this, board, postID, root, cb); - }); - } else { - root.textContent = "Post No." + postID + " has not been found."; - } - return; - } - pc = Get.cleanPost(d.importNode(pc, true)); - _ref = $$('.quotelink', pc); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - href = quote.getAttribute('href'); - if (href[0] === '/') { - continue; - } - quote.href = "/" + board + "/res/" + href; - } - link = $('a[title="Highlight this post"]', pc); - link.href = "/" + board + "/res/" + threadID + "#p" + postID; - link.nextSibling.href = "/" + board + "/res/" + threadID + "#q" + postID; - $.replace(root.firstChild, pc); - if (cb) { - return cb(); - } - }, - parseArchivedPost: function(req, board, postID, root, cb) { - var bq, br, capcode, data, email, file, filename, filesize, isOP, name, nameBlock, pc, pi, piM, span, spoiler, subject, threadID, threshold, thumb_src, timestamp, trip; - data = JSON.parse(req.response); - $.addClass(root, 'archivedPost'); - if (data.error) { - root.textContent = data.error; - return; - } - threadID = data.thread_num; - isOP = postID === threadID; - name = data.name, trip = data.trip, timestamp = data.timestamp; - subject = data.title; - piM = $.el('div', { - id: "pim" + postID, - className: 'postInfoM mobile', - innerHTML: "
" + data.fourchan_date + "
No." + postID + "
" - }); - $('.name', piM).textContent = name; - $('.subject', piM).textContent = subject; - br = $('br', piM); - if (trip) { - $.before(br, [ - $.tn(' '), $.el('span', { - className: 'postertrip', - textContent: trip - }) - ]); - } - capcode = data.capcode; - if (capcode !== 'N') { - $.addClass(br.parentNode, capcode === 'A' ? 'capcodeAdmin' : 'capcodeMod'); - $.before(br, [ - $.tn(' '), $.el('strong', { - className: 'capcode', - textContent: capcode === 'A' ? '## Admin' : '## Mod' - }), $.tn(' '), $.el('img', { - src: capcode === 'A' ? '//static.4chan.org/image/adminicon.gif' : '//static.4chan.org/image/modicon.gif', - alt: capcode === 'A' ? 'This user is the 4chan Administrator.' : 'This user is a 4chan Moderator.', - title: capcode === 'A' ? 'This user is the 4chan Administrator.' : 'This user is a 4chan Moderator.', - className: 'identityIcon' - }) - ]); - } - pi = $.el('div', { - id: "pi" + postID, - className: 'postInfo desktop', - innerHTML: " data.fourchan_date No." + postID + "" + (isOP ? '   ' : '') + " " - }); - $('.subject', pi).textContent = subject; - nameBlock = $('.nameBlock', pi); - if (data.email) { - email = $.el('a', { - className: 'useremail', - href: "mailto:" + data.email - }); - $.add(nameBlock, email); - nameBlock = email; - } - $.add(nameBlock, $.el('span', { - className: 'name', - textContent: data.name - })); - if (trip) { - $.add(nameBlock, [ - $.tn(' '), $.el('span', { - className: 'postertrip', - textContent: trip - }) - ]); - } - if (capcode !== 'N') { - $.add(nameBlock, [ - $.tn(' '), $.el('strong', { - className: capcode === 'A' ? 'capcode capcodeAdmin' : 'capcode', - textContent: capcode === 'A' ? '## Admin' : '## Mod' - }) - ]); - nameBlock = $('.nameBlock', pi); - $.addClass(nameBlock, capcode === 'A' ? 'capcodeAdmin' : 'capcodeMod'); - $.add(nameBlock, [ - $.tn(' '), $.el('img', { - src: capcode === 'A' ? '//static.4chan.org/image/adminicon.gif' : '//static.4chan.org/image/modicon.gif', - alt: capcode === 'A' ? 'This user is the 4chan Administrator.' : 'This user is a 4chan Moderator.', - title: capcode === 'A' ? 'This user is the 4chan Administrator.' : 'This user is a 4chan Moderator.', - className: 'identityIcon' - }) - ]); - } - bq = $.el('blockquote', { - id: "m" + postID, - className: 'postMessage', - textContent: data.comment - }); - bq.innerHTML = bq.innerHTML.replace(/\n|\[\/?b\]|\[\/?spoiler\]|\[\/?code\]|\[\/?moot\]|\[\/?banned\]/g, function(text) { - switch (text) { - case '\n': - return '
'; - case '[b]': - return ''; - case '[/b]': - return ''; - case '[spoiler]': - return ''; - case '[/spoiler]': - return ''; - case '[code]': - return '
';
-          case '[/code]':
-            return '
'; - case '[moot]': - return '
'; - case '[/moot]': - return '
'; - case '[banned]': - return ''; - case '[/banned]': - return ''; - } - }); - bq.innerHTML = bq.innerHTML.replace(/(^|>)(>[^<$]+)(<|$)/g, '$1$2$3'); - pc = $.el('div', { - id: "pc" + postID, - className: "postContainer " + (isOP ? 'op' : 'reply') + "Container", - innerHTML: "
" - }); - $.add(pc.firstChild, [piM, pi, bq]); - if (filename = data.media_filename) { - file = $.el('div', { - id: "f" + postID, - className: 'file' - }); - spoiler = data.spoiler === '1'; - filesize = $.bytesToString(data.media_size); - $.add(file, $.el('div', { - className: 'fileInfo', - innerHTML: "File: " + data.media_orig + "-(" + (spoiler ? 'Spoiler Image, ' : '') + filesize + ", " + data.media_w + "x" + data.media_h + ", )" - })); - span = $('span[title]', file); - span.title = filename; - threshold = isOP ? 40 : 30; - span.textContent = filename.replace(/\.\w+$/, '').length > threshold ? "" + filename.slice(0, threshold - 5) + "(...)" + (filename.match(/\.\w+$/)) : filename; - thumb_src = data.media_status === 'available' ? "src=" + data.thumb_link : ''; - $.add(file, $.el('a', { - className: spoiler ? 'fileThumb imgspoiler' : 'fileThumb', - href: data.media_link || data.remote_media_link, - target: '_blank', - innerHTML: "" + (data.media_status !== " - })); - $.after((isOP ? piM : pi), file); - } - $.replace(root.firstChild, Get.cleanPost(pc)); - if (cb) { - return cb(); - } - }, - cleanPost: function(root) { - var child, el, els, inline, inlined, now, post, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2; - post = $('.post', root); - _ref = Array.prototype.slice.call(root.childNodes); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - child = _ref[_i]; - if (child !== post) { - $.rm(child); - } - } - _ref1 = $$('.inline', post); - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - inline = _ref1[_j]; - $.rm(inline); - } - _ref2 = $$('.inlined', post); - for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { - inlined = _ref2[_k]; - $.rmClass(inlined, 'inlined'); - } - now = Date.now(); - els = $$('[id]', root); - els.push(root); - for (_l = 0, _len3 = els.length; _l < _len3; _l++) { - el = els[_l]; - el.id = "" + now + "_" + el.id; - } - $.rmClass(root, 'forwarded'); - $.rmClass(root, 'qphl'); - $.rmClass(post, 'highlight'); - $.rmClass(post, 'qphl'); - root.hidden = post.hidden = false; - return root; - }, - title: function(thread) { - var el, op, span; - op = $('.op', thread); - el = $('.subject', op); - if (!el.textContent) { - el = $('blockquote', op); - if (!el.textContent) { - el = $('.nameBlock', op); - } - } - span = $.el('span', { - innerHTML: el.innerHTML.replace(/
/g, ' ') - }); - return "/" + g.BOARD + "/ - " + (span.textContent.trim()); - } - }; - - TitlePost = { - init: function() { - return d.title = Get.title(); - } - }; - - QuoteBacklink = { - init: function() { - var format; - format = Conf['backlink'].replace(/%id/g, "' + id + '"); - this.funk = Function('id', "return '" + format + "'"); - return Main.callbacks.push(this.node); - }, - node: function(post) { - var a, container, el, link, qid, quote, quotes, _i, _len, _ref; - if (post.isInlined) { - return; - } - quotes = {}; - _ref = post.quotes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if (qid = quote.hash.slice(2)) { - quotes[qid] = true; - } - } - a = $.el('a', { - href: "/" + g.BOARD + "/res/" + post.threadID + "#p" + post.ID, - className: post.el.hidden ? 'filtered backlink' : 'backlink', - textContent: QuoteBacklink.funk(post.ID) - }); - for (qid in quotes) { - if (!(el = $.id("pi" + qid)) || !Conf['OP Backlinks'] && /\bop\b/.test(el.parentNode.className)) { - continue; - } - link = a.cloneNode(true); - if (Conf['Quote Preview']) { - $.on(link, 'mouseover', QuotePreview.mouseover); - } - if (Conf['Quote Inline']) { - $.on(link, 'click', QuoteInline.toggle); - } else { - link.setAttribute('onclick', "replyhl('" + post.ID + "');"); - } - if (!(container = $.id("blc" + qid))) { - container = $.el('span', { - className: 'container', - id: "blc" + qid - }); - $.add(el, container); - } - $.add(container, [$.tn(' '), link]); - } - } - }; - - QuoteInline = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - var quote, _i, _j, _len, _len1, _ref, _ref1; - _ref = post.quotes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if (!(quote.hash || /\bdeadlink\b/.test(quote.className))) { - continue; - } - quote.removeAttribute('onclick'); - $.on(quote, 'click', QuoteInline.toggle); - } - _ref1 = post.backlinks; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - quote = _ref1[_j]; - $.on(quote, 'click', QuoteInline.toggle); - } - }, - toggle: function(e) { - var id; - if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) { - return; - } - e.preventDefault(); - id = this.dataset.id || this.hash.slice(2); - if (/\binlined\b/.test(this.className)) { - QuoteInline.rm(this, id); - } else { - if ($.x("ancestor::div[contains(@id,'p" + id + "')]", this)) { - return; - } - QuoteInline.add(this, id); - } - return this.classList.toggle('inlined'); - }, - add: function(q, id) { - var board, el, i, inline, isBacklink, path, postID, root, threadID; - if (q.host === 'boards.4chan.org') { - path = q.pathname.split('/'); - board = path[1]; - threadID = path[3]; - postID = id; - } else { - board = q.dataset.board; - threadID = 0; - postID = q.dataset.id; - } - el = board === g.BOARD ? $.id("p" + postID) : false; - inline = $.el('div', { - id: "i" + postID, - className: el ? 'inline' : 'inline crosspost' - }); - root = (isBacklink = /\bbacklink\b/.test(q.className)) ? q.parentNode : $.x('ancestor-or-self::*[parent::blockquote][1]', q); - $.after(root, inline); - Get.post(board, threadID, postID, inline); - if (!el) { - return; - } - if (isBacklink && Conf['Forward Hiding']) { - $.addClass(el.parentNode, 'forwarded'); - ++el.dataset.forwarded || (el.dataset.forwarded = 1); - } - if ((i = Unread.replies.indexOf(el)) !== -1) { - Unread.replies.splice(i, 1); - return Unread.update(true); - } - }, - rm: function(q, id) { - var div, inlined, _i, _len, _ref; - div = $.x("following::div[@id='i" + id + "']", q); - $.rm(div); - if (!Conf['Forward Hiding']) { - return; - } - _ref = $$('.backlink.inlined', div); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - inlined = _ref[_i]; - div = $.id(inlined.hash.slice(1)); - if (!--div.dataset.forwarded) { - $.rmClass(div.parentNode, 'forwarded'); - } - } - if (/\bbacklink\b/.test(q.className)) { - div = $.id("p" + id); - if (!--div.dataset.forwarded) { - return $.rmClass(div.parentNode, 'forwarded'); - } - } - } - }; - - QuotePreview = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - var quote, _i, _j, _len, _len1, _ref, _ref1; - _ref = post.quotes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if (quote.hash || /\bdeadlink\b/.test(quote.className)) { - $.on(quote, 'mouseover', QuotePreview.mouseover); - } - } - _ref1 = post.backlinks; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - quote = _ref1[_j]; - $.on(quote, 'mouseover', QuotePreview.mouseover); - } - }, - mouseover: function(e) { - var board, el, path, postID, qp, quote, quoterID, threadID, _i, _len, _ref; - if (/\binlined\b/.test(this.className)) { - return; - } - if (qp = $.id('qp')) { - if (qp === UI.el) { - delete UI.el; - } - $.rm(qp); - } - if (UI.el) { - return; - } - if (this.host === 'boards.4chan.org') { - path = this.pathname.split('/'); - board = path[1]; - threadID = path[3]; - postID = this.hash.slice(2); - } else { - board = this.dataset.board; - threadID = 0; - postID = this.dataset.id; - } - qp = UI.el = $.el('div', { - id: 'qp', - className: 'reply dialog' - }); - UI.hover(e); - $.add(d.body, qp); - if (board === g.BOARD) { - el = $.id("p" + postID); - } - Get.post(board, threadID, postID, qp, function() { - var bq, img, post; - bq = $('blockquote', qp); - Main.prettify(bq); - post = { - el: qp, - blockquote: bq, - isArchived: /\barchivedPost\b/.test(qp.className) - }; - if (img = $('img[data-md5]', qp)) { - post.fileInfo = img.parentNode.previousElementSibling; - post.img = img; - } - if (Conf['Reveal Spoilers']) { - RevealSpoilers.node(post); - } - if (Conf['Image Auto-Gif']) { - AutoGif.node(post); - } - if (Conf['Time Formatting']) { - Time.node(post); - } - if (Conf['File Info Formatting']) { - FileInfo.node(post); - } - if (Conf['Resurrect Quotes']) { - return Quotify.node(post); - } - }); - $.on(this, 'mousemove', UI.hover); - $.on(this, 'mouseout click', QuotePreview.mouseout); - if (!el) { - return; - } - if (Conf['Quote Highlighting']) { - if (/\bop\b/.test(el.className)) { - $.addClass(el.parentNode, 'qphl'); - } else { - $.addClass(el, 'qphl'); - } - } - quoterID = $.x('ancestor::*[@id][1]', this).id.match(/\d+$/)[0]; - _ref = $$('.quotelink, .backlink', qp); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if (quote.hash.slice(2) === quoterID) { - $.addClass(quote, 'forwardlink'); - } - } - }, - mouseout: function(e) { - var el; - UI.hoverend(); - if (el = $.id(this.hash.slice(1))) { - $.rmClass(el, 'qphl'); - $.rmClass(el.parentNode, 'qphl'); - } - $.off(this, 'mousemove', UI.hover); - return $.off(this, 'mouseout click', QuotePreview.mouseout); - } - }; - - QuoteOP = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - var quote, _i, _len, _ref; - if (post.isInlined && !post.isCrosspost) { - return; - } - _ref = post.quotes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if (quote.hash.slice(2) === post.threadID) { - $.add(quote, $.tn('\u00A0(OP)')); - } - } - } - }; - - QuoteCT = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - var path, quote, _i, _len, _ref; - if (post.isInlined && !post.isCrosspost) { - return; - } - _ref = post.quotes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if (!quote.hash) { - continue; - } - path = quote.pathname.split('/'); - if (path[1] === g.BOARD && path[3] !== post.threadID) { - $.add(quote, $.tn('\u00A0(Cross-thread)')); - } - } - } - }; - - Quotify = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - var a, board, data, i, id, index, m, node, nodes, quote, quotes, snapshot, text, _i, _j, _len, _ref; - if (post.isInlined && !post.isCrosspost) { - return; - } - snapshot = d.evaluate('.//text()[not(parent::a)]', post.blockquote, null, 6, null); - for (i = _i = 0, _ref = snapshot.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { - node = snapshot.snapshotItem(i); - data = node.data; - if (!(quotes = data.match(/>>(>\/[a-z\d]+\/)?\d+/g))) { - continue; - } - nodes = []; - for (_j = 0, _len = quotes.length; _j < _len; _j++) { - quote = quotes[_j]; - index = data.indexOf(quote); - if (text = data.slice(0, index)) { - nodes.push($.tn(text)); - } - id = quote.match(/\d+$/)[0]; - board = (m = quote.match(/^>>>\/([a-z\d]+)/)) ? m[1] : $('a[title="Highlight this post"]', post.el).pathname.split('/')[1]; - nodes.push(a = $.el('a', { - textContent: "" + quote + "\u00A0(Dead)" - })); - if (board === g.BOARD && $.id("p" + id)) { - a.href = "#p" + id; - a.className = 'quotelink'; - a.setAttribute('onclick', "replyhl('" + id + "');"); - } else { - a.href = Redirect.thread(board, 0, id); - a.className = 'deadlink'; - a.target = '_blank'; - if (Redirect.post(board, id)) { - $.addClass(a, 'quotelink'); - a.setAttribute('data-board', board); - a.setAttribute('data-id', id); - } - } - data = data.slice(index + quote.length); - } - if (data) { - nodes.push($.tn(data)); - } - $.replace(node, nodes); - } - } - }; - - DeleteLink = { - init: function() { - var aImage, aPost, children, div; - div = $.el('div', { - className: 'delete_link', - textContent: 'Delete' - }); - aPost = $.el('a', { - className: 'delete_post', - href: 'javascript:;' - }); - aImage = $.el('a', { - className: 'delete_image', - href: 'javascript:;' - }); - children = []; - children.push({ - el: aPost, - open: function() { - aPost.textContent = 'Post'; - $.on(aPost, 'click', DeleteLink["delete"]); - return true; - } - }); - children.push({ - el: aImage, - open: function(post) { - if (!post.img) { - return false; - } - aImage.textContent = 'Image'; - $.on(aImage, 'click', DeleteLink["delete"]); - return true; - } - }); - Menu.addEntry({ - el: div, - open: function(post) { - var node, seconds; - if (post.isArchived) { - return false; - } - node = div.firstChild; - if (seconds = DeleteLink.cooldown[post.ID]) { - node.textContent = "Delete (" + seconds + ")"; - DeleteLink.cooldown.el = node; - } else { - node.textContent = 'Delete'; - delete DeleteLink.cooldown.el; - } - return true; - }, - children: children - }); - return $.on(d, 'QRPostSuccessful', this.cooldown.start); - }, - "delete": function() { - var board, form, id, m, menu, pwd, self; - menu = $.id('menu'); - id = menu.dataset.id; - if (DeleteLink.cooldown[id]) { - return; - } - $.off(this, 'click', DeleteLink["delete"]); - this.textContent = 'Deleting...'; - pwd = (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $.id('delPassword').value; - board = $('a[title="Highlight this post"]', $.id(menu.dataset.rootid)).pathname.split('/')[1]; - self = this; - form = { - mode: 'usrdel', - onlyimgdel: /\bdelete_image\b/.test(this.className), - pwd: pwd - }; - form[id] = 'delete'; - return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + board + "/"), { - onload: function() { - return DeleteLink.load(self, this.response); - }, - onerror: function() { - return DeleteLink.error(self); - } - }, { - form: $.formData(form) - }); - }, - load: function(self, html) { - var doc, msg, s; - doc = d.implementation.createHTMLDocument(''); - doc.documentElement.innerHTML = html; - if (doc.title === '4chan - Banned') { - s = 'Banned!'; - } else if (msg = doc.getElementById('errmsg')) { - s = msg.textContent; - $.on(self, 'click', DeleteLink["delete"]); - } else { - s = 'Deleted'; - } - return self.textContent = s; - }, - error: function(self) { - self.textContent = 'Connection error, please retry.'; - return $.on(self, 'click', DeleteLink["delete"]); - }, - cooldown: { - start: function(e) { - return DeleteLink.cooldown.count(e.detail.postID, 30); - }, - count: function(postID, seconds) { - var el; - if (!((0 <= seconds && seconds <= 30))) { - return; - } - setTimeout(DeleteLink.cooldown.count, 1000, postID, seconds - 1); - el = DeleteLink.cooldown.el; - if (seconds === 0) { - if (el != null) { - el.textContent = 'Delete'; - } - delete DeleteLink.cooldown[postID]; - delete DeleteLink.cooldown.el; - return; - } - if (el != null) { - el.textContent = "Delete (" + seconds + ")"; - } - return DeleteLink.cooldown[postID] = seconds; - } - } - }; - - ReportLink = { - init: function() { - var a; - a = $.el('a', { - className: 'report_link', - href: 'javascript:;', - textContent: 'Report this post' - }); - $.on(a, 'click', this.report); - return Menu.addEntry({ - el: a, - open: function(post) { - return post.isArchived === false; - } - }); - }, - report: function() { - var a, id, set, url; - a = $('a[title="Highlight this post"]', $.id(this.parentNode.dataset.rootid)); - url = "//sys.4chan.org/" + (a.pathname.split('/')[1]) + "/imgboard.php?mode=report&no=" + this.parentNode.dataset.id; - id = Date.now(); - set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200"; - return window.open(url, id, set); - } - }; - - DownloadLink = { - init: function() { - var a; - if ($.el('a').download === void 0) { - return; - } - a = $.el('a', { - className: 'download_link', - textContent: 'Download file' - }); - return Menu.addEntry({ - el: a, - open: function(post) { - var fileText; - if (!post.img) { - return false; - } - a.href = post.img.parentNode.href; - fileText = post.fileInfo.firstElementChild; - a.download = Conf['File Info Formatting'] ? fileText.dataset.filename : $('span', fileText).title; - return true; - } - }); - } - }; - - ArchiveLink = { - init: function() { - var a; - a = $.el('a', { - className: 'archive_link', - target: '_blank', - textContent: 'Archived post' - }); - return Menu.addEntry({ - el: a, - open: function(post) { - var href, path; - path = $('a[title="Highlight this post"]', post.el).pathname.split('/'); - if ((href = Redirect.thread(path[1], path[3], post.ID)) === ("//boards.4chan.org/" + path[1] + "/")) { - return false; - } - a.href = href; - return true; - } - }); - } - }; - - ThreadStats = { - init: function() { - var dialog; - dialog = UI.dialog('stats', 'bottom: 0; left: 0;', '
0 / 0
'); - dialog.className = 'dialog'; - $.add(d.body, dialog); - this.posts = this.images = 0; - this.imgLimit = (function() { - switch (g.BOARD) { - case 'a': - case 'b': - case 'v': - case 'co': - case 'mlp': - return 251; - case 'vg': - return 501; - default: - return 151; - } - })(); - return Main.callbacks.push(this.node); - }, - node: function(post) { - var imgcount; - if (post.isInlined) { - return; - } - $.id('postcount').textContent = ++ThreadStats.posts; - if (!post.img) { - return; - } - imgcount = $.id('imagecount'); - imgcount.textContent = ++ThreadStats.images; - if (ThreadStats.images > ThreadStats.imgLimit) { - return $.addClass(imgcount, 'warning'); - } - } - }; - - Unread = { - init: function() { - this.title = d.title; - $.on(d, 'QRPostSuccessful', this.post); - this.update(); - $.on(window, 'scroll', Unread.scroll); - return Main.callbacks.push(this.node); - }, - replies: [], - foresee: [], - post: function(e) { - return Unread.foresee.push(e.detail.postID); - }, - node: function(post) { - var count, el, index; - if ((index = Unread.foresee.indexOf(post.ID)) !== -1) { - Unread.foresee.splice(index, 1); - return; - } - el = post.el; - if (el.hidden || /\bop\b/.test(post["class"]) || post.isInlined) { - return; - } - count = Unread.replies.push(el); - return Unread.update(count === 1); - }, - scroll: function() { - var bottom, height, i, reply, _i, _len, _ref; - height = d.documentElement.clientHeight; - _ref = Unread.replies; - for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { - reply = _ref[i]; - bottom = reply.getBoundingClientRect().bottom; - if (bottom > height) { - break; - } - } - if (i === 0) { - return; - } - Unread.replies = Unread.replies.slice(i); - return Unread.update(Unread.replies.length === 0); - }, - setTitle: function(count) { - if (this.scheduled) { - clearTimeout(this.scheduled); - delete Unread.scheduled; - this.setTitle(count); - return; - } - return this.scheduled = setTimeout((function() { - return d.title = "(" + count + ") " + Unread.title; - }), 5); - }, - update: function(updateFavicon) { - var count; - if (!g.REPLY) { - return; - } - count = this.replies.length; - if (Conf['Unread Count']) { - this.setTitle(count); - } - if (!(Conf['Unread Favicon'] && updateFavicon)) { - return; - } - if ($.engine === 'presto') { - $.rm(Favicon.el); - } - Favicon.el.href = g.dead ? count ? Favicon.unreadDead : Favicon.dead : count ? Favicon.unread : Favicon["default"]; - if (g.dead) { - $.addClass(Favicon.el, 'dead'); - } else { - $.rmClass(Favicon.el, 'dead'); - } - if (count) { - $.addClass(Favicon.el, 'unread'); - } else { - $.rmClass(Favicon.el, 'unread'); - } - if ($.engine !== 'webkit') { - return $.add(d.head, Favicon.el); - } - } - }; - - Favicon = { - init: function() { - var href; - if (this.el) { - return; - } - this.el = $('link[rel="shortcut icon"]', d.head); - this.el.type = 'image/x-icon'; - href = this.el.href; - this.SFW = /ws.ico$/.test(href); - this["default"] = href; - return this["switch"](); - }, - "switch": function() { - switch (Conf['favicon']) { - case 'ferongr': - this.unreadDead = 'data:image/gif;base64,R0lGODlhEAAQAOMHAOgLAnMFAL8AAOgLAukMA/+AgP+rq////////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw=='; - this.unreadSFW = 'data:image/gif;base64,R0lGODlhEAAQAOMHAADX8QBwfgC2zADX8QDY8nnl8qLp8v///////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw=='; - this.unreadNSFW = 'data:image/gif;base64,R0lGODlhEAAQAOMHAFT+ACh5AEncAFT+AFX/Acz/su7/5v///////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw=='; - break; - case 'xat-': - this.unreadDead = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA2ElEQVQ4y61TQQrCMBDMQ8WDIEV6LbT2A4og2Hq0veo7fIAH04dY9N4xmyYlpGmI2MCQTWYy3Wy2DAD7B2wWAzWgcTgVeZKlZRxHNYFi2jM18oBh0IcKtC6ixf22WT4IFLs0owxswXu9egm0Ls6bwfCFfNsJYJKfqoEkd3vgUgFVLWObtzNgVKyruC+ljSzr5OEnBzjvjcQecaQhbZgBb4CmGQw+PoMkTUtdbd8VSEPakcGxPOcsoIgUKy0LecY29BmdBrqRfjIwZ93KLs5loHvBnL3cLH/jF+C/+z5dgUysAAAAAElFTkSuQmCC'; - this.unreadSFW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA30lEQVQ4y2P4//8/AyWYgSoGQMF/GJ7Y11VVUVoyKTM9ey4Ig9ggMWQ1YA1IBvzXm34YjkH8mPyJB+Nqlp8FYRAbmxoMF6ArSNrw6T0Qf8Amh9cFMEWVR/7/A+L/uORxhgEIt5/+/3/2lf//5wAxiI0uj+4CBlBgxVUvOwtydgXQZpDmi2/+/7/0GmIQSAwkB1IDUkuUAZeABlx+g2zAZ9wGlAOjChba+LwAUgNSi2HA5Am9VciBhSsQQWyoWgZiovEDsdGI1QBYQiLJAGQalpSxyWEzAJYWkGm8clTJjQCZ1hkoVG0CygAAAABJRU5ErkJggg=='; - this.unreadNSFW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA4ElEQVQ4y2P4//8/AyWYgSoGQMF/GJ7YNbGqrKRiUnp21lwQBrFBYshqwBqQDPifdsYYjkH8mInxB+OWx58FYRAbmxoMF6ArKPmU9B6IP2CTw+sCmKKe/5X/gPg/LnmcYQDCs/63/1/9fzYQzwGz0eXRXcAACqy4ZfFnQc7u+V/xD6T55v+LQHwJbBBIDCQHUgNSS5QBt4Cab/2/jDDgMx4DykrKJ8FCG58XQGpAajEMmNw7uQo5sHAFIogNVctATDR+IDYasRoAS0gkGYBMw5IyNjlsBsDSAjKNV44quREAx58Mr9vt5wQAAAAASUVORK5CYII='; - break; - case 'Mayhem': - this.unreadDead = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABIUlEQVQ4jZ2ScWuDMBDFgw4pIkU0WsoQkWAYIkXZH4N9/+/V3dmfXSrKYIFHwt17j8vdGWNMIkgFuaDgzgQnwRs4EQs5KdolUQtagRN0givEDBTEOjgtGs0Zq8F7cKqqusVxrMQLaDUWcjBSrXkn8gs51tpJSWLk9b3HUa0aNIL5gPBR1/V4kJvR7lTwl8GmAm1Gf9+c3S+89qBHa8502AsmSrtBaEBPbIbj0ah2madlNAPEccdgJDfAtWifBjqWKShRBT6KoiH8QlEUn/qt0CCjnNdmPUwmFWzj9Oe6LpKuZXcwqq88z78Pch3aZU3dPwwc2sWlfZKCW5tWluV8kGvXClLm6dYN4/aUqfCbnEOzNDGhGZbNargvxCzvMGfRJD8UaDVvgkzo6QAAAABJRU5ErkJggg=='; - this.unreadSFW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABCElEQVQ4jZ2S4crCMAxF+0OGDJEPKYrIGKOsiJSx/fJRfSAfTJNyKqXfiuDg0C25N2RJjTGmEVrhTzhw7oStsIEtsVzT4o2Jo9ALThiEM8IdHIgNaHo8mjNWg6/ske8bohPo+63QOLzmooHp8fyAICBSQkVz0QKdsFQEV6WSW/D+7+BbgbIDHcb4Kp61XyjyI16zZ8JemGltQtDBSGxB4/GoN+7TpkkjDCsFArm0IYv3U0BbnYtf8BCy+JytsE0X6VyuKhPPK/GAJ14kvZZDZVV3pZIb8MZr6n4o4PDGKn0S5SdDmyq5PnXQsk+Xbhinp03FFzmHJw6xYRiWm9VxnohZ3vOcxdO8ARmXRvbWdtzQAAAAAElFTkSuQmCC'; - this.unreadNSFW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABCklEQVQ4jZ2S0WrDMAxF/TBCCKWMYhZKCSGYmFJMSNjD/mhf239qJXNcjBdTWODgRLpXKJKNMaYROuFTOHEehFb4gJZYrunwxsSXMApOmIQzwgOciE1oRjyaM1aDj+yR7xuiHvT9VmgcXnPRwO/9+wWCgEgJFc1FCwzCVhFclUpuw/u3g3cFyg50GPOjePZ+ocjPeM2RCXthpbUFwQAzsQ2Nx6PeuE+bJo0w7BQI5NKGLN5XAW11LX7BQ8jia7bCLl2kc7mqTLzuxAOeeJH0Wk6VVf0oldyEN15T948CDm+sMiZRfjK0pZIbUwcd+3TphnF62lR8kXN44hAbhmG5WQNnT8zynucsnuYJhFpBfkMzqD4AAAAASUVORK5CYII='; - break; - case 'Original': - this.unreadDead = 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs='; - this.unreadSFW = 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAC6Xw////////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs='; - this.unreadNSFW = 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs='; - } - return this.unread = this.SFW ? this.unreadSFW : this.unreadNSFW; - }, - empty: 'data:image/gif;base64,R0lGODlhEAAQAJEAAAAAAP///9vb2////yH5BAEAAAMALAAAAAAQABAAAAIvnI+pq+D9DBAUoFkPFnbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==', - dead: 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAIALAAAAAAQABAAAAIvlI+pq+D9DAgUoFkPDlbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==' - }; - - Redirect = { - image: function(board, filename) { - switch (board) { - case 'a': - case 'jp': - case 'm': - case 'sp': - case 'tg': - case 'vg': - case 'wsg': - return "//archive.foolz.us/" + board + "/full_image/" + filename; - case 'u': - return "//nsfw.foolz.us/" + board + "/full_image/" + filename; - } - }, - post: function(board, postID) { - switch (board) { - case 'a': - case 'co': - case 'jp': - case 'm': - case 'sp': - case 'tg': - case 'tv': - case 'v': - case 'vg': - case 'wsg': - case 'dev': - case 'foolz': - return "//archive.foolz.us/api/chan/post/board/" + board + "/num/" + postID + "/format/json"; - case 'u': - case 'kuku': - return "//nsfw.foolz.us/api/chan/post/board/" + board + "/num/" + postID + "/format/json"; - } - }, - thread: function(board, threadID, postID) { - var path, url; - if (postID) { - postID = postID.match(/\d+/)[0]; - } - path = threadID ? "" + board + "/thread/" + threadID : "" + board + "/post/" + postID; - switch (board) { - case 'a': - case 'co': - case 'jp': - case 'm': - case 'sp': - case 'tg': - case 'tv': - case 'v': - case 'vg': - case 'wsg': - case 'dev': - case 'foolz': - url = "//archive.foolz.us/" + path + "/"; - if (threadID && postID) { - url += "#" + postID; - } - break; - case 'u': - case 'kuku': - url = "//nsfw.foolz.us/" + path + "/"; - if (threadID && postID) { - url += "#" + postID; - } - break; - case 'ck': - case 'lit': - url = "//fuuka.warosu.org/" + path; - if (threadID && postID) { - url += "#p" + postID; - } - break; - case 'diy': - case 'g': - case 'sci': - url = "//archive.installgentoo.net/" + path; - if (threadID && postID) { - url += "#p" + postID; - } - break; - case 'cgl': - case 'mu': - case 'soc': - case 'w': - url = "//archive.rebeccablacktech.com/" + path; - if (threadID && postID) { - url += "#p" + postID; - } - break; - case 'an': - case 'fit': - case 'k': - case 'r9k': - case 'toy': - case 'x': - url = "http://archive.maidlab.jp/" + path; - if (threadID && postID) { - url += "#p" + postID; - } - break; - case 'e': - url = "https://md401.homelinux.net/4chan/cgi-board.pl/" + path; - if (threadID && postID) { - url += "#p" + postID; - } - break; - default: - if (threadID) { - url = "//boards.4chan.org/" + board + "/"; - } - } - return url || null; - } - }; - - ImageHover = { - init: function() { - return Main.callbacks.push(this.node); - }, - node: function(post) { - if (!post.img) { - return; - } - return $.on(post.img, 'mouseover', ImageHover.mouseover); - }, - mouseover: function() { - var el; - if (el = $.id('ihover')) { - if (el === UI.el) { - delete UI.el; - } - $.rm(el); - } - if (UI.el) { - return; - } - el = UI.el = $.el('img', { - id: 'ihover', - src: this.parentNode.href - }); - $.add(d.body, el); - $.on(el, 'load', ImageHover.load); - $.on(el, 'error', ImageHover.error); - $.on(this, 'mousemove', UI.hover); - return $.on(this, 'mouseout', ImageHover.mouseout); - }, - load: function() { - var style; - if (!this.parentNode) { - return; - } - style = this.style; - return UI.hover({ - clientX: -45 + parseInt(style.left), - clientY: 120 + parseInt(style.top) - }); - }, - error: function() { - var src, timeoutID, url, - _this = this; - src = this.src.split('/'); - if (!(src[2] === 'images.4chan.org' && (url = Redirect.image(src[3], src[5])))) { - if (g.dead) { - return; - } - url = "//images.4chan.org/" + src[3] + "/src/" + src[5]; - } - if ($.engine !== 'webkit' && url.split('/')[2] === 'images.4chan.org') { - return; - } - timeoutID = setTimeout((function() { - return _this.src = url; - }), 3000); - if ($.engine !== 'webkit' || url.split('/')[2] !== 'images.4chan.org') { - return; - } - return $.ajax(url, { - onreadystatechange: (function() { - if (this.status === 404) { - return clearTimeout(timeoutID); - } - }) - }, { - type: 'head' - }); - }, - mouseout: function() { - UI.hoverend(); - $.off(this, 'mousemove', UI.hover); - return $.off(this, 'mouseout', ImageHover.mouseout); - } - }; - - AutoGif = { - init: function() { - var _ref; - if ((_ref = g.BOARD) === 'gif' || _ref === 'wsg') { - return; - } - return Main.callbacks.push(this.node); - }, - node: function(post) { - var gif, img, src; - img = post.img; - if (post.el.hidden || !img) { - return; - } - src = img.parentNode.href; - if (/gif$/.test(src) && !/spoiler/.test(img.src)) { - gif = $.el('img'); - $.on(gif, 'load', function() { - return img.src = src; - }); - return gif.src = src; - } - } - }; - - ImageExpand = { - init: function() { - Main.callbacks.push(this.node); - return this.dialog(); - }, - node: function(post) { - var a; - if (!post.img) { - return; - } - a = post.img.parentNode; - $.on(a, 'click', ImageExpand.cb.toggle); - if (ImageExpand.on && !post.el.hidden) { - return ImageExpand.expand(post.img); - } - }, - cb: { - toggle: function(e) { - if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) { - return; - } - e.preventDefault(); - return ImageExpand.toggle(this); - }, - all: function() { - var i, thumb, thumbs, _i, _j, _k, _len, _len1, _len2, _ref; - ImageExpand.on = this.checked; - if (ImageExpand.on) { - thumbs = $$('img[data-md5]'); - if (Conf['Expand From Current']) { - for (i = _i = 0, _len = thumbs.length; _i < _len; i = ++_i) { - thumb = thumbs[i]; - if (thumb.getBoundingClientRect().top > 0) { - break; - } - } - thumbs = thumbs.slice(i); - } - for (_j = 0, _len1 = thumbs.length; _j < _len1; _j++) { - thumb = thumbs[_j]; - ImageExpand.expand(thumb); - } - } else { - _ref = $$('img[data-md5][hidden]'); - for (_k = 0, _len2 = _ref.length; _k < _len2; _k++) { - thumb = _ref[_k]; - ImageExpand.contract(thumb); - } - } - }, - typeChange: function() { - var klass; - switch (this.value) { - case 'full': - klass = ''; - break; - case 'fit width': - klass = 'fitwidth'; - break; - case 'fit height': - klass = 'fitheight'; - break; - case 'fit screen': - klass = 'fitwidth fitheight'; - } - $.id('delform').className = klass; - if (/\bfitheight\b/.test(klass)) { - $.on(window, 'resize', ImageExpand.resize); - if (!ImageExpand.style) { - ImageExpand.style = $.addStyle(''); - } - return ImageExpand.resize(); - } else if (ImageExpand.style) { - return $.off(window, 'resize', ImageExpand.resize); - } - } - }, - toggle: function(a) { - var rect, thumb; - thumb = a.firstChild; - if (thumb.hidden) { - rect = a.getBoundingClientRect(); - if ($.engine === 'webkit') { - if (rect.top < 0) { - d.body.scrollTop += rect.top - 42; - } - if (rect.left < 0) { - d.body.scrollLeft += rect.left; - } - } else { - if (rect.top < 0) { - d.documentElement.scrollTop += rect.top - 42; - } - if (rect.left < 0) { - d.documentElement.scrollLeft += rect.left; - } - } - return ImageExpand.contract(thumb); - } else { - return ImageExpand.expand(thumb); - } - }, - contract: function(thumb) { - thumb.hidden = false; - thumb.nextSibling.hidden = true; - return $.rmClass(thumb.parentNode.parentNode.parentNode, 'image_expanded'); - }, - expand: function(thumb, url) { - var a, img; - if ($.x('ancestor-or-self::*[@hidden]', thumb)) { - return; - } - thumb.hidden = true; - $.addClass(thumb.parentNode.parentNode.parentNode, 'image_expanded'); - if (img = thumb.nextSibling) { - img.hidden = false; - return; - } - a = thumb.parentNode; - img = $.el('img', { - src: url || a.href - }); - $.on(img, 'error', ImageExpand.error); - return $.add(a, img); - }, - error: function() { - var src, thumb, timeoutID, url; - thumb = this.previousSibling; - ImageExpand.contract(thumb); - $.rm(this); - src = this.src.split('/'); - if (!(src[2] === 'images.4chan.org' && (url = Redirect.image(src[3], src[5])))) { - if (g.dead) { - return; - } - url = "//images.4chan.org/" + src[3] + "/src/" + src[5]; - } - if ($.engine !== 'webkit' && url.split('/')[2] === 'images.4chan.org') { - return; - } - timeoutID = setTimeout(ImageExpand.expand, 10000, thumb, url); - if ($.engine !== 'webkit' || url.split('/')[2] !== 'images.4chan.org') { - return; - } - return $.ajax(url, { - onreadystatechange: (function() { - if (this.status === 404) { - return clearTimeout(timeoutID); - } - }) - }, { - type: 'head' - }); - }, - dialog: function() { - var controls, imageType, select; - controls = $.el('div', { - id: 'imgControls', - innerHTML: "" - }); - imageType = $.get('imageType', 'full'); - select = $('select', controls); - select.value = imageType; - ImageExpand.cb.typeChange.call(select); - $.on(select, 'change', $.cb.value); - $.on(select, 'change', ImageExpand.cb.typeChange); - $.on($('input', controls), 'click', ImageExpand.cb.all); - return $.prepend($.id('delform'), controls); - }, - resize: function() { - return ImageExpand.style.textContent = ".fitheight img[data-md5] + img {max-height:" + d.documentElement.clientHeight + "px;}"; - } - }; - - Main = { - init: function() { - var cutoff, hiddenThreads, id, key, now, path, pathname, temp, timestamp, val, _ref; - Main.flatten(null, Config); - path = location.pathname; - pathname = path.slice(1).split('/'); - g.BOARD = pathname[0], temp = pathname[1]; - if (temp === 'res') { - g.REPLY = true; - g.THREAD_ID = pathname[2]; - } - for (key in Conf) { - val = Conf[key]; - Conf[key] = $.get(key, val); - } - switch (location.hostname) { - case 'sys.4chan.org': - if (/report/.test(location.search)) { - $.ready(function() { - return $.on($.id('recaptcha_response_field'), 'keydown', function(e) { - if (e.keyCode === 8 && !e.target.value) { - return window.location = 'javascript:Recaptcha.reload()'; - } - }); - }); - } - return; - case 'images.4chan.org': - $.ready(function() { - var url; - if (/^4chan - 404/.test(d.title) && Conf['404 Redirect']) { - path = location.pathname.split('/'); - url = Redirect.image(path[1], path[3]); - if (url) { - return location.href = url; - } - } - }); - return; - } - $.ready(Options.init); - if (Conf['Quick Reply'] && Conf['Hide Original Post Form']) { - Main.css += '#postForm { display: none; }'; - } - Main.addStyle(); - now = Date.now(); - if (Conf['Check for Updates'] && $.get('lastUpdate', 0) < now - 6 * $.HOUR) { - $.ready(function() { - $.on(window, 'message', Main.message); - $.set('lastUpdate', now); - return $.add(d.head, $.el('script', { - src: 'https://github.com/MayhemYDG/4chan-x/raw/master/latest.js' - })); - }); - } - g.hiddenReplies = $.get("hiddenReplies/" + g.BOARD + "/", {}); - if ($.get('lastChecked', 0) < now - 1 * $.DAY) { - $.set('lastChecked', now); - cutoff = now - 7 * $.DAY; - hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {}); - for (id in hiddenThreads) { - timestamp = hiddenThreads[id]; - if (timestamp < cutoff) { - delete hiddenThreads[id]; - } - } - _ref = g.hiddenReplies; - for (id in _ref) { - timestamp = _ref[id]; - if (timestamp < cutoff) { - delete g.hiddenReplies[id]; - } - } - $.set("hiddenThreads/" + g.BOARD + "/", hiddenThreads); - $.set("hiddenReplies/" + g.BOARD + "/", g.hiddenReplies); - } - if (Conf['Filter']) { - Filter.init(); - } - if (Conf['Reply Hiding']) { - ReplyHiding.init(); - } - if (Conf['Filter'] || Conf['Reply Hiding']) { - StrikethroughQuotes.init(); - } - if (Conf['Anonymize']) { - Anonymize.init(); - } - if (Conf['Time Formatting']) { - Time.init(); - } - if (Conf['File Info Formatting']) { - FileInfo.init(); - } - if (Conf['Sauce']) { - Sauce.init(); - } - if (Conf['Reveal Spoilers']) { - RevealSpoilers.init(); - } - if (Conf['Image Auto-Gif']) { - AutoGif.init(); - } - if (Conf['Image Hover']) { - ImageHover.init(); - } - if (Conf['Menu']) { - Menu.init(); - if (Conf['Report Link']) { - ReportLink.init(); - } - if (Conf['Delete Link']) { - DeleteLink.init(); - } - if (Conf['Filter']) { - Filter.menuInit(); - } - if (Conf['Download Link']) { - DownloadLink.init(); - } - if (Conf['Archive Link']) { - ArchiveLink.init(); - } - } - if (Conf['Resurrect Quotes']) { - Quotify.init(); - } - if (Conf['Quote Inline']) { - QuoteInline.init(); - } - if (Conf['Quote Preview']) { - QuotePreview.init(); - } - if (Conf['Quote Backlinks']) { - QuoteBacklink.init(); - } - if (Conf['Indicate OP quote']) { - QuoteOP.init(); - } - if (Conf['Indicate Cross-thread Quotes']) { - QuoteCT.init(); - } - return $.ready(Main.ready); - }, - ready: function() { - var MutationObserver, a, board, nav, node, nodes, observer, _i, _j, _len, _len1, _ref, _ref1; - if (/^4chan - 404/.test(d.title)) { - if (Conf['404 Redirect'] && /^\d+$/.test(g.THREAD_ID)) { - location.href = Redirect.thread(g.BOARD, g.THREAD_ID, location.hash); - } - return; - } - if (!$.id('navtopr')) { - return; - } - $.addClass(d.body, $.engine); - $.addClass(d.body, 'fourchan_x'); - _ref = ['boardNavDesktop', 'boardNavDesktopFoot']; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - nav = _ref[_i]; - if (a = $("a[href$='/" + g.BOARD + "/']", $.id(nav))) { - $.addClass(a, 'current'); - } - } - Favicon.init(); - if (Conf['Quick Reply']) { - QR.init(); - } - if (Conf['Image Expansion']) { - ImageExpand.init(); - } - if (Conf['Thread Watcher']) { - setTimeout(function() { - return Watcher.init(); - }); - } - if (Conf['Keybinds']) { - setTimeout(function() { - return Keybinds.init(); - }); - } - if (g.REPLY) { - if (Conf['Thread Updater']) { - setTimeout(function() { - return Updater.init(); - }); - } - if (Conf['Thread Stats']) { - ThreadStats.init(); - } - if (Conf['Reply Navigation']) { - setTimeout(function() { - return Nav.init(); - }); - } - if (Conf['Post in Title']) { - TitlePost.init(); - } - if (Conf['Unread Count'] || Conf['Unread Favicon']) { - Unread.init(); - } - } else { - if (Conf['Thread Hiding']) { - ThreadHiding.init(); - } - if (Conf['Thread Expansion']) { - setTimeout(function() { - return ExpandThread.init(); - }); - } - if (Conf['Comment Expansion']) { - setTimeout(function() { - return ExpandComment.init(); - }); - } - if (Conf['Index Navigation']) { - setTimeout(function() { - return Nav.init(); - }); - } - } - board = $('.board'); - nodes = []; - _ref1 = $$('.postContainer', board); - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - node = _ref1[_j]; - nodes.push(Main.preParse(node)); - } - Main.node(nodes, true); - Main.hasCodeTags = !!$('script[src="//static.4chan.org/js/prettify/prettify.js"]'); - if (MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.OMutationObserver) { - observer = new MutationObserver(Main.observer); - observer.observe(board, { - childList: true, - subtree: true - }); - } else { - $.on(board, 'DOMNodeInserted', Main.listener); - } - }, - flatten: function(parent, obj) { - var key, val; - if (obj instanceof Array) { - Conf[parent] = obj[0]; - } else if (typeof obj === 'object') { - for (key in obj) { - val = obj[key]; - Main.flatten(key, val); - } - } else { - Conf[parent] = obj; - } - }, - addStyle: function() { - $.off(d, 'DOMNodeInserted', Main.addStyle); - if (d.head) { - return $.addStyle(Main.css); - } else { - return $.on(d, 'DOMNodeInserted', Main.addStyle); - } - }, - message: function(e) { - var version; - version = e.data.version; - if (version && version !== Main.version && confirm('An updated version of 4chan X is available, would you like to install it now?')) { - return window.location = "https://raw.github.com/mayhemydg/4chan-x/" + version + "/4chan_x.user.js"; - } - }, - preParse: function(node) { - var el, img, parentClass, post; - parentClass = node.parentNode.className; - el = $('.post', node); - post = { - root: node, - el: el, - "class": el.className, - ID: el.id.match(/\d+$/)[0], - threadID: g.THREAD_ID || $.x('ancestor::div[parent::div[@class="board"]]', node).id.match(/\d+$/)[0], - isArchived: /\barchivedPost\b/.test(parentClass), - isInlined: /\binline\b/.test(parentClass), - isCrosspost: /\bcrosspost\b/.test(parentClass), - blockquote: el.lastElementChild, - quotes: el.getElementsByClassName('quotelink'), - backlinks: el.getElementsByClassName('backlink'), - fileInfo: false, - img: false - }; - if (img = $('img[data-md5]', el)) { - post.fileInfo = img.parentNode.previousElementSibling; - post.img = img; - } - Main.prettify(post.blockquote); - return post; - }, - node: function(nodes, notify) { - var callback, node, _i, _j, _len, _len1, _ref; - _ref = Main.callbacks; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - callback = _ref[_i]; - try { - for (_j = 0, _len1 = nodes.length; _j < _len1; _j++) { - node = nodes[_j]; - callback(node); - } - } catch (err) { - if (notify) { - alert("4chan X (" + Main.version + ") error: " + err.message + "\nReport the bug at mayhemydg.github.com/4chan-x/#bug-report\n\nURL: " + window.location + "\n" + err.stack); - } - } - } - }, - observer: function(mutations) { - var addedNode, mutation, nodes, _i, _j, _len, _len1, _ref; - nodes = []; - for (_i = 0, _len = mutations.length; _i < _len; _i++) { - mutation = mutations[_i]; - _ref = mutation.addedNodes; - for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { - addedNode = _ref[_j]; - if (/\bpostContainer\b/.test(addedNode.className)) { - nodes.push(Main.preParse(addedNode)); - } - } - } - if (nodes.length) { - return Main.node(nodes); - } - }, - listener: function(e) { - var target; - target = e.target; - if (/\bpostContainer\b/.test(target.className)) { - return Main.node([Main.preParse(target)]); - } - }, - prettify: function(bq) { - var code; - if (!Main.hasCodeTags) { - return; - } - code = function() { - var pre, _i, _len, _ref; - _ref = document.getElementById('_id_').getElementsByClassName('prettyprint'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - pre = _ref[_i]; - pre.innerHTML = prettyPrintOne(pre.innerHTML.replace(/\s/g, ' ')); - } - }; - return $.globalEval(("(" + code + ")()").replace('_id_', bq.id)); - }, - namespace: '4chan_x.', - version: '2.34.3', - callbacks: [], - css: '\ -/* dialog styling */\ -.dialog.reply {\ - display: block;\ - border: 1px solid rgba(0,0,0,.25);\ - padding: 0;\ -}\ -.move {\ - cursor: move;\ -}\ -label, .favicon {\ - cursor: pointer;\ -}\ -a[href="javascript:;"] {\ - text-decoration: none;\ -}\ -.warning {\ - color: red;\ -}\ -\ -.hide_thread_button:not(.hidden_thread) {\ - float: left;\ -}\ -\ -.thread > .hidden_thread ~ *,\ -[hidden],\ -#content > [name=tab]:not(:checked) + div,\ -#updater:not(:hover) > :not(.move),\ -.autohide:not(:hover) > form,\ -#qp input, .forwarded {\ - display: none !important;\ -}\ -\ -.menu_button {\ - display: inline-block;\ -}\ -.menu_button > span {\ - border-top: .5em solid;\ - border-right: .3em solid transparent;\ - border-left: .3em solid transparent;\ - display: inline-block;\ - margin: 2px;\ - vertical-align: middle;\ -}\ -#menu {\ - position: absolute;\ - outline: none;\ -}\ -.entry {\ - border-bottom: 1px solid rgba(0, 0, 0, .25);\ - cursor: pointer;\ - display: block;\ - outline: none;\ - padding: 3px 7px;\ - position: relative;\ - text-decoration: none;\ - white-space: nowrap;\ -}\ -.entry:last-child {\ - border: none;\ -}\ -.focused.entry {\ - background: rgba(255, 255, 255, .33);\ -}\ -.entry.hasSubMenu {\ - padding-right: 1.5em;\ -}\ -.hasSubMenu::after {\ - content: "";\ - border-left: .5em solid;\ - border-top: .3em solid transparent;\ - border-bottom: .3em solid transparent;\ - display: inline-block;\ - margin: .3em;\ - position: absolute;\ - right: 3px;\ -}\ -.hasSubMenu:not(.focused) > .subMenu {\ - display: none;\ -}\ -.subMenu {\ - position: absolute;\ - left: 100%;\ - top: 0;\ - margin-top: -1px;\ -}\ -\ -h1 {\ - text-align: center;\ -}\ -#qr > .move {\ - min-width: 300px;\ - overflow: hidden;\ - box-sizing: border-box;\ - -moz-box-sizing: border-box;\ - padding: 0 2px;\ -}\ -#qr > .move > span {\ - float: right;\ -}\ -#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {\ - cursor: pointer;\ -}\ -#qr select,\ -#qr > form {\ - margin: 0;\ -}\ -#dump {\ - background: -webkit-linear-gradient(#EEE, #CCC);\ - background: -moz-linear-gradient(#EEE, #CCC);\ - background: -o-linear-gradient(#EEE, #CCC);\ - background: linear-gradient(#EEE, #CCC);\ - width: 10%;\ - padding: -moz-calc(1px) 0 2px;\ -}\ -#dump:hover, #dump:focus {\ - background: -webkit-linear-gradient(#FFF, #DDD);\ - background: -moz-linear-gradient(#FFF, #DDD);\ - background: -o-linear-gradient(#FFF, #DDD);\ - background: linear-gradient(#FFF, #DDD);\ -}\ -#dump:active, .dump #dump:not(:hover):not(:focus) {\ - background: -webkit-linear-gradient(#CCC, #DDD);\ - background: -moz-linear-gradient(#CCC, #DDD);\ - background: -o-linear-gradient(#CCC, #DDD);\ - background: linear-gradient(#CCC, #DDD);\ -}\ -#qr:not(.dump) #replies, .dump > form > label {\ - display: none;\ -}\ -#replies {\ - display: block;\ - height: 100px;\ - position: relative;\ - -webkit-user-select: none;\ - -moz-user-select: none;\ - -o-user-select: none;\ - user-select: none;\ -}\ -#replies > div {\ - counter-reset: thumbnails;\ - top: 0; right: 0; bottom: 0; left: 0;\ - margin: 0; padding: 0;\ - overflow: hidden;\ - position: absolute;\ - white-space: pre;\ -}\ -#replies > div:hover {\ - bottom: -10px;\ - overflow-x: auto;\ - z-index: 1;\ -}\ -.thumbnail {\ - background-color: rgba(0,0,0,.2) !important;\ - background-position: 50% 20% !important;\ - background-size: cover !important;\ - border: 1px solid #666;\ - box-sizing: border-box;\ - -moz-box-sizing: border-box;\ - cursor: move;\ - display: inline-block;\ - height: 90px; width: 90px;\ - margin: 5px; padding: 2px;\ - opacity: .5;\ - outline: none;\ - overflow: hidden;\ - position: relative;\ - text-shadow: 0 1px 1px #000;\ - -webkit-transition: opacity .25s ease-in-out;\ - -moz-transition: opacity .25s ease-in-out;\ - -o-transition: opacity .25s ease-in-out;\ - transition: opacity .25s ease-in-out;\ - vertical-align: top;\ -}\ -.thumbnail:hover, .thumbnail:focus {\ - opacity: .9;\ -}\ -.thumbnail#selected {\ - opacity: 1;\ -}\ -.thumbnail::before {\ - counter-increment: thumbnails;\ - content: counter(thumbnails);\ - color: #FFF;\ - font-weight: 700;\ - padding: 3px;\ - position: absolute;\ - top: 0;\ - right: 0;\ - text-shadow: 0 0 3px #000, 0 0 8px #000;\ -}\ -.thumbnail.drag {\ - box-shadow: 0 0 10px rgba(0,0,0,.5);\ -}\ -.thumbnail.over {\ - border-color: #FFF;\ -}\ -.thumbnail > span {\ - color: #FFF;\ -}\ -.remove {\ - background: none;\ - color: #E00;\ - font-weight: 700;\ - padding: 3px;\ -}\ -.remove:hover::after {\ - content: " Remove";\ -}\ -.thumbnail > label {\ - background: rgba(0,0,0,.5);\ - color: #FFF;\ - right: 0; bottom: 0; left: 0;\ - position: absolute;\ - text-align: center;\ -}\ -.thumbnail > label > input {\ - margin: 0;\ -}\ -#addReply {\ - color: #333;\ - font-size: 3.5em;\ - line-height: 100px;\ -}\ -#addReply:hover, #addReply:focus {\ - color: #000;\ -}\ -.field {\ - border: 1px solid #CCC;\ - box-sizing: border-box;\ - -moz-box-sizing: border-box;\ - color: #333;\ - font: 13px sans-serif;\ - margin: 0;\ - padding: 2px 4px 3px;\ - -webkit-transition: color .25s, border .25s;\ - -moz-transition: color .25s, border .25s;\ - -o-transition: color .25s, border .25s;\ - transition: color .25s, border .25s;\ -}\ -.field:-moz-placeholder,\ -.field:hover:-moz-placeholder {\ - color: #AAA;\ -}\ -.field:hover, .field:focus {\ - border-color: #999;\ - color: #000;\ - outline: none;\ -}\ -#qr > form > div:first-child > .field:not(#dump) {\ - width: 30%;\ -}\ -#qr textarea.field {\ - display: -webkit-box;\ - min-height: 120px;\ - min-width: 100%;\ -}\ -.textarea {\ - position: relative;\ -}\ -#charCount {\ - color: #000;\ - background: hsla(0, 0%, 100%, .5);\ - position: absolute;\ - top: 100%;\ - right: 0;\ -}\ -#charCount.warning {\ - color: red;\ -}\ -.captchainput > .field {\ - min-width: 100%;\ -}\ -.captchaimg {\ - background: #FFF;\ - outline: 1px solid #CCC;\ - outline-offset: -1px;\ - text-align: center;\ -}\ -.captchaimg > img {\ - display: block;\ - height: 57px;\ - width: 300px;\ -}\ -#qr [type=file] {\ - margin: 1px 0;\ - width: 70%;\ -}\ -#qr [type=submit] {\ - margin: 1px 0;\ - padding: 1px; /* not Gecko */\ - padding: 0 -moz-calc(1px); /* Gecko does not respect box-sizing: border-box */\ - width: 30%;\ -}\ -\ -.fileText:hover .fntrunc,\ -.fileText:not(:hover) .fnfull {\ - display: none;\ -}\ -.fitwidth img[data-md5] + img {\ - max-width: 100%;\ -}\ -.gecko .fitwidth img[data-md5] + img,\ -.presto .fitwidth img[data-md5] + img {\ - width: 100%;\ -}\ -\ -#qr, #qp, #updater, #stats, #ihover, #overlay, #navlinks {\ - position: fixed;\ -}\ -\ -#ihover {\ - max-height: 97%;\ - max-width: 75%;\ - padding-bottom: 18px;\ -}\ -\ -#navlinks {\ - font-size: 16px;\ - top: 25px;\ - right: 5px;\ -}\ -\ -body {\ - box-sizing: border-box;\ - -moz-box-sizing: border-box;\ -}\ -body.unscroll {\ - overflow: hidden;\ -}\ -#overlay {\ - top: 0;\ - left: 0;\ - width: 100%;\ - height: 100%;\ - text-align: center;\ - background: rgba(0,0,0,.5);\ - z-index: 1;\ -}\ -#overlay::after {\ - content: "";\ - display: inline-block;\ - height: 100%;\ - vertical-align: middle;\ -}\ -#options {\ - box-sizing: border-box;\ - -moz-box-sizing: border-box;\ - display: inline-block;\ - padding: 5px;\ - position: relative;\ - text-align: left;\ - vertical-align: middle;\ - width: 600px;\ - max-width: 100%;\ - height: 500px;\ - max-height: 100%;\ -}\ -#credits {\ - float: right;\ -}\ -#options ul {\ - padding: 0;\ -}\ -#options article li {\ - margin: 10px 0 10px 2em;\ -}\ -#options code {\ - background: hsla(0, 0%, 100%, .5);\ - color: #000;\ - padding: 0 1px;\ -}\ -#options label {\ - text-decoration: underline;\ -}\ -#content {\ - overflow: auto;\ - position: absolute;\ - top: 2.5em;\ - right: 5px;\ - bottom: 5px;\ - left: 5px;\ -}\ -#content textarea {\ - font-family: monospace;\ - min-height: 350px;\ - resize: vertical;\ - width: 100%;\ -}\ -\ -#updater {\ - text-align: right;\ -}\ -#updater:not(:hover) {\ - border: none;\ - background: transparent;\ -}\ -#updater input[type=number] {\ - width: 4em;\ -}\ -.new {\ - background: lime;\ -}\ -\ -#watcher {\ - padding-bottom: 5px;\ - position: absolute;\ - overflow: hidden;\ - white-space: nowrap;\ -}\ -#watcher:not(:hover) {\ - max-height: 220px;\ -}\ -#watcher > div {\ - max-width: 200px;\ - overflow: hidden;\ - padding-left: 5px;\ - padding-right: 5px;\ - text-overflow: ellipsis;\ -}\ -#watcher > .move {\ - padding-top: 5px;\ - text-decoration: underline;\ -}\ -\ -#qp {\ - padding: 2px 2px 5px;\ -}\ -#qp .post {\ - border: none;\ - margin: 0;\ - padding: 0;\ -}\ -#qp img {\ - max-height: 300px;\ - max-width: 500px;\ -}\ -.qphl {\ - outline: 2px solid rgba(216, 94, 49, .7);\ -}\ -.inlined {\ - opacity: .5;\ -}\ -.inline {\ - background-color: rgba(255, 255, 255, 0.15);\ - border: 1px solid rgba(128, 128, 128, 0.5);\ - display: table;\ - margin: 2px;\ - padding: 2px;\ -}\ -.inline .post {\ - background: none;\ - border: none;\ - margin: 0;\ - padding: 0;\ -}\ -div.opContainer {\ - display: block !important;\ -}\ -.opContainer.filter_highlight {\ - box-shadow: inset 5px 0 rgba(255,0,0,0.5);\ -}\ -.filter_highlight > .reply {\ - box-shadow: -5px 0 rgba(255,0,0,0.5);\ -}\ -.filtered {\ - text-decoration: underline line-through;\ -}\ -.quotelink.forwardlink,\ -.backlink.forwardlink {\ - text-decoration: none;\ - border-bottom: 1px dashed;\ -}\ -' - }; - - Main.init(); - }).call(this); diff --git a/Cakefile b/Cakefile index 2e5f4b805..7619ac4cb 100644 --- a/Cakefile +++ b/Cakefile @@ -2,27 +2,27 @@ {exec} = require 'child_process' fs = require 'fs' -VERSION = '2.34.3' - -HEADER = """ +VERSION = '3.0.0' +CAKEFILE = 'Cakefile' +INFILE = 'script.coffee' +OUTFILE = '4chan_x.user.js' +CHANGELOG = 'changelog' +LATEST = 'latest.js' +HEADER = """ // ==UserScript== -// @name 4chan x -// @version #{VERSION} -// @namespace aeosynth -// @description Adds various features. -// @copyright 2009-2011 James Campos -// @copyright 2012 Nicolas Stepien -// @license MIT; http://en.wikipedia.org/wiki/Mit_license -// @include http://boards.4chan.org/* -// @include https://boards.4chan.org/* -// @include http://images.4chan.org/* -// @include https://images.4chan.org/* -// @include http://sys.4chan.org/* -// @include https://sys.4chan.org/* -// @run-at document-start -// @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js -// @downloadURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js -// @icon http://mayhemydg.github.com/4chan-x/favicon.gif +// @name 4chan X alpha +// @version #{VERSION} +// @description Adds various features. +// @copyright 2009-2011 James Campos +// @copyright 2012 Nicolas Stepien +// @license MIT; http://en.wikipedia.org/wiki/Mit_license +// @match *://boards.4chan.org/* +// @match *://images.4chan.org/* +// @match *://sys.4chan.org/* +// @run-at document-start +// @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js +// @downloadURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js +// @icon http://mayhemydg.github.com/4chan-x/favicon.gif // ==/UserScript== /* LICENSE @@ -86,12 +86,6 @@ HEADER = """ """ -CAKEFILE = 'Cakefile' -INFILE = 'script.coffee' -OUTFILE = '4chan_x.user.js' -CHANGELOG = 'changelog' -LATEST = 'latest.js' - option '-v', '--version [version]', 'Upgrade version.' task 'upgrade', (options) -> @@ -101,10 +95,10 @@ task 'upgrade', (options) -> return regexp = RegExp VERSION, 'g' for file in [CAKEFILE, INFILE, OUTFILE, LATEST] - data = fs.readFileSync file, 'utf8' - fs.writeFileSync file, data.replace regexp, version - data = fs.readFileSync CHANGELOG, 'utf8' - fs.writeFileSync CHANGELOG, data.replace 'master', "master\n\n#{version}" + data = fs.readFileSync(file, 'utf8').replace regexp, version + fs.writeFileSync file, data + # data = fs.readFileSync CHANGELOG, 'utf8' + # fs.writeFileSync CHANGELOG, data.replace 'master', "master\n\n#{version}" exec "git commit -am 'Release #{version}.' && git tag -a #{version} -m '#{version}' && git tag -af stable -m '#{version}'" task 'build', -> diff --git a/changelog b/changelog index f0603e95d..1770e9436 100644 --- a/changelog +++ b/changelog @@ -1,4 +1,6 @@ -master +alpha +- Mayhem + Fix Chrome's install warning that 4chan X would execute on all domains. 2.34.3 - Mayhem diff --git a/script.coffee b/script.coffee index 43b4c503f..ae5f139c0 100644 --- a/script.coffee +++ b/script.coffee @@ -163,6 +163,10 @@ Config = 'Auto Update': [true, 'Automatically fetch new posts'] 'Interval': 30 +# Opera doesn't support the @match metadata key, +# return 4chan X here if we're not on 4chan. +return unless /^(boards|images|sys)\.4chan\.org$/.test location.hostname + Conf = {} d = document g = {} @@ -415,4139 +419,3 @@ $.extend $, $$ = (selector, root=d.body) -> Array::slice.call root.querySelectorAll selector - -Filter = - filters: {} - init: -> - for key of Config.filter - @filters[key] = [] - for filter in Conf[key].split '\n' - continue if filter[0] is '#' - - unless regexp = filter.match /\/(.+)\/(\w*)/ - continue - - # Don't mix up filter flags with the regular expression. - filter = filter.replace regexp[0], '' - - # Do not add this filter to the list if it's not a global one - # and it's not specifically applicable to the current board. - # Defaults to global. - boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global' - if boards isnt 'global' and boards.split(',').indexOf(g.BOARD) is -1 - continue - - try - if key is 'md5' - # MD5 filter will use strings instead of regular expressions. - regexp = regexp[1] - else - # Please, don't write silly regular expressions. - regexp = RegExp regexp[1], regexp[2] - catch e - # I warned you, bro. - alert e.message - continue - - # Filter OPs along with their threads, replies only, or both. - # Defaults to replies only. - op = filter.match(/[^t]op:(yes|no|only)/)?[1] or 'no' - - # Overrule the `Show Stubs` setting. - # Defaults to stub showing. - stub = switch filter.match(/stub:(yes|no)/)?[1] - when 'yes' - true - when 'no' - false - else - Conf['Show Stubs'] - - # Highlight the post, or hide it. - # If not specified, the highlight class will be filter_highlight. - # Defaults to post hiding. - if hl = /highlight/.test filter - hl = filter.match(/highlight:(\w+)/)?[1] or 'filter_highlight' - # Put highlighted OP's thread on top of the board page or not. - # Defaults to on top. - top = filter.match(/top:(yes|no)/)?[1] or 'yes' - top = top is 'yes' # Turn it into a boolean - - @filters[key].push @createFilter regexp, op, stub, hl, top - - # Only execute filter types that contain valid filters. - unless @filters[key].length - delete @filters[key] - - if Object.keys(@filters).length - Main.callbacks.push @node - - createFilter: (regexp, op, stub, hl, top) -> - test = - if typeof regexp is 'string' - # MD5 checking - (value) -> regexp is value - else - (value) -> regexp.test value - settings = - hide: !hl - stub: stub - class: hl - top: top - (value, isOP) -> - if isOP and op is 'no' or !isOP and op is 'only' - return false - unless test value - return false - settings - - node: (post) -> - return if post.isInlined - isOP = post.ID is post.threadID - {root} = post - for key of Filter.filters - value = Filter[key] post - if value is false - # Continue if there's nothing to filter (no tripcode for example). - continue - for filter in Filter.filters[key] - unless result = filter value, isOP - continue - - # Hide - if result.hide - if isOP - unless g.REPLY - ThreadHiding.hide root.parentNode, result.stub - else - continue - else - ReplyHiding.hide root, result.stub - return - - # Highlight - $.addClass root, result.class - if isOP and result.top and not g.REPLY - # Put the highlighted OPs' thread on top of the board page... - thisThread = root.parentNode - # ...before the first non highlighted thread. - if firstThread = $('div[class="postContainer opContainer"]').parentNode - $.before firstThread, [thisThread, thisThread.nextElementSibling] - - name: (post) -> - $('.name', post.el).textContent - uniqueid: (post) -> - if uid = $ '.posteruid', post.el - return uid.textContent[5...-1] - false - tripcode: (post) -> - if trip = $ '.postertrip', post.el - return trip.textContent - false - mod: (post) -> - if mod = $ '.capcode', post.el - return mod.textContent - false - email: (post) -> - if mail = $ '.useremail', post.el - # remove 'mailto:' - # decode %20 into space for example - return decodeURIComponent mail.href[7..] - false - subject: (post) -> - $('.postInfo .subject', post.el).textContent or false - comment: (post) -> - text = [] - # XPathResult.ORDERED_NODE_SNAPSHOT_TYPE is 7 - nodes = d.evaluate './/br|.//text()', post.blockquote, null, 7, null - for i in [0...nodes.snapshotLength] - text.push if data = nodes.snapshotItem(i).data then data else '\n' - text.join '' - country: (post) -> - if flag = $ '.countryFlag', post.el - return flag.title - false - filename: (post) -> - {fileInfo} = post - if fileInfo - if file = $ '.fileText > span', fileInfo - return file.title - else - return fileInfo.firstElementChild.dataset.filename - false - dimensions: (post) -> - {fileInfo} = post - if fileInfo and match = fileInfo.textContent.match /\d+x\d+/ - return match[0] - false - filesize: (post) -> - {img} = post - if img - return img.alt - false - md5: (post) -> - {img} = post - if img - return img.dataset.md5 - false - - menuInit: -> - div = $.el 'div', - textContent: 'Filter' - - entry = - el: div - open: -> true - children: [] - - for type in [ - ['Name', 'name'] - ['Unique ID', 'uniqueid'] - ['Tripcode', 'tripcode'] - ['Admin/Mod', 'mod'] - ['E-mail', 'email'] - ['Subject', 'subject'] - ['Comment', 'comment'] - ['Country', 'country'] - ['Filename', 'filename'] - ['Image dimensions', 'dimensions'] - ['Filesize', 'filesize'] - ['Image MD5', 'md5'] - ] - # Add a sub entry for each filter type. - entry.children.push Filter.createSubEntry type[0], type[1] - - Menu.addEntry entry - - createSubEntry: (text, type) -> - el = $.el 'a', - href: 'javascript:;' - textContent: text - # Define the onclick var outside of open's scope to $.off it properly. - onclick = null - - open = (post) -> - value = Filter[type] post - return false if value is false - $.off el, 'click', onclick - onclick = -> - # Convert value -> regexp, unless type is md5 - re = if type is 'md5' then value else value.replace /// - / - | \\ - | \^ - | \$ - | \n - | \. - | \( - | \) - | \{ - | \} - | \[ - | \] - | \? - | \* - | \+ - | \| - ///g, (c) -> - if c is '\n' - '\\n' - else if c is '\\' - '\\\\' - else - "\\#{c}" - - re = - if type is 'md5' - "/#{value}/" - else - "/^#{re}$/" - if /\bop\b/.test post.class - re += ';op:yes' - - # Add a new line before the regexp unless the text is empty. - save = if save = $.get type, '' then "#{save}\n#{re}" else re - $.set type, save - - # Open the options and display & focus the relevant filter textarea. - Options.dialog() - select = $ 'select[name=filter]', $.id 'options' - select.value = type - $.event select, new Event 'change' - $.id('filter_tab').checked = true - ta = select.nextElementSibling - tl = ta.textLength - ta.setSelectionRange tl, tl - ta.focus() - $.on el, 'click', onclick - true - - return el: el, open: open - -StrikethroughQuotes = - init: -> - Main.callbacks.push @node - node: (post) -> - return if post.isInlined - for quote in post.quotes - if (el = $.id quote.hash[1..]) and el.hidden - $.addClass quote, 'filtered' - if Conf['Recursive Filtering'] - show_stub = !!$.x 'preceding-sibling::div[contains(@class,"stub")]', el - ReplyHiding.hide post.root, show_stub - return - -ExpandComment = - init: -> - for a in $$ '.abbr' - $.on a.firstElementChild, 'click', ExpandComment.expand - return - expand: (e) -> - e.preventDefault() - [_, threadID, replyID] = @href.match /(\d+)#p(\d+)/ - @textContent = "Loading #{replyID}..." - a = @ - $.cache @pathname, -> ExpandComment.parse @, a, threadID, replyID - parse: (req, a, threadID, replyID) -> - if req.status isnt 200 - a.textContent = "#{req.status} #{req.statusText}" - return - - doc = d.implementation.createHTMLDocument '' - doc.documentElement.innerHTML = req.response - - # Import the node to fix quote.hashes - # as they're empty when in a different document. - node = d.importNode doc.getElementById("m#{replyID}"), true - - quotes = node.getElementsByClassName 'quotelink' - for quote in quotes - href = quote.getAttribute 'href' - continue if href[0] is '/' # Cross-board quote - quote.href = "res/#{href}" # Fix pathnames - post = - blockquote: node - threadID: threadID - quotes: quotes - backlinks: [] - if Conf['Resurrect Quotes'] - Quotify.node post - if Conf['Quote Preview'] - QuotePreview.node post - if Conf['Quote Inline'] - QuoteInline.node post - if Conf['Indicate OP quote'] - QuoteOP.node post - if Conf['Indicate Cross-thread Quotes'] - QuoteCT.node post - $.replace a.parentNode.parentNode, node - Main.prettify node - -ExpandThread = - init: -> - for span in $$ '.summary' - a = $.el 'a', - textContent: "+ #{span.textContent}" - className: 'summary desktop' - href: 'javascript:;' - $.on a, 'click', -> ExpandThread.toggle @parentNode - $.replace span, a - - toggle: (thread) -> - pathname = "/#{g.BOARD}/res/#{thread.id[1..]}" - a = $ '.summary', thread - - switch a.textContent[0] - when '+' - a.textContent = a.textContent.replace '+', '× Loading...' - $.cache pathname, -> ExpandThread.parse @, thread, a - - when '×' - a.textContent = a.textContent.replace '× Loading...', '+' - $.cache.requests[pathname].abort() - - when '-' - a.textContent = a.textContent.replace '-', '+' - #goddamit moot - num = switch g.BOARD - when 'b', 'vg' then 3 - when 't' then 1 - else 5 - replies = $$ '.replyContainer', thread - replies.splice replies.length - num, num - for reply in replies - $.rm reply - return - - parse: (req, thread, a) -> - if req.status isnt 200 - a.textContent = "#{req.status} #{req.statusText}" - $.off a, 'click', ExpandThread.cb.toggle - return - - a.textContent = a.textContent.replace '× Loading...', '-' - - doc = d.implementation.createHTMLDocument '' - doc.documentElement.innerHTML = req.response - - threadID = thread.id[1..] - nodes = [] - for reply in $$ '.replyContainer', doc - reply = d.importNode reply, true - for quote in $$ '.quotelink', reply - href = quote.getAttribute 'href' - continue if href[0] is '/' # Cross-board quote - quote.href = "res/#{href}" # Fix pathnames - id = reply.id[2..] - link = $ 'a[title="Highlight this post"]', reply - link.href = "res/#{threadID}#p#{id}" - link.nextSibling.href = "res/#{threadID}#q#{id}" - nodes.push reply - # eat everything, then replace with fresh full posts - for post in $$ '.summary ~ .replyContainer', a.parentNode - $.rm post - for backlink in $$ '.backlink', a.previousElementSibling - # Keep backlinks from other threads. - $.rm backlink unless $.id backlink.hash[1..] - $.after a, nodes - -ThreadHiding = - init: -> - hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} - for thread in $$ '.thread' - a = $.el 'a', - className: 'hide_thread_button' - innerHTML: '[ - ]' - href: 'javascript:;' - $.on a, 'click', ThreadHiding.cb - $.prepend thread, a - - if thread.id[1..] of hiddenThreads - ThreadHiding.hide thread - return - - cb: -> - ThreadHiding.toggle $.x 'ancestor::div[parent::div[@class="board"]]', @ - - toggle: (thread) -> - hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} - id = thread.id[1..] - if thread.hidden or /\bhidden_thread\b/.test thread.firstChild.className - ThreadHiding.show thread - delete hiddenThreads[id] - else - ThreadHiding.hide thread - hiddenThreads[id] = Date.now() - $.set "hiddenThreads/#{g.BOARD}/", hiddenThreads - - hide: (thread, show_stub=Conf['Show Stubs']) -> - unless show_stub - thread.hidden = true - thread.nextElementSibling.hidden = true - return - - return if /\bhidden_thread\b/.test thread.firstChild.className # already hidden once by the filter - - num = 0 - if span = $ '.summary', thread - num = Number span.textContent.match /\d+/ - num += $$('.opContainer ~ .replyContainer', thread).length - text = if num is 1 then '1 reply' else "#{num} replies" - opInfo = $('.desktop > .nameBlock', thread).textContent - - stub = $.el 'div', - className: 'hide_thread_button hidden_thread' - innerHTML: '[ + ] ' - a = stub.firstChild - $.on a, 'click', ThreadHiding.cb - $.add a, $.tn "#{opInfo} (#{text})" - if Conf['Menu'] - menuButton = Menu.a.cloneNode true - $.on menuButton, 'click', Menu.toggle - $.add stub, [$.tn(' '), menuButton] - $.prepend thread, stub - - show: (thread) -> - if stub = $ '.hidden_thread', thread - $.rm stub - thread.hidden = false - thread.nextElementSibling.hidden = false - -ReplyHiding = - init: -> - Main.callbacks.push @node - - node: (post) -> - return if post.isInlined or post.ID is post.threadID - side = $ '.sideArrows', post.root - $.addClass side, 'hide_reply_button' - side.innerHTML = '[ - ]' - $.on side.firstChild, 'click', ReplyHiding.toggle - - if post.ID of g.hiddenReplies - ReplyHiding.hide post.root - - toggle: -> - button = @parentNode - root = button.parentNode - id = root.id[2..] - quotes = $$ ".quotelink[href$='#p#{id}'], .backlink[href$='#p#{id}']" - if /\bstub\b/.test button.className - ReplyHiding.show root - for quote in quotes - $.rmClass quote, 'filtered' - delete g.hiddenReplies[id] - else - ReplyHiding.hide root - for quote in quotes - $.addClass quote, 'filtered' - g.hiddenReplies[id] = Date.now() - $.set "hiddenReplies/#{g.BOARD}/", g.hiddenReplies - - hide: (root, show_stub=Conf['Show Stubs']) -> - side = $ '.sideArrows', root - return if side.hidden # already hidden once by the filter - side.hidden = true - el = side.nextElementSibling - el.hidden = true - - return unless show_stub - - stub = $.el 'div', - className: 'hide_reply_button stub' - innerHTML: '[ + ] ' - a = stub.firstChild - $.on a, 'click', ReplyHiding.toggle - $.add a, $.tn $('.desktop > .nameBlock', el).textContent - if Conf['Menu'] - menuButton = Menu.a.cloneNode true - $.on menuButton, 'click', Menu.toggle - $.add stub, [$.tn(' '), menuButton] - $.prepend root, stub - - show: (root) -> - if stub = $ '.stub', root - $.rm stub - $('.sideArrows', root).hidden = false - $('.post', root).hidden = false - -Menu = - entries: [] - init: -> - @a = $.el 'a', - className: 'menu_button' - href: 'javascript:;' - innerHTML: '[]' - @el = $.el 'div', - className: 'reply dialog' - id: 'menu' - tabIndex: 0 - $.on @el, 'click', (e) -> e.stopPropagation() - $.on @el, 'keydown', @keybinds - - # Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API - $.on d, 'AddMenuEntry', (e) -> Menu.addEntry e.detail - - Main.callbacks.push @node - node: (post) -> - if post.isInlined and !post.isCrosspost - a = $ '.menu_button', post.el - else - a = Menu.a.cloneNode true - # \u00A0 is nbsp - $.add $('.postInfo', post.el), [$.tn('\u00A0'), a] - $.on a, 'click', Menu.toggle - - toggle: (e) -> - e.preventDefault() - e.stopPropagation() - - if Menu.el.parentNode - # Close if it's already opened. - # Reopen if we clicked on another button. - {lastOpener} = Menu - Menu.close() - return if lastOpener is @ - - Menu.lastOpener = @ - post = - if /\bhidden_thread\b/.test @parentNode.className - $.x 'ancestor::div[parent::div[@class="board"]]/child::div[contains(@class,"opContainer")]', @ - else - $.x 'ancestor::div[contains(@class,"postContainer")][1]', @ - Menu.open @, Main.preParse post - open: (button, post) -> - {el} = Menu - # XXX GM/Scriptish require setAttribute - el.setAttribute 'data-id', post.ID - el.setAttribute 'data-rootid', post.root.id - - funk = (entry, parent) -> - {children} = entry - return unless entry.open post - $.add parent, entry.el - - return unless children - if subMenu = $ '.subMenu', entry.el - # Reset sub menu, remove irrelevant entries. - $.rm subMenu - subMenu = $.el 'div', - className: 'reply dialog subMenu' - $.add entry.el, subMenu - for child in children - funk child, subMenu - return - for entry in Menu.entries - funk entry, el - - Menu.focus $ '.entry', Menu.el - $.on d, 'click', Menu.close - $.add d.body, el - - # Position - mRect = el.getBoundingClientRect() - bRect = button.getBoundingClientRect() - bTop = d.documentElement.scrollTop + d.body.scrollTop + bRect.top - bLeft = d.documentElement.scrollLeft + d.body.scrollLeft + bRect.left - el.style.top = - if bRect.top + bRect.height + mRect.height < d.documentElement.clientHeight - bTop + bRect.height + 2 + 'px' - else - bTop - mRect.height - 2 + 'px' - el.style.left = - if bRect.left + mRect.width < d.documentElement.clientWidth - bLeft + 'px' - else - bLeft + bRect.width - mRect.width + 'px' - - el.focus() - close: -> - {el} = Menu - $.rm el - for focused in $$ '.focused.entry', el - $.rmClass focused, 'focused' - el.innerHTML = null - el.removeAttribute 'style' - delete Menu.lastOpener - delete Menu.focusedEntry - $.off d, 'click', Menu.close - - keybinds: (e) -> - el = Menu.focusedEntry - - switch Keybinds.keyCode(e) or e.keyCode - when 'Esc' - Menu.lastOpener.focus() - Menu.close() - when 13, 32 # 'Enter', 'Space' - el.click() - when 'Up' - if next = el.previousElementSibling - Menu.focus next - when 'Down' - if next = el.nextElementSibling - Menu.focus next - when 'Right' - if (subMenu = $ '.subMenu', el) and next = subMenu.firstElementChild - Menu.focus next - when 'Left' - if next = $.x 'parent::*[contains(@class,"subMenu")]/parent::*', el - Menu.focus next - else - return - - e.preventDefault() - e.stopPropagation() - focus: (el) -> - if focused = $.x 'parent::*/child::*[contains(@class,"focused")]', el - $.rmClass focused, 'focused' - for focused in $$ '.focused', el - $.rmClass focused, 'focused' - Menu.focusedEntry = el - $.addClass el, 'focused' - - addEntry: (entry) -> - funk = (entry) -> - {el, children} = entry - $.addClass el, 'entry' - $.on el, 'focus mouseover', (e) -> - e.stopPropagation() - Menu.focus @ - return unless children - $.addClass el, 'hasSubMenu' - for child in children - funk child - return - funk entry - Menu.entries.push entry - -Keybinds = - init: -> - for node in $$ '[accesskey]' - node.removeAttribute 'accesskey' - $.on d, 'keydown', Keybinds.keydown - - keydown: (e) -> - return unless key = Keybinds.keyCode e - {target} = e - if /TEXTAREA|INPUT/.test target.nodeName - return unless (key is 'Esc') or (/\+/.test key) - - thread = Nav.getThread() - switch key - # QR & Options - when Conf.openQR - Keybinds.qr thread, true - when Conf.openEmptyQR - Keybinds.qr thread - when Conf.openOptions - Options.dialog() unless $.id 'overlay' - when Conf.close - if o = $.id 'overlay' - Options.close.call o - else if QR.el - QR.close() - when Conf.submit - QR.submit() if QR.el and !QR.status() - when Conf.spoiler - return if target.nodeName isnt 'TEXTAREA' - Keybinds.tags 'spoiler', target - when Conf.code - return if target.nodeName isnt 'TEXTAREA' - Keybinds.tags 'code', target - # Thread related - when Conf.watch - Watcher.toggle thread - when Conf.update - Updater.update() - when Conf.unreadCountTo0 - Unread.replies = [] - Unread.update true - # Images - when Conf.expandImage - Keybinds.img thread - when Conf.expandAllImages - Keybinds.img thread, true - # Board Navigation - when Conf.zero - window.location = "/#{g.BOARD}/0#delform" - when Conf.nextPage - if link = $ 'link[rel=next]', d.head - window.location = link.href - when Conf.previousPage - if link = $ 'link[rel=prev]', d.head - window.location.href = link.href - # Thread Navigation - when Conf.nextThread - return if g.REPLY - Nav.scroll +1 - when Conf.previousThread - return if g.REPLY - Nav.scroll -1 - when Conf.expandThread - ExpandThread.toggle thread - when Conf.openThread - Keybinds.open thread - when Conf.openThreadTab - Keybinds.open thread, true - # Reply Navigation - when Conf.nextReply - Keybinds.hl +1, thread - when Conf.previousReply - Keybinds.hl -1, thread - when Conf.hide - ThreadHiding.toggle thread if /\bthread\b/.test thread.className - else - return - e.preventDefault() - - keyCode: (e) -> - key = switch kc = e.keyCode - when 8 - '' - when 27 - 'Esc' - when 37 - 'Left' - when 38 - 'Up' - when 39 - 'Right' - when 40 - 'Down' - when 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90 #0-9, A-Z - c = String.fromCharCode kc - if e.shiftKey then c else c.toLowerCase() - else - null - if key - if e.altKey then key = 'alt+' + key - if e.ctrlKey then key = 'ctrl+' + key - if e.metaKey then key = 'meta+' + key - key - - tags: (tag, ta) -> - value = ta.value - selStart = ta.selectionStart - selEnd = ta.selectionEnd - - ta.value = - value[...selStart] + - "[#{tag}]" + value[selStart...selEnd] + "[/#{tag}]" + - value[selEnd..] - - range = "[#{tag}]".length + selEnd - # Move the caret to the end of the selection. - ta.setSelectionRange range, range - - # Fire the 'input' event - $.event ta, new Event 'input' - - img: (thread, all) -> - if all - $.id('imageExpand').click() - else - thumb = $ 'img[data-md5]', $('.post.highlight', thread) or thread - ImageExpand.toggle thumb.parentNode - - qr: (thread, quote) -> - if quote - QR.quote.call $ 'a[title="Quote this post"]', $('.post.highlight', thread) or thread - else - QR.open() - $('textarea', QR.el).focus() - - open: (thread, tab) -> - id = thread.id[1..] - url = "//boards.4chan.org/#{g.BOARD}/res/#{id}" - if tab - $.open url - else - location.href = url - - hl: (delta, thread) -> - if post = $ '.reply.highlight', thread - $.rmClass post, 'highlight' - post.removeAttribute 'tabindex' - rect = post.getBoundingClientRect() - if rect.bottom >= 0 and rect.top <= d.documentElement.clientHeight # We're at least partially visible - next = $.x 'child::div[contains(@class,"post reply")]', - if delta is +1 then post.parentNode.nextElementSibling else post.parentNode.previousElementSibling - unless next - @focus post - return - return unless g.REPLY or $.x('ancestor::div[parent::div[@class="board"]]', next) is thread - rect = next.getBoundingClientRect() - if rect.top < 0 or rect.bottom > d.documentElement.clientHeight - next.scrollIntoView delta is -1 - @focus next - return - - replies = $$ '.reply', thread - replies.reverse() if delta is -1 - for reply in replies - rect = reply.getBoundingClientRect() - if delta is +1 and rect.top >= 0 or delta is -1 and rect.bottom <= d.documentElement.clientHeight - @focus reply - return - - focus: (post) -> - $.addClass post, 'highlight' - post.tabIndex = 0 - post.focus() - -Nav = - # ◀ ▶ - init: -> - span = $.el 'span', - id: 'navlinks' - prev = $.el 'a', - textContent: '▲' - href: 'javascript:;' - next = $.el 'a', - textContent: '▼' - href: 'javascript:;' - - $.on prev, 'click', @prev - $.on next, 'click', @next - - $.add span, [prev, $.tn(' '), next] - $.add d.body, span - - prev: -> - if g.REPLY - window.scrollTo 0, 0 - else - Nav.scroll -1 - - next: -> - if g.REPLY - window.scrollTo 0, d.body.scrollHeight - else - Nav.scroll +1 - - getThread: (full) -> - Nav.threads = $$ '.thread:not([hidden])' - for thread, i in Nav.threads - rect = thread.getBoundingClientRect() - {bottom} = rect - if bottom > 0 #we have not scrolled past - if full - return [thread, i, rect] - return thread - return $ '.board' - - scroll: (delta) -> - [thread, i, rect] = Nav.getThread true - {top} = rect - - #unless we're not at the beginning of the current thread - # (and thus wanting to move to beginning) - # or we're above the first thread and don't want to skip it - unless (delta is -1 and Math.ceil(top) < 0) or (delta is +1 and top > 1) - i += delta - - {top} = Nav.threads[i]?.getBoundingClientRect() - window.scrollBy 0, top - -QR = - init: -> - return unless $.id 'postForm' - Main.callbacks.push @node - setTimeout @asyncInit - - asyncInit: -> - if Conf['Hide Original Post Form'] - link = $.el 'h1', innerHTML: "#{if g.REPLY then 'Reply to Thread' else 'Start a Thread'}" - $.on link.firstChild, 'click', -> - QR.open() - $('select', QR.el).value = 'new' unless g.REPLY - $('textarea', QR.el).focus() - $.before $.id('postForm'), link - - if Conf['Persistent QR'] - QR.dialog() - QR.hide() if Conf['Auto Hide QR'] - $.on d, 'dragover', QR.dragOver - $.on d, 'drop', QR.dropFile - $.on d, 'dragstart dragend', QR.drag - - node: (post) -> - $.on $('a[title="Quote this post"]', post.el), 'click', QR.quote - - open: -> - if QR.el - QR.el.hidden = false - QR.unhide() - else - QR.dialog() - close: -> - QR.el.hidden = true - QR.abort() - d.activeElement.blur() - $.rmClass QR.el, 'dump' - for i in QR.replies - QR.replies[0].rm() - QR.cooldown.auto = false - QR.status() - QR.resetFileInput() - if not Conf['Remember Spoiler'] and (spoiler = $.id 'spoiler').checked - spoiler.click() - QR.cleanError() - hide: -> - d.activeElement.blur() - $.addClass QR.el, 'autohide' - $.id('autohide').checked = true - unhide: -> - $.rmClass QR.el, 'autohide' - $.id('autohide').checked = false - toggleHide: -> - @checked and QR.hide() or QR.unhide() - - error: (err) -> - el = $ '.warning', QR.el - if typeof err is 'string' - el.textContent = err - else - el.innerHTML = null - $.add el, err - QR.open() - if /captcha|verification/i.test el.textContent - # Focus the captcha input on captcha error. - $('[autocomplete]', QR.el).focus() - alert el.textContent if d.hidden or d.oHidden or d.mozHidden or d.webkitHidden - cleanError: -> - $('.warning', QR.el).textContent = null - - status: (data={}) -> - return unless QR.el - if g.dead - value = 404 - disabled = true - QR.cooldown.auto = false - value = QR.cooldown.seconds or data.progress or value - {input} = QR.status - input.value = - if QR.cooldown.auto and Conf['Cooldown'] - if value then "Auto #{value}" else 'Auto' - else - value or 'Submit' - input.disabled = disabled or false - - cooldown: - init: -> - return unless Conf['Cooldown'] - QR.cooldown.start $.get "/#{g.BOARD}/cooldown", 0 - $.sync "/#{g.BOARD}/cooldown", QR.cooldown.start - start: (timeout) -> - seconds = Math.floor (timeout - Date.now()) / 1000 - QR.cooldown.count seconds - set: (seconds) -> - return unless Conf['Cooldown'] - QR.cooldown.count seconds - $.set "/#{g.BOARD}/cooldown", Date.now() + seconds*$.SECOND - count: (seconds) -> - return unless 0 <= seconds <= 60 - setTimeout QR.cooldown.count, 1000, seconds-1 - QR.cooldown.seconds = seconds - if seconds is 0 - $.delete "/#{g.BOARD}/cooldown" - QR.submit() if QR.cooldown.auto - QR.status() - - quote: (e) -> - e?.preventDefault() - QR.open() - unless g.REPLY - $('select', QR.el).value = $.x('ancestor::div[parent::div[@class="board"]]', @).id[1..] - # Make sure we get the correct number, even with XXX censors - id = @previousSibling.hash[2..] - text = ">>#{id}\n" - - sel = window.getSelection() - if (s = sel.toString()) and id is $.x('ancestor-or-self::blockquote', sel.anchorNode)?.id.match(/\d+$/)[0] - # XXX Opera needs d.getSelection() to retain linebreaks from the selected text - s = d.getSelection() if $.engine is 'presto' - s = s.replace /\n/g, '\n>' - text += ">#{s}\n" - - ta = $ 'textarea', QR.el - caretPos = ta.selectionStart - # Replace selection for text. - ta.value = ta.value[...caretPos] + text + ta.value[ta.selectionEnd..] - ta.focus() - # Move the caret to the end of the new quote. - range = caretPos + text.length - # XXX Opera counts newlines as double - range += text.match(/\n/g).length if $.engine is 'presto' - ta.setSelectionRange range, range - - # Fire the 'input' event - $.event ta, new Event 'input' - - characterCount: -> - counter = QR.charaCounter - count = @textLength - counter.textContent = count - counter.hidden = count < 1000 - (if count > 1500 then $.addClass else $.rmClass) counter, 'warning' - - drag: (e) -> - # Let it drag anything from the page. - i = if e.type is 'dragstart' then 'off' else 'on' - $[i] d, 'dragover', QR.dragOver - $[i] d, 'drop', QR.dropFile - dragOver: (e) -> - e.preventDefault() - e.dataTransfer.dropEffect = 'copy' # cursor feedback - dropFile: (e) -> - # Let it only handle files from the desktop. - return unless e.dataTransfer.files.length - e.preventDefault() - QR.open() - QR.fileInput.call e.dataTransfer - $.addClass QR.el, 'dump' - fileInput: -> - QR.cleanError() - # Set or change current reply's file. - if @files.length is 1 - file = @files[0] - if file.size > @max - QR.error 'File too large.' - QR.resetFileInput() - else if -1 is QR.mimeTypes.indexOf file.type - QR.error 'Unsupported file type.' - QR.resetFileInput() - else - QR.selected.setFile file - return - # Create new replies with these files. - for file in @files - if file.size > @max - QR.error "File #{file.name} is too large." - break - else if -1 is QR.mimeTypes.indexOf file.type - QR.error "#{file.name}: Unsupported file type." - break - unless QR.replies[QR.replies.length - 1].file - # set last reply's file - QR.replies[QR.replies.length - 1].setFile file - else - new QR.reply().setFile file - $.addClass QR.el, 'dump' - QR.resetFileInput() # reset input - resetFileInput: -> - input = $ '[type=file]', QR.el - input.value = null - return unless $.engine is 'presto' - # XXX Opera needs extra care to reset its file input's value - clone = $.el 'input', - type: 'file' - accept: input.accept - max: input.max - multiple: input.multiple - size: input.size - title: input.title - $.on clone, 'change', QR.fileInput - $.on clone, 'click', (e) -> if e.shiftKey then QR.selected.rmFile() or e.preventDefault() - $.replace input, clone - - replies: [] - reply: class - constructor: -> - # set values, or null, to avoid 'undefined' values in inputs - prev = QR.replies[QR.replies.length-1] - persona = $.get 'QR.persona', {} - @name = if prev then prev.name else persona.name or null - @email = if prev and !/^sage$/.test prev.email then prev.email else persona.email or null - @sub = if prev and Conf['Remember Subject'] then prev.sub else if Conf['Remember Subject'] then persona.sub else null - @spoiler = if prev and Conf['Remember Spoiler'] then prev.spoiler else false - @com = null - - @el = $.el 'a', - className: 'thumbnail' - draggable: true - href: 'javascript:;' - innerHTML: '×' - $('input', @el).checked = @spoiler - $.on @el, 'click', => @select() - $.on $('.remove', @el), 'click', (e) => - e.stopPropagation() - @rm() - $.on $('label', @el), 'click', (e) => e.stopPropagation() - $.on $('input', @el), 'change', (e) => - @spoiler = e.target.checked - $.id('spoiler').checked = @spoiler if @el.id is 'selected' - $.before $('#addReply', QR.el), @el - - $.on @el, 'dragstart', @dragStart - $.on @el, 'dragenter', @dragEnter - $.on @el, 'dragleave', @dragLeave - $.on @el, 'dragover', @dragOver - $.on @el, 'dragend', @dragEnd - $.on @el, 'drop', @drop - - QR.replies.push @ - setFile: (@file) -> - @el.title = "#{file.name} (#{$.bytesToString file.size})" - $('label', @el).hidden = false if QR.spoiler - unless /^image/.test file.type - @el.style.backgroundImage = null - return - url = window.URL or window.webkitURL - # XXX Opera does not support window.URL.revokeObjectURL - url.revokeObjectURL? @url - - # Create a redimensioned thumbnail. - fileUrl = url.createObjectURL file - img = $.el 'img' - - $.on img, 'load', => - # Generate thumbnails only if they're really big. - # Resized pictures through canvases look like ass, - # so we generate thumbnails `s` times bigger then expected - # to avoid crappy resized quality. - s = 90*3 - if img.height < s or img.width < s - @url = fileUrl - @el.style.backgroundImage = "url(#{@url})" - return - if img.height <= img.width - img.width = s / img.height * img.width - img.height = s - else - img.height = s / img.width * img.height - img.width = s - c = $.el 'canvas' - c.height = img.height - c.width = img.width - c.getContext('2d').drawImage img, 0, 0, img.width, img.height - # Support for toBlob fucking when? - data = atob c.toDataURL().split(',')[1] - - # DataUrl to Binary code from Aeosynth's 4chan X repo - l = data.length - ui8a = new Uint8Array l - for i in [0...l] - ui8a[i] = data.charCodeAt i - - @url = url.createObjectURL new Blob [ui8a.buffer], type: 'image/png' - @el.style.backgroundImage = "url(#{@url})" - url.revokeObjectURL? fileUrl - - img.src = fileUrl - rmFile: -> - QR.resetFileInput() - delete @file - @el.title = null - @el.style.backgroundImage = null - $('label', @el).hidden = true if QR.spoiler - (window.URL or window.webkitURL).revokeObjectURL? @url - select: -> - QR.selected?.el.id = null - QR.selected = @ - @el.id = 'selected' - # Scroll the list to center the focused reply. - rectEl = @el.getBoundingClientRect() - rectList = @el.parentNode.getBoundingClientRect() - @el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2 - # Load this reply's values. - for data in ['name', 'email', 'sub', 'com'] - $("[name=#{data}]", QR.el).value = @[data] - QR.characterCount.call $ 'textarea', QR.el - $('#spoiler', QR.el).checked = @spoiler - dragStart: -> - $.addClass @, 'drag' - dragEnter: -> - $.addClass @, 'over' - dragLeave: -> - $.rmClass @, 'over' - dragOver: (e) -> - e.preventDefault() - e.dataTransfer.dropEffect = 'move' - drop: -> - el = $ '.drag', @parentNode - index = (el) -> Array::slice.call(el.parentNode.children).indexOf el - oldIndex = index el - newIndex = index @ - if oldIndex < newIndex - $.after @, el - else - $.before @, el - reply = QR.replies.splice(oldIndex, 1)[0] - QR.replies.splice newIndex, 0, reply - dragEnd: -> - $.rmClass @, 'drag' - if el = $ '.over', @parentNode - $.rmClass el, 'over' - rm: -> - QR.resetFileInput() - $.rm @el - index = QR.replies.indexOf @ - if QR.replies.length is 1 - new QR.reply().select() - else if @el.id is 'selected' - (QR.replies[index-1] or QR.replies[index+1]).select() - QR.replies.splice index, 1 - (window.URL or window.webkitURL).revokeObjectURL? @url - delete @ - - captcha: - init: -> - return unless QR.captchaIsEnabled = !!$.id 'captchaFormPart' - if $.id 'recaptcha_challenge_field_holder' - @ready() - else - @onready = => @ready() - $.on $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready - ready: -> - if @challenge = $.id 'recaptcha_challenge_field_holder' - $.off $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready - delete @onready - else - return - $.after $('.textarea', QR.el), $.el 'div', - className: 'captchaimg' - title: 'Reload' - innerHTML: '' - $.after $('.captchaimg', QR.el), $.el 'div', - className: 'captchainput' - innerHTML: '' - @img = $ '.captchaimg > img', QR.el - @input = $ '.captchainput > input', QR.el - $.on @img.parentNode, 'click', @reload - $.on @input, 'keydown', @keydown - $.on @challenge, 'DOMNodeInserted', => @load() - $.sync 'captchas', (arr) => @count arr.length - @count $.get('captchas', []).length - # start with an uncached captcha - @reload() - save: -> - return unless response = @input.value - captchas = $.get 'captchas', [] - # Remove old captchas. - while (captcha = captchas[0]) and captcha.time < Date.now() - captchas.shift() - captchas.push - challenge: @challenge.firstChild.value - response: response - time: @timeout - $.set 'captchas', captchas - @count captchas.length - @reload() - load: -> - # Timeout is available at RecaptchaState.timeout in seconds. - # We use 5-1 minutes to give upload some time. - @timeout = Date.now() + 4*$.MINUTE - challenge = @challenge.firstChild.value - @img.alt = challenge - @img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}" - @input.value = null - count: (count) -> - @input.placeholder = switch count - when 0 - 'Verification (Shift + Enter to cache)' - when 1 - 'Verification (1 cached captcha)' - else - "Verification (#{count} cached captchas)" - @input.alt = count # For XTRM RICE. - reload: (focus) -> - # the "t" argument prevents the input from being focused - window.location = 'javascript:Recaptcha.reload("t")' - # Focus if we meant to. - QR.captcha.input.focus() if focus - keydown: (e) -> - c = QR.captcha - if e.keyCode is 8 and not c.input.value - c.reload() - else if e.keyCode is 13 and e.shiftKey - c.save() - else - return - e.preventDefault() - - dialog: -> - QR.el = UI.dialog 'qr', 'top:0;right:0;', ' -
- Quick Reply - × -
-
-
- -
-
- -
-
' - - if Conf['Remember QR size'] and $.engine is 'gecko' - $.on ta = $('textarea', QR.el), 'mouseup', -> - $.set 'QR.size', @style.cssText - ta.style.cssText = $.get 'QR.size', '' - - # Allow only this board's supported files. - mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) -> - switch type - when 'jpg' - 'image/jpeg' - when 'pdf' - 'application/pdf' - when 'swf' - 'application/x-shockwave-flash' - else - "image/#{type}" - QR.mimeTypes = mimeTypes.split ', ' - # Add empty mimeType to avoid errors with URLs selected in Window's file dialog. - QR.mimeTypes.push '' - fileInput = $ 'input[type=file]', QR.el - fileInput.max = $('input[name=MAX_FILE_SIZE]').value - fileInput.accept = mimeTypes if $.engine isnt 'presto' # Opera's accept attribute is fucked up - - QR.spoiler = !!$ 'input[name=spoiler]' - spoiler = $ '#spoilerLabel', QR.el - spoiler.hidden = !QR.spoiler - - QR.charaCounter = $ '#charCount', QR.el - ta = $ 'textarea', QR.el - - unless g.REPLY - # Make a list with visible threads and an option to create a new one. - threads = '' - for thread in $$ '.thread' - id = thread.id[1..] - threads += "" - $.prepend $('.move > span', QR.el), $.el 'select' - innerHTML: threads - title: 'Create a new thread / Reply to a thread' - $.on $('select', QR.el), 'mousedown', (e) -> e.stopPropagation() - $.on $('#autohide', QR.el), 'change', QR.toggleHide - $.on $('.close', QR.el), 'click', QR.close - $.on $('#dump', QR.el), 'click', -> QR.el.classList.toggle 'dump' - $.on $('#addReply', QR.el), 'click', -> new QR.reply().select() - $.on $('form', QR.el), 'submit', QR.submit - $.on ta, 'input', -> QR.selected.el.lastChild.textContent = @value - $.on ta, 'input', QR.characterCount - $.on fileInput, 'change', QR.fileInput - $.on fileInput, 'click', (e) -> if e.shiftKey then QR.selected.rmFile() or e.preventDefault() - $.on spoiler.firstChild, 'change', -> $('input', QR.selected.el).click() - $.on $('.warning', QR.el), 'click', QR.cleanError - - new QR.reply().select() - # save selected reply's data - for name in ['name', 'email', 'sub', 'com'] - # The input event replaces keyup, change and paste events. - $.on $("[name=#{name}]", QR.el), 'input', -> - QR.selected[@name] = @value - # Disable auto-posting if you're typing in the first reply - # during the last 5 seconds of the cooldown. - if QR.cooldown.auto and QR.selected is QR.replies[0] and 0 < QR.cooldown.seconds < 6 - QR.cooldown.auto = false - - QR.status.input = $ 'input[type=submit]', QR.el - QR.status() - QR.cooldown.init() - QR.captcha.init() - $.add d.body, QR.el - - # Create a custom event when the QR dialog is first initialized. - # Use it to extend the QR's functionalities, or for XTRM RICE. - $.event QR.el, new CustomEvent 'QRDialogCreation', - bubbles: true - - submit: (e) -> - e?.preventDefault() - if QR.cooldown.seconds - QR.cooldown.auto = !QR.cooldown.auto - QR.status() - return - QR.abort() - reply = QR.replies[0] - - threadID = g.THREAD_ID or $('select', QR.el).value - - # prevent errors - unless threadID is 'new' and reply.file or threadID isnt 'new' and (reply.com or reply.file) - err = 'No file selected.' - else if QR.captchaIsEnabled - # get oldest valid captcha - captchas = $.get 'captchas', [] - # remove old captchas - while (captcha = captchas[0]) and captcha.time < Date.now() - captchas.shift() - if captcha = captchas.shift() - challenge = captcha.challenge - response = captcha.response - else - challenge = QR.captcha.img.alt - if response = QR.captcha.input.value then QR.captcha.reload() - $.set 'captchas', captchas - QR.captcha.count captchas.length - unless response - err = 'No valid captcha.' - - if err - # stop auto-posting - QR.cooldown.auto = false - QR.status() - QR.error err - return - QR.cleanError() - - # Enable auto-posting if we have stuff to post, disable it otherwise. - QR.cooldown.auto = QR.replies.length > 1 - if Conf['Auto Hide QR'] and not QR.cooldown.auto - QR.hide() - if not QR.cooldown.auto and $.x 'ancestor::div[@id="qr"]', d.activeElement - # Unfocus the focused element if it is one within the QR and we're not auto-posting. - d.activeElement.blur() - - # Starting to upload might take some time. - # Provide some feedback that we're starting to submit. - QR.status progress: '...' - - post = - resto: threadID - name: reply.name - email: reply.email - sub: reply.sub - com: reply.com - upfile: reply.file - spoiler: reply.spoiler - mode: 'regist' - pwd: if m = d.cookie.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value - recaptcha_challenge_field: challenge - recaptcha_response_field: response + ' ' - - callbacks = - onload: -> - QR.response @response - onerror: -> - # Connection error, or - # CORS disabled error on www.4chan.org/banned - QR.status() - QR.error $.el 'a', - href: '//www.4chan.org/banned' - target: '_blank' - textContent: 'Connection error, or you are banned.' - opts = - form: $.formData post - upCallbacks: - onload: -> - # Upload done, waiting for response. - QR.status progress: '...' - onprogress: (e) -> - # Uploading... - QR.status progress: "#{Math.round e.loaded / e.total * 100}%" - - QR.ajax = $.ajax $.id('postForm').parentNode.action, callbacks, opts - - response: (html) -> - doc = d.implementation.createHTMLDocument '' - doc.documentElement.innerHTML = html - if doc.title is '4chan - Banned' # Ban/warn check - bs = $$ 'b', doc - err = $.el 'span', - innerHTML: - if /^You were issued a warning/.test $('.boxcontent', doc).textContent.trim() - "You were issued a warning on #{bs[0].innerHTML} as #{bs[3].innerHTML}.
Warning reason: #{bs[1].innerHTML}" - else - "You are banned! ;_;
Please click HERE to see the reason." - else if msg = doc.getElementById 'errmsg' # error! - err = msg.textContent - if msg.firstChild.tagName # duplicate image link - err = msg.firstChild - err.target = '_blank' - else unless msg = $ 'b', doc - err = 'Connection error with sys.4chan.org.' - - if err - if /captcha|verification/i.test(err) or err is 'Connection error with sys.4chan.org.' - # Enable auto-post if we have some cached captchas. - QR.cooldown.auto = !!$.get('captchas', []).length - # Too many frequent mistyped captchas will auto-ban you! - # On connection error, the post most likely didn't go through. - QR.cooldown.set 2 - else # stop auto-posting - QR.cooldown.auto = false - QR.status() - QR.error err - return - - reply = QR.replies[0] - - persona = $.get 'QR.persona', {} - persona = - name: reply.name - email: if /^sage$/.test reply.email then persona.email else reply.email - sub: if Conf['Remember Subject'] then reply.sub else null - $.set 'QR.persona', persona - - [_, threadID, postID] = msg.lastChild.textContent.match /thread:(\d+),no:(\d+)/ - - # Post/upload confirmed as successful. - $.event QR.el, new CustomEvent 'QRPostSuccessful', - bubbles: true - detail: - threadID: threadID - postID: postID - - if threadID is '0' # new thread - # auto-noko - location.pathname = "/#{g.BOARD}/res/#{postID}" - else - # Enable auto-posting if we have stuff to post, disable it otherwise. - QR.cooldown.auto = QR.replies.length > 1 - QR.cooldown.set if /sage/i.test reply.email then 60 else 30 - if Conf['Open Reply in New Tab'] && !g.REPLY && !QR.cooldown.auto - $.open "//boards.4chan.org/#{g.BOARD}/res/#{threadID}#p#{postID}" - - if Conf['Persistent QR'] or QR.cooldown.auto - reply.rm() - else - QR.close() - - QR.status() - QR.resetFileInput() - - abort: -> - QR.ajax?.abort() - delete QR.ajax - QR.status() - -Options = - init: -> - for settings in ['navtopr', 'navbotr'] - a = $.el 'a', - href: 'javascript:;' - className: 'settingsWindowLink' - textContent: '4chan X Settings' - $.on a, 'click', Options.dialog - el = $.id(settings).firstElementChild - el.hidden = true - $.before el, a - unless $.get 'firstrun' - # Prevent race conditions - Favicon.init() unless Favicon.el - $.set 'firstrun', true - Options.dialog() - - dialog: -> - dialog = $.el 'div' - id: 'options' - className: 'reply dialog' - innerHTML: '
- -
- - | - | - | - | -
-
-
-
- -
- -
-
Sauce is disabled.
- Lines starting with a # will be ignored.
- You can specify a certain display text by appending ;text:[text] to the url. -
    These parameters will be replaced by their corresponding values: -
  • $1: Thumbnail url.
  • -
  • $2: Full image url.
  • -
  • $3: MD5 hash.
  • -
  • $4: Current board.
  • -
- -
- -
-
Filter is disabled.
- -
- -
-
Quote Backlinks are disabled.
-
    - Backlink formatting -
  • :
  • -
-
Time Formatting is disabled.
-
    - Time formatting -
  • :
  • -
  • Supported format specifiers:
  • -
  • Day: %a, %A, %d, %e
  • -
  • Month: %m, %b, %B
  • -
  • Year: %y
  • -
  • Hour: %k, %H, %l (lowercase L), %I (uppercase i), %p, %P
  • -
  • Minutes: %M
  • -
  • Seconds: %S
  • -
-
File Info Formatting is disabled.
-
    - File Info Formatting -
  • :
  • -
  • Link (with original file name): %l (lowercase L, truncated), %L (untruncated)
  • -
  • Original file name: %n (Truncated), %N (Untruncated)
  • -
  • Spoiler indicator: %p
  • -
  • Size: %B (Bytes), %K (KB), %M (MB), %s (4chan default)
  • -
  • Resolution: %r (Displays PDF on /po/, for PDFs)
  • -
-
Unread Favicon is disabled.
- Unread favicons
- - -
- -
-
Keybinds are disabled.
-
Allowed keys: Ctrl, Alt, Meta, a-z, A-Z, 0-9, Up, Down, Right, Left.
- - -
ActionsKeybinds
-
-
' - - #main - for key, obj of Config.main - ul = $.el 'ul', - textContent: key - for key, arr of obj - checked = if $.get(key, Conf[key]) then 'checked' else '' - description = arr[1] - li = $.el 'li', - innerHTML: ": #{description}" - $.on $('input', li), 'click', $.cb.checked - $.add ul, li - $.add $('#main_tab + div', dialog), ul - - hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} - hiddenNum = Object.keys(g.hiddenReplies).length + Object.keys(hiddenThreads).length - li = $.el 'li', - innerHTML: " : Forget all hidden posts. Useful if you accidentally hide a post and have \"Show Stubs\" disabled." - $.on $('button', li), 'click', Options.clearHidden - $.add $('ul:nth-child(2)', dialog), li - - #filter - filter = $ 'select[name=filter]', dialog - $.on filter, 'change', Options.filter - - #sauce - sauce = $ '#sauces', dialog - sauce.value = $.get sauce.name, Conf[sauce.name] - $.on sauce, 'change', $.cb.value - - #rice - (back = $ '[name=backlink]', dialog).value = $.get 'backlink', Conf['backlink'] - (time = $ '[name=time]', dialog).value = $.get 'time', Conf['time'] - (fileInfo = $ '[name=fileInfo]', dialog).value = $.get 'fileInfo', Conf['fileInfo'] - $.on back, 'input', $.cb.value - $.on back, 'input', Options.backlink - $.on time, 'input', $.cb.value - $.on time, 'input', Options.time - $.on fileInfo, 'input', $.cb.value - $.on fileInfo, 'input', Options.fileInfo - favicon = $ 'select[name=favicon]', dialog - favicon.value = $.get 'favicon', Conf['favicon'] - $.on favicon, 'change', $.cb.value - $.on favicon, 'change', Options.favicon - - #keybinds - for key, arr of Config.hotkeys - tr = $.el 'tr', - innerHTML: "#{arr[1]}" - input = $ 'input', tr - input.value = $.get key, Conf[key] - $.on input, 'keydown', Options.keybind - $.add $('#keybinds_tab + div tbody', dialog), tr - - #indicate if the settings require a feature to be enabled - indicators = {} - for indicator in $$ '.warning', dialog - key = indicator.firstChild.textContent - indicator.hidden = $.get key, Conf[key] - indicators[key] = indicator - $.on $("[name='#{key}']", dialog), 'click', -> - indicators[@name].hidden = @checked - - overlay = $.el 'div', id: 'overlay' - $.on overlay, 'click', Options.close - $.on dialog, 'click', (e) -> e.stopPropagation() - $.add overlay, dialog - $.add d.body, overlay - d.body.style.setProperty 'width', "#{d.body.clientWidth}px", null - $.addClass d.body, 'unscroll' - - Options.filter.call filter - Options.backlink.call back - Options.time.call time - Options.fileInfo.call fileInfo - Options.favicon.call favicon - - close: -> - $.rm this - d.body.style.removeProperty 'width' - $.rmClass d.body, 'unscroll' - - clearHidden: -> - #'hidden' might be misleading; it's the number of IDs we're *looking* for, - # not the number of posts actually hidden on the page. - $.delete "hiddenReplies/#{g.BOARD}/" - $.delete "hiddenThreads/#{g.BOARD}/" - @textContent = "hidden: 0" - g.hiddenReplies = {} - keybind: (e) -> - return if e.keyCode is 9 - e.preventDefault() - e.stopPropagation() - return unless (key = Keybinds.keyCode e)? - @value = key - $.cb.value.call @ - filter: -> - el = @nextSibling - - if (name = @value) isnt 'guide' - ta = $.el 'textarea', - name: name - className: 'field' - value: $.get name, Conf[name] - $.on ta, 'change', $.cb.value - $.replace el, ta - return - - $.rm el if el - $.after @, $.el 'article', - innerHTML: '

Use regular expressions, one per line.
- Lines starting with a # will be ignored.
- For example, /weeaboo/i will filter posts containing the string `weeaboo`, case-insensitive.

-
    You can use these settings with each regular expression, separate them with semicolons: -
  • - Per boards, separate them with commas. It is global if not specified.
    - For example: boards:a,jp;. -
  • -
  • - Filter OPs only along with their threads (`only`), replies only (`no`, this is default), or both (`yes`).
    - For example: op:only;, op:no; or op:yes;. -
  • -
  • - Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).
    - For example: stub:yes; or stub:no;. -
  • -
  • - Highlight instead of hiding. You can specify a class name to use with a userstyle.
    - For example: highlight; or highlight:wallpaper;. -
  • -
  • - Highlighted OPs will have their threads put on top of board pages by default.
    - For example: top:yes; or top:no;. -
  • -
' - time: -> - Time.foo() - Time.date = new Date() - $.id('timePreview').textContent = Time.funk Time - backlink: -> - $.id('backlinkPreview').textContent = Conf['backlink'].replace /%id/, '123456789' - fileInfo: -> - FileInfo.data = - link: 'javascript:;' - spoiler: true - size: '276' - unit: 'KB' - resolution: '1280x720' - fullname: 'd9bb2efc98dd0df141a94399ff5880b7.jpg' - shortname: 'd9bb2efc98dd0df141a94399ff5880(...).jpg' - FileInfo.setFormats() - $.id('fileInfoPreview').innerHTML = FileInfo.funk FileInfo - favicon: -> - Favicon.switch() - Unread.update true - @nextElementSibling.innerHTML = " " - -Updater = - init: -> - html = '
' - {checkbox} = Config.updater - for name of checkbox - title = checkbox[name][1] - checked = if Conf[name] then 'checked' else '' - html += "
" - - checked = if Conf['Auto Update'] then 'checked' else '' - html += " -
-
-
" - - dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html - - @count = $ '#count', dialog - @timer = $ '#timer', dialog - @thread = $.id "t#{g.THREAD_ID}" - - @unsuccessfulFetchCount = 0 - @lastModified = '0' - - for input in $$ 'input', dialog - if input.type is 'checkbox' - $.on input, 'click', $.cb.checked - switch input.name - when 'Scroll BG' - $.on input, 'click', @cb.scrollBG - @cb.scrollBG.call input - when 'Verbose' - $.on input, 'click', @cb.verbose - @cb.verbose.call input - when 'Auto Update This' - $.on input, 'click', @cb.autoUpdate - @cb.autoUpdate.call input - when 'Interval' - input.value = Conf['Interval'] - $.on input, 'change', @cb.interval - @cb.interval.call input - when 'Update Now' - $.on input, 'click', @update - - $.add d.body, dialog - - $.on d, 'QRPostSuccessful', @cb.post - $.on d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', @cb.visibility - - cb: - post: -> - return unless Conf['Auto Update This'] - Updater.unsuccessfulFetchCount = 0 - setTimeout Updater.update, 500 - visibility: -> - state = d.visibilityState or d.oVisibilityState or d.mozVisibilityState or d.webkitVisibilityState - return if state isnt 'visible' - # Reset the counter when we focus this tab. - Updater.unsuccessfulFetchCount = 0 - if Updater.timer.textContent < -Conf['Interval'] - Updater.set 'timer', -Updater.getInterval() - interval: -> - val = parseInt @value, 10 - @value = if val > 5 then val else 5 - $.cb.value.call @ - Updater.set 'timer', -Updater.getInterval() - verbose: -> - if Conf['Verbose'] - Updater.set 'count', '+0' - Updater.timer.hidden = false - else - Updater.set 'count', 'Thread Updater' - Updater.count.className = '' - Updater.timer.hidden = true - autoUpdate: -> - if Conf['Auto Update This'] = @checked - Updater.timeoutID = setTimeout Updater.timeout, 1000 - else - clearTimeout Updater.timeoutID - scrollBG: -> - Updater.scrollBG = - if @checked - -> true - else - -> !(d.hidden or d.oHidden or d.mozHidden or d.webkitHidden) - update: -> - if @status is 404 - Updater.set 'timer', '' - Updater.set 'count', 404 - Updater.count.className = 'warning' - clearTimeout Updater.timeoutID - g.dead = true - if Conf['Unread Count'] - Unread.title = Unread.title.match(/^.+-/)[0] + ' 404' - else - d.title = d.title.match(/^.+-/)[0] + ' 404' - Unread.update true - QR.abort() - return - unless @status in [0, 200, 304] - # XXX 304 -> 0 in Opera - if Conf['Verbose'] - Updater.set 'count', @statusText - Updater.count.className = 'warning' - Updater.unsuccessfulFetchCount++ - return - - Updater.unsuccessfulFetchCount++ - Updater.set 'timer', -Updater.getInterval() - - ### - Status Code 304: Not modified - By sending the `If-Modified-Since` header we get a proper status code, and no response. - This saves bandwidth for both the user and the servers, avoid unnecessary computation, - and won't load images and scripts when parsing the response. - ### - if @status in [0, 304] - # XXX 304 -> 0 in Opera - if Conf['Verbose'] - Updater.set 'count', '+0' - Updater.count.className = null - return - Updater.lastModified = @getResponseHeader 'Last-Modified' - - doc = d.implementation.createHTMLDocument '' - doc.documentElement.innerHTML = @response - - lastPost = Updater.thread.lastElementChild - id = lastPost.id[2..] - nodes = [] - for reply in $$('.replyContainer', doc).reverse() - break if reply.id[2..] <= id #make sure to not insert older posts - nodes.push reply - - count = nodes.length - if Conf['Verbose'] - Updater.set 'count', "+#{count}" - Updater.count.className = if count then 'new' else null - - return unless count - - Updater.unsuccessfulFetchCount = 0 - Updater.set 'timer', -Updater.getInterval() - scroll = Conf['Scrolling'] && Updater.scrollBG() && - lastPost.getBoundingClientRect().bottom - d.documentElement.clientHeight < 25 - $.add Updater.thread, nodes.reverse() - if scroll - nodes[0].scrollIntoView() - - set: (name, text) -> - el = Updater[name] - if node = el.firstChild - # Prevent the creation of a new DOM Node - # by setting the text node's data. - node.data = text - else - el.textContent = text - - getInterval: -> - i = +Conf['Interval'] - j = Math.min @unsuccessfulFetchCount, 9 - unless d.hidden or d.oHidden or d.mozHidden or d.webkitHidden - # Don't increase the refresh rate too much on visible tabs. - j = Math.min j, 6 - Math.max i, [5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] - - timeout: -> - Updater.timeoutID = setTimeout Updater.timeout, 1000 - n = 1 + Number Updater.timer.firstChild.data - - if n is 0 - Updater.update() - else if n >= Updater.getInterval() - Updater.unsuccessfulFetchCount++ - Updater.set 'count', 'Retry' - Updater.count.className = null - Updater.update() - else - Updater.set 'timer', n - - update: -> - Updater.set 'timer', 0 - Updater.request?.abort() - # Fool the cache. - url = location.pathname + '?' + Date.now() - Updater.request = $.ajax url, onload: Updater.cb.update, - headers: 'If-Modified-Since': Updater.lastModified - -Watcher = - init: -> - html = '
Thread Watcher
' - @dialog = UI.dialog 'watcher', 'top: 50px; left: 0px;', html - $.add d.body, @dialog - - #add watch buttons - for input in $$ '.op input' - favicon = $.el 'img', - className: 'favicon' - $.on favicon, 'click', @cb.toggle - $.before input, favicon - - if g.THREAD_ID is $.get 'autoWatch', 0 - @watch g.THREAD_ID - $.delete 'autoWatch' - else - #populate watcher, display watch buttons - @refresh() - - $.on d, 'QRPostSuccessful', @cb.post - $.sync 'watched', @refresh - - refresh: (watched) -> - watched or= $.get 'watched', {} - nodes = [] - for board of watched - for id, props of watched[board] - x = $.el 'a', - textContent: '×' - href: 'javascript:;' - $.on x, 'click', Watcher.cb.x - link = $.el 'a', props - link.title = link.textContent - - div = $.el 'div' - $.add div, [x, $.tn(' '), link] - nodes.push div - - for div in $$ 'div:not(.move)', Watcher.dialog - $.rm div - $.add Watcher.dialog, nodes - - watchedBoard = watched[g.BOARD] or {} - for favicon in $$ '.favicon' - id = favicon.nextSibling.name - if id of watchedBoard - favicon.src = Favicon.default - else - favicon.src = Favicon.empty - return - - cb: - toggle: -> - Watcher.toggle @parentNode - x: -> - thread = @nextElementSibling.pathname.split '/' - Watcher.unwatch thread[3], thread[1] - post: (e) -> - {postID, threadID} = e.detail - if threadID is '0' - if Conf['Auto Watch'] - $.set 'autoWatch', postID - else if Conf['Auto Watch Reply'] - Watcher.watch threadID - - toggle: (thread) -> - id = $('.favicon + input', thread).name - Watcher.watch(id) or Watcher.unwatch id, g.BOARD - - unwatch: (id, board) -> - watched = $.get 'watched', {} - delete watched[board][id] - $.set 'watched', watched - Watcher.refresh() - - watch: (id) -> - thread = $.id "t#{id}" - return false if $('.favicon', thread).src is Favicon.default - - watched = $.get 'watched', {} - watched[g.BOARD] or= {} - watched[g.BOARD][id] = - href: "/#{g.BOARD}/res/#{id}" - textContent: Get.title thread - $.set 'watched', watched - Watcher.refresh() - true - -Anonymize = - init: -> - Main.callbacks.push @node - node: (post) -> - return if post.isInlined and not post.isCrosspost - name = $ '.postInfo .name', post.el - name.textContent = 'Anonymous' - if (trip = name.nextElementSibling) and trip.className is 'postertrip' - $.rm trip - if (parent = name.parentNode).className is 'useremail' and not /^mailto:sage$/i.test parent.href - $.replace parent, name - -Sauce = - init: -> - return if g.BOARD is 'f' - @links = [] - for link in Conf['sauces'].split '\n' - continue if link[0] is '#' - # XXX .trim() is there to fix Opera reading two different line breaks. - @links.push @createSauceLink link.trim() - return unless @links.length - Main.callbacks.push @node - - createSauceLink: (link) -> - link = link.replace /(\$\d)/g, (parameter) -> - switch parameter - when '$1' - "' + (isArchived ? img.firstChild.src : 'http://thumbs.4chan.org' + img.pathname.replace(/src(\\/\\d+).+$/, 'thumb$1s.jpg')) + '" - when '$2' - "' + img.href + '" - when '$3' - "' + encodeURIComponent(img.firstChild.dataset.md5) + '" - when '$4' - g.BOARD - else - parameter - domain = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1] - href = link.replace /;text:.+$/, '' - href = Function 'img', 'isArchived', "return '#{href}'" - el = $.el 'a', - target: '_blank' - textContent: domain - (img, isArchived) -> - a = el.cloneNode true - a.href = href img, isArchived - a - - node: (post) -> - {img} = post - return if post.isInlined and not post.isCrosspost or not img - img = img.parentNode - nodes = [] - for link in Sauce.links - # \u00A0 is nbsp - nodes.push $.tn('\u00A0'), link img, post.isArchived - $.add post.fileInfo, nodes - -RevealSpoilers = - init: -> - Main.callbacks.push @node - node: (post) -> - {img} = post - if not (img and /^Spoiler/.test img.alt) or post.isInlined and not post.isCrosspost or post.isArchived - return - img.removeAttribute 'style' - # revealed spoilers do not have height/width set, this fixes auto-gifs dimensions. - s = img.style - s.maxHeight = s.maxWidth = if /\bop\b/.test post.class then '250px' else '125px' - img.src = "//thumbs.4chan.org#{img.parentNode.pathname.replace /src(\/\d+).+$/, 'thumb$1s.jpg'}" - -Time = - init: -> - Time.foo() - Main.callbacks.push @node - node: (post) -> - return if post.isInlined and not post.isCrosspost - node = $ '.postInfo > .dateTime', post.el - Time.date = new Date node.dataset.utc * 1000 - node.textContent = Time.funk Time - foo: -> - code = Conf['time'].replace /%([A-Za-z])/g, (s, c) -> - if c of Time.formatters - "' + Time.formatters.#{c}() + '" - else - s - Time.funk = Function 'Time', "return '#{code}'" - day: [ - 'Sunday' - 'Monday' - 'Tuesday' - 'Wednesday' - 'Thursday' - 'Friday' - 'Saturday' - ] - month: [ - 'January' - 'February' - 'March' - 'April' - 'May' - 'June' - 'July' - 'August' - 'September' - 'October' - 'November' - 'December' - ] - zeroPad: (n) -> if n < 10 then '0' + n else n - formatters: - a: -> Time.day[Time.date.getDay()][...3] - A: -> Time.day[Time.date.getDay()] - b: -> Time.month[Time.date.getMonth()][...3] - B: -> Time.month[Time.date.getMonth()] - d: -> Time.zeroPad Time.date.getDate() - e: -> Time.date.getDate() - H: -> Time.zeroPad Time.date.getHours() - I: -> Time.zeroPad Time.date.getHours() % 12 or 12 - k: -> Time.date.getHours() - l: -> Time.date.getHours() % 12 or 12 - m: -> Time.zeroPad Time.date.getMonth() + 1 - M: -> Time.zeroPad Time.date.getMinutes() - p: -> if Time.date.getHours() < 12 then 'AM' else 'PM' - P: -> if Time.date.getHours() < 12 then 'am' else 'pm' - S: -> Time.zeroPad Time.date.getSeconds() - y: -> Time.date.getFullYear() - 2000 - -FileInfo = - init: -> - return if g.BOARD is 'f' - @setFormats() - Main.callbacks.push @node - node: (post) -> - return if post.isInlined and not post.isCrosspost or not post.fileInfo - node = post.fileInfo.firstElementChild - alt = post.img.alt - span = $ 'span', node - FileInfo.data = - link: post.img.parentNode.href - spoiler: /^Spoiler/.test alt - size: alt.match(/\d+\.?\d*/)[0] - unit: alt.match(/\w+$/)[0] - resolution: span.previousSibling.textContent.match(/\d+x\d+|PDF/)[0] - fullname: span.title - shortname: span.textContent - # XXX GM/Scriptish - node.setAttribute 'data-filename', span.title - node.innerHTML = FileInfo.funk FileInfo - setFormats: -> - code = Conf['fileInfo'].replace /%([BKlLMnNprs])/g, (s, c) -> - if c of FileInfo.formatters - "' + f.formatters.#{c}() + '" - else - s - @funk = Function 'f', "return '#{code}'" - convertUnit: (unitT) -> - size = @data.size - unitF = @data.unit - if unitF isnt unitT - units = ['B', 'KB', 'MB'] - i = units.indexOf(unitF) - units.indexOf unitT - unitT = 'Bytes' if unitT is 'B' - if i > 0 - size *= 1024 while i-- > 0 - else if i < 0 - size /= 1024 while i++ < 0 - if size < 1 and size.toString().length > size.toFixed(2).length - size = size.toFixed 2 - "#{size} #{unitT}" - formatters: - l: -> "#{@n()}" - L: -> "#{@N()}" - n: -> - if FileInfo.data.fullname is FileInfo.data.shortname - FileInfo.data.fullname - else - "#{FileInfo.data.shortname}#{FileInfo.data.fullname}" - N: -> FileInfo.data.fullname - p: -> if FileInfo.data.spoiler then 'Spoiler, ' else '' - s: -> "#{FileInfo.data.size} #{FileInfo.data.unit}" - B: -> FileInfo.convertUnit 'B' - K: -> FileInfo.convertUnit 'KB' - M: -> FileInfo.convertUnit 'MB' - r: -> FileInfo.data.resolution - -Get = - post: (board, threadID, postID, root, cb) -> - if board is g.BOARD and post = $.id "pc#{postID}" - $.add root, Get.cleanPost post.cloneNode true - return - - root.textContent = "Loading post No.#{postID}..." - if threadID - $.cache "/#{board}/res/#{threadID}", -> - Get.parsePost @, board, threadID, postID, root, cb - else if url = Redirect.post board, postID - $.cache url, -> - Get.parseArchivedPost @, board, postID, root, cb - parsePost: (req, board, threadID, postID, root, cb) -> - {status} = req - if status isnt 200 - # The thread can die by the time we check a quote. - if url = Redirect.post board, postID - $.cache url, -> - Get.parseArchivedPost @, board, postID, root, cb - else - root.textContent = - if status is 404 - "Thread No.#{threadID} has not been found." - else - "Error #{req.status}: #{req.statusText}." - return - - doc = d.implementation.createHTMLDocument '' - doc.documentElement.innerHTML = req.response - - unless pc = doc.getElementById "pc#{postID}" - # The post can be deleted by the time we check a quote. - if url = Redirect.post board, postID - $.cache url, -> - Get.parseArchivedPost @, board, postID, root, cb - else - root.textContent = "Post No.#{postID} has not been found." - return - pc = Get.cleanPost d.importNode pc, true - - for quote in $$ '.quotelink', pc - href = quote.getAttribute 'href' - continue if href[0] is '/' # Cross-board quote, or board link - quote.href = "/#{board}/res/#{href}" # Fix pathnames - link = $ 'a[title="Highlight this post"]', pc - link.href = "/#{board}/res/#{threadID}#p#{postID}" - link.nextSibling.href = "/#{board}/res/#{threadID}#q#{postID}" - - $.replace root.firstChild, pc - cb() if cb - parseArchivedPost: (req, board, postID, root, cb) -> - data = JSON.parse req.response - $.addClass root, 'archivedPost' - if data.error - root.textContent = data.error - return - - threadID = data.thread_num - isOP = postID is threadID - {name, trip, timestamp} = data - subject = data.title - - # post info (mobile) - piM = $.el 'div', - id: "pim#{postID}" - className: 'postInfoM mobile' - innerHTML: "
#{data.fourchan_date}
No.#{postID}
" - $('.name', piM).textContent = name - $('.subject', piM).textContent = subject - br = $ 'br', piM - if trip - $.before br, [$.tn(' '), $.el 'span', - className: 'postertrip' - textContent: trip - ] - {capcode} = data - if capcode isnt 'N' # 'A'dmin or 'M'od - $.addClass br.parentNode, if capcode is 'A' then 'capcodeAdmin' else 'capcodeMod' - $.before br, [ - $.tn(' '), - $.el('strong', - className: 'capcode', - textContent: if capcode is 'A' then '## Admin' else '## Mod' - ), - $.tn(' '), - $.el('img', - src: if capcode is 'A' then '//static.4chan.org/image/adminicon.gif' else '//static.4chan.org/image/modicon.gif', - alt: if capcode is 'A' then 'This user is the 4chan Administrator.' else 'This user is a 4chan Moderator.', - title: if capcode is 'A' then 'This user is the 4chan Administrator.' else 'This user is a 4chan Moderator.', - className: 'identityIcon' - ) - ] - - # post info - pi = $.el 'div', - id: "pi#{postID}" - className: 'postInfo desktop' - innerHTML: " data.fourchan_date No.#{postID}#{if isOP then '   ' else ''} " - # subject - $('.subject', pi).textContent = subject - nameBlock = $ '.nameBlock', pi - if data.email - email = $.el 'a', - className: 'useremail' - href: "mailto:#{data.email}" - $.add nameBlock, email - nameBlock = email - $.add nameBlock, $.el 'span', - className: 'name' - textContent: data.name - if trip - $.add nameBlock, [$.tn(' '), $.el('span', className: 'postertrip', textContent: trip)] - if capcode isnt 'N' # 'A'dmin or 'M'od - $.add nameBlock, [ - $.tn(' '), - $.el('strong', - className: if capcode is 'A' then 'capcode capcodeAdmin' else 'capcode', - textContent: if capcode is 'A' then '## Admin' else '## Mod' - ) - ] - nameBlock = $ '.nameBlock', pi - $.addClass nameBlock, if capcode is 'A' then 'capcodeAdmin' else 'capcodeMod' - $.add nameBlock, [ - $.tn(' '), - $.el('img', - src: if capcode is 'A' then '//static.4chan.org/image/adminicon.gif' else '//static.4chan.org/image/modicon.gif', - alt: if capcode is 'A' then 'This user is the 4chan Administrator.' else 'This user is a 4chan Moderator.', - title: if capcode is 'A' then 'This user is the 4chan Administrator.' else 'This user is a 4chan Moderator.', - className: 'identityIcon' - ) - ] - - # comment - bq = $.el 'blockquote', - id: "m#{postID}" - className: 'postMessage' - textContent: data.comment # set this first to convert text to HTML entities - # https://github.com/eksopl/fuuka/blob/master/Board/Yotsuba.pm#L413-452 - # https://github.com/eksopl/asagi/blob/master/src/main/java/net/easymodo/asagi/Yotsuba.java#L109-138 - bq.innerHTML = bq.innerHTML.replace /// - \n - | \[/?b\] - | \[/?spoiler\] - | \[/?code\] - | \[/?moot\] - | \[/?banned\] - ///g, (text) -> - switch text - when '\n' - '
' - when '[b]' - '' - when '[/b]' - '' - when '[spoiler]' - '' - when '[/spoiler]' - '' - when '[code]' - '
'
-          when '[/code]'
-            '
' - when '[moot]' - '
' - when '[/moot]' - '
' - when '[banned]' - '' - when '[/banned]' - '' - # greentext - bq.innerHTML = bq.innerHTML.replace /(^|>)(>[^<$]+)(<|$)/g, '$1$2$3' - - # post container - pc = $.el 'div', - id: "pc#{postID}" - className: "postContainer #{if isOP then 'op' else 'reply'}Container" - innerHTML: "
" - $.add pc.firstChild, [piM, pi, bq] - - # file - if filename = data.media_filename - file = $.el 'div', - id: "f#{postID}" - className: 'file' - spoiler = data.spoiler is '1' - filesize = $.bytesToString data.media_size - $.add file, $.el 'div', - className: 'fileInfo' - innerHTML: "File: #{data.media_orig}-(#{if spoiler then 'Spoiler Image, ' else ''}#{filesize}, #{data.media_w}x#{data.media_h}, )" - span = $ 'span[title]', file - span.title = filename - threshold = if isOP then 40 else 30 - span.textContent = - # FILENAME SHORTENING SCIENCE: - # OPs have a +10 characters threshold. - # The file extension is not taken into account. - if filename.replace(/\.\w+$/, '').length > threshold - "#{filename[...threshold - 5]}(...)#{filename.match(/\.\w+$/)}" - else - filename - thumb_src = if data.media_status is 'available' then "src=#{data.thumb_link}" else '' - $.add file, $.el 'a', - className: if spoiler then 'fileThumb imgspoiler' else 'fileThumb' - href: data.media_link or data.remote_media_link - target: '_blank' - innerHTML: "#{if data.media_status isnt " - $.after (if isOP then piM else pi), file - - $.replace root.firstChild, Get.cleanPost pc - cb() if cb - cleanPost: (root) -> - post = $ '.post', root - for child in Array::slice.call root.childNodes - $.rm child unless child is post - - # Remove inlined posts inside of this post. - for inline in $$ '.inline', post - $.rm inline - for inlined in $$ '.inlined', post - $.rmClass inlined, 'inlined' - - # Don't mess with other features - now = Date.now() - els = $$ '[id]', root - els.push root - for el in els - el.id = "#{now}_#{el.id}" - - $.rmClass root, 'forwarded' - $.rmClass root, 'qphl' # op - $.rmClass post, 'highlight' - $.rmClass post, 'qphl' # reply - root.hidden = post.hidden = false - - root - title: (thread) -> - op = $ '.op', thread - el = $ '.subject', op - unless el.textContent - el = $ 'blockquote', op - unless el.textContent - el = $ '.nameBlock', op - span = $.el 'span', innerHTML: el.innerHTML.replace /
/g, ' ' - "/#{g.BOARD}/ - #{span.textContent.trim()}" - -TitlePost = - init: -> - d.title = Get.title() - -QuoteBacklink = - init: -> - format = Conf['backlink'].replace /%id/g, "' + id + '" - @funk = Function 'id', "return '#{format}'" - Main.callbacks.push @node - node: (post) -> - return if post.isInlined - quotes = {} - for quote in post.quotes - # Don't process >>>/b/. - if qid = quote.hash[2..] - # Duplicate quotes get overwritten. - quotes[qid] = true - a = $.el 'a', - href: "/#{g.BOARD}/res/#{post.threadID}#p#{post.ID}" - className: if post.el.hidden then 'filtered backlink' else 'backlink' - textContent: QuoteBacklink.funk post.ID - for qid of quotes - # Don't backlink the OP. - continue if !(el = $.id "pi#{qid}") or !Conf['OP Backlinks'] and /\bop\b/.test el.parentNode.className - link = a.cloneNode true - if Conf['Quote Preview'] - $.on link, 'mouseover', QuotePreview.mouseover - if Conf['Quote Inline'] - $.on link, 'click', QuoteInline.toggle - else - link.setAttribute 'onclick', "replyhl('#{post.ID}');" - unless container = $.id "blc#{qid}" - container = $.el 'span', - className: 'container' - id: "blc#{qid}" - $.add el, container - $.add container, [$.tn(' '), link] - return - -QuoteInline = - init: -> - Main.callbacks.push @node - node: (post) -> - for quote in post.quotes - continue unless quote.hash or /\bdeadlink\b/.test quote.className - quote.removeAttribute 'onclick' - $.on quote, 'click', QuoteInline.toggle - for quote in post.backlinks - $.on quote, 'click', QuoteInline.toggle - return - toggle: (e) -> - return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 - e.preventDefault() - id = @dataset.id or @hash[2..] - if /\binlined\b/.test @className - QuoteInline.rm @, id - else - return if $.x "ancestor::div[contains(@id,'p#{id}')]", @ - QuoteInline.add @, id - @classList.toggle 'inlined' - - add: (q, id) -> - if q.host is 'boards.4chan.org' - path = q.pathname.split '/' - board = path[1] - threadID = path[3] - postID = id - else - board = q.dataset.board - threadID = 0 - postID = q.dataset.id - - el = if board is g.BOARD then $.id "p#{postID}" else false - inline = $.el 'div', - id: "i#{postID}" - className: if el then 'inline' else 'inline crosspost' - - root = - if isBacklink = /\bbacklink\b/.test q.className - q.parentNode - else - $.x 'ancestor-or-self::*[parent::blockquote][1]', q - $.after root, inline - Get.post board, threadID, postID, inline - - return unless el - - # Will only unhide if there's no inlined backlinks of it anymore. - if isBacklink and Conf['Forward Hiding'] - $.addClass el.parentNode, 'forwarded' - ++el.dataset.forwarded or el.dataset.forwarded = 1 - - # Decrease the unread count if this post is in the array of unread reply. - if (i = Unread.replies.indexOf el) isnt -1 - Unread.replies.splice i, 1 - Unread.update true - - rm: (q, id) -> - # select the corresponding inlined quote or loading quote - div = $.x "following::div[@id='i#{id}']", q - $.rm div - return unless Conf['Forward Hiding'] - for inlined in $$ '.backlink.inlined', div - div = $.id inlined.hash[1..] - $.rmClass div.parentNode, 'forwarded' unless --div.dataset.forwarded - if /\bbacklink\b/.test q.className - div = $.id "p#{id}" - $.rmClass div.parentNode, 'forwarded' unless --div.dataset.forwarded - -QuotePreview = - init: -> - Main.callbacks.push @node - node: (post) -> - for quote in post.quotes - $.on quote, 'mouseover', QuotePreview.mouseover if quote.hash or /\bdeadlink\b/.test quote.className - for quote in post.backlinks - $.on quote, 'mouseover', QuotePreview.mouseover - return - mouseover: (e) -> - return if /\binlined\b/.test @className - - # Make sure to remove the previous qp - # in case it got stuck. Opera-only bug? - if qp = $.id 'qp' - if qp is UI.el - delete UI.el - $.rm qp - - # Don't stop other elements from dragging - return if UI.el - - if @host is 'boards.4chan.org' - path = @pathname.split '/' - board = path[1] - threadID = path[3] - postID = @hash[2..] - else - board = @dataset.board - threadID = 0 - postID = @dataset.id - - qp = UI.el = $.el 'div', - id: 'qp' - className: 'reply dialog' - UI.hover e - $.add d.body, qp - el = $.id "p#{postID}" if board is g.BOARD - Get.post board, threadID, postID, qp, -> - bq = $ 'blockquote', qp - Main.prettify bq - post = - el: qp - blockquote: bq - isArchived: /\barchivedPost\b/.test qp.className - if img = $ 'img[data-md5]', qp - post.fileInfo = img.parentNode.previousElementSibling - post.img = img - if Conf['Reveal Spoilers'] - RevealSpoilers.node post - if Conf['Image Auto-Gif'] - AutoGif.node post - if Conf['Time Formatting'] - Time.node post - if Conf['File Info Formatting'] - FileInfo.node post - if Conf['Resurrect Quotes'] - Quotify.node post - - $.on @, 'mousemove', UI.hover - $.on @, 'mouseout click', QuotePreview.mouseout - - return unless el - if Conf['Quote Highlighting'] - if /\bop\b/.test el.className - $.addClass el.parentNode, 'qphl' - else - $.addClass el, 'qphl' - quoterID = $.x('ancestor::*[@id][1]', @).id.match(/\d+$/)[0] - for quote in $$ '.quotelink, .backlink', qp - if quote.hash[2..] is quoterID - $.addClass quote, 'forwardlink' - return - mouseout: (e) -> - UI.hoverend() - if el = $.id @hash[1..] - $.rmClass el, 'qphl' # reply - $.rmClass el.parentNode, 'qphl' # op - $.off @, 'mousemove', UI.hover - $.off @, 'mouseout click', QuotePreview.mouseout - -QuoteOP = - init: -> - Main.callbacks.push @node - node: (post) -> - return if post.isInlined and not post.isCrosspost - for quote in post.quotes - if quote.hash[2..] is post.threadID - # \u00A0 is nbsp - $.add quote, $.tn '\u00A0(OP)' - return - -QuoteCT = - init: -> - Main.callbacks.push @node - node: (post) -> - return if post.isInlined and not post.isCrosspost - for quote in post.quotes - unless quote.hash - # Make sure this isn't a link to the board we're on. - continue - path = quote.pathname.split '/' - # If quote leads to a different thread id and is located on the same board. - if path[1] is g.BOARD and path[3] isnt post.threadID - # \u00A0 is nbsp - $.add quote, $.tn '\u00A0(Cross-thread)' - return - -Quotify = - init: -> - Main.callbacks.push @node - node: (post) -> - return if post.isInlined and not post.isCrosspost - - # XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE is 6 - # Get all the text nodes that are not inside an anchor. - snapshot = d.evaluate './/text()[not(parent::a)]', post.blockquote, null, 6, null - - for i in [0...snapshot.snapshotLength] - node = snapshot.snapshotItem i - data = node.data - - unless quotes = data.match />>(>\/[a-z\d]+\/)?\d+/g - # Only accept nodes with potentially valid links - continue - - nodes = [] - - for quote in quotes - index = data.indexOf quote - if text = data[...index] - # Potential text before this valid quote. - nodes.push $.tn text - - id = quote.match(/\d+$/)[0] - board = - if m = quote.match /^>>>\/([a-z\d]+)/ - m[1] - else - # Get the post's board, whether it's inlined or not. - $('a[title="Highlight this post"]', post.el).pathname.split('/')[1] - - nodes.push a = $.el 'a', - # \u00A0 is nbsp - textContent: "#{quote}\u00A0(Dead)" - - if board is g.BOARD and $.id "p#{id}" - a.href = "#p#{id}" - a.className = 'quotelink' - a.setAttribute 'onclick', "replyhl('#{id}');" - else - a.href = Redirect.thread board, 0, id - a.className = 'deadlink' - a.target = '_blank' - if Redirect.post board, id - $.addClass a, 'quotelink' - # XXX WTF Scriptish/Greasemonkey? - # Setting dataset attributes that way doesn't affect the HTML, - # but are, I suspect, kept as object key/value pairs and GC'd later. - # a.dataset.board = board - # a.dataset.id = id - a.setAttribute 'data-board', board - a.setAttribute 'data-id', id - - data = data[index + quote.length..] - - if data - # Potential text after the last valid quote. - nodes.push $.tn data - - $.replace node, nodes - return - -DeleteLink = - init: -> - div = $.el 'div', - className: 'delete_link' - textContent: 'Delete' - aPost = $.el 'a', - className: 'delete_post' - href: 'javascript:;' - aImage = $.el 'a', - className: 'delete_image' - href: 'javascript:;' - - children = [] - - children.push - el: aPost - open: -> - aPost.textContent = 'Post' - $.on aPost, 'click', DeleteLink.delete - true - - children.push - el: aImage - open: (post) -> - return false unless post.img - aImage.textContent = 'Image' - $.on aImage, 'click', DeleteLink.delete - true - - Menu.addEntry - el: div - open: (post) -> - if post.isArchived - return false - node = div.firstChild - if seconds = DeleteLink.cooldown[post.ID] - node.textContent = "Delete (#{seconds})" - DeleteLink.cooldown.el = node - else - node.textContent = 'Delete' - delete DeleteLink.cooldown.el - true - children: children - - $.on d, 'QRPostSuccessful', @cooldown.start - - delete: -> - menu = $.id 'menu' - {id} = menu.dataset - return if DeleteLink.cooldown[id] - - $.off @, 'click', DeleteLink.delete - @textContent = 'Deleting...' - - pwd = - if m = d.cookie.match /4chan_pass=([^;]+)/ - decodeURIComponent m[1] - else - $.id('delPassword').value - - board = $('a[title="Highlight this post"]', - $.id menu.dataset.rootid).pathname.split('/')[1] - self = @ - - form = - mode: 'usrdel' - onlyimgdel: /\bdelete_image\b/.test @className - pwd: pwd - form[id] = 'delete' - - $.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{board}/"), { - onload: -> DeleteLink.load self, @response - onerror: -> DeleteLink.error self - }, { - form: $.formData form - } - load: (self, html) -> - doc = d.implementation.createHTMLDocument '' - doc.documentElement.innerHTML = html - if doc.title is '4chan - Banned' # Ban/warn check - s = 'Banned!' - else if msg = doc.getElementById 'errmsg' # error! - s = msg.textContent - $.on self, 'click', DeleteLink.delete - else - s = 'Deleted' - self.textContent = s - error: (self) -> - self.textContent = 'Connection error, please retry.' - $.on self, 'click', DeleteLink.delete - - cooldown: - start: (e) -> - DeleteLink.cooldown.count e.detail.postID, 30 - count: (postID, seconds) -> - return unless 0 <= seconds <= 30 - setTimeout DeleteLink.cooldown.count, 1000, postID, seconds-1 - {el} = DeleteLink.cooldown - if seconds is 0 - el?.textContent = 'Delete' - delete DeleteLink.cooldown[postID] - delete DeleteLink.cooldown.el - return - el?.textContent = "Delete (#{seconds})" - DeleteLink.cooldown[postID] = seconds - -ReportLink = - init: -> - a = $.el 'a', - className: 'report_link' - href: 'javascript:;' - textContent: 'Report this post' - $.on a, 'click', @report - Menu.addEntry - el: a - open: (post) -> - post.isArchived is false - report: -> - a = $ 'a[title="Highlight this post"]', $.id @parentNode.dataset.rootid - url = "//sys.4chan.org/#{a.pathname.split('/')[1]}/imgboard.php?mode=report&no=#{@parentNode.dataset.id}" - id = Date.now() - set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200" - window.open url, id, set - -DownloadLink = - init: -> - # Test for download feature support. - return if $.el('a').download is undefined - a = $.el 'a', - className: 'download_link' - textContent: 'Download file' - Menu.addEntry - el: a - open: (post) -> - unless post.img - return false - a.href = post.img.parentNode.href - fileText = post.fileInfo.firstElementChild - a.download = - if Conf['File Info Formatting'] - fileText.dataset.filename - else - $('span', fileText).title - true - -ArchiveLink = - init: -> - a = $.el 'a', - className: 'archive_link' - target: '_blank' - textContent: 'Archived post' - Menu.addEntry - el: a - open: (post) -> - path = $('a[title="Highlight this post"]', post.el).pathname.split '/' - if (href = Redirect.thread path[1], path[3], post.ID) is "//boards.4chan.org/#{path[1]}/" - return false - a.href = href - true - -ThreadStats = - init: -> - dialog = UI.dialog 'stats', 'bottom: 0; left: 0;', '
0 / 0
' - dialog.className = 'dialog' - $.add d.body, dialog - @posts = @images = 0 - @imgLimit = - switch g.BOARD - when 'a', 'b', 'v', 'co', 'mlp' - 251 - when 'vg' - 501 - else - 151 - Main.callbacks.push @node - node: (post) -> - return if post.isInlined - $.id('postcount').textContent = ++ThreadStats.posts - return unless post.img - imgcount = $.id 'imagecount' - imgcount.textContent = ++ThreadStats.images - if ThreadStats.images > ThreadStats.imgLimit - $.addClass imgcount, 'warning' - -Unread = - init: -> - @title = d.title - $.on d, 'QRPostSuccessful', @post - @update() - $.on window, 'scroll', Unread.scroll - Main.callbacks.push @node - - replies: [] - foresee: [] - - post: (e) -> - Unread.foresee.push e.detail.postID - - node: (post) -> - if (index = Unread.foresee.indexOf post.ID) isnt -1 - Unread.foresee.splice index, 1 - return - {el} = post - return if el.hidden or /\bop\b/.test(post.class) or post.isInlined - count = Unread.replies.push el - Unread.update count is 1 - - scroll: -> - height = d.documentElement.clientHeight - for reply, i in Unread.replies - {bottom} = reply.getBoundingClientRect() - if bottom > height #post is not completely read - break - return if i is 0 - - Unread.replies = Unread.replies[i..] - Unread.update Unread.replies.length is 0 - - setTitle: (count) -> - if @scheduled - clearTimeout @scheduled - delete Unread.scheduled - @setTitle count - return - @scheduled = setTimeout (-> - d.title = "(#{count}) #{Unread.title}" - ), 5 - - update: (updateFavicon) -> - return unless g.REPLY - - count = @replies.length - - if Conf['Unread Count'] - @setTitle count - - unless Conf['Unread Favicon'] and updateFavicon - return - - if $.engine is 'presto' - $.rm Favicon.el - - Favicon.el.href = - if g.dead - if count - Favicon.unreadDead - else - Favicon.dead - else - if count - Favicon.unread - else - Favicon.default - - if g.dead - $.addClass Favicon.el, 'dead' - else - $.rmClass Favicon.el, 'dead' - if count - $.addClass Favicon.el, 'unread' - else - $.rmClass Favicon.el, 'unread' - - # `favicon.href = href` doesn't work on Firefox - # `favicon.href = href` isn't enough on Opera - # Opera won't always update the favicon if the href didn't change - unless $.engine is 'webkit' - $.add d.head, Favicon.el - -Favicon = - init: -> - return if @el # Prevent race condition with options first run - @el = $ 'link[rel="shortcut icon"]', d.head - @el.type = 'image/x-icon' - {href} = @el - @SFW = /ws.ico$/.test href - @default = href - @switch() - - switch: -> - switch Conf['favicon'] - when 'ferongr' - @unreadDead = 'data:image/gif;base64,R0lGODlhEAAQAOMHAOgLAnMFAL8AAOgLAukMA/+AgP+rq////////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw==' - @unreadSFW = 'data:image/gif;base64,R0lGODlhEAAQAOMHAADX8QBwfgC2zADX8QDY8nnl8qLp8v///////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw==' - @unreadNSFW = 'data:image/gif;base64,R0lGODlhEAAQAOMHAFT+ACh5AEncAFT+AFX/Acz/su7/5v///////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw==' - when 'xat-' - @unreadDead = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA2ElEQVQ4y61TQQrCMBDMQ8WDIEV6LbT2A4og2Hq0veo7fIAH04dY9N4xmyYlpGmI2MCQTWYy3Wy2DAD7B2wWAzWgcTgVeZKlZRxHNYFi2jM18oBh0IcKtC6ixf22WT4IFLs0owxswXu9egm0Ls6bwfCFfNsJYJKfqoEkd3vgUgFVLWObtzNgVKyruC+ljSzr5OEnBzjvjcQecaQhbZgBb4CmGQw+PoMkTUtdbd8VSEPakcGxPOcsoIgUKy0LecY29BmdBrqRfjIwZ93KLs5loHvBnL3cLH/jF+C/+z5dgUysAAAAAElFTkSuQmCC' - @unreadSFW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA30lEQVQ4y2P4//8/AyWYgSoGQMF/GJ7Y11VVUVoyKTM9ey4Ig9ggMWQ1YA1IBvzXm34YjkH8mPyJB+Nqlp8FYRAbmxoMF6ArSNrw6T0Qf8Amh9cFMEWVR/7/A+L/uORxhgEIt5/+/3/2lf//5wAxiI0uj+4CBlBgxVUvOwtydgXQZpDmi2/+/7/0GmIQSAwkB1IDUkuUAZeABlx+g2zAZ9wGlAOjChba+LwAUgNSi2HA5Am9VciBhSsQQWyoWgZiovEDsdGI1QBYQiLJAGQalpSxyWEzAJYWkGm8clTJjQCZ1hkoVG0CygAAAABJRU5ErkJggg==' - @unreadNSFW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA4ElEQVQ4y2P4//8/AyWYgSoGQMF/GJ7YNbGqrKRiUnp21lwQBrFBYshqwBqQDPifdsYYjkH8mInxB+OWx58FYRAbmxoMF6ArKPmU9B6IP2CTw+sCmKKe/5X/gPg/LnmcYQDCs/63/1/9fzYQzwGz0eXRXcAACqy4ZfFnQc7u+V/xD6T55v+LQHwJbBBIDCQHUgNSS5QBt4Cab/2/jDDgMx4DykrKJ8FCG58XQGpAajEMmNw7uQo5sHAFIogNVctATDR+IDYasRoAS0gkGYBMw5IyNjlsBsDSAjKNV44quREAx58Mr9vt5wQAAAAASUVORK5CYII=' - when 'Mayhem' - @unreadDead = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABIUlEQVQ4jZ2ScWuDMBDFgw4pIkU0WsoQkWAYIkXZH4N9/+/V3dmfXSrKYIFHwt17j8vdGWNMIkgFuaDgzgQnwRs4EQs5KdolUQtagRN0givEDBTEOjgtGs0Zq8F7cKqqusVxrMQLaDUWcjBSrXkn8gs51tpJSWLk9b3HUa0aNIL5gPBR1/V4kJvR7lTwl8GmAm1Gf9+c3S+89qBHa8502AsmSrtBaEBPbIbj0ah2madlNAPEccdgJDfAtWifBjqWKShRBT6KoiH8QlEUn/qt0CCjnNdmPUwmFWzj9Oe6LpKuZXcwqq88z78Pch3aZU3dPwwc2sWlfZKCW5tWluV8kGvXClLm6dYN4/aUqfCbnEOzNDGhGZbNargvxCzvMGfRJD8UaDVvgkzo6QAAAABJRU5ErkJggg==' - @unreadSFW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABCElEQVQ4jZ2S4crCMAxF+0OGDJEPKYrIGKOsiJSx/fJRfSAfTJNyKqXfiuDg0C25N2RJjTGmEVrhTzhw7oStsIEtsVzT4o2Jo9ALThiEM8IdHIgNaHo8mjNWg6/ske8bohPo+63QOLzmooHp8fyAICBSQkVz0QKdsFQEV6WSW/D+7+BbgbIDHcb4Kp61XyjyI16zZ8JemGltQtDBSGxB4/GoN+7TpkkjDCsFArm0IYv3U0BbnYtf8BCy+JytsE0X6VyuKhPPK/GAJ14kvZZDZVV3pZIb8MZr6n4o4PDGKn0S5SdDmyq5PnXQsk+Xbhinp03FFzmHJw6xYRiWm9VxnohZ3vOcxdO8ARmXRvbWdtzQAAAAAElFTkSuQmCC' - @unreadNSFW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABCklEQVQ4jZ2S0WrDMAxF/TBCCKWMYhZKCSGYmFJMSNjD/mhf239qJXNcjBdTWODgRLpXKJKNMaYROuFTOHEehFb4gJZYrunwxsSXMApOmIQzwgOciE1oRjyaM1aDj+yR7xuiHvT9VmgcXnPRwO/9+wWCgEgJFc1FCwzCVhFclUpuw/u3g3cFyg50GPOjePZ+ocjPeM2RCXthpbUFwQAzsQ2Nx6PeuE+bJo0w7BQI5NKGLN5XAW11LX7BQ8jia7bCLl2kc7mqTLzuxAOeeJH0Wk6VVf0oldyEN15T948CDm+sMiZRfjK0pZIbUwcd+3TphnF62lR8kXN44hAbhmG5WQNnT8zynucsnuYJhFpBfkMzqD4AAAAASUVORK5CYII=' - when 'Original' - @unreadDead = 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs=' - @unreadSFW = 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAC6Xw////////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs=' - @unreadNSFW = 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs=' - @unread = if @SFW then @unreadSFW else @unreadNSFW - - empty: 'data:image/gif;base64,R0lGODlhEAAQAJEAAAAAAP///9vb2////yH5BAEAAAMALAAAAAAQABAAAAIvnI+pq+D9DBAUoFkPFnbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==' - dead: 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAIALAAAAAAQABAAAAIvlI+pq+D9DAgUoFkPDlbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==' - -Redirect = - image: (board, filename) -> - # Do not use g.BOARD, the image url can originate from a cross-quote. - switch board - when 'a', 'jp', 'm', 'sp', 'tg', 'vg', 'wsg' - "//archive.foolz.us/#{board}/full_image/#{filename}" - when 'u' - "//nsfw.foolz.us/#{board}/full_image/#{filename}" - # these will work whenever https://github.com/eksopl/fuuka/issues/23 is done - # when 'cgl', 'g', 'w' - # "//archive.rebeccablacktech.com/#{board}/full_image/#{filename}" - # when 'an', 'k', 'toy', 'x' - # "http://archive.maidlab.jp/#{board}/full_image/#{filename}" - # when 'e' - # "https://md401.homelinux.net/4chan/cgi-board.pl/#{board}/full_image/#{filename}" - post: (board, postID) -> - switch board - when 'a', 'co', 'jp', 'm', 'sp', 'tg', 'tv', 'v', 'vg', 'wsg', 'dev', 'foolz' - "//archive.foolz.us/api/chan/post/board/#{board}/num/#{postID}/format/json" - when 'u', 'kuku' - "//nsfw.foolz.us/api/chan/post/board/#{board}/num/#{postID}/format/json" - thread: (board, threadID, postID) -> - # keep the number only if the location.hash was sent f.e. - postID = postID.match(/\d+/)[0] if postID - path = - if threadID - "#{board}/thread/#{threadID}" - else - "#{board}/post/#{postID}" - switch board - when 'a', 'co', 'jp', 'm', 'sp', 'tg', 'tv', 'v', 'vg', 'wsg', 'dev', 'foolz' - url = "//archive.foolz.us/#{path}/" - if threadID and postID - url += "##{postID}" - when 'u', 'kuku' - url = "//nsfw.foolz.us/#{path}/" - if threadID and postID - url += "##{postID}" - when 'ck', 'lit' - url = "//fuuka.warosu.org/#{path}" - if threadID and postID - url += "#p#{postID}" - when 'diy', 'g', 'sci' - url = "//archive.installgentoo.net/#{path}" - if threadID and postID - url += "#p#{postID}" - when 'cgl', 'mu', 'soc', 'w' - url = "//archive.rebeccablacktech.com/#{path}" - if threadID and postID - url += "#p#{postID}" - when 'an', 'fit', 'k', 'r9k', 'toy', 'x' - url = "http://archive.maidlab.jp/#{path}" - if threadID and postID - url += "#p#{postID}" - when 'e' - url = "https://md401.homelinux.net/4chan/cgi-board.pl/#{path}" - if threadID and postID - url += "#p#{postID}" - else - if threadID - url = "//boards.4chan.org/#{board}/" - url or null - -ImageHover = - init: -> - Main.callbacks.push @node - node: (post) -> - return unless post.img - $.on post.img, 'mouseover', ImageHover.mouseover - mouseover: -> - # Make sure to remove the previous image hover - # in case it got stuck. Opera-only bug? - if el = $.id 'ihover' - if el is UI.el - delete UI.el - $.rm el - - # Don't stop other elements from dragging - return if UI.el - - el = UI.el = $.el 'img' - id: 'ihover' - src: @parentNode.href - $.add d.body, el - $.on el, 'load', ImageHover.load - $.on el, 'error', ImageHover.error - $.on @, 'mousemove', UI.hover - $.on @, 'mouseout', ImageHover.mouseout - load: -> - return unless @parentNode - # 'Fake' mousemove event by giving required values. - {style} = @ - UI.hover - clientX: - 45 + parseInt style.left - clientY: 120 + parseInt style.top - error: -> - src = @src.split '/' - unless src[2] is 'images.4chan.org' and url = Redirect.image src[3], src[5] - return if g.dead - url = "//images.4chan.org/#{src[3]}/src/#{src[5]}" - return if $.engine isnt 'webkit' and url.split('/')[2] is 'images.4chan.org' - timeoutID = setTimeout (=> @src = url), 3000 - # Only Chrome let userscripts do cross domain requests. - # Don't check for 404'd status in the archivers. - return if $.engine isnt 'webkit' or url.split('/')[2] isnt 'images.4chan.org' - $.ajax url, onreadystatechange: (-> clearTimeout timeoutID if @status is 404), - type: 'head' - mouseout: -> - UI.hoverend() - $.off @, 'mousemove', UI.hover - $.off @, 'mouseout', ImageHover.mouseout - -AutoGif = - init: -> - return if g.BOARD in ['gif', 'wsg'] - Main.callbacks.push @node - node: (post) -> - {img} = post - return if post.el.hidden or not img - src = img.parentNode.href - if /gif$/.test(src) and !/spoiler/.test img.src - gif = $.el 'img' - $.on gif, 'load', -> - # Replace the thumbnail once the GIF has finished loading. - img.src = src - gif.src = src - -ImageExpand = - init: -> - Main.callbacks.push @node - @dialog() - - node: (post) -> - return unless post.img - a = post.img.parentNode - $.on a, 'click', ImageExpand.cb.toggle - if ImageExpand.on and !post.el.hidden - ImageExpand.expand post.img - cb: - toggle: (e) -> - return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 - e.preventDefault() - ImageExpand.toggle @ - all: -> - ImageExpand.on = @checked - if ImageExpand.on #expand - thumbs = $$ 'img[data-md5]' - if Conf['Expand From Current'] - for thumb, i in thumbs - if thumb.getBoundingClientRect().top > 0 - break - thumbs = thumbs[i...] - for thumb in thumbs - ImageExpand.expand thumb - else #contract - for thumb in $$ 'img[data-md5][hidden]' - ImageExpand.contract thumb - return - typeChange: -> - switch @value - when 'full' - klass = '' - when 'fit width' - klass = 'fitwidth' - when 'fit height' - klass = 'fitheight' - when 'fit screen' - klass = 'fitwidth fitheight' - $.id('delform').className = klass - if /\bfitheight\b/.test klass - $.on window, 'resize', ImageExpand.resize - unless ImageExpand.style - ImageExpand.style = $.addStyle '' - ImageExpand.resize() - else if ImageExpand.style - $.off window, 'resize', ImageExpand.resize - - toggle: (a) -> - thumb = a.firstChild - if thumb.hidden - rect = a.getBoundingClientRect() - if $.engine is 'webkit' - d.body.scrollTop += rect.top - 42 if rect.top < 0 - d.body.scrollLeft += rect.left if rect.left < 0 - else - d.documentElement.scrollTop += rect.top - 42 if rect.top < 0 - d.documentElement.scrollLeft += rect.left if rect.left < 0 - ImageExpand.contract thumb - else - ImageExpand.expand thumb - - contract: (thumb) -> - thumb.hidden = false - thumb.nextSibling.hidden = true - $.rmClass thumb.parentNode.parentNode.parentNode, 'image_expanded' - - expand: (thumb, url) -> - # Do not expand images of hidden/filtered replies, or already expanded pictures. - return if $.x 'ancestor-or-self::*[@hidden]', thumb - thumb.hidden = true - $.addClass thumb.parentNode.parentNode.parentNode, 'image_expanded' - if img = thumb.nextSibling - # Expand already loaded picture - img.hidden = false - return - a = thumb.parentNode - img = $.el 'img', - src: url or a.href - $.on img, 'error', ImageExpand.error - $.add a, img - - error: -> - thumb = @previousSibling - ImageExpand.contract thumb - $.rm @ - src = @src.split '/' - unless src[2] is 'images.4chan.org' and url = Redirect.image src[3], src[5] - return if g.dead - url = "//images.4chan.org/#{src[3]}/src/#{src[5]}" - return if $.engine isnt 'webkit' and url.split('/')[2] is 'images.4chan.org' - timeoutID = setTimeout ImageExpand.expand, 10000, thumb, url - # Only Chrome let userscripts do cross domain requests. - # Don't check for 404'd status in the archivers. - return if $.engine isnt 'webkit' or url.split('/')[2] isnt 'images.4chan.org' - $.ajax url, onreadystatechange: (-> clearTimeout timeoutID if @status is 404), - type: 'head' - - dialog: -> - controls = $.el 'div', - id: 'imgControls' - innerHTML: - "" - imageType = $.get 'imageType', 'full' - select = $ 'select', controls - select.value = imageType - ImageExpand.cb.typeChange.call select - $.on select, 'change', $.cb.value - $.on select, 'change', ImageExpand.cb.typeChange - $.on $('input', controls), 'click', ImageExpand.cb.all - - $.prepend $.id('delform'), controls - - resize: -> - ImageExpand.style.textContent = ".fitheight img[data-md5] + img {max-height:#{d.documentElement.clientHeight}px;}" - -Main = - init: -> - Main.flatten null, Config - - path = location.pathname - pathname = path[1..].split '/' - [g.BOARD, temp] = pathname - if temp is 'res' - g.REPLY = true - g.THREAD_ID = pathname[2] - - # Load values from localStorage. - for key, val of Conf - Conf[key] = $.get key, val - - switch location.hostname - when 'sys.4chan.org' - if /report/.test location.search - $.ready -> - $.on $.id('recaptcha_response_field'), 'keydown', (e) -> - window.location = 'javascript:Recaptcha.reload()' if e.keyCode is 8 and not e.target.value - return - when 'images.4chan.org' - $.ready -> - if /^4chan - 404/.test(d.title) and Conf['404 Redirect'] - path = location.pathname.split '/' - url = Redirect.image path[1], path[3] - location.href = url if url - return - - $.ready Options.init - - if Conf['Quick Reply'] and Conf['Hide Original Post Form'] - Main.css += '#postForm { display: none; }' - - Main.addStyle() - - now = Date.now() - if Conf['Check for Updates'] and $.get('lastUpdate', 0) < now - 6*$.HOUR - $.ready -> - $.on window, 'message', Main.message - $.set 'lastUpdate', now - $.add d.head, $.el 'script', - src: 'https://github.com/MayhemYDG/4chan-x/raw/master/latest.js' - - g.hiddenReplies = $.get "hiddenReplies/#{g.BOARD}/", {} - if $.get('lastChecked', 0) < now - 1*$.DAY - $.set 'lastChecked', now - - cutoff = now - 7*$.DAY - hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} - - for id, timestamp of hiddenThreads - if timestamp < cutoff - delete hiddenThreads[id] - - for id, timestamp of g.hiddenReplies - if timestamp < cutoff - delete g.hiddenReplies[id] - - $.set "hiddenThreads/#{g.BOARD}/", hiddenThreads - $.set "hiddenReplies/#{g.BOARD}/", g.hiddenReplies - - - #major features - if Conf['Filter'] - Filter.init() - - if Conf['Reply Hiding'] - ReplyHiding.init() - - if Conf['Filter'] or Conf['Reply Hiding'] - StrikethroughQuotes.init() - - if Conf['Anonymize'] - Anonymize.init() - - if Conf['Time Formatting'] - Time.init() - - if Conf['File Info Formatting'] - FileInfo.init() - - if Conf['Sauce'] - Sauce.init() - - if Conf['Reveal Spoilers'] - RevealSpoilers.init() - - if Conf['Image Auto-Gif'] - AutoGif.init() - - if Conf['Image Hover'] - ImageHover.init() - - if Conf['Menu'] - Menu.init() - - if Conf['Report Link'] - ReportLink.init() - - if Conf['Delete Link'] - DeleteLink.init() - - if Conf['Filter'] - Filter.menuInit() - - if Conf['Download Link'] - DownloadLink.init() - - if Conf['Archive Link'] - ArchiveLink.init() - - if Conf['Resurrect Quotes'] - Quotify.init() - - if Conf['Quote Inline'] - QuoteInline.init() - - if Conf['Quote Preview'] - QuotePreview.init() - - if Conf['Quote Backlinks'] - QuoteBacklink.init() - - if Conf['Indicate OP quote'] - QuoteOP.init() - - if Conf['Indicate Cross-thread Quotes'] - QuoteCT.init() - - $.ready Main.ready - - ready: -> - if /^4chan - 404/.test d.title - if Conf['404 Redirect'] and /^\d+$/.test g.THREAD_ID - location.href = Redirect.thread g.BOARD, g.THREAD_ID, location.hash - return - unless $.id 'navtopr' - return - $.addClass d.body, $.engine - $.addClass d.body, 'fourchan_x' - for nav in ['boardNavDesktop', 'boardNavDesktopFoot'] - if a = $ "a[href$='/#{g.BOARD}/']", $.id nav - # Gotta make it work in temporary boards. - $.addClass a, 'current' - Favicon.init() - - # Major features. - if Conf['Quick Reply'] - QR.init() - - if Conf['Image Expansion'] - ImageExpand.init() - - if Conf['Thread Watcher'] - setTimeout -> Watcher.init() - - if Conf['Keybinds'] - setTimeout -> Keybinds.init() - - if g.REPLY - if Conf['Thread Updater'] - setTimeout -> Updater.init() - - if Conf['Thread Stats'] - ThreadStats.init() - - if Conf['Reply Navigation'] - setTimeout -> Nav.init() - - if Conf['Post in Title'] - TitlePost.init() - - if Conf['Unread Count'] or Conf['Unread Favicon'] - Unread.init() - - else #not reply - if Conf['Thread Hiding'] - ThreadHiding.init() - - if Conf['Thread Expansion'] - setTimeout -> ExpandThread.init() - - if Conf['Comment Expansion'] - setTimeout -> ExpandComment.init() - - if Conf['Index Navigation'] - setTimeout -> Nav.init() - - board = $ '.board' - nodes = [] - for node in $$ '.postContainer', board - nodes.push Main.preParse node - Main.node nodes, true - - # Execute these scripts on inserted posts, not page init. - Main.hasCodeTags = !! $ 'script[src="//static.4chan.org/js/prettify/prettify.js"]' - - if MutationObserver = window.MutationObserver or window.WebKitMutationObserver or window.OMutationObserver - observer = new MutationObserver Main.observer - observer.observe board, - childList: true - subtree: true - else - $.on board, 'DOMNodeInserted', Main.listener - return - - flatten: (parent, obj) -> - if obj instanceof Array - Conf[parent] = obj[0] - else if typeof obj is 'object' - for key, val of obj - Main.flatten key, val - else # string or number - Conf[parent] = obj - return - - addStyle: -> - $.off d, 'DOMNodeInserted', Main.addStyle - if d.head - $.addStyle Main.css - else # XXX fox - $.on d, 'DOMNodeInserted', Main.addStyle - - message: (e) -> - {version} = e.data - if version and version isnt Main.version and confirm 'An updated version of 4chan X is available, would you like to install it now?' - window.location = "https://raw.github.com/mayhemydg/4chan-x/#{version}/4chan_x.user.js" - - preParse: (node) -> - parentClass = node.parentNode.className - el = $ '.post', node - post = - root: node - el: el - class: el.className - ID: el.id.match(/\d+$/)[0] - threadID: g.THREAD_ID or $.x('ancestor::div[parent::div[@class="board"]]', node).id.match(/\d+$/)[0] - isArchived: /\barchivedPost\b/.test parentClass - isInlined: /\binline\b/.test parentClass - isCrosspost: /\bcrosspost\b/.test parentClass - blockquote: el.lastElementChild - quotes: el.getElementsByClassName 'quotelink' - backlinks: el.getElementsByClassName 'backlink' - fileInfo: false - img: false - if img = $ 'img[data-md5]', el - # Make sure to not add deleted images, - # those do not have a data-md5 attribute. - post.fileInfo = img.parentNode.previousElementSibling - post.img = img - Main.prettify post.blockquote - post - node: (nodes, notify) -> - for callback in Main.callbacks - try - callback node for node in nodes - catch err - alert "4chan X (#{Main.version}) error: #{err.message}\nReport the bug at mayhemydg.github.com/4chan-x/#bug-report\n\nURL: #{window.location}\n#{err.stack}" if notify - return - observer: (mutations) -> - nodes = [] - for mutation in mutations - for addedNode in mutation.addedNodes - if /\bpostContainer\b/.test addedNode.className - nodes.push Main.preParse addedNode - Main.node nodes if nodes.length - listener: (e) -> - {target} = e - if /\bpostContainer\b/.test target.className - Main.node [Main.preParse target] - - prettify: (bq) -> - return unless Main.hasCodeTags - code = -> - for pre in document.getElementById('_id_').getElementsByClassName 'prettyprint' - pre.innerHTML = prettyPrintOne pre.innerHTML.replace /\s/g, ' ' - return - $.globalEval "(#{code})()".replace '_id_', bq.id - - namespace: '4chan_x.' - version: '2.34.3' - callbacks: [] - css: ' -/* dialog styling */ -.dialog.reply { - display: block; - border: 1px solid rgba(0,0,0,.25); - padding: 0; -} -.move { - cursor: move; -} -label, .favicon { - cursor: pointer; -} -a[href="javascript:;"] { - text-decoration: none; -} -.warning { - color: red; -} - -.hide_thread_button:not(.hidden_thread) { - float: left; -} - -.thread > .hidden_thread ~ *, -[hidden], -#content > [name=tab]:not(:checked) + div, -#updater:not(:hover) > :not(.move), -.autohide:not(:hover) > form, -#qp input, .forwarded { - display: none !important; -} - -.menu_button { - display: inline-block; -} -.menu_button > span { - border-top: .5em solid; - border-right: .3em solid transparent; - border-left: .3em solid transparent; - display: inline-block; - margin: 2px; - vertical-align: middle; -} -#menu { - position: absolute; - outline: none; -} -.entry { - border-bottom: 1px solid rgba(0, 0, 0, .25); - cursor: pointer; - display: block; - outline: none; - padding: 3px 7px; - position: relative; - text-decoration: none; - white-space: nowrap; -} -.entry:last-child { - border: none; -} -.focused.entry { - background: rgba(255, 255, 255, .33); -} -.entry.hasSubMenu { - padding-right: 1.5em; -} -.hasSubMenu::after { - content: ""; - border-left: .5em solid; - border-top: .3em solid transparent; - border-bottom: .3em solid transparent; - display: inline-block; - margin: .3em; - position: absolute; - right: 3px; -} -.hasSubMenu:not(.focused) > .subMenu { - display: none; -} -.subMenu { - position: absolute; - left: 100%; - top: 0; - margin-top: -1px; -} - -h1 { - text-align: center; -} -#qr > .move { - min-width: 300px; - overflow: hidden; - box-sizing: border-box; - -moz-box-sizing: border-box; - padding: 0 2px; -} -#qr > .move > span { - float: right; -} -#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning { - cursor: pointer; -} -#qr select, -#qr > form { - margin: 0; -} -#dump { - background: -webkit-linear-gradient(#EEE, #CCC); - background: -moz-linear-gradient(#EEE, #CCC); - background: -o-linear-gradient(#EEE, #CCC); - background: linear-gradient(#EEE, #CCC); - width: 10%; - padding: -moz-calc(1px) 0 2px; -} -#dump:hover, #dump:focus { - background: -webkit-linear-gradient(#FFF, #DDD); - background: -moz-linear-gradient(#FFF, #DDD); - background: -o-linear-gradient(#FFF, #DDD); - background: linear-gradient(#FFF, #DDD); -} -#dump:active, .dump #dump:not(:hover):not(:focus) { - background: -webkit-linear-gradient(#CCC, #DDD); - background: -moz-linear-gradient(#CCC, #DDD); - background: -o-linear-gradient(#CCC, #DDD); - background: linear-gradient(#CCC, #DDD); -} -#qr:not(.dump) #replies, .dump > form > label { - display: none; -} -#replies { - display: block; - height: 100px; - position: relative; - -webkit-user-select: none; - -moz-user-select: none; - -o-user-select: none; - user-select: none; -} -#replies > div { - counter-reset: thumbnails; - top: 0; right: 0; bottom: 0; left: 0; - margin: 0; padding: 0; - overflow: hidden; - position: absolute; - white-space: pre; -} -#replies > div:hover { - bottom: -10px; - overflow-x: auto; - z-index: 1; -} -.thumbnail { - background-color: rgba(0,0,0,.2) !important; - background-position: 50% 20% !important; - background-size: cover !important; - border: 1px solid #666; - box-sizing: border-box; - -moz-box-sizing: border-box; - cursor: move; - display: inline-block; - height: 90px; width: 90px; - margin: 5px; padding: 2px; - opacity: .5; - outline: none; - overflow: hidden; - position: relative; - text-shadow: 0 1px 1px #000; - -webkit-transition: opacity .25s ease-in-out; - -moz-transition: opacity .25s ease-in-out; - -o-transition: opacity .25s ease-in-out; - transition: opacity .25s ease-in-out; - vertical-align: top; -} -.thumbnail:hover, .thumbnail:focus { - opacity: .9; -} -.thumbnail#selected { - opacity: 1; -} -.thumbnail::before { - counter-increment: thumbnails; - content: counter(thumbnails); - color: #FFF; - font-weight: 700; - padding: 3px; - position: absolute; - top: 0; - right: 0; - text-shadow: 0 0 3px #000, 0 0 8px #000; -} -.thumbnail.drag { - box-shadow: 0 0 10px rgba(0,0,0,.5); -} -.thumbnail.over { - border-color: #FFF; -} -.thumbnail > span { - color: #FFF; -} -.remove { - background: none; - color: #E00; - font-weight: 700; - padding: 3px; -} -.remove:hover::after { - content: " Remove"; -} -.thumbnail > label { - background: rgba(0,0,0,.5); - color: #FFF; - right: 0; bottom: 0; left: 0; - position: absolute; - text-align: center; -} -.thumbnail > label > input { - margin: 0; -} -#addReply { - color: #333; - font-size: 3.5em; - line-height: 100px; -} -#addReply:hover, #addReply:focus { - color: #000; -} -.field { - border: 1px solid #CCC; - box-sizing: border-box; - -moz-box-sizing: border-box; - color: #333; - font: 13px sans-serif; - margin: 0; - padding: 2px 4px 3px; - -webkit-transition: color .25s, border .25s; - -moz-transition: color .25s, border .25s; - -o-transition: color .25s, border .25s; - transition: color .25s, border .25s; -} -.field:-moz-placeholder, -.field:hover:-moz-placeholder { - color: #AAA; -} -.field:hover, .field:focus { - border-color: #999; - color: #000; - outline: none; -} -#qr > form > div:first-child > .field:not(#dump) { - width: 30%; -} -#qr textarea.field { - display: -webkit-box; - min-height: 120px; - min-width: 100%; -} -.textarea { - position: relative; -} -#charCount { - color: #000; - background: hsla(0, 0%, 100%, .5); - position: absolute; - top: 100%; - right: 0; -} -#charCount.warning { - color: red; -} -.captchainput > .field { - min-width: 100%; -} -.captchaimg { - background: #FFF; - outline: 1px solid #CCC; - outline-offset: -1px; - text-align: center; -} -.captchaimg > img { - display: block; - height: 57px; - width: 300px; -} -#qr [type=file] { - margin: 1px 0; - width: 70%; -} -#qr [type=submit] { - margin: 1px 0; - padding: 1px; /* not Gecko */ - padding: 0 -moz-calc(1px); /* Gecko does not respect box-sizing: border-box */ - width: 30%; -} - -.fileText:hover .fntrunc, -.fileText:not(:hover) .fnfull { - display: none; -} -.fitwidth img[data-md5] + img { - max-width: 100%; -} -.gecko .fitwidth img[data-md5] + img, -.presto .fitwidth img[data-md5] + img { - width: 100%; -} - -#qr, #qp, #updater, #stats, #ihover, #overlay, #navlinks { - position: fixed; -} - -#ihover { - max-height: 97%; - max-width: 75%; - padding-bottom: 18px; -} - -#navlinks { - font-size: 16px; - top: 25px; - right: 5px; -} - -body { - box-sizing: border-box; - -moz-box-sizing: border-box; -} -body.unscroll { - overflow: hidden; -} -#overlay { - top: 0; - left: 0; - width: 100%; - height: 100%; - text-align: center; - background: rgba(0,0,0,.5); - z-index: 1; -} -#overlay::after { - content: ""; - display: inline-block; - height: 100%; - vertical-align: middle; -} -#options { - box-sizing: border-box; - -moz-box-sizing: border-box; - display: inline-block; - padding: 5px; - position: relative; - text-align: left; - vertical-align: middle; - width: 600px; - max-width: 100%; - height: 500px; - max-height: 100%; -} -#credits { - float: right; -} -#options ul { - padding: 0; -} -#options article li { - margin: 10px 0 10px 2em; -} -#options code { - background: hsla(0, 0%, 100%, .5); - color: #000; - padding: 0 1px; -} -#options label { - text-decoration: underline; -} -#content { - overflow: auto; - position: absolute; - top: 2.5em; - right: 5px; - bottom: 5px; - left: 5px; -} -#content textarea { - font-family: monospace; - min-height: 350px; - resize: vertical; - width: 100%; -} - -#updater { - text-align: right; -} -#updater:not(:hover) { - border: none; - background: transparent; -} -#updater input[type=number] { - width: 4em; -} -.new { - background: lime; -} - -#watcher { - padding-bottom: 5px; - position: absolute; - overflow: hidden; - white-space: nowrap; -} -#watcher:not(:hover) { - max-height: 220px; -} -#watcher > div { - max-width: 200px; - overflow: hidden; - padding-left: 5px; - padding-right: 5px; - text-overflow: ellipsis; -} -#watcher > .move { - padding-top: 5px; - text-decoration: underline; -} - -#qp { - padding: 2px 2px 5px; -} -#qp .post { - border: none; - margin: 0; - padding: 0; -} -#qp img { - max-height: 300px; - max-width: 500px; -} -.qphl { - outline: 2px solid rgba(216, 94, 49, .7); -} -.inlined { - opacity: .5; -} -.inline { - background-color: rgba(255, 255, 255, 0.15); - border: 1px solid rgba(128, 128, 128, 0.5); - display: table; - margin: 2px; - padding: 2px; -} -.inline .post { - background: none; - border: none; - margin: 0; - padding: 0; -} -div.opContainer { - display: block !important; -} -.opContainer.filter_highlight { - box-shadow: inset 5px 0 rgba(255,0,0,0.5); -} -.filter_highlight > .reply { - box-shadow: -5px 0 rgba(255,0,0,0.5); -} -.filtered { - text-decoration: underline line-through; -} -.quotelink.forwardlink, -.backlink.forwardlink { - text-decoration: none; - border-bottom: 1px dashed; -} -' - -Main.init()