\
// ==UserScript== // @name 4chan X // @version 3.20.15 // @minGMVer 1.14 // @minFFVer 26 // @namespace 4chan-X // @description Cross-browser extension for productive lurking on 4chan. // @license MIT; https://github.com/MayhemYDG/4chan-x/blob/v3/LICENSE // @match *://boards.4chan.org/* // @match *://sys.4chan.org/* // @match *://a.4cdn.org/* // @match *://i.4cdn.org/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_openInTab // @run-at document-start // @updateURL https://4chan-x.just-believe.in/builds/4chan-X.meta.js // @downloadURL https://4chan-x.just-believe.in/builds/4chan-X.user.js // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII= // ==/UserScript== /* 4chan X - Version 3.20.15 - 2014-10-27 * https://4chan-x.just-believe.in/ * * Copyrights and License: https://github.com/MayhemYDG/4chan-x/blob/v3/LICENSE * * Contributors: * https://github.com/MayhemYDG/4chan-x/graphs/contributors * Non-GitHub contributors: * ferongr, xat-, Ongpot, thisisanon and Anonymous - favicon contributions * e000 - cooldown sanity check * Seiba - chrome quick reply focusing * herpaderpderp - recaptcha fixes * WakiMiko - recaptcha tab order http://userscripts.org/scripts/show/82657 * * All the people who've taken the time to write bug reports and provide feedback. * * Thank you. */ 'use strict'; (function() { var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, CatalogThread, Clone, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, ExpandThread, Favicon, FileInfo, Filter, Fourchan, Get, Header, IDColor, ImageExpand, ImageHover, Index, Keybinds, Labels, Linkify, Main, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, Quotify, Recursive, Redirect, RelativeDates, Report, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, g, __slice = [].slice, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; Config = { main: { 'Miscellaneous': { 'Desktop Notifications': [true, 'Enables desktop notifications across various 4chan X features.'], '404 Redirect': [true, 'Redirect dead threads and images.'], 'Keybinds': [true, 'Bind actions to keyboard shortcuts.'], 'Linkify': [true, 'Convert text links into hyperlinks.'], 'Time Formatting': [true, 'Localize and format timestamps.'], 'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.'], 'File Info Formatting': [true, 'Reformat the file information.'], 'Thread Expansion': [true, 'Add buttons to expand threads.'], 'Index Navigation': [false, 'Add buttons to navigate between threads.'], 'Reply Navigation': [false, 'Add buttons to navigate to top / bottom of thread.'], 'Show Dice Roll': [true, 'Show dice that were entered into the email field.'] }, 'Filtering': { 'Anonymize': [false, 'Make everyone Anonymous.'], 'Filter': [true, 'Self-moderation placebo.'], 'Post Hiding': [true, 'Add buttons to hide threads and replies.'], 'Stubs': [true, 'Show stubs of hidden posts.'], 'Recursive Hiding': [true, 'Hide replies of hidden posts, recursively.'] }, 'Images': { 'Auto-GIF': [false, 'Animate GIF thumbnails (disabled on /gif/, /wsg/).'], 'Image Expansion': [true, 'Expand images inline.'], 'Image Hover': [false, 'Show a floating expanded image on hover.'], 'Image Hover in Catalog': [false, 'Show a floating expanded image on hover in the catalog.'], 'Sauce': [true, 'Add sauce links to images.'], 'Reveal Spoilers': [false, 'Reveal spoiler thumbnails.'] }, 'Menu': { 'Menu': [true, 'Add a drop-down menu to posts.'], 'Report Link': [true, 'Add a report link to the menu.'], 'Post Hiding Link': [true, 'Add a link to hide threads and replies.'], 'Delete Link': [true, 'Add post and image deletion links to the menu.'], 'Archive Link': [true, 'Add an archive link to the menu.'] }, 'Monitoring': { 'Thread Updater': [true, 'Fetch and insert new replies. Has more options in its own dialog.'], 'Unread Count': [true, 'Show the unread posts count in the tab title.'], 'Hide Unread Count at (0)': [false, 'Hide the unread posts count when it reaches 0.'], 'Unread Tab Icon': [true, 'Show a different favicon when there are unread posts.'], 'Unread Line': [true, 'Show a line to distinguish read posts from unread ones.'], 'Scroll to Last Read Post': [true, 'Scroll back to the last read post when reopening a thread.'], 'Thread Excerpt': [true, 'Show an excerpt of the thread in the tab title.'], 'Thread Stats': [true, 'Display reply, image, and page count.'], 'Thread Watcher': [true, 'Bookmark threads.'], 'Color User IDs': [true, 'Assign unique colors to user IDs on boards that use them.'] }, 'Posting': { 'Quick Reply': [true, 'All-in-one form to reply, create threads, automate dumping and more.'], 'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'], 'Auto-Hide QR': [false, 'Automatically hide the quick reply when posting.'], 'Open Post in New Tab': [true, 'Open new threads or replies to a thread from the index in a new tab.'], 'Remember QR Size': [false, 'Remember the size of the Quick reply.'], 'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'], 'Hide Original Post Form': [true, 'Hide the normal post form.'], 'Cooldown': [true, 'Indicate the remaining time before posting again.'] }, 'Quote Links': { 'Quote Backlinks': [true, 'Add quote backlinks.'], 'OP Backlinks': [true, 'Add backlinks to the OP.'], 'Quote Inlining': [true, 'Inline quoted post on click.'], 'Forward Hiding': [true, 'Hide original posts of inlined backlinks.'], 'Quote Previewing': [true, 'Show quoted post on hover.'], 'Quote Highlighting': [true, 'Highlight the previewed post.'], 'Resurrect Quotes': [true, 'Link dead quotes to the archives.'], 'Quote Markers': [true, 'Add "(You)", "(OP)", "(Cross-thread)", "(Dead)" markers to quote links.'] } }, imageExpansion: { 'Fit width': [true, ''], 'Fit height': [false, ''], 'Expand spoilers': [false, 'Expand all images along with spoilers.'], 'Expand from here': [true, 'Expand all images only from current position to thread end.'] }, threadWatcher: { 'Current Board': [false, 'Only show watched threads from the current board.'], 'Auto Watch': [true, 'Automatically watch threads you start.'], 'Auto Watch Reply': [false, 'Automatically watch threads you reply to.'], 'Auto Prune': [false, 'Automatically prune 404\'d threads.'] }, filter: { name: "# Filter any namefags:\n#/^(?!Anonymous$)/", uniqueID: "# Filter a specific ID:\n#/Txhvk1Tl/", tripcode: "# Filter any tripfag\n#/^!/", capcode: "# Set a custom class for mods:\n#/Mod$/;highlight:mod;op:yes\n# Set a custom class for moot:\n#/Admin$/;highlight:moot;op:yes", email: "", subject: "# Filter Generals on /v/:\n#/general/i;boards:v;op:only", comment: "# Filter Stallman copypasta on /g/:\n#/what you're refer+ing to as linux/i;boards:g", flag: "", filename: "", dimensions: "# Highlight potential wallpapers:\n#/1920x1080/;op:yes;highlight;top:no;boards:w,wg", filesize: "", MD5: "" }, sauces: "https://www.google.com/searchbyimage?image_url=%TURL\nhttp://iqdb.org/?url=%TURL\n#//tineye.com/search?url=%TURL\n#http://saucenao.com/search.php?url=%TURL\n#http://3d.iqdb.org/?url=%TURL\n#http://regex.info/exif.cgi?imgurl=%URL\n# uploaders:\n#http://imgur.com/upload?url=%URL;text:Upload to imgur\n#http://ompldr.org/upload?url1=%URL;text:Upload to ompldr\n# \"View Same\" in archives:\n#//archive.foolz.us/_/search/image/%MD5/;text:View same on foolz\n#//archive.foolz.us/%board/search/image/%MD5/;text:View same on foolz /%board/\n#//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/", 'Custom CSS': false, Index: { 'Index Mode': 'paged', 'Previous Index Mode': 'paged', 'Index Sort': 'bump', 'Index Size': 'small', 'Threads per Page': 0, 'Open threads in a new tab': false, 'Show Replies': true, 'Refreshed Navigation': false }, Header: { 'Header auto-hide': false, 'Header auto-hide on scroll': false, 'Bottom header': false, 'Top Board List': false, 'Bottom Board List': false, 'Custom Board Navigation': true }, QR: { 'QR.personas': "#email:\"sage\";boards:jp;always\nemail:\"sage\"" }, boardnav: '[current-title / toggle-all]', time: '%m/%d/%y(%a)%H:%M:%S', backlink: '>>%id', fileInfo: '%l (%p%s, %r)', favicon: 'ferongr', usercss: '', hotkeys: { 'Toggle board list': ['Ctrl+b', 'Toggle the full board list.'], 'Open empty QR': ['q', 'Open QR without post number inserted.'], 'Open QR': ['Shift+q', 'Open QR with post number inserted.'], 'Open settings': ['Alt+o', 'Open Settings.'], 'Close': ['Esc', 'Close Settings, Notifications or QR.'], 'Spoiler tags': ['Ctrl+s', 'Insert spoiler tags.'], 'Code tags': ['Alt+c', 'Insert code tags.'], 'Eqn tags': ['Alt+e', 'Insert eqn tags.'], 'Math tags': ['Alt+m', 'Insert math tags.'], 'Submit QR': ['Alt+s', 'Submit post.'], 'Update': ['r', 'Refresh the index/thread.'], 'Watch': ['w', 'Watch thread.'], 'Expand image': ['Shift+e', 'Expand selected image.'], 'Expand images': ['e', 'Expand all images.'], 'Front page': ['0', 'Jump to page 0.'], 'Open front page': ['Shift+0', 'Open page 0 in a new tab.'], 'Next page': ['Right', 'Jump to the next page.'], 'Previous page': ['Left', 'Jump to the previous page.'], 'Search form': ['Ctrl+Alt+s', 'Focus the search field on the board index.'], 'Paged mode': ['Ctrl+1', 'Sets the index mode to paged.'], 'All pages mode': ['Ctrl+2', 'Sets the index mode to all threads.'], 'Catalog mode': ['Ctrl+3', 'Sets the index mode to catalog.'], 'Cycle sort type': ['Ctrl+x', 'Cycle through index sort types.'], 'Next thread': ['Down', 'See next thread.'], 'Previous thread': ['Up', 'See previous thread.'], 'Expand thread': ['Ctrl+e', 'Expand thread.'], 'Open thread': ['o', 'Open thread in current tab.'], 'Open thread tab': ['Shift+o', 'Open thread in new tab.'], 'Next reply': ['j', 'Select next reply.'], 'Previous reply': ['k', 'Select previous reply.'], 'Deselect reply': ['Shift+d', 'Deselect reply.'], 'Hide': ['x', 'Hide thread.'] }, updater: { checkbox: { 'Beep': [false, 'Beep on new post to completely read thread.'], 'Auto Scroll': [false, 'Scroll updated posts into view. Only enabled at bottom of page.'], 'Bottom Scroll': [false, 'Always scroll to the bottom, not the first new post. Useful for event threads.'], 'Scroll BG': [false, 'Auto-scroll background tabs.'], 'Auto Update': [true, 'Automatically fetch new posts.'] }, 'Interval': 30 } }; Conf = {}; c = console; d = document; doc = d.documentElement; g = { VERSION: '3.20.15', NAMESPACE: '4chan X.', boards: {}, threads: {}, posts: {} }; $ = function(selector, root) { if (root == null) { root = d.body; } return root.querySelector(selector); }; $$ = function(selector, root) { if (root == null) { root = d.body; } return __slice.call(root.querySelectorAll(selector)); }; $.SECOND = 1000; $.MINUTE = 1000 * 60; $.HOUR = 1000 * 60 * 60; $.DAY = 1000 * 60 * 60 * 24; $.id = function(id) { return d.getElementById(id); }; $.ready = function(fc) { var cb; if (d.readyState !== 'loading') { $.queueTask(fc); return; } cb = function() { $.off(d, 'DOMContentLoaded', cb); return fc(); }; return $.on(d, 'DOMContentLoaded', cb); }; $.formData = function(form) { var fd, key, val; if (form instanceof HTMLFormElement) { return new FormData(form); } fd = new FormData(); for (key in form) { val = form[key]; if (val) { if (typeof val === 'object' && 'newName' in val) { fd.append(key, val, val.newName); } else { fd.append(key, val); } } } return fd; }; $.extend = function(object, properties) { var key, val; for (key in properties) { val = properties[key]; object[key] = val; } }; $.ajax = (function() { var lastModified; lastModified = {}; return function(url, options, extra) { var form, r, sync, type, upCallbacks, whenModified; if (extra == null) { extra = {}; } type = extra.type, whenModified = extra.whenModified, upCallbacks = extra.upCallbacks, form = extra.form, sync = extra.sync; r = new XMLHttpRequest(); type || (type = form && 'post' || 'get'); r.open(type, url, !sync); if (whenModified) { if (url in lastModified) { r.setRequestHeader('If-Modified-Since', lastModified[url]); } $.on(r, 'load', function() { return lastModified[url] = r.getResponseHeader('Last-Modified'); }); } if (/\.json$/.test(url)) { r.responseType = 'json'; } $.extend(r, options); $.extend(r.upload, upCallbacks); r.send(form); return r; }; })(); $.cache = (function() { var reqs; reqs = {}; return function(url, cb, options) { var req, rm; if (req = reqs[url]) { if (req.readyState === 4) { cb.call(req, req.evt); } else { req.callbacks.push(cb); } return; } rm = function() { return delete reqs[url]; }; req = $.ajax(url, options); $.on(req, 'load', function(e) { var _i, _len, _ref; _ref = this.callbacks; for (_i = 0, _len = _ref.length; _i < _len; _i++) { cb = _ref[_i]; cb.call(this, e); } this.evt = e; return delete this.callbacks; }); $.on(req, 'abort', rm); $.on(req, 'error', rm); req.callbacks = [cb]; return reqs[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; } }; $.asap = function(test, cb) { if (test()) { return cb(); } else { return setTimeout($.asap, 25, test, cb); } }; $.addStyle = function(css) { var style; style = $.el('style', { textContent: css }); $.asap((function() { return d.head; }), function() { return $.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() { var className, el, _ref; el = arguments[0], className = 2 <= arguments.length ? __slice.call(arguments, 1) : []; return (_ref = el.classList).add.apply(_ref, className); }; $.rmClass = function() { var className, el, _ref; el = arguments[0], className = 2 <= arguments.length ? __slice.call(arguments, 1) : []; return (_ref = el.classList).remove.apply(_ref, className); }; $.hasClass = function(el, className) { return el.classList.contains(className); }; $.rm = function(el) { return el.remove(); }; $.rmAll = function(root) { return root.textContent = null; }; $.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, el) { return parent.appendChild($.nodes(el)); }; $.prepend = function(parent, el) { return parent.insertBefore($.nodes(el), 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); } }; $.event = function(event, detail, root) { if (root == null) { root = d; } if ((detail != null) && typeof cloneInto === 'function') { detail = cloneInto(detail, d.defaultView); } return root.dispatchEvent(new CustomEvent(event, { bubbles: true, detail: detail })); }; $.open = GM_openInTab; $.debounce = function(wait, fn) { var args, exec, lastCall, that, timeout; lastCall = 0; timeout = null; that = null; args = null; exec = function() { lastCall = Date.now(); return fn.apply(that, args); }; return function() { args = arguments; that = this; if (lastCall < Date.now() - wait) { return exec(); } clearTimeout(timeout); return timeout = setTimeout(exec, wait); }; }; $.queueTask = (function() { var execTask, taskChannel, taskQueue; taskQueue = []; execTask = function() { var args, func, task; task = taskQueue.shift(); func = task[0]; args = Array.prototype.slice.call(task, 1); return func.apply(func, args); }; if (window.MessageChannel) { taskChannel = new MessageChannel(); taskChannel.port1.onmessage = execTask; return function() { taskQueue.push(arguments); return taskChannel.port2.postMessage(null); }; } else { return function() { taskQueue.push(arguments); return setTimeout(execTask, 0); }; } })(); $.globalEval = function(code) { var script; script = $.el('script', { textContent: code }); $.add(d.head || doc, 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]; }; $.item = function(key, val) { var item; item = {}; item[key] = val; return item; }; $.syncing = {}; $.sync = (function() { $.on(window, 'storage', function(_arg) { var cb, key, newValue; key = _arg.key, newValue = _arg.newValue; if (cb = $.syncing[key]) { return cb(JSON.parse(newValue), key); } }); return function(key, cb) { return $.syncing[g.NAMESPACE + key] = cb; }; })(); $["delete"] = function(keys) { var key, _i, _len; if (!(keys instanceof Array)) { keys = [keys]; } for (_i = 0, _len = keys.length; _i < _len; _i++) { key = keys[_i]; key = g.NAMESPACE + key; localStorage.removeItem(key); GM_deleteValue(key); } }; $.get = function(key, val, cb) { var items; if (typeof cb === 'function') { items = $.item(key, val); } else { items = key; cb = val; } return $.queueTask(function() { for (key in items) { if (val = GM_getValue(g.NAMESPACE + key)) { items[key] = JSON.parse(val); } } return cb(items); }); }; $.set = (function() { var set; set = function(key, val) { key = g.NAMESPACE + key; val = JSON.stringify(val); if (key in $.syncing) { localStorage.setItem(key, val); } return GM_setValue(key, val); }; return function(keys, val) { var key; if (typeof keys === 'string') { set(keys, val); return; } for (key in keys) { val = keys[key]; set(key, val); } }; })(); $.clear = function(cb) { $["delete"](GM_listValues().map(function(key) { return key.replace(g.NAMESPACE, ''); })); return typeof cb === "function" ? cb() : void 0; }; Polyfill = { init: function() {}, toBlob: function() { var _base; return (_base = HTMLCanvasElement.prototype).toBlob || (_base.toBlob = function(cb) { var data, i, l, ui8a, _i; data = atob(this.toDataURL().slice(22)); l = data.length; ui8a = new Uint8Array(l); for (i = _i = 0; _i < l; i = _i += 1) { ui8a[i] = data.charCodeAt(i); } return cb(new Blob([ui8a], { type: 'image/png' })); }); } }; UI = (function() { var Menu, dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchend, touchmove; dialog = function(id, position, html) { var el; el = $.el('div', { className: 'dialog', innerHTML: html, id: id }); el.style.cssText = position; $.get("" + id + ".position", position, function(item) { return el.style.cssText = item["" + id + ".position"]; }); $.on($('.move', el), 'touchstart mousedown', dragstart); return el; }; Menu = (function() { var currentMenu, lastToggledButton; currentMenu = null; lastToggledButton = null; function Menu() { this.onFocus = __bind(this.onFocus, this); this.keybinds = __bind(this.keybinds, this); this.close = __bind(this.close, this); this.entries = []; } Menu.prototype.makeMenu = function() { var menu; menu = $.el('div', { className: 'dialog', id: 'menu', tabIndex: 0 }); $.on(menu, 'click', function(e) { return e.stopPropagation(); }); $.on(menu, 'keydown', this.keybinds); return menu; }; Menu.prototype.toggle = function(e, button, data) { var previousButton; e.preventDefault(); e.stopPropagation(); if (currentMenu) { previousButton = lastToggledButton; this.close(); if (previousButton === button) { return; } } if (!this.entries.length) { return; } return this.open(button, data); }; Menu.prototype.open = function(button, data) { var bLeft, bRect, bTop, cHeight, cWidth, entry, mRect, menu, prevEntry, _i, _len, _ref; menu = this.makeMenu(); currentMenu = menu; lastToggledButton = button; $.addClass(button, 'open'); _ref = this.entries; for (_i = 0, _len = _ref.length; _i < _len; _i++) { entry = _ref[_i]; this.insertEntry(entry, menu, data); } entry = $('.entry', menu); while (prevEntry = this.findNextEntry(entry, -1)) { entry = prevEntry; } this.focus(entry); $.on(d, 'click', this.close); $.on(d, 'CloseMenu', this.close); $.add(button, menu); mRect = menu.getBoundingClientRect(); bRect = button.getBoundingClientRect(); bTop = window.scrollY + bRect.top; bLeft = window.scrollX + bRect.left; cHeight = doc.clientHeight; cWidth = doc.clientWidth; if (bRect.top + bRect.height + mRect.height < cHeight) { $.addClass(menu, 'top'); $.rmClass(menu, 'bottom'); } else { $.addClass(menu, 'bottom'); $.rmClass(menu, 'top'); } if (bRect.left + mRect.width < cWidth) { $.addClass(menu, 'left'); $.rmClass(menu, 'right'); } else { $.addClass(menu, 'right'); $.rmClass(menu, 'left'); } return menu.focus(); }; Menu.prototype.insertEntry = function(entry, parent, data) { var subEntry, submenu, _i, _len, _ref; if (typeof entry.open === 'function') { if (!entry.open(data, (function(_this) { return function(subEntry) { _this.parseEntry(subEntry); return entry.subEntries.push(subEntry); }; })(this))) { return; } } $.add(parent, entry.el); if (!entry.subEntries) { return; } if (submenu = $('.submenu', entry.el)) { $.rm(submenu); } submenu = $.el('div', { className: 'dialog submenu' }); _ref = entry.subEntries; for (_i = 0, _len = _ref.length; _i < _len; _i++) { subEntry = _ref[_i]; this.insertEntry(subEntry, submenu, data); } $.add(entry.el, submenu); }; Menu.prototype.close = function() { $.rm(currentMenu); $.rmClass(lastToggledButton, 'open'); currentMenu = null; lastToggledButton = null; return $.off(d, 'click CloseMenu', this.close); }; Menu.prototype.findNextEntry = function(entry, direction) { var entries; entries = __slice.call(entry.parentNode.children); entries.sort(function(first, second) { return first.style.order - second.style.order; }); return entries[entries.indexOf(entry) + direction]; }; Menu.prototype.keybinds = function(e) { var entry, next, nextPrev, subEntry, submenu; entry = $('.focused', currentMenu); while (subEntry = $('.focused', entry)) { entry = subEntry; } switch (e.keyCode) { case 27: lastToggledButton.focus(); this.close(); break; case 13: case 32: entry.click(); break; case 38: if (next = this.findNextEntry(entry, -1)) { this.focus(next); } break; case 40: if (next = this.findNextEntry(entry, +1)) { this.focus(next); } break; case 39: if ((submenu = $('.submenu', entry)) && (next = submenu.firstElementChild)) { while (nextPrev = this.findNextEntry(next, -1)) { next = nextPrev; } this.focus(next); } break; case 37: if (next = $.x('parent::*[contains(@class,"submenu")]/parent::*', entry)) { this.focus(next); } break; default: return; } e.preventDefault(); return e.stopPropagation(); }; Menu.prototype.onFocus = function(e) { e.stopPropagation(); return this.focus(e.target); }; Menu.prototype.focus = function(entry) { var cHeight, cWidth, eRect, focused, sRect, submenu, _i, _len, _ref; while (focused = $.x('parent::*/child::*[contains(@class,"focused")]', entry)) { $.rmClass(focused, 'focused'); } _ref = $$('.focused', entry); for (_i = 0, _len = _ref.length; _i < _len; _i++) { focused = _ref[_i]; $.rmClass(focused, 'focused'); } $.addClass(entry, 'focused'); if (!(submenu = $('.submenu', entry))) { return; } sRect = submenu.getBoundingClientRect(); eRect = entry.getBoundingClientRect(); cHeight = doc.clientHeight; cWidth = doc.clientWidth; if (eRect.top + sRect.height < cHeight) { $.addClass(submenu, 'top'); $.rmClass(submenu, 'bottom'); } else { $.addClass(submenu, 'bottom'); $.rmClass(submenu, 'top'); } if (eRect.right + sRect.width < cWidth) { $.addClass(submenu, 'left'); return $.rmClass(submenu, 'right'); } else { $.addClass(submenu, 'right'); return $.rmClass(submenu, 'left'); } }; Menu.prototype.addEntry = function(entry) { this.parseEntry(entry); return this.entries.push(entry); }; Menu.prototype.parseEntry = function(entry) { var el, subEntries, subEntry, _i, _len; el = entry.el, subEntries = entry.subEntries; $.addClass(el, 'entry'); $.on(el, 'focus mouseover', this.onFocus); el.style.order = entry.order || 100; if (!subEntries) { return; } $.addClass(el, 'has-submenu'); for (_i = 0, _len = subEntries.length; _i < _len; _i++) { subEntry = subEntries[_i]; this.parseEntry(subEntry); } }; return Menu; })(); dragstart = function(e) { var el, isTouching, o, rect, screenHeight, screenWidth, _ref; if (e.type === 'mousedown' && e.button !== 0) { return; } e.preventDefault(); if (isTouching = e.type === 'touchstart') { _ref = e.changedTouches, e = _ref[_ref.length - 1]; } el = $.x('ancestor::div[contains(@class,"dialog")][1]', this); rect = el.getBoundingClientRect(); screenHeight = doc.clientHeight; screenWidth = doc.clientWidth; o = { id: el.id, style: el.style, dx: e.clientX - rect.left, dy: e.clientY - rect.top, height: screenHeight - rect.height, width: screenWidth - rect.width, screenHeight: screenHeight, screenWidth: screenWidth, isTouching: isTouching }; if (isTouching) { o.identifier = e.identifier; o.move = touchmove.bind(o); o.up = touchend.bind(o); $.on(d, 'touchmove', o.move); return $.on(d, 'touchend touchcancel', o.up); } else { o.move = drag.bind(o); o.up = dragend.bind(o); $.on(d, 'mousemove', o.move); return $.on(d, 'mouseup', o.up); } }; touchmove = function(e) { var touch, _i, _len, _ref; _ref = e.changedTouches; for (_i = 0, _len = _ref.length; _i < _len; _i++) { touch = _ref[_i]; if (touch.identifier === this.identifier) { drag.call(this, touch); return; } } }; drag = function(e) { var bottom, clientX, clientY, left, right, style, top; clientX = e.clientX, clientY = e.clientY; left = clientX - this.dx; left = left < 10 ? 0 : this.width - left < 10 ? null : left / this.screenWidth * 100 + '%'; top = clientY - this.dy; top = top < 10 ? 0 : this.height - top < 10 ? null : top / this.screenHeight * 100 + '%'; right = left === null ? 0 : null; bottom = top === null ? 0 : null; style = this.style; style.left = left; style.right = right; style.top = top; return style.bottom = bottom; }; touchend = function(e) { var touch, _i, _len, _ref; _ref = e.changedTouches; for (_i = 0, _len = _ref.length; _i < _len; _i++) { touch = _ref[_i]; if (touch.identifier === this.identifier) { dragend.call(this); return; } } }; dragend = function() { if (this.isTouching) { $.off(d, 'touchmove', this.move); $.off(d, 'touchend touchcancel', this.up); } else { $.off(d, 'mousemove', this.move); $.off(d, 'mouseup', this.up); } return $.set("" + this.id + ".position", this.style.cssText); }; hoverstart = function(_arg) { var asapTest, cb, el, endEvents, latestEvent, o, offsetX, offsetY, root; root = _arg.root, el = _arg.el, latestEvent = _arg.latestEvent, endEvents = _arg.endEvents, asapTest = _arg.asapTest, cb = _arg.cb, offsetX = _arg.offsetX, offsetY = _arg.offsetY; o = { root: root, el: el, style: el.style, cb: cb, endEvents: endEvents, latestEvent: latestEvent, clientHeight: doc.clientHeight, clientWidth: doc.clientWidth, offsetX: offsetX || 45, offsetY: offsetY || -120 }; o.hover = hover.bind(o); o.hoverend = hoverend.bind(o); if (asapTest) { $.asap(function() { return !el.parentNode || asapTest(); }, function() { if (el.parentNode) { return o.hover(o.latestEvent); } }); } $.on(root, endEvents, o.hoverend); $.on(root, 'mousemove', o.hover); o.workaround = function(e) { if (!root.contains(e.target)) { return o.hoverend(); } }; return $.on(doc, 'mousemove', o.workaround); }; hover = function(e) { var clientX, clientY, height, left, right, style, top, _ref; this.latestEvent = e; height = this.el.offsetHeight; clientX = e.clientX, clientY = e.clientY; top = clientY + this.offsetY; top = this.clientHeight <= height || top <= 0 ? 0 : top + height >= this.clientHeight ? this.clientHeight - height : top; _ref = clientX <= this.clientWidth / 2 ? [clientX + this.offsetX + 'px', null] : [null, this.clientWidth - clientX + this.offsetX + 'px'], left = _ref[0], right = _ref[1]; style = this.style; style.top = top + 'px'; style.left = left; return style.right = right; }; hoverend = function() { $.rm(this.el); $.off(this.root, this.endEvents, this.hoverend); $.off(this.root, 'mousemove', this.hover); $.off(doc, 'mousemove', this.workaround); if (this.cb) { return this.cb.call(this); } }; return { dialog: dialog, Menu: Menu, hover: hoverstart }; })(); Header = { init: function() { var barPositionToggler, botBoardToggler, customNavToggler, editCustomNav, headerEl, headerToggler, menuButton, scrollHeaderToggler, topBoardToggler; headerEl = $.el('div', { id: 'header', innerHTML: "<div id=\"header-bar\" class=\"dialog\"><span id=\"shortcuts\" class=\"brackets-wrap\"></span><span id=\"board-list\"><span id=\"custom-board-list\"></span><span id=\"full-board-list\" hidden></span></span><div id=\"header-bar-hitzone\"></div></div><div id=\"notifications\"></div>" }); this.bar = $('#header-bar', headerEl); this.hitzone = $('#header-bar-hitzone', this.bar); this.noticesRoot = $('#notifications', headerEl); this.menu = new UI.Menu(); menuButton = $.el('a', { className: 'menu-button', innerHTML: '<i class="fa fa-bars"></i>', href: 'javascript:;' }); $.on(menuButton, 'click', this.menuToggle); this.addShortcut(menuButton, 0); $.on(window, 'load hashchange', Header.hashScroll); $.on(d, 'CreateNotification', this.createNotification); headerToggler = $.el('label', { innerHTML: '<input type=checkbox name="Header auto-hide"> Auto-hide header' }); scrollHeaderToggler = $.el('label', { innerHTML: '<input type=checkbox name="Header auto-hide on scroll"> Auto-hide header on scroll' }); barPositionToggler = $.el('label', { innerHTML: '<input type=checkbox name="Bottom header"> Bottom header' }); topBoardToggler = $.el('label', { innerHTML: '<input type=checkbox name="Top Board List"> Top original board list' }); botBoardToggler = $.el('label', { innerHTML: '<input type=checkbox name="Bottom Board List"> Bottom original board list' }); customNavToggler = $.el('label', { innerHTML: '<input type=checkbox name="Custom Board Navigation"> Custom board navigation' }); editCustomNav = $.el('a', { textContent: 'Edit custom board navigation', href: 'javascript:;' }); this.headerToggler = headerToggler.firstElementChild; this.scrollHeaderToggler = scrollHeaderToggler.firstElementChild; this.barPositionToggler = barPositionToggler.firstElementChild; this.topBoardToggler = topBoardToggler.firstElementChild; this.botBoardToggler = botBoardToggler.firstElementChild; this.customNavToggler = customNavToggler.firstElementChild; $.on(this.headerToggler, 'change', this.toggleBarVisibility); $.on(this.scrollHeaderToggler, 'change', this.toggleHideBarOnScroll); $.on(this.barPositionToggler, 'change', this.toggleBarPosition); $.on(this.topBoardToggler, 'change', this.toggleOriginalBoardList); $.on(this.botBoardToggler, 'change', this.toggleOriginalBoardList); $.on(this.customNavToggler, 'change', this.toggleCustomNav); $.on(editCustomNav, 'click', this.editCustomNav); this.setBarVisibility(Conf['Header auto-hide']); this.setHideBarOnScroll(Conf['Header auto-hide on scroll']); this.setBarPosition(Conf['Bottom header']); this.setTopBoardList(Conf['Top Board List']); this.setBotBoardList(Conf['Bottom Board List']); $.sync('Header auto-hide', this.setBarVisibility); $.sync('Header auto-hide on scroll', this.setHideBarOnScroll); $.sync('Bottom header', this.setBarPosition); $.sync('Top Board List', this.setTopBoardList); $.sync('Bottom Board List', this.setBotBoardList); this.menu.addEntry({ el: $.el('span', { textContent: 'Header' }), order: 105, subEntries: [ { el: headerToggler }, { el: scrollHeaderToggler }, { el: barPositionToggler }, { el: topBoardToggler }, { el: botBoardToggler }, { el: customNavToggler }, { el: editCustomNav } ] }); $.asap((function() { return d.body; }), function() { if (!Main.isThisPageLegit()) { return; } $.asap((function() { return $.id('boardNavMobile') || d.readyState !== 'loading'; }), Header.setBoardList); return $.prepend(d.body, headerEl); }); $.ready(function() { var a; if (a = $("a[href*='/" + g.BOARD + "/']", $.id('boardNavDesktopFoot'))) { return a.className = 'current'; } }); return this.enableDesktopNotifications(); }, setBoardList: function() { var a, btn, fullBoardList, nav; nav = $.id('boardNavDesktop'); if (a = $("a[href*='/" + g.BOARD + "/']", nav)) { a.className = 'current'; } fullBoardList = $('#full-board-list', Header.bar); fullBoardList.innerHTML = nav.innerHTML; $.rm($('#navtopright', fullBoardList)); btn = $.el('span', { className: 'hide-board-list-button brackets-wrap', innerHTML: '<a href=javascript:;> - </a>' }); $.on(btn, 'click', Header.toggleBoardList); $.add(fullBoardList, btn); Header.setCustomNav(Conf['Custom Board Navigation']); Header.generateBoardList(Conf['boardnav']); $.sync('Custom Board Navigation', Header.setCustomNav); return $.sync('boardnav', Header.generateBoardList); }, generateBoardList: function(text) { var as, list, nodes, re; list = $('#custom-board-list', Header.bar); $.rmAll(list); if (!text) { return; } as = $$('.boardList a[title]', Header.bar); re = /[\w@]+(-(all|title|replace|full|archive|(mode|sort|text):"[^"]+"))*|[^\w@]+/g; nodes = text.match(re).map(function(t) { var a, boardID, href, m, type, _i, _len; if (/^[^\w@]/.test(t)) { return $.tn(t); } if (/^toggle-all/.test(t)) { a = $.el('a', { className: 'show-board-list-button', textContent: (t.match(/-text:"(.+)"/) || [null, '+'])[1], href: 'javascript:;' }); $.on(a, 'click', Header.toggleBoardList); return a; } boardID = t.split('-')[0]; if (boardID === 'current') { boardID = g.BOARD.ID; } for (_i = 0, _len = as.length; _i < _len; _i++) { a = as[_i]; if (!(a.textContent === boardID)) { continue; } a = a.cloneNode(); break; } if (a.parentNode) { return $.tn(boardID); } a.textContent = /-title/.test(t) || /-replace/.test(t) && boardID === g.BOARD.ID ? a.title : /-full/.test(t) ? "/" + boardID + "/ - " + a.title : (m = t.match(/-text:"([^"]+)"/)) ? m[1] : boardID; if (/-archive/.test(t)) { if (href = Redirect.to('board', { boardID: boardID })) { a.href = href; } else { return a.firstChild; } } if (m = t.match(/-mode:"([^"]+)"/)) { type = m[1].toLowerCase(); a.dataset.indexMode = (function() { switch (type) { case 'all threads': return 'all pages'; case 'paged': case 'catalog': return type; default: return 'paged'; } })(); } if (m = t.match(/-sort:"([^"]+)"/)) { type = m[1].toLowerCase(); a.dataset.indexSort = (function() { switch (type) { case 'bump order': return 'bump'; case 'last reply': return 'lastreply'; case 'creation date': return 'birth'; case 'reply count': return 'replycount'; case 'file count': return 'filecount'; default: return 'bump'; } })(); } if (boardID === '@') { $.addClass(a, 'navSmall'); } return a; }); return $.add(list, nodes); }, toggleBoardList: function() { var bar, custom, full, showBoardList; bar = Header.bar; custom = $('#custom-board-list', bar); full = $('#full-board-list', bar); showBoardList = !full.hidden; custom.hidden = !showBoardList; return full.hidden = showBoardList; }, setBarVisibility: function(hide) { Header.headerToggler.checked = hide; return (hide ? $.addClass : $.rmClass)(Header.bar, 'autohide'); }, toggleBarVisibility: function(e) { var hide; hide = this.checked; Conf['Header auto-hide'] = hide; $.set('Header auto-hide', hide); return Header.setBarVisibility(hide); }, setHideBarOnScroll: function(hide) { Header.scrollHeaderToggler.checked = hide; if (hide) { $.on(window, 'scroll', Header.hideBarOnScroll); return; } $.off(window, 'scroll', Header.hideBarOnScroll); $.rmClass(Header.bar, 'scroll'); if (!Conf['Header auto-hide']) { return $.rmClass(Header.bar, 'autohide'); } }, toggleHideBarOnScroll: function() { $.cb.checked.call(this); return Header.setHideBarOnScroll(this.checked); }, hideBarOnScroll: function() { var offsetY; offsetY = window.pageYOffset; if (offsetY > (Header.previousOffset || 0)) { $.addClass(Header.bar, 'autohide', 'scroll'); } else { $.rmClass(Header.bar, 'autohide', 'scroll'); } return Header.previousOffset = offsetY; }, setBarPosition: function(bottom) { Header.barPositionToggler.checked = bottom; $.event('CloseMenu'); if (bottom) { $.addClass(doc, 'bottom-header'); $.rmClass(doc, 'top-header'); return Header.bar.parentNode.className = 'bottom'; } else { $.addClass(doc, 'top-header'); $.rmClass(doc, 'bottom-header'); return Header.bar.parentNode.className = 'top'; } }, toggleBarPosition: function() { $.cb.checked.call(this); return Header.setBarPosition(this.checked); }, setTopBoardList: function(show) { Header.topBoardToggler.checked = show; if (show) { return $.addClass(doc, 'show-original-top-board-list'); } else { return $.rmClass(doc, 'show-original-top-board-list'); } }, setBotBoardList: function(show) { Header.botBoardToggler.checked = show; if (show) { return $.addClass(doc, 'show-original-bot-board-list'); } else { return $.rmClass(doc, 'show-original-bot-board-list'); } }, toggleOriginalBoardList: function() { $.cb.checked.call(this); return (this.name === 'Top Board List' ? Header.setTopBoardList : Header.setBotBoardList)(this.checked); }, setCustomNav: function(show) { var btn, cust, full, _ref; Header.customNavToggler.checked = show; cust = $('#custom-board-list', Header.bar); full = $('#full-board-list', Header.bar); btn = $('.hide-board-list-button', full); return _ref = show ? [false, true, false] : [true, false, true], cust.hidden = _ref[0], full.hidden = _ref[1], btn.hidden = _ref[2], _ref; }, toggleCustomNav: function() { $.cb.checked.call(this); return Header.setCustomNav(this.checked); }, editCustomNav: function() { var settings; Settings.open('Rice'); settings = $.id('fourchanx-settings'); return $('input[name=boardnav]', settings).focus(); }, hashScroll: function() { var hash, post; hash = this.location.hash.slice(1); if (!(/^p\d+$/.test(hash) && (post = $.id(hash)))) { return; } if ((Get.postFromNode(post)).isHidden) { return; } return Header.scrollTo(post); }, scrollTo: function(root, down, needed) { var height, x; if (down) { x = Header.getBottomOf(root); if (Conf['Header auto-hide on scroll'] && Conf['Bottom header']) { height = Header.bar.getBoundingClientRect().height; if (x <= 0) { if (!Header.isHidden()) { x += height; } } else { if (Header.isHidden()) { x -= height; } } } if (!(needed && x >= 0)) { return window.scrollBy(0, -x); } } else { x = Header.getTopOf(root); if (Conf['Header auto-hide on scroll'] && !Conf['Bottom header']) { height = Header.bar.getBoundingClientRect().height; if (x >= 0) { if (!Header.isHidden()) { x += height; } } else { if (Header.isHidden()) { x -= height; } } } if (!(needed && x >= 0)) { return window.scrollBy(0, x); } } }, scrollToIfNeeded: function(root, down) { return Header.scrollTo(root, down, true); }, getTopOf: function(root) { var headRect, top; top = root.getBoundingClientRect().top; if (!Conf['Bottom header']) { headRect = ($.hasClass(Header.bar, 'autohide') ? Header.hitzone : Header.bar).getBoundingClientRect(); top -= headRect.top + headRect.height; } return top; }, getBottomOf: function(root) { var bottom, clientHeight, headRect; clientHeight = doc.clientHeight; bottom = clientHeight - root.getBoundingClientRect().bottom; if (Conf['Bottom header']) { headRect = ($.hasClass(Header.bar, 'autohide') ? Header.hitzone : Header.bar).getBoundingClientRect(); bottom -= clientHeight - headRect.bottom + headRect.height; } return bottom; }, isNodeVisible: function(node) { var height; height = node.getBoundingClientRect().height; return Header.getTopOf(node) + height >= 0 && Header.getBottomOf(node) + height >= 0; }, isHidden: function() { var top; top = Header.bar.getBoundingClientRect().top; if (Conf['Bottom header']) { return top === doc.clientHeight; } else { return top < 0; } }, addShortcut: function(el, index) { var shortcut, shortcuts; shortcut = $.el('span', { className: 'shortcut' }); shortcut.dataset.index = index; $.add(shortcut, el); shortcuts = $('#shortcuts', Header.bar); return $.add(shortcuts, __slice.call(shortcuts.childNodes).concat(shortcut).sort(function(a, b) { return a.dataset.index - b.dataset.index; })); }, menuToggle: function(e) { return Header.menu.toggle(e, this, g); }, createNotification: function(e) { var content, lifetime, notice, type, _ref; _ref = e.detail, type = _ref.type, content = _ref.content, lifetime = _ref.lifetime; return notice = new Notice(type, content, lifetime); }, areNotificationsEnabled: false, enableDesktopNotifications: function() { var authorize, disable, el, notice, _ref; if (!(window.Notification && Conf['Desktop Notifications'])) { return; } switch (Notification.permission) { case 'granted': Header.areNotificationsEnabled = true; return; case 'denied': return; } el = $.el('span', { innerHTML: "Desktop notification permissions are not granted.\n[<a href='https://github.com/MayhemYDG/4chan-x/wiki/FAQ#desktop-notifications' target=_blank>FAQ</a>]<br>\n<button>Authorize</button> or <button>Disable</button>" }); _ref = $$('button', el), authorize = _ref[0], disable = _ref[1]; $.on(authorize, 'click', function() { return Notification.requestPermission(function(status) { Header.areNotificationsEnabled = status === 'granted'; if (status === 'default') { return; } return notice.close(); }); }); $.on(disable, 'click', function() { $.set('Desktop Notifications', false); return notice.close(); }); return notice = new Notice('info', el); } }; Notice = (function() { function Notice(type, content, timeout) { this.timeout = timeout; this.close = __bind(this.close, this); this.add = __bind(this.add, this); this.el = $.el('div', { innerHTML: '<a href=javascript:; class="close fa fa-times" title=Close></a><div class=message></div>' }); this.el.style.opacity = 0; this.setType(type); $.on(this.el.firstElementChild, 'click', this.close); if (typeof content === 'string') { content = $.tn(content); } $.add(this.el.lastElementChild, content); $.ready(this.add); } Notice.prototype.setType = function(type) { return this.el.className = "notification " + type; }; Notice.prototype.add = function() { if (d.hidden) { $.on(d, 'visibilitychange', this.add); return; } $.off(d, 'visibilitychange', this.add); $.add(Header.noticesRoot, this.el); this.el.clientHeight; this.el.style.opacity = 1; if (this.timeout) { return setTimeout(this.close, this.timeout * $.SECOND); } }; Notice.prototype.close = function() { $.off(d, 'visibilitychange', this.add); return $.rm(this.el); }; return Notice; })(); Settings = { init: function() { var link, settings; link = $.el('a', { className: 'settings-link', textContent: '4chan X Settings', href: 'javascript:;' }); $.on(link, 'click', Settings.open); Header.menu.addEntry({ el: link, order: 111 }); Settings.addSection('Main', Settings.main); Settings.addSection('Filter', Settings.filter); Settings.addSection('QR', Settings.qr); Settings.addSection('Sauce', Settings.sauce); Settings.addSection('Rice', Settings.rice); Settings.addSection('Archives', Settings.archives); Settings.addSection('Keybinds', Settings.keybinds); $.on(d, 'OpenSettings', function(e) { return Settings.open(e.detail); }); settings = JSON.parse(localStorage.getItem('4chan-settings')) || {}; if (settings.disableAll) { return; } settings.disableAll = true; return localStorage.setItem('4chan-settings', JSON.stringify(settings)); }, open: function(openSection) { var html, link, links, overlay, section, sectionToOpen, _i, _len, _ref; if (Settings.dialog) { return; } $.event('CloseMenu'); html = "<div id=\"fourchanx-settings\" class=\"dialog\"><nav><div class=\"sections-list\"></div><div class=\"credits\"><a href=\"https://4chan-x.just-believe.in/\" target=\"_blank\">4chan X</a> | <a href=\"https://github.com/MayhemYDG/4chan-x/blob/v3/CHANGELOG.md\" target=\"_blank\">" + g.VERSION + "</a> | <a href=\"https://github.com/MayhemYDG/4chan-x/blob/v3/CONTRIBUTING.md#reporting-bugs-and-suggestions\" target=\"_blank\">Issues</a> | <a href=\"javascript:;\" class=\"close fa fa-times\" title=\"Close\"></a></div></nav><section></section></div>"; Settings.dialog = overlay = $.el('div', { id: 'overlay', innerHTML: html }); links = []; _ref = Settings.sections; for (_i = 0, _len = _ref.length; _i < _len; _i++) { section = _ref[_i]; link = $.el('a', { className: "tab-" + section.hyphenatedTitle, textContent: section.title, href: 'javascript:;' }); $.on(link, 'click', Settings.openSection.bind(section)); links.push(link, $.tn(' | ')); if (section.title === openSection) { sectionToOpen = link; } } links.pop(); $.add($('.sections-list', overlay), links); (sectionToOpen ? sectionToOpen : links[0]).click(); $.on($('.close', overlay), 'click', Settings.close); $.on(overlay, 'click', Settings.close); $.on(overlay.firstElementChild, 'click', function(e) { return e.stopPropagation(); }); d.body.style.width = "" + d.body.clientWidth + "px"; $.addClass(d.body, 'unscroll'); return $.add(d.body, overlay); }, close: function() { if (!Settings.dialog) { return; } d.body.style.removeProperty('width'); $.rmClass(d.body, 'unscroll'); $.rm(Settings.dialog); return delete Settings.dialog; }, sections: [], addSection: function(title, open) { var hyphenatedTitle; hyphenatedTitle = title.toLowerCase().replace(/\s+/g, '-'); return Settings.sections.push({ title: title, hyphenatedTitle: hyphenatedTitle, open: open }); }, openSection: function() { var section, selected; if (selected = $('.tab-selected', Settings.dialog)) { $.rmClass(selected, 'tab-selected'); } $.addClass($(".tab-" + this.hyphenatedTitle, Settings.dialog), 'tab-selected'); section = $('section', Settings.dialog); $.rmAll(section); section.className = "section-" + this.hyphenatedTitle; this.open(section, g); return section.scrollTop = 0; }, main: function(section) { var arr, button, description, div, fs, input, inputs, items, key, obj, _ref; section.innerHTML = "<button class=\"export\">Export Settings</button><button class=\"import\">Import Settings</button><button class=\"reset\">Reset Settings</button><input type=\"file\" hidden>"; $.on($('.export', section), 'click', Settings["export"]); $.on($('.import', section), 'click', Settings["import"]); $.on($('.reset', section), 'click', Settings.reset); $.on($('input', section), 'change', Settings.onImport); items = {}; inputs = {}; _ref = Config.main; for (key in _ref) { obj = _ref[key]; fs = $.el('fieldset', { innerHTML: "<legend>" + key + "</legend>" }); for (key in obj) { arr = obj[key]; description = arr[1]; div = $.el('div', { innerHTML: "<label><input type=checkbox name=\"" + key + "\">" + key + "</label><span class=description>: " + description + "</span>" }); input = $('input', div); $.on(input, 'change', $.cb.checked); items[key] = Conf[key]; inputs[key] = input; $.add(fs, div); } $.add(section, fs); } $.get(items, function(items) { var val; for (key in items) { val = items[key]; inputs[key].checked = val; } }); div = $.el('div', { innerHTML: "<button></button><span class=description>: Clear manually-hidden threads and posts on all boards. Reload the page to apply." }); button = $('button', div); $.get('hiddenPosts', {}, function(_arg) { var ID, board, hiddenNum, hiddenPosts, thread, _ref1; hiddenPosts = _arg.hiddenPosts; hiddenNum = 0; _ref1 = hiddenPosts.boards; for (ID in _ref1) { board = _ref1[ID]; for (ID in board) { thread = board[ID]; hiddenNum += Object.keys(thread).length; } } return button.textContent = "Hidden: " + hiddenNum; }); $.on(button, 'click', function() { this.textContent = 'Hidden: 0'; return $["delete"]('hiddenPosts'); }); return $.after($('input[name="Recursive Hiding"]', section).parentNode.parentNode, div); }, "export": function() { return $.get(Conf, function(Conf) { delete Conf['archives']; return Settings.downloadExport('Settings', { version: g.VERSION, date: Date.now(), Conf: Conf }); }); }, downloadExport: function(title, data) { var a; a = $.el('a', { download: "4chan X v" + g.VERSION + " " + title + "." + data.date + ".json", href: "data:application/json;base64," + (btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))))) }); $.add(d.body, a); a.click(); return $.rm(a); }, "import": function() { return $('input[type=file]', this.parentNode).click(); }, onImport: function() { var file, reader; if (!(file = this.files[0])) { return; } if (!confirm('Your current settings will be entirely overwritten, are you sure?')) { return; } reader = new FileReader(); reader.onload = function(e) { var err; try { Settings.loadSettings(JSON.parse(e.target.result)); } catch (_error) { err = _error; alert('Import failed due to an error.'); c.error(err.stack); return; } if (confirm('Import successful. Reload now?')) { return window.location.reload(); } }; return reader.readAsText(file); }, loadSettings: function(data) { var convertSettings, key, val, version, _ref; version = data.version.split('.'); if (version[0] === '2') { convertSettings = function(data, map) { var newKey, prevKey; for (prevKey in map) { newKey = map[prevKey]; if (newKey) { data.Conf[newKey] = data.Conf[prevKey]; } delete data.Conf[prevKey]; } return data; }; data = Settings.convertSettings(data, { 'Disable 4chan\'s extension': '', 'Catalog Links': '', 'Reply Navigation': '', 'Show Stubs': 'Stubs', 'Image Auto-Gif': 'Auto-GIF', 'Expand From Current': '', 'Unread Favicon': 'Unread Tab Icon', 'Post in Title': 'Thread Excerpt', 'Auto Hide QR': '', 'Open Reply in New Tab': '', 'Remember QR size': '', 'Quote Inline': 'Quote Inlining', 'Quote Preview': 'Quote Previewing', 'Indicate OP quote': '', 'Indicate Cross-thread Quotes': '', 'uniqueid': 'uniqueID', 'mod': 'capcode', 'country': 'flag', 'md5': 'MD5', 'openEmptyQR': 'Open empty QR', 'openQR': 'Open QR', 'openOptions': 'Open settings', 'close': 'Close', 'spoiler': 'Spoiler tags', 'code': 'Code tags', 'submit': 'Submit QR', 'watch': 'Watch', 'update': 'Update', 'unreadCountTo0': '', 'expandAllImages': 'Expand images', 'expandImage': 'Expand image', 'zero': 'Front page', 'nextPage': 'Next page', 'previousPage': 'Previous page', 'nextThread': 'Next thread', 'previousThread': 'Previous thread', 'expandThread': 'Expand thread', 'openThreadTab': 'Open thread', 'openThread': 'Open thread tab', 'nextReply': 'Next reply', 'previousReply': 'Previous reply', 'hide': 'Hide', 'Scrolling': 'Auto Scroll', 'Verbose': '' }); data.Conf.sauces = data.Conf.sauces.replace(/\$\d/g, function(c) { switch (c) { case '$1': return '%TURL'; case '$2': return '%URL'; case '$3': return '%MD5'; case '$4': return '%board'; default: return c; } }); _ref = Config.hotkeys; for (key in _ref) { val = _ref[key]; if (key in data.Conf) { data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, function(s) { return "" + (s[0].toUpperCase()) + s.slice(1); }).replace(/(^|.+\+)[A-Z]$/g, function(s) { return "Shift+" + s.slice(0, -1) + (s.slice(-1).toLowerCase()); }); } } data.Conf['WatchedThreads'] = data.WatchedThreads; } if (data.Conf['WatchedThreads']) { data.Conf['watchedThreads'] = { boards: ThreadWatcher.convert(data.Conf['WatchedThreads']) }; delete data.Conf['WatchedThreads']; } return $.clear(function() { return $.set(data.Conf); }); }, reset: function() { if (confirm('Your current settings will be entirely wiped, are you sure?')) { return $.clear(function() { if (confirm('Reset successful. Reload now?')) { return window.location.reload(); } }); } }, filter: function(section) { var select; section.innerHTML = "<select name=\"filter\"><option value=\"guide\">Guide</option><option value=\"name\">Name</option><option value=\"uniqueID\">Unique ID</option><option value=\"tripcode\">Tripcode</option><option value=\"capcode\">Capcode</option><option value=\"email\">E-mail</option><option value=\"subject\">Subject</option><option value=\"comment\">Comment</option><option value=\"flag\">Flag</option><option value=\"filename\">Filename</option><option value=\"dimensions\">Image dimensions</option><option value=\"filesize\">Filesize</option><option value=\"MD5\">Image MD5</option></select><div></div>"; select = $('select', section); $.on(select, 'change', Settings.selectFilter); return Settings.selectFilter.call(select); }, selectFilter: function() { var div, name, ta; div = this.nextElementSibling; if ((name = this.value) !== 'guide') { $.rmAll(div); ta = $.el('textarea', { name: name, className: 'field', spellcheck: false }); $.get(name, Conf[name], function(item) { return ta.value = item[name]; }); $.on(ta, 'change', $.cb.value); $.add(div, ta); return; } return div.innerHTML = "<div class=\"warning\" " + (Conf['Filter'] ? 'hidden' : '') + "><code>Filter</code> is disabled.</div><p>Use <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions\">regular expressions</a>, one per line.<br>Lines starting with a <code>#</code> will be ignored.<br>For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br>MD5 filtering uses exact string matching, not regular expressions.</p><ul>You can use these settings with each regular expression, separate them with semicolons:<li>Per boards, separate them with commas. It is global if not specified.<br>For example: <code>boards:a,jp;</code>.</li><li>Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).<br>For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.</li><li>Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).<br>For example: <code>stub:yes;</code> or <code>stub:no;</code>.</li><li>Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.</li><li>Highlighted OPs will have their threads put on top of the board index by default.<br>For example: <code>top:yes;</code> or <code>top:no;</code>.</li></ul>"; }, qr: function(section) { var ta; section.innerHTML = "<fieldset><legend>Quick Reply Personas <span class=\"warning\" " + (Conf['Quick Reply'] ? 'hidden' : '') + ">is disabled.</span></legend><textarea name=\"QR.personas\" class=\"field\" spellcheck=\"false\"></textarea><p>One item per line.<br>Items will be added in the relevant input's auto-completion list.<br>Password items will always be used, since there is no password input.<br>Lines starting with a <code>#</code> will be ignored.</p><ul>You can use these settings with each item, separate them with semicolons:<li>Possible items are: <code>name</code>, <code>email</code>, <code>subject</code> and <code>password</code>.</li><li>Wrap values of items with quotes, like this: <code>email:\"sage\"</code>.</li><li>Force values as defaults with the <code>always</code> keyword, for example: <code>email:\"sage\";always</code>.</li><li>Select specific boards for an item, separated with commas, for example: <code>email:\"sage\";boards:jp;always</code>.</li></ul></fieldset>"; ta = $('textarea', section); $.get('QR.personas', Conf['QR.personas'], function(item) { return ta.value = item['QR.personas']; }); return $.on(ta, 'change', $.cb.value); }, sauce: function(section) { var ta; section.innerHTML = "<div class=\"warning\" " + (Conf['Sauce'] ? 'hidden' : '') + "><code>Sauce</code> is disabled.</div><div>Lines starting with a <code>#</code> will be ignored.</div><div>You can specify a display text by appending <code>;text:[text]</code> to the URL.</div><ul>These parameters will be replaced by their corresponding values:<li><code>%TURL</code>: Thumbnail URL.</li><li><code>%URL</code>: Full image URL.</li><li><code>%MD5</code>: MD5 hash.</li><li><code>%name</code>: Original file name.</li><li><code>%board</code>: Current board.</li></ul><textarea name=\"sauces\" class=\"field\" spellcheck=\"false\"></textarea>"; ta = $('textarea', section); $.get('sauces', Conf['sauces'], function(item) { return ta.value = item['sauces']; }); return $.on(ta, 'change', $.cb.value); }, rice: function(section) { var input, inputs, items, name, _i, _len, _ref; section.innerHTML = "<fieldset><legend>Custom Board Navigation <span class=\"warning\" " + (Conf['Custom Board Navigation'] ? 'hidden' : '') + ">is disabled.</span></legend><div><input name=\"boardnav\" class=\"field\" spellcheck=\"false\"></div><div>In the following, <code>board</code> can translate to a board ID (<code>a</code>, <code>b</code>, etc...), the current board (<code>current</code>), or the Twitter link (<code>@</code>).</div><div>Board link: <code>board</code></div><div>Archive link: <code>board-archive</code></div><div>Title link: <code>board-title</code></div><div>Board link (Replace with title when on that board): <code>board-replace</code></div><div>Full text link: <code>board-full</code></div><div>Custom text link: <code>board-text:\"VIP Board\"</code></div><div>Index mode: <code>board-mode:\"type\"</code> where type is <code>paged</code>, <code>all threads</code> or <code>catalog</code></div><div>Index sort: <code>board-sort:\"type\"</code> where type is <code>bump order</code>, <code>last reply</code>, <code>creation date</code>, <code>reply count</code> or <code>file count</code></div><div>Combinations are possible: <code>board-text:\"VIP Catalog\"-mode:\"catalog\"-sort:\"creation date\"</code></div><div>Full board list toggle: <code>toggle-all</code></div></fieldset><fieldset><legend>Time Formatting <span class=\"warning\" " + (Conf['Time Formatting'] ? 'hidden' : '') + ">is disabled.</span></legend><div><input name=\"time\" class=\"field\" spellcheck=\"false\">: <span class=\"time-preview\"></span></div><div>Supported <a href=\"//en.wikipedia.org/wiki/Date_%28Unix%29#Formatting\">format specifiers</a>:</div><div>Day: <code>%a</code>, <code>%A</code>, <code>%d</code>, <code>%e</code></div><div>Month: <code>%m</code>, <code>%b</code>, <code>%B</code></div><div>Year: <code>%y</code>, <code>20%y</code></div><div>Hour: <code>%k</code>, <code>%H</code>, <code>%l</code>, <code>%I</code>, <code>%p</code>, <code>%P</code></div><div>Minute: <code>%M</code></div><div>Second: <code>%S</code></div></fieldset><fieldset><legend>Quote Backlinks formatting <span class=\"warning\" " + (Conf['Quote Backlinks'] ? 'hidden' : '') + ">is disabled.</span></legend><div><input name=\"backlink\" class=\"field\" spellcheck=\"false\">: <span class=\"backlink-preview\"></span></div></fieldset><fieldset><legend>File Info Formatting <span class=\"warning\" " + (Conf['File Info Formatting'] ? 'hidden' : '') + ">is disabled.</span></legend><div><input name=\"fileInfo\" class=\"field\" spellcheck=\"false\">: <span class=\"file-info file-info-preview\"></span></div><div>Link: <code>%l</code> (truncated), <code>%L</code> (untruncated), <code>%T</code> (Unix timestamp)</div><div>Original file name: <code>%n</code> (truncated), <code>%N</code> (untruncated), <code>%t</code> (Unix timestamp)</div><div>Spoiler indicator: <code>%p</code></div><div>Size: <code>%B</code> (Bytes), <code>%K</code> (KB), <code>%M</code> (MB), <code>%s</code> (4chan default)</div><div>Resolution: <code>%r</code> (Displays 'PDF' for PDF files)</div></fieldset><fieldset><legend>Unread Tab Icon <span class=\"warning\" " + (Conf['Unread Tab Icon'] ? 'hidden' : '') + ">is disabled.</span></legend><select name=\"favicon\"><option value=\"ferongr\">ferongr</option><option value=\"xat-\">xat-</option><option value=\"Mayhem\">Mayhem</option><option value=\"Original\">Original</option></select><span class=\"favicon-preview\"></span></fieldset><fieldset><legend><label><input type=\"checkbox\" name=\"Custom CSS\" " + (Conf['Custom CSS'] ? 'checked' : '') + "> Custom CSS</label></legend><button id=\"apply-css\">Apply CSS</button><textarea name=\"usercss\" class=\"field\" spellcheck=\"false\" " + (Conf['Custom CSS'] ? '' : 'disabled') + "></textarea></fieldset>"; items = {}; inputs = {}; _ref = ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss']; for (_i = 0, _len = _ref.length; _i < _len; _i++) { name = _ref[_i]; input = $("[name=" + name + "]", section); items[name] = Conf[name]; inputs[name] = input; $.on(input, 'change', $.cb.value); } $.get(items, function(items) { var event, key, val; for (key in items) { val = items[key]; input = inputs[key]; input.value = val; if (key === 'usercss') { continue; } event = key === 'favicon' || key === 'usercss' ? 'change' : 'input'; $.on(input, event, Settings[key]); Settings[key].call(input); } }); $.on($('input[name="Custom CSS"]', section), 'change', Settings.togglecss); return $.on($('#apply-css', section), 'click', Settings.usercss); }, boardnav: function() { return Header.generateBoardList(this.value); }, time: function() { var funk; funk = Time.createFunc(this.value); return this.nextElementSibling.textContent = funk(Time, new Date()); }, backlink: function() { return this.nextElementSibling.textContent = this.value.replace(/%id/, '123456789'); }, fileInfo: function() { var data, funk; data = { isReply: true, file: { URL: '//i.4cdn.org/g/1334437723720.jpg', name: 'd9bb2efc98dd0df141a94399ff5880b7.jpg', size: '276 KB', sizeInBytes: 276 * 1024, dimensions: '1280x720', isImage: true, isVideo: false, isSpoiler: true } }; funk = FileInfo.createFunc(this.value); return this.nextElementSibling.innerHTML = funk(FileInfo, data); }, favicon: function() { Favicon["switch"](); if (g.VIEW === 'thread' && Conf['Unread Tab Icon']) { Unread.update(); } return this.nextElementSibling.innerHTML = "<img src=" + Favicon["default"] + ">\n<img src=" + Favicon.unreadSFW + ">\n<img src=" + Favicon.unreadNSFW + ">\n<img src=" + Favicon.unreadDead + ">"; }, togglecss: function() { if ($('textarea[name=usercss]', $.x('ancestor::fieldset[1]', this)).disabled = !this.checked) { CustomCSS.rmStyle(); } else { CustomCSS.addStyle(); } return $.cb.checked.call(this); }, usercss: function() { return CustomCSS.update(); }, archives: function(section) { var button; section.innerHTML = "<div class=\"warning\" " + (Conf['404 Redirect'] ? 'hidden' : '') + "><code>404 Redirect</code> is disabled.</div><p>Disabled selections indicate that only one archive is available for that board and redirection type.</p><p><button>Update now</button> Last updated: <time></time></p><table><caption>Archived boards</caption><thead><th>Board</th><th>Thread redirection</th><th>Post fetching</th><th>File redirection</th></thead><tbody></tbody></table>"; button = $('button', section); $.on(button, 'click', function() { $["delete"]('lastarchivecheck'); button.textContent = '...'; button.disabled = true; return Redirect.update(function() { button.textContent = 'Updated'; return Settings.addArchivesTable(section); }); }); return Settings.addArchivesTable(section); }, addArchivesTable: function(section) { var archive, boardID, boards, data, row, rows, tbody, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2; boards = {}; _ref = Conf['archives']; for (_i = 0, _len = _ref.length; _i < _len; _i++) { archive = _ref[_i]; _ref1 = archive.boards; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { boardID = _ref1[_j]; data = boards[boardID] || (boards[boardID] = { thread: [], post: [], file: [] }); data.thread.push(archive); if (archive.software === 'foolfuuka') { data.post.push(archive); } if (__indexOf.call(archive.files, boardID) >= 0) { data.file.push(archive); } } } rows = []; _ref2 = Object.keys(boards).sort(); for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { boardID = _ref2[_k]; row = $.el('tr'); rows.push(row); $.add(row, $.el('th', { textContent: "/" + boardID + "/", className: boardID === g.BOARD.ID ? 'warning' : '' })); data = boards[boardID]; Settings.addArchiveCell(row, boardID, data, 'thread'); Settings.addArchiveCell(row, boardID, data, 'post'); Settings.addArchiveCell(row, boardID, data, 'file'); } tbody = $('tbody', section); $.rmAll(tbody); $.add(tbody, rows); return $.get({ lastarchivecheck: 0, selectedArchives: Conf['selectedArchives'] }, function(_arg) { var lastarchivecheck, option, selectedArchives, type, uid; lastarchivecheck = _arg.lastarchivecheck, selectedArchives = _arg.selectedArchives; for (boardID in selectedArchives) { data = selectedArchives[boardID]; for (type in data) { uid = data[type]; if (option = $("select[data-board-i-d='" + boardID + "'][data-type='" + type + "'] > option[value='" + uid + "']", section)) { option.selected = true; } } } return $('time', section).textContent = new Date(lastarchivecheck).toLocaleString(); }); }, addArchiveCell: function(row, boardID, data, type) { var archive, length, options, select, td, _i, _len, _ref; options = []; _ref = data[type]; for (_i = 0, _len = _ref.length; _i < _len; _i++) { archive = _ref[_i]; options.push($.el('option', { textContent: archive.name, value: archive.uid })); } td = $.el('td'); length = options.length; if (length) { td.innerHTML = '<select></select>'; select = td.firstElementChild; if (!(select.disabled = length === 1)) { $.extend(select.dataset, { boardID: boardID, type: type }); $.on(select, 'change', Settings.saveSelectedArchive); } $.add(select, options); } else { td.textContent = 'N/A'; } return $.add(row, td); }, saveSelectedArchive: function() { return $.get('selectedArchives', Conf['selectedArchives'], (function(_this) { return function(_arg) { var selectedArchives, _name; selectedArchives = _arg.selectedArchives; (selectedArchives[_name = _this.dataset.boardID] || (selectedArchives[_name] = {}))[_this.dataset.type] = +_this.value; Conf['selectedArchives'] = selectedArchives; Redirect.selectArchives(); return $.set('selectedArchives', selectedArchives); }; })(this)); }, keybinds: function(section) { var arr, input, inputs, items, key, tbody, tr, _ref; section.innerHTML = "<div class=\"warning\" " + (Conf['Keybinds'] ? 'hidden' : '') + "><code>Keybinds</code> are disabled.</div><div>Allowed keys: <kbd>a-z</kbd>, <kbd>0-9</kbd>, <kbd>Ctrl</kbd>, <kbd>Shift</kbd>, <kbd>Alt</kbd>, <kbd>Meta</kbd>, <kbd>Enter</kbd>, <kbd>Esc</kbd>, <kbd>Up</kbd>, <kbd>Down</kbd>, <kbd>Right</kbd>, <kbd>Left</kbd>.</div><div>Press <kbd>Backspace</kbd> to disable a keybind.</div><table><tbody><tr><th>Actions</th><th>Keybinds</th></tr></tbody></table>"; tbody = $('tbody', section); items = {}; inputs = {}; _ref = Config.hotkeys; for (key in _ref) { arr = _ref[key]; tr = $.el('tr', { innerHTML: "<td>" + arr[1] + "</td><td><input class=field></td>" }); input = $('input', tr); input.name = key; input.spellcheck = false; items[key] = Conf[key]; inputs[key] = input; $.on(input, 'keydown', Settings.keybind); $.add(tbody, tr); } return $.get(items, function(items) { var val; for (key in items) { val = items[key]; inputs[key].value = val; } }); }, 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); } }; Index = { showHiddenThreads: false, init: function() { var input, label, name, refNavEntry, repliesEntry, select, targetEntry, threadNumEntry, threadsNumInput, _i, _j, _len, _len1, _ref, _ref1; if (g.VIEW !== 'index') { $.ready(this.setupNavLinks); return; } if (g.BOARD.ID === 'f') { return; } this.db = new DataBoard('pinnedThreads'); Thread.callbacks.push({ name: 'Thread Pinning', cb: this.threadNode }); CatalogThread.callbacks.push({ name: 'Catalog Features', cb: this.catalogNode }); this.button = $.el('a', { className: 'index-refresh-shortcut fa fa-refresh', title: 'Refresh Index', href: 'javascript:;' }); $.on(this.button, 'click', this.update); Header.addShortcut(this.button, 1); threadNumEntry = { el: $.el('span', { textContent: 'Threads per page' }), subEntries: [ { el: $.el('label', { innerHTML: '<input type=number min=0 name="Threads per Page">', title: 'Use 0 for default value' }) } ] }; threadsNumInput = threadNumEntry.subEntries[0].el.firstChild; threadsNumInput.value = Conf['Threads per Page']; $.on(threadsNumInput, 'change', $.cb.value); $.on(threadsNumInput, 'change', this.cb.threadsNum); targetEntry = { el: $.el('label', { innerHTML: '<input type=checkbox name="Open threads in a new tab"> Open threads in a new tab', title: 'Catalog-only setting.' }) }; repliesEntry = { el: $.el('label', { innerHTML: '<input type=checkbox name="Show Replies"> Show replies' }) }; refNavEntry = { el: $.el('label', { innerHTML: '<input type=checkbox name="Refreshed Navigation"> Refreshed navigation', title: 'Refresh index when navigating through pages.' }) }; _ref = [targetEntry, repliesEntry, refNavEntry]; for (_i = 0, _len = _ref.length; _i < _len; _i++) { label = _ref[_i]; input = label.el.firstChild; name = input.name; input.checked = Conf[name]; $.on(input, 'change', $.cb.checked); switch (name) { case 'Open threads in a new tab': $.on(input, 'change', this.cb.target); break; case 'Show Replies': $.on(input, 'change', this.cb.replies); } } Header.menu.addEntry({ el: $.el('span', { textContent: 'Index Navigation' }), order: 90, subEntries: [threadNumEntry, targetEntry, repliesEntry, refNavEntry] }); $.addClass(doc, 'index-loading'); this.update(); this.navLinks = $.el('div', { id: 'nav-links', innerHTML: "<input type=\"search\" id=\"index-search\" class=\"field\" placeholder=\"Search\"><a id=\"index-search-clear\" class=\"fa fa-times-circle\" href=\"javascript:;\"></a> <time id=\"index-last-refresh\" title=\"Last index refresh\">...</time><span id=\"hidden-label\" hidden> — <span id=\"hidden-count\"></span> <span id=\"hidden-toggle\">[<a href=\"javascript:;\">Show</a>]</span></span><span style=\"flex:1\"></span><select id=\"index-mode\" name=\"Index Mode\"><option disabled>Index Mode</option><option value=\"paged\">Paged</option><option value=\"all pages\">All threads</option><option value=\"catalog\">Catalog</option></select><select id=\"index-sort\" name=\"Index Sort\"><option disabled>Index Sort</option><option value=\"bump\">Bump order</option><option value=\"lastreply\">Last reply</option><option value=\"birth\">Creation date</option><option value=\"replycount\">Reply count</option><option value=\"filecount\">File count</option></select><select id=\"index-size\" name=\"Index Size\"><option disabled>Image Size</option><option value=\"small\">Small</option><option value=\"large\">Large</option></select>" }); this.searchInput = $('#index-search', this.navLinks); this.hideLabel = $('#hidden-label', this.navLinks); this.selectMode = $('#index-mode', this.navLinks); this.selectSort = $('#index-sort', this.navLinks); this.selectSize = $('#index-size', this.navLinks); $.on(this.searchInput, 'input', this.onSearchInput); $.on($('#index-search-clear', this.navLinks), 'click', this.clearSearch); $.on($('#hidden-toggle a', this.navLinks), 'click', this.cb.toggleHiddenThreads); _ref1 = [this.selectMode, this.selectSort, this.selectSize]; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { select = _ref1[_j]; select.value = Conf[select.name]; $.on(select, 'change', $.cb.value); } $.on(this.selectMode, 'change', this.cb.mode); $.on(this.selectSort, 'change', this.cb.sort); $.on(this.selectSize, 'change', this.cb.size); this.root = $.el('div', { className: 'board' }); this.pagelist = $.el('div', { className: 'pagelist', hidden: true, innerHTML: "<div class=\"prev\"><a><button disabled>Previous</button></a></div><div class=\"pages\"></div><div class=\"next\"><a><button disabled>Next</button></a></div><div class=\"pages cataloglink\"><a href=\"./\" data-index-mode=\"catalog\">Catalog</a></div>" }); this.currentPage = this.getCurrentPage(); $.on(window, 'popstate', this.cb.popstate); $.on(this.pagelist, 'click', this.cb.pageNav); $.on($('#custom-board-list', Header.bar), 'click', this.cb.headerNav); this.cb.toggleCatalogMode(); return $.asap((function() { return $('.board', doc) || d.readyState !== 'loading'; }), function() { var board, navLink, _k, _len2, _ref2; board = $('.board'); $.replace(board, Index.root); d.implementation.createDocument(null, null, null).appendChild(board); _ref2 = $$('.navLinks'); for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { navLink = _ref2[_k]; $.rm(navLink); } $.before($.x('child::form[@name="delform"]/preceding-sibling::hr[1]'), Index.navLinks); return $.asap((function() { return $('.pagelist') || d.readyState !== 'loading'; }), function() { var pagelist; if (pagelist = $('.pagelist')) { $.replace(pagelist, Index.pagelist); } return $.rmClass(doc, 'index-loading'); }); }); }, menu: { init: function() { if (g.VIEW !== 'index' || !Conf['Menu'] || g.BOARD.ID === 'f') { return; } return Menu.menu.addEntry({ el: $.el('a', { href: 'javascript:;' }), order: 19, open: function(_arg) { var thread; thread = _arg.thread; if (Conf['Index Mode'] !== 'catalog') { return false; } this.el.textContent = thread.isPinned ? 'Unpin thread' : 'Pin thread'; if (this.cb) { $.off(this.el, 'click', this.cb); } this.cb = function() { $.event('CloseMenu'); return Index.togglePin(thread); }; $.on(this.el, 'click', this.cb); return true; } }); } }, threadNode: function() { if (!Index.db.get({ boardID: this.board.ID, threadID: this.ID })) { return; } return this.pin(); }, catalogNode: function() { $.on(this.nodes.thumb, 'click', Index.onClick); if (Conf['Image Hover in Catalog']) { return; } return $.on(