// ==UserScript== // @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 * * Copyright (c) 2009-2011 James Campos * Copyright (c) 2012 Nicolas Stepien * http://mayhemydg.github.com/4chan-x/ * 4chan X 3.0.0 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * HACKING * * 4chan X is written in CoffeeScript[1], and developed on GitHub[2]. * * [1]: http://coffeescript.org/ * [2]: https://github.com/MayhemYDG/4chan-x * * CONTRIBUTORS * * noface - unique ID fixes * desuwa - Firefox filename upload fix * seaweed - bottom padding for image hover * e000 - cooldown sanity check * ahodesuka - scroll back when unexpanding images, file info formatting * Shou- - pentadactyl fixes * ferongr - new favicons * xat- - new favicons * Zixaphir - fix qr textarea - captcha-image gap * Ongpot - sfw favicon * thisisanon - nsfw + 404 favicons * Anonymous - empty favicon * Seiba - chrome quick reply focusing * herpaderpderp - recaptcha fixes * WakiMiko - recaptcha tab order http://userscripts.org/scripts/show/82657 * btmcsweeney - allow users to specify text for sauce links * * All the people who've taken the time to write bug reports. * * Thank you. */ (function() { var $, $$, Conf, Config, UI, d, g, _base; Config = { main: { Enhancing: { '404 Redirect': [true, 'Redirect dead threads and images'], 'Keybinds': [true, 'Binds actions to keys'], 'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time'], 'File Info Formatting': [true, 'Reformats the file information'], 'Comment Expansion': [true, 'Expand too long comments'], 'Thread Expansion': [true, 'View all replies'], 'Index Navigation': [true, 'Navigate to previous / next thread'], 'Reply Navigation': [false, 'Navigate to top / bottom of thread'], 'Check for Updates': [true, 'Check for updated versions of 4chan X'] }, Filtering: { 'Anonymize': [false, 'Make everybody anonymous'], 'Filter': [true, 'Self-moderation placebo'], 'Recursive Filtering': [true, 'Filter replies of filtered posts, recursively'], 'Reply Hiding': [true, 'Hide single replies'], 'Thread Hiding': [true, 'Hide entire threads'], 'Show Stubs': [true, 'Of hidden threads / replies'] }, Imaging: { 'Image Auto-Gif': [false, 'Animate gif thumbnails'], 'Image Expansion': [true, 'Expand images'], 'Image Hover': [false, 'Show full image on mouseover'], 'Sauce': [true, 'Add sauce to images'], 'Reveal Spoilers': [false, 'Replace spoiler thumbnails by the original thumbnail'], 'Expand From Current': [false, 'Expand images from current position to thread end.'] }, Menu: { 'Menu': [true, 'Add a drop-down menu in posts.'], 'Report Link': [true, 'Add a report link to the menu.'], 'Delete Link': [true, 'Add post and image deletion links to the menu.'], 'Download Link': [true, 'Add a download with original filename link to the menu. Chrome-only currently.'], 'Archive Link': [true, 'Add an archive link to the menu.'] }, Monitoring: { 'Thread Updater': [true, 'Update threads. Has more options in its own dialog.'], 'Unread Count': [true, 'Show unread post count in tab title'], 'Unread Favicon': [true, 'Show a different favicon when there are unread posts'], 'Post in Title': [true, 'Show the op\'s post in the tab title'], 'Thread Stats': [true, 'Display reply and image count'], 'Thread Watcher': [true, 'Bookmark threads'], 'Auto Watch': [true, 'Automatically watch threads that you start'], 'Auto Watch Reply': [false, 'Automatically watch threads that you reply to'] }, Posting: { 'Quick Reply': [true, 'Reply without leaving the page.'], 'Cooldown': [true, 'Prevent "flood detected" errors.'], 'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'], 'Auto Hide QR': [true, 'Automatically hide the quick reply when posting.'], 'Open Reply in New Tab': [false, 'Open replies in a new tab that are made from the main board.'], 'Remember QR size': [false, 'Remember the size of the Quick reply (Firefox only).'], 'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.'], 'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'], 'Hide Original Post Form': [true, 'Replace the normal post form with a shortcut to open the QR.'] }, Quoting: { 'Quote Backlinks': [true, 'Add quote backlinks'], 'OP Backlinks': [false, 'Add backlinks to the OP'], 'Quote Highlighting': [true, 'Highlight the previewed post'], 'Quote Inline': [true, 'Show quoted post inline on quote click'], 'Quote Preview': [true, 'Show quote content on hover'], 'Resurrect Quotes': [true, 'Linkify dead quotes to archives'], 'Indicate OP quote': [true, 'Add \'(OP)\' to OP quotes'], 'Indicate Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes'], 'Forward Hiding': [true, 'Hide original posts of inlined backlinks'] } }, filter: { name: ['# Filter any namefags:', '#/^(?!Anonymous$)/'].join('\n'), uniqueid: ['# Filter a specific ID:', '#/Txhvk1Tl/'].join('\n'), tripcode: ['# Filter any tripfags', '#/^!/'].join('\n'), mod: ['# Set a custom class for mods:', '#/Mod$/;highlight:mod;op:yes', '# Set a custom class for moot:', '#/Admin$/;highlight:moot;op:yes'].join('\n'), email: ['# Filter any e-mails that are not `sage` on /a/ and /jp/:', '#/^(?!sage$)/;boards:a,jp'].join('\n'), subject: ['# Filter Generals on /v/:', '#/general/i;boards:v;op:only'].join('\n'), comment: ['# Filter Stallman copypasta on /g/:', '#/what you\'re refer+ing to as linux/i;boards:g'].join('\n'), country: [''].join('\n'), filename: [''].join('\n'), dimensions: ['# Highlight potential wallpapers:', '#/1920x1080/;op:yes;highlight;top:no;boards:w,wg'].join('\n'), filesize: [''].join('\n'), md5: [''].join('\n') }, sauces: ['http://iqdb.org/?url=$1', 'http://www.google.com/searchbyimage?image_url=$1', '#http://tineye.com/search?url=$1', '#http://saucenao.com/search.php?db=999&url=$1', '#http://3d.iqdb.org/?url=$1', '#http://regex.info/exif.cgi?imgurl=$2', '# uploaders:', '#http://imgur.com/upload?url=$2;text:Upload to imgur', '#http://omploader.org/upload?url1=$2;text:Upload to omploader', '# "View Same" in archives:', '#http://archive.foolz.us/search/image/$3/;text:View same on foolz', '#http://archive.foolz.us/$4/search/image/$3/;text:View same on foolz /$4/', '#https://archive.installgentoo.net/$4/image/$3;text:View same on installgentoo /$4/'].join('\n'), time: '%m/%d/%y(%a)%H:%M', backlink: '>>%id', fileInfo: '%l (%p%s, %r)', favicon: 'ferongr', hotkeys: { openQR: ['i', 'Open QR with post number inserted'], openEmptyQR: ['I', 'Open QR without post number inserted'], openOptions: ['ctrl+o', 'Open Options'], close: ['Esc', 'Close Options or QR'], spoiler: ['ctrl+s', 'Quick spoiler tags'], code: ['alt+c', 'Quick code tags'], submit: ['alt+s', 'Submit post'], watch: ['w', 'Watch thread'], update: ['u', 'Update now'], unreadCountTo0: ['z', 'Reset unread status'], expandImage: ['m', 'Expand selected image'], expandAllImages: ['M', 'Expand all images'], zero: ['0', 'Jump to page 0'], nextPage: ['L', 'Jump to the next page'], previousPage: ['H', 'Jump to the previous page'], nextThread: ['n', 'See next thread'], previousThread: ['p', 'See previous thread'], expandThread: ['e', 'Expand thread'], openThreadTab: ['o', 'Open thread in current tab'], openThread: ['O', 'Open thread in new tab'], nextReply: ['J', 'Select next reply'], previousReply: ['K', 'Select previous reply'], hide: ['x', 'Hide thread'] }, updater: { checkbox: { 'Scrolling': [false, 'Scroll updated posts into view. Only enabled at bottom of page.'], 'Scroll BG': [false, 'Scroll background tabs'], 'Verbose': [true, 'Show countdown timer, new post count'], 'Auto Update': [true, 'Automatically fetch new posts'] }, 'Interval': 30 } }; if (!/^(boards|images|sys)\.4chan\.org$/.test(location.hostname)) { return; } Conf = {}; d = document; g = {}; UI = { dialog: function(id, position, html) { var el; el = d.createElement('div'); el.className = 'reply dialog'; el.innerHTML = html; el.id = id; el.style.cssText = localStorage.getItem("" + Main.namespace + id + ".position") || position; el.querySelector('.move').addEventListener('mousedown', UI.dragstart, false); return el; }, dragstart: function(e) { var el, rect; e.preventDefault(); UI.el = el = this.parentNode; d.addEventListener('mousemove', UI.drag, false); d.addEventListener('mouseup', UI.dragend, false); rect = el.getBoundingClientRect(); UI.dx = e.clientX - rect.left; UI.dy = e.clientY - rect.top; UI.width = d.documentElement.clientWidth - rect.width; return UI.height = d.documentElement.clientHeight - rect.height; }, drag: function(e) { var left, style, top; left = e.clientX - UI.dx; top = e.clientY - UI.dy; left = left < 10 ? '0px' : UI.width - left < 10 ? null : left + 'px'; top = top < 10 ? '0px' : UI.height - top < 10 ? null : top + 'px'; style = UI.el.style; style.left = left; style.top = top; style.right = left === null ? '0px' : null; return style.bottom = top === null ? '0px' : null; }, dragend: function() { localStorage.setItem("" + Main.namespace + UI.el.id + ".position", UI.el.style.cssText); d.removeEventListener('mousemove', UI.drag, false); d.removeEventListener('mouseup', UI.dragend, false); return delete UI.el; }, hover: function(e) { var clientHeight, clientWidth, clientX, clientY, height, style, top, _ref; clientX = e.clientX, clientY = e.clientY; style = UI.el.style; _ref = d.documentElement, clientHeight = _ref.clientHeight, clientWidth = _ref.clientWidth; height = UI.el.offsetHeight; top = clientY - 120; style.top = clientHeight <= height || top <= 0 ? '0px' : top + height >= clientHeight ? clientHeight - height + 'px' : top + 'px'; if (clientX <= clientWidth - 400) { style.left = clientX + 45 + 'px'; return style.right = null; } else { style.left = null; return style.right = clientWidth - clientX + 45 + 'px'; } }, hoverend: function() { $.rm(UI.el); return delete UI.el; } }; /* loosely follows the jquery api: http://api.jquery.com/ not chainable */ $ = function(selector, root) { if (root == null) { root = d.body; } return root.querySelector(selector); }; $.extend = function(object, properties) { var key, val; for (key in properties) { val = properties[key]; object[key] = val; } }; $.extend($, { SECOND: 1000, MINUTE: 1000 * 60, HOUR: 1000 * 60 * 60, DAY: 1000 * 60 * 60 * 24, log: typeof (_base = console.log).bind === "function" ? _base.bind(console) : void 0, engine: /WebKit|Presto|Gecko/.exec(navigator.userAgent)[0].toLowerCase(), ready: function(fc) { var cb; if (/interactive|complete/.test(d.readyState)) { return setTimeout(fc); } cb = function() { $.off(d, 'DOMContentLoaded', cb); return fc(); }; return $.on(d, 'DOMContentLoaded', cb); }, sync: function(key, cb) { return $.on(window, 'storage', function(e) { if (e.key === ("" + Main.namespace + key)) { return cb(JSON.parse(e.newValue)); } }); }, id: function(id) { return d.getElementById(id); }, formData: function(arg) { var fd, key, val; if (arg instanceof HTMLFormElement) { fd = new FormData(arg); } else { fd = new FormData(); for (key in arg) { val = arg[key]; if (val) { fd.append(key, val); } } } return fd; }, ajax: function(url, callbacks, opts) { var form, headers, key, r, type, upCallbacks, val; if (opts == null) { opts = {}; } type = opts.type, headers = opts.headers, upCallbacks = opts.upCallbacks, form = opts.form; r = new XMLHttpRequest(); type || (type = form && 'post' || 'get'); r.open(type, url, true); for (key in headers) { val = headers[key]; r.setRequestHeader(key, val); } $.extend(r, callbacks); $.extend(r.upload, upCallbacks); r.send(form); return r; }, cache: function(url, cb) { var req; if (req = $.cache.requests[url]) { if (req.readyState === 4) { return cb.call(req); } else { return req.callbacks.push(cb); } } else { req = $.ajax(url, { onload: function() { var _i, _len, _ref, _results; _ref = this.callbacks; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { cb = _ref[_i]; _results.push(cb.call(this)); } return _results; }, onabort: function() { return delete $.cache.requests[url]; }, onerror: function() { return delete $.cache.requests[url]; } }); req.callbacks = [cb]; return $.cache.requests[url] = req; } }, cb: { checked: function() { $.set(this.name, this.checked); return Conf[this.name] = this.checked; }, value: function() { $.set(this.name, this.value.trim()); return Conf[this.name] = this.value; } }, addStyle: function(css) { var style; style = $.el('style', { textContent: css }); $.add(d.head, style); return style; }, x: function(path, root) { if (root == null) { root = d.body; } return d.evaluate(path, root, null, 8, null).singleNodeValue; }, addClass: function(el, className) { return el.classList.add(className); }, rmClass: function(el, className) { return el.classList.remove(className); }, rm: function(el) { return el.parentNode.removeChild(el); }, tn: function(s) { return d.createTextNode(s); }, nodes: function(nodes) { var frag, node, _i, _len; if (!(nodes instanceof Array)) { return nodes; } frag = d.createDocumentFragment(); for (_i = 0, _len = nodes.length; _i < _len; _i++) { node = nodes[_i]; frag.appendChild(node); } return frag; }, add: function(parent, children) { return parent.appendChild($.nodes(children)); }, prepend: function(parent, children) { return parent.insertBefore($.nodes(children), parent.firstChild); }, after: function(root, el) { return root.parentNode.insertBefore($.nodes(el), root.nextSibling); }, before: function(root, el) { return root.parentNode.insertBefore($.nodes(el), root); }, replace: function(root, el) { return root.parentNode.replaceChild($.nodes(el), root); }, el: function(tag, properties) { var el; el = d.createElement(tag); if (properties) { $.extend(el, properties); } return el; }, on: function(el, events, handler) { var event, _i, _len, _ref; _ref = events.split(' '); for (_i = 0, _len = _ref.length; _i < _len; _i++) { event = _ref[_i]; el.addEventListener(event, handler, false); } }, off: function(el, events, handler) { var event, _i, _len, _ref; _ref = events.split(' '); for (_i = 0, _len = _ref.length; _i < _len; _i++) { event = _ref[_i]; el.removeEventListener(event, handler, false); } }, open: function(url) { return (GM_openInTab || window.open)(location.protocol + url, '_blank'); }, event: function(el, e) { return el.dispatchEvent(e); }, globalEval: function(code) { var script; script = $.el('script', { textContent: code }); $.add(d.head, script); return $.rm(script); }, bytesToString: function(size) { var unit; unit = 0; while (size >= 1024) { size /= 1024; unit++; } size = unit > 1 ? Math.round(size * 100) / 100 : Math.round(size); return "" + size + " " + ['B', 'KB', 'MB', 'GB'][unit]; } }); $.cache.requests = {}; $.extend($, typeof GM_deleteValue !== "undefined" && GM_deleteValue !== null ? { "delete": function(name) { name = Main.namespace + name; return GM_deleteValue(name); }, get: function(name, defaultValue) { var value; name = Main.namespace + name; if (value = GM_getValue(name)) { return JSON.parse(value); } else { return defaultValue; } }, set: function(name, value) { name = Main.namespace + name; localStorage.setItem(name, JSON.stringify(value)); return GM_setValue(name, JSON.stringify(value)); } } : { "delete": function(name) { return localStorage.removeItem(Main.namespace + name); }, get: function(name, defaultValue) { var value; if (value = localStorage.getItem(Main.namespace + name)) { return JSON.parse(value); } else { return defaultValue; } }, set: function(name, value) { return localStorage.setItem(Main.namespace + name, JSON.stringify(value)); } }); $$ = function(selector, root) { if (root == null) { root = d.body; } return Array.prototype.slice.call(root.querySelectorAll(selector)); }; }).call(this);