\ 4chan X - /g/pasta 2.4
From Flying Echidna, 4 Years ago, written in JavaScript.
Embed
  1. // ==UserScript==
  2. // @name         4chan X
  3. // @version      3.20.15
  4. // @minGMVer     1.14
  5. // @minFFVer     26
  6. // @namespace    4chan-X
  7. // @description  Cross-browser extension for productive lurking on 4chan.
  8. // @license      MIT; https://github.com/MayhemYDG/4chan-x/blob/v3/LICENSE
  9. // @match        *://boards.4chan.org/*
  10. // @match        *://sys.4chan.org/*
  11. // @match        *://a.4cdn.org/*
  12. // @match        *://i.4cdn.org/*
  13. // @grant        GM_getValue
  14. // @grant        GM_setValue
  15. // @grant        GM_deleteValue
  16. // @grant        GM_listValues
  17. // @grant        GM_openInTab
  18. // @run-at       document-start
  19. // @updateURL    https://4chan-x.just-believe.in/builds/4chan-X.meta.js
  20. // @downloadURL  https://4chan-x.just-believe.in/builds/4chan-X.user.js
  21. // @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
  22. // ==/UserScript==
  23.  
  24. /* 4chan X - Version 3.20.15 - 2014-10-27
  25.  * https://4chan-x.just-believe.in/
  26.  *
  27.  * Copyrights and License: https://github.com/MayhemYDG/4chan-x/blob/v3/LICENSE
  28.  *
  29.  * Contributors:
  30.  * https://github.com/MayhemYDG/4chan-x/graphs/contributors
  31.  * Non-GitHub contributors:
  32.  * ferongr, xat-, Ongpot, thisisanon and Anonymous - favicon contributions
  33.  * e000 - cooldown sanity check
  34.  * Seiba - chrome quick reply focusing
  35.  * herpaderpderp - recaptcha fixes
  36.  * WakiMiko - recaptcha tab order http://userscripts.org/scripts/show/82657
  37.  *
  38.  * All the people who've taken the time to write bug reports and provide feedback.
  39.  *
  40.  * Thank you.
  41.  */
  42.  
  43. 'use strict';
  44.  
  45. (function() {
  46.   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,
  47.     __slice = [].slice,
  48.     __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
  49.     __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; },
  50.     __hasProp = {}.hasOwnProperty,
  51.     __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; };
  52.  
  53.   Config = {
  54.     main: {
  55.       'Miscellaneous': {
  56.         'Desktop Notifications': [true, 'Enables desktop notifications across various 4chan X features.'],
  57.         '404 Redirect': [true, 'Redirect dead threads and images.'],
  58.         'Keybinds': [true, 'Bind actions to keyboard shortcuts.'],
  59.         'Linkify': [true, 'Convert text links into hyperlinks.'],
  60.         'Time Formatting': [true, 'Localize and format timestamps.'],
  61.         'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.'],
  62.         'File Info Formatting': [true, 'Reformat the file information.'],
  63.         'Thread Expansion': [true, 'Add buttons to expand threads.'],
  64.         'Index Navigation': [false, 'Add buttons to navigate between threads.'],
  65.         'Reply Navigation': [false, 'Add buttons to navigate to top / bottom of thread.'],
  66.         'Show Dice Roll': [true, 'Show dice that were entered into the email field.']
  67.       },
  68.       'Filtering': {
  69.         'Anonymize': [false, 'Make everyone Anonymous.'],
  70.         'Filter': [true, 'Self-moderation placebo.'],
  71.         'Post Hiding': [true, 'Add buttons to hide threads and replies.'],
  72.         'Stubs': [true, 'Show stubs of hidden posts.'],
  73.         'Recursive Hiding': [true, 'Hide replies of hidden posts, recursively.']
  74.       },
  75.       'Images': {
  76.         'Auto-GIF': [false, 'Animate GIF thumbnails (disabled on /gif/, /wsg/).'],
  77.         'Image Expansion': [true, 'Expand images inline.'],
  78.         'Image Hover': [false, 'Show a floating expanded image on hover.'],
  79.         'Image Hover in Catalog': [false, 'Show a floating expanded image on hover in the catalog.'],
  80.         'Sauce': [true, 'Add sauce links to images.'],
  81.         'Reveal Spoilers': [false, 'Reveal spoiler thumbnails.']
  82.       },
  83.       'Menu': {
  84.         'Menu': [true, 'Add a drop-down menu to posts.'],
  85.         'Report Link': [true, 'Add a report link to the menu.'],
  86.         'Post Hiding Link': [true, 'Add a link to hide threads and replies.'],
  87.         'Delete Link': [true, 'Add post and image deletion links to the menu.'],
  88.         'Archive Link': [true, 'Add an archive link to the menu.']
  89.       },
  90.       'Monitoring': {
  91.         'Thread Updater': [true, 'Fetch and insert new replies. Has more options in its own dialog.'],
  92.         'Unread Count': [true, 'Show the unread posts count in the tab title.'],
  93.         'Hide Unread Count at (0)': [false, 'Hide the unread posts count when it reaches 0.'],
  94.         'Unread Tab Icon': [true, 'Show a different favicon when there are unread posts.'],
  95.         'Unread Line': [true, 'Show a line to distinguish read posts from unread ones.'],
  96.         'Scroll to Last Read Post': [true, 'Scroll back to the last read post when reopening a thread.'],
  97.         'Thread Excerpt': [true, 'Show an excerpt of the thread in the tab title.'],
  98.         'Thread Stats': [true, 'Display reply, image, and page count.'],
  99.         'Thread Watcher': [true, 'Bookmark threads.'],
  100.         'Color User IDs': [true, 'Assign unique colors to user IDs on boards that use them.']
  101.       },
  102.       'Posting': {
  103.         'Quick Reply': [true, 'All-in-one form to reply, create threads, automate dumping and more.'],
  104.         'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'],
  105.         'Auto-Hide QR': [false, 'Automatically hide the quick reply when posting.'],
  106.         'Open Post in New Tab': [true, 'Open new threads or replies to a thread from the index in a new tab.'],
  107.         'Remember QR Size': [false, 'Remember the size of the Quick reply.'],
  108.         'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'],
  109.         'Hide Original Post Form': [true, 'Hide the normal post form.'],
  110.         'Cooldown': [true, 'Indicate the remaining time before posting again.']
  111.       },
  112.       'Quote Links': {
  113.         'Quote Backlinks': [true, 'Add quote backlinks.'],
  114.         'OP Backlinks': [true, 'Add backlinks to the OP.'],
  115.         'Quote Inlining': [true, 'Inline quoted post on click.'],
  116.         'Forward Hiding': [true, 'Hide original posts of inlined backlinks.'],
  117.         'Quote Previewing': [true, 'Show quoted post on hover.'],
  118.         'Quote Highlighting': [true, 'Highlight the previewed post.'],
  119.         'Resurrect Quotes': [true, 'Link dead quotes to the archives.'],
  120.         'Quote Markers': [true, 'Add "(You)", "(OP)", "(Cross-thread)", "(Dead)" markers to quote links.']
  121.       }
  122.     },
  123.     imageExpansion: {
  124.       'Fit width': [true, ''],
  125.       'Fit height': [false, ''],
  126.       'Expand spoilers': [false, 'Expand all images along with spoilers.'],
  127.       'Expand from here': [true, 'Expand all images only from current position to thread end.']
  128.     },
  129.     threadWatcher: {
  130.       'Current Board': [false, 'Only show watched threads from the current board.'],
  131.       'Auto Watch': [true, 'Automatically watch threads you start.'],
  132.       'Auto Watch Reply': [false, 'Automatically watch threads you reply to.'],
  133.       'Auto Prune': [false, 'Automatically prune 404\'d threads.']
  134.     },
  135.     filter: {
  136.       name: "# Filter any namefags:\n#/^(?!Anonymous$)/",
  137.       uniqueID: "# Filter a specific ID:\n#/Txhvk1Tl/",
  138.       tripcode: "# Filter any tripfag\n#/^!/",
  139.       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",
  140.       email: "",
  141.       subject: "# Filter Generals on /v/:\n#/general/i;boards:v;op:only",
  142.       comment: "# Filter Stallman copypasta on /g/:\n#/what you're refer+ing to as linux/i;boards:g",
  143.       flag: "",
  144.       filename: "",
  145.       dimensions: "# Highlight potential wallpapers:\n#/1920x1080/;op:yes;highlight;top:no;boards:w,wg",
  146.       filesize: "",
  147.       MD5: ""
  148.     },
  149.     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/",
  150.     'Custom CSS': false,
  151.     Index: {
  152.       'Index Mode': 'paged',
  153.       'Previous Index Mode': 'paged',
  154.       'Index Sort': 'bump',
  155.       'Index Size': 'small',
  156.       'Threads per Page': 0,
  157.       'Open threads in a new tab': false,
  158.       'Show Replies': true,
  159.       'Refreshed Navigation': false
  160.     },
  161.     Header: {
  162.       'Header auto-hide': false,
  163.       'Header auto-hide on scroll': false,
  164.       'Bottom header': false,
  165.       'Top Board List': false,
  166.       'Bottom Board List': false,
  167.       'Custom Board Navigation': true
  168.     },
  169.     QR: {
  170.       'QR.personas': "#email:\"sage\";boards:jp;always\nemail:\"sage\""
  171.     },
  172.     boardnav: '[current-title / toggle-all]',
  173.     time: '%m/%d/%y(%a)%H:%M:%S',
  174.     backlink: '>>%id',
  175.     fileInfo: '%l (%p%s, %r)',
  176.     favicon: 'ferongr',
  177.     usercss: '',
  178.     hotkeys: {
  179.       'Toggle board list': ['Ctrl+b', 'Toggle the full board list.'],
  180.       'Open empty QR': ['q', 'Open QR without post number inserted.'],
  181.       'Open QR': ['Shift+q', 'Open QR with post number inserted.'],
  182.       'Open settings': ['Alt+o', 'Open Settings.'],
  183.       'Close': ['Esc', 'Close Settings, Notifications or QR.'],
  184.       'Spoiler tags': ['Ctrl+s', 'Insert spoiler tags.'],
  185.       'Code tags': ['Alt+c', 'Insert code tags.'],
  186.       'Eqn tags': ['Alt+e', 'Insert eqn tags.'],
  187.       'Math tags': ['Alt+m', 'Insert math tags.'],
  188.       'Submit QR': ['Alt+s', 'Submit post.'],
  189.       'Update': ['r', 'Refresh the index/thread.'],
  190.       'Watch': ['w', 'Watch thread.'],
  191.       'Expand image': ['Shift+e', 'Expand selected image.'],
  192.       'Expand images': ['e', 'Expand all images.'],
  193.       'Front page': ['0', 'Jump to page 0.'],
  194.       'Open front page': ['Shift+0', 'Open page 0 in a new tab.'],
  195.       'Next page': ['Right', 'Jump to the next page.'],
  196.       'Previous page': ['Left', 'Jump to the previous page.'],
  197.       'Search form': ['Ctrl+Alt+s', 'Focus the search field on the board index.'],
  198.       'Paged mode': ['Ctrl+1', 'Sets the index mode to paged.'],
  199.       'All pages mode': ['Ctrl+2', 'Sets the index mode to all threads.'],
  200.       'Catalog mode': ['Ctrl+3', 'Sets the index mode to catalog.'],
  201.       'Cycle sort type': ['Ctrl+x', 'Cycle through index sort types.'],
  202.       'Next thread': ['Down', 'See next thread.'],
  203.       'Previous thread': ['Up', 'See previous thread.'],
  204.       'Expand thread': ['Ctrl+e', 'Expand thread.'],
  205.       'Open thread': ['o', 'Open thread in current tab.'],
  206.       'Open thread tab': ['Shift+o', 'Open thread in new tab.'],
  207.       'Next reply': ['j', 'Select next reply.'],
  208.       'Previous reply': ['k', 'Select previous reply.'],
  209.       'Deselect reply': ['Shift+d', 'Deselect reply.'],
  210.       'Hide': ['x', 'Hide thread.']
  211.     },
  212.     updater: {
  213.       checkbox: {
  214.         'Beep': [false, 'Beep on new post to completely read thread.'],
  215.         'Auto Scroll': [false, 'Scroll updated posts into view. Only enabled at bottom of page.'],
  216.         'Bottom Scroll': [false, 'Always scroll to the bottom, not the first new post. Useful for event threads.'],
  217.         'Scroll BG': [false, 'Auto-scroll background tabs.'],
  218.         'Auto Update': [true, 'Automatically fetch new posts.']
  219.       },
  220.       'Interval': 30
  221.     }
  222.   };
  223.  
  224.   Conf = {};
  225.  
  226.   c = console;
  227.  
  228.   d = document;
  229.  
  230.   doc = d.documentElement;
  231.  
  232.   g = {
  233.     VERSION: '3.20.15',
  234.     NAMESPACE: '4chan X.',
  235.     boards: {},
  236.     threads: {},
  237.     posts: {}
  238.   };
  239.  
  240.   $ = function(selector, root) {
  241.     if (root == null) {
  242.       root = d.body;
  243.     }
  244.     return root.querySelector(selector);
  245.   };
  246.  
  247.   $$ = function(selector, root) {
  248.     if (root == null) {
  249.       root = d.body;
  250.     }
  251.     return __slice.call(root.querySelectorAll(selector));
  252.   };
  253.  
  254.   $.SECOND = 1000;
  255.  
  256.   $.MINUTE = 1000 * 60;
  257.  
  258.   $.HOUR = 1000 * 60 * 60;
  259.  
  260.   $.DAY = 1000 * 60 * 60 * 24;
  261.  
  262.   $.id = function(id) {
  263.     return d.getElementById(id);
  264.   };
  265.  
  266.   $.ready = function(fc) {
  267.     var cb;
  268.     if (d.readyState !== 'loading') {
  269.       $.queueTask(fc);
  270.       return;
  271.     }
  272.     cb = function() {
  273.       $.off(d, 'DOMContentLoaded', cb);
  274.       return fc();
  275.     };
  276.     return $.on(d, 'DOMContentLoaded', cb);
  277.   };
  278.  
  279.   $.formData = function(form) {
  280.     var fd, key, val;
  281.     if (form instanceof HTMLFormElement) {
  282.       return new FormData(form);
  283.     }
  284.     fd = new FormData();
  285.     for (key in form) {
  286.       val = form[key];
  287.       if (val) {
  288.         if (typeof val === 'object' && 'newName' in val) {
  289.           fd.append(key, val, val.newName);
  290.         } else {
  291.           fd.append(key, val);
  292.         }
  293.       }
  294.     }
  295.     return fd;
  296.   };
  297.  
  298.   $.extend = function(object, properties) {
  299.     var key, val;
  300.     for (key in properties) {
  301.       val = properties[key];
  302.       object[key] = val;
  303.     }
  304.   };
  305.  
  306.   $.ajax = (function() {
  307.     var lastModified;
  308.     lastModified = {};
  309.     return function(url, options, extra) {
  310.       var form, r, sync, type, upCallbacks, whenModified;
  311.       if (extra == null) {
  312.         extra = {};
  313.       }
  314.       type = extra.type, whenModified = extra.whenModified, upCallbacks = extra.upCallbacks, form = extra.form, sync = extra.sync;
  315.       r = new XMLHttpRequest();
  316.       type || (type = form && 'post' || 'get');
  317.       r.open(type, url, !sync);
  318.       if (whenModified) {
  319.         if (url in lastModified) {
  320.           r.setRequestHeader('If-Modified-Since', lastModified[url]);
  321.         }
  322.         $.on(r, 'load', function() {
  323.           return lastModified[url] = r.getResponseHeader('Last-Modified');
  324.         });
  325.       }
  326.       if (/\.json$/.test(url)) {
  327.         r.responseType = 'json';
  328.       }
  329.       $.extend(r, options);
  330.       $.extend(r.upload, upCallbacks);
  331.       r.send(form);
  332.       return r;
  333.     };
  334.   })();
  335.  
  336.   $.cache = (function() {
  337.     var reqs;
  338.     reqs = {};
  339.     return function(url, cb, options) {
  340.       var req, rm;
  341.       if (req = reqs[url]) {
  342.         if (req.readyState === 4) {
  343.           cb.call(req, req.evt);
  344.         } else {
  345.           req.callbacks.push(cb);
  346.         }
  347.         return;
  348.       }
  349.       rm = function() {
  350.         return delete reqs[url];
  351.       };
  352.       req = $.ajax(url, options);
  353.       $.on(req, 'load', function(e) {
  354.         var _i, _len, _ref;
  355.         _ref = this.callbacks;
  356.         for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  357.           cb = _ref[_i];
  358.           cb.call(this, e);
  359.         }
  360.         this.evt = e;
  361.         return delete this.callbacks;
  362.       });
  363.       $.on(req, 'abort', rm);
  364.       $.on(req, 'error', rm);
  365.       req.callbacks = [cb];
  366.       return reqs[url] = req;
  367.     };
  368.   })();
  369.  
  370.   $.cb = {
  371.     checked: function() {
  372.       $.set(this.name, this.checked);
  373.       return Conf[this.name] = this.checked;
  374.     },
  375.     value: function() {
  376.       $.set(this.name, this.value.trim());
  377.       return Conf[this.name] = this.value;
  378.     }
  379.   };
  380.  
  381.   $.asap = function(test, cb) {
  382.     if (test()) {
  383.       return cb();
  384.     } else {
  385.       return setTimeout($.asap, 25, test, cb);
  386.     }
  387.   };
  388.  
  389.   $.addStyle = function(css) {
  390.     var style;
  391.     style = $.el('style', {
  392.       textContent: css
  393.     });
  394.     $.asap((function() {
  395.       return d.head;
  396.     }), function() {
  397.       return $.add(d.head, style);
  398.     });
  399.     return style;
  400.   };
  401.  
  402.   $.x = function(path, root) {
  403.     if (root == null) {
  404.       root = d.body;
  405.     }
  406.     return d.evaluate(path, root, null, 8, null).singleNodeValue;
  407.   };
  408.  
  409.   $.addClass = function() {
  410.     var className, el, _ref;
  411.     el = arguments[0], className = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
  412.     return (_ref = el.classList).add.apply(_ref, className);
  413.   };
  414.  
  415.   $.rmClass = function() {
  416.     var className, el, _ref;
  417.     el = arguments[0], className = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
  418.     return (_ref = el.classList).remove.apply(_ref, className);
  419.   };
  420.  
  421.   $.hasClass = function(el, className) {
  422.     return el.classList.contains(className);
  423.   };
  424.  
  425.   $.rm = function(el) {
  426.     return el.remove();
  427.   };
  428.  
  429.   $.rmAll = function(root) {
  430.     return root.textContent = null;
  431.   };
  432.  
  433.   $.tn = function(s) {
  434.     return d.createTextNode(s);
  435.   };
  436.  
  437.   $.nodes = function(nodes) {
  438.     var frag, node, _i, _len;
  439.     if (!(nodes instanceof Array)) {
  440.       return nodes;
  441.     }
  442.     frag = d.createDocumentFragment();
  443.     for (_i = 0, _len = nodes.length; _i < _len; _i++) {
  444.       node = nodes[_i];
  445.       frag.appendChild(node);
  446.     }
  447.     return frag;
  448.   };
  449.  
  450.   $.add = function(parent, el) {
  451.     return parent.appendChild($.nodes(el));
  452.   };
  453.  
  454.   $.prepend = function(parent, el) {
  455.     return parent.insertBefore($.nodes(el), parent.firstChild);
  456.   };
  457.  
  458.   $.after = function(root, el) {
  459.     return root.parentNode.insertBefore($.nodes(el), root.nextSibling);
  460.   };
  461.  
  462.   $.before = function(root, el) {
  463.     return root.parentNode.insertBefore($.nodes(el), root);
  464.   };
  465.  
  466.   $.replace = function(root, el) {
  467.     return root.parentNode.replaceChild($.nodes(el), root);
  468.   };
  469.  
  470.   $.el = function(tag, properties) {
  471.     var el;
  472.     el = d.createElement(tag);
  473.     if (properties) {
  474.       $.extend(el, properties);
  475.     }
  476.     return el;
  477.   };
  478.  
  479.   $.on = function(el, events, handler) {
  480.     var event, _i, _len, _ref;
  481.     _ref = events.split(' ');
  482.     for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  483.       event = _ref[_i];
  484.       el.addEventListener(event, handler, false);
  485.     }
  486.   };
  487.  
  488.   $.off = function(el, events, handler) {
  489.     var event, _i, _len, _ref;
  490.     _ref = events.split(' ');
  491.     for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  492.       event = _ref[_i];
  493.       el.removeEventListener(event, handler, false);
  494.     }
  495.   };
  496.  
  497.   $.event = function(event, detail, root) {
  498.     if (root == null) {
  499.       root = d;
  500.     }
  501.     if ((detail != null) && typeof cloneInto === 'function') {
  502.       detail = cloneInto(detail, d.defaultView);
  503.     }
  504.     return root.dispatchEvent(new CustomEvent(event, {
  505.       bubbles: true,
  506.       detail: detail
  507.     }));
  508.   };
  509.  
  510.   $.open = GM_openInTab;
  511.  
  512.   $.debounce = function(wait, fn) {
  513.     var args, exec, lastCall, that, timeout;
  514.     lastCall = 0;
  515.     timeout = null;
  516.     that = null;
  517.     args = null;
  518.     exec = function() {
  519.       lastCall = Date.now();
  520.       return fn.apply(that, args);
  521.     };
  522.     return function() {
  523.       args = arguments;
  524.       that = this;
  525.       if (lastCall < Date.now() - wait) {
  526.         return exec();
  527.       }
  528.       clearTimeout(timeout);
  529.       return timeout = setTimeout(exec, wait);
  530.     };
  531.   };
  532.  
  533.   $.queueTask = (function() {
  534.     var execTask, taskChannel, taskQueue;
  535.     taskQueue = [];
  536.     execTask = function() {
  537.       var args, func, task;
  538.       task = taskQueue.shift();
  539.       func = task[0];
  540.       args = Array.prototype.slice.call(task, 1);
  541.       return func.apply(func, args);
  542.     };
  543.     if (window.MessageChannel) {
  544.       taskChannel = new MessageChannel();
  545.       taskChannel.port1.onmessage = execTask;
  546.       return function() {
  547.         taskQueue.push(arguments);
  548.         return taskChannel.port2.postMessage(null);
  549.       };
  550.     } else {
  551.       return function() {
  552.         taskQueue.push(arguments);
  553.         return setTimeout(execTask, 0);
  554.       };
  555.     }
  556.   })();
  557.  
  558.   $.globalEval = function(code) {
  559.     var script;
  560.     script = $.el('script', {
  561.       textContent: code
  562.     });
  563.     $.add(d.head || doc, script);
  564.     return $.rm(script);
  565.   };
  566.  
  567.   $.bytesToString = function(size) {
  568.     var unit;
  569.     unit = 0;
  570.     while (size >= 1024) {
  571.       size /= 1024;
  572.       unit++;
  573.     }
  574.     size = unit > 1 ? Math.round(size * 100) / 100 : Math.round(size);
  575.     return "" + size + " " + ['B', 'KB', 'MB', 'GB'][unit];
  576.   };
  577.  
  578.   $.item = function(key, val) {
  579.     var item;
  580.     item = {};
  581.     item[key] = val;
  582.     return item;
  583.   };
  584.  
  585.   $.syncing = {};
  586.  
  587.   $.sync = (function() {
  588.     $.on(window, 'storage', function(_arg) {
  589.       var cb, key, newValue;
  590.       key = _arg.key, newValue = _arg.newValue;
  591.       if (cb = $.syncing[key]) {
  592.         return cb(JSON.parse(newValue), key);
  593.       }
  594.     });
  595.     return function(key, cb) {
  596.       return $.syncing[g.NAMESPACE + key] = cb;
  597.     };
  598.   })();
  599.  
  600.   $["delete"] = function(keys) {
  601.     var key, _i, _len;
  602.     if (!(keys instanceof Array)) {
  603.       keys = [keys];
  604.     }
  605.     for (_i = 0, _len = keys.length; _i < _len; _i++) {
  606.       key = keys[_i];
  607.       key = g.NAMESPACE + key;
  608.       localStorage.removeItem(key);
  609.       GM_deleteValue(key);
  610.     }
  611.   };
  612.  
  613.   $.get = function(key, val, cb) {
  614.     var items;
  615.     if (typeof cb === 'function') {
  616.       items = $.item(key, val);
  617.     } else {
  618.       items = key;
  619.       cb = val;
  620.     }
  621.     return $.queueTask(function() {
  622.       for (key in items) {
  623.         if (val = GM_getValue(g.NAMESPACE + key)) {
  624.           items[key] = JSON.parse(val);
  625.         }
  626.       }
  627.       return cb(items);
  628.     });
  629.   };
  630.  
  631.   $.set = (function() {
  632.     var set;
  633.     set = function(key, val) {
  634.       key = g.NAMESPACE + key;
  635.       val = JSON.stringify(val);
  636.       if (key in $.syncing) {
  637.         localStorage.setItem(key, val);
  638.       }
  639.       return GM_setValue(key, val);
  640.     };
  641.     return function(keys, val) {
  642.       var key;
  643.       if (typeof keys === 'string') {
  644.         set(keys, val);
  645.         return;
  646.       }
  647.       for (key in keys) {
  648.         val = keys[key];
  649.         set(key, val);
  650.       }
  651.     };
  652.   })();
  653.  
  654.   $.clear = function(cb) {
  655.     $["delete"](GM_listValues().map(function(key) {
  656.       return key.replace(g.NAMESPACE, '');
  657.     }));
  658.     return typeof cb === "function" ? cb() : void 0;
  659.   };
  660.  
  661.   Polyfill = {
  662.     init: function() {},
  663.     toBlob: function() {
  664.       var _base;
  665.       return (_base = HTMLCanvasElement.prototype).toBlob || (_base.toBlob = function(cb) {
  666.         var data, i, l, ui8a, _i;
  667.         data = atob(this.toDataURL().slice(22));
  668.         l = data.length;
  669.         ui8a = new Uint8Array(l);
  670.         for (i = _i = 0; _i < l; i = _i += 1) {
  671.           ui8a[i] = data.charCodeAt(i);
  672.         }
  673.         return cb(new Blob([ui8a], {
  674.           type: 'image/png'
  675.         }));
  676.       });
  677.     }
  678.   };
  679.  
  680.   UI = (function() {
  681.     var Menu, dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchend, touchmove;
  682.     dialog = function(id, position, html) {
  683.       var el;
  684.       el = $.el('div', {
  685.         className: 'dialog',
  686.         innerHTML: html,
  687.         id: id
  688.       });
  689.       el.style.cssText = position;
  690.       $.get("" + id + ".position", position, function(item) {
  691.         return el.style.cssText = item["" + id + ".position"];
  692.       });
  693.       $.on($('.move', el), 'touchstart mousedown', dragstart);
  694.       return el;
  695.     };
  696.     Menu = (function() {
  697.       var currentMenu, lastToggledButton;
  698.  
  699.       currentMenu = null;
  700.  
  701.       lastToggledButton = null;
  702.  
  703.       function Menu() {
  704.         this.onFocus = __bind(this.onFocus, this);
  705.         this.keybinds = __bind(this.keybinds, this);
  706.         this.close = __bind(this.close, this);
  707.         this.entries = [];
  708.       }
  709.  
  710.       Menu.prototype.makeMenu = function() {
  711.         var menu;
  712.         menu = $.el('div', {
  713.           className: 'dialog',
  714.           id: 'menu',
  715.           tabIndex: 0
  716.         });
  717.         $.on(menu, 'click', function(e) {
  718.           return e.stopPropagation();
  719.         });
  720.         $.on(menu, 'keydown', this.keybinds);
  721.         return menu;
  722.       };
  723.  
  724.       Menu.prototype.toggle = function(e, button, data) {
  725.         var previousButton;
  726.         e.preventDefault();
  727.         e.stopPropagation();
  728.         if (currentMenu) {
  729.           previousButton = lastToggledButton;
  730.           this.close();
  731.           if (previousButton === button) {
  732.             return;
  733.           }
  734.         }
  735.         if (!this.entries.length) {
  736.           return;
  737.         }
  738.         return this.open(button, data);
  739.       };
  740.  
  741.       Menu.prototype.open = function(button, data) {
  742.         var bLeft, bRect, bTop, cHeight, cWidth, entry, mRect, menu, prevEntry, _i, _len, _ref;
  743.         menu = this.makeMenu();
  744.         currentMenu = menu;
  745.         lastToggledButton = button;
  746.         $.addClass(button, 'open');
  747.         _ref = this.entries;
  748.         for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  749.           entry = _ref[_i];
  750.           this.insertEntry(entry, menu, data);
  751.         }
  752.         entry = $('.entry', menu);
  753.         while (prevEntry = this.findNextEntry(entry, -1)) {
  754.           entry = prevEntry;
  755.         }
  756.         this.focus(entry);
  757.         $.on(d, 'click', this.close);
  758.         $.on(d, 'CloseMenu', this.close);
  759.         $.add(button, menu);
  760.         mRect = menu.getBoundingClientRect();
  761.         bRect = button.getBoundingClientRect();
  762.         bTop = window.scrollY + bRect.top;
  763.         bLeft = window.scrollX + bRect.left;
  764.         cHeight = doc.clientHeight;
  765.         cWidth = doc.clientWidth;
  766.         if (bRect.top + bRect.height + mRect.height < cHeight) {
  767.           $.addClass(menu, 'top');
  768.           $.rmClass(menu, 'bottom');
  769.         } else {
  770.           $.addClass(menu, 'bottom');
  771.           $.rmClass(menu, 'top');
  772.         }
  773.         if (bRect.left + mRect.width < cWidth) {
  774.           $.addClass(menu, 'left');
  775.           $.rmClass(menu, 'right');
  776.         } else {
  777.           $.addClass(menu, 'right');
  778.           $.rmClass(menu, 'left');
  779.         }
  780.         return menu.focus();
  781.       };
  782.  
  783.       Menu.prototype.insertEntry = function(entry, parent, data) {
  784.         var subEntry, submenu, _i, _len, _ref;
  785.         if (typeof entry.open === 'function') {
  786.           if (!entry.open(data, (function(_this) {
  787.             return function(subEntry) {
  788.               _this.parseEntry(subEntry);
  789.               return entry.subEntries.push(subEntry);
  790.             };
  791.           })(this))) {
  792.             return;
  793.           }
  794.         }
  795.         $.add(parent, entry.el);
  796.         if (!entry.subEntries) {
  797.           return;
  798.         }
  799.         if (submenu = $('.submenu', entry.el)) {
  800.           $.rm(submenu);
  801.         }
  802.         submenu = $.el('div', {
  803.           className: 'dialog submenu'
  804.         });
  805.         _ref = entry.subEntries;
  806.         for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  807.           subEntry = _ref[_i];
  808.           this.insertEntry(subEntry, submenu, data);
  809.         }
  810.         $.add(entry.el, submenu);
  811.       };
  812.  
  813.       Menu.prototype.close = function() {
  814.         $.rm(currentMenu);
  815.         $.rmClass(lastToggledButton, 'open');
  816.         currentMenu = null;
  817.         lastToggledButton = null;
  818.         return $.off(d, 'click CloseMenu', this.close);
  819.       };
  820.  
  821.       Menu.prototype.findNextEntry = function(entry, direction) {
  822.         var entries;
  823.         entries = __slice.call(entry.parentNode.children);
  824.         entries.sort(function(first, second) {
  825.           return first.style.order - second.style.order;
  826.         });
  827.         return entries[entries.indexOf(entry) + direction];
  828.       };
  829.  
  830.       Menu.prototype.keybinds = function(e) {
  831.         var entry, next, nextPrev, subEntry, submenu;
  832.         entry = $('.focused', currentMenu);
  833.         while (subEntry = $('.focused', entry)) {
  834.           entry = subEntry;
  835.         }
  836.         switch (e.keyCode) {
  837.           case 27:
  838.             lastToggledButton.focus();
  839.             this.close();
  840.             break;
  841.           case 13:
  842.           case 32:
  843.             entry.click();
  844.             break;
  845.           case 38:
  846.             if (next = this.findNextEntry(entry, -1)) {
  847.               this.focus(next);
  848.             }
  849.             break;
  850.           case 40:
  851.             if (next = this.findNextEntry(entry, +1)) {
  852.               this.focus(next);
  853.             }
  854.             break;
  855.           case 39:
  856.             if ((submenu = $('.submenu', entry)) && (next = submenu.firstElementChild)) {
  857.               while (nextPrev = this.findNextEntry(next, -1)) {
  858.                 next = nextPrev;
  859.               }
  860.               this.focus(next);
  861.             }
  862.             break;
  863.           case 37:
  864.             if (next = $.x('parent::*[contains(@class,"submenu")]/parent::*', entry)) {
  865.               this.focus(next);
  866.             }
  867.             break;
  868.           default:
  869.             return;
  870.         }
  871.         e.preventDefault();
  872.         return e.stopPropagation();
  873.       };
  874.  
  875.       Menu.prototype.onFocus = function(e) {
  876.         e.stopPropagation();
  877.         return this.focus(e.target);
  878.       };
  879.  
  880.       Menu.prototype.focus = function(entry) {
  881.         var cHeight, cWidth, eRect, focused, sRect, submenu, _i, _len, _ref;
  882.         while (focused = $.x('parent::*/child::*[contains(@class,"focused")]', entry)) {
  883.           $.rmClass(focused, 'focused');
  884.         }
  885.         _ref = $$('.focused', entry);
  886.         for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  887.           focused = _ref[_i];
  888.           $.rmClass(focused, 'focused');
  889.         }
  890.         $.addClass(entry, 'focused');
  891.         if (!(submenu = $('.submenu', entry))) {
  892.           return;
  893.         }
  894.         sRect = submenu.getBoundingClientRect();
  895.         eRect = entry.getBoundingClientRect();
  896.         cHeight = doc.clientHeight;
  897.         cWidth = doc.clientWidth;
  898.         if (eRect.top + sRect.height < cHeight) {
  899.           $.addClass(submenu, 'top');
  900.           $.rmClass(submenu, 'bottom');
  901.         } else {
  902.           $.addClass(submenu, 'bottom');
  903.           $.rmClass(submenu, 'top');
  904.         }
  905.         if (eRect.right + sRect.width < cWidth) {
  906.           $.addClass(submenu, 'left');
  907.           return $.rmClass(submenu, 'right');
  908.         } else {
  909.           $.addClass(submenu, 'right');
  910.           return $.rmClass(submenu, 'left');
  911.         }
  912.       };
  913.  
  914.       Menu.prototype.addEntry = function(entry) {
  915.         this.parseEntry(entry);
  916.         return this.entries.push(entry);
  917.       };
  918.  
  919.       Menu.prototype.parseEntry = function(entry) {
  920.         var el, subEntries, subEntry, _i, _len;
  921.         el = entry.el, subEntries = entry.subEntries;
  922.         $.addClass(el, 'entry');
  923.         $.on(el, 'focus mouseover', this.onFocus);
  924.         el.style.order = entry.order || 100;
  925.         if (!subEntries) {
  926.           return;
  927.         }
  928.         $.addClass(el, 'has-submenu');
  929.         for (_i = 0, _len = subEntries.length; _i < _len; _i++) {
  930.           subEntry = subEntries[_i];
  931.           this.parseEntry(subEntry);
  932.         }
  933.       };
  934.  
  935.       return Menu;
  936.  
  937.     })();
  938.     dragstart = function(e) {
  939.       var el, isTouching, o, rect, screenHeight, screenWidth, _ref;
  940.       if (e.type === 'mousedown' && e.button !== 0) {
  941.         return;
  942.       }
  943.       e.preventDefault();
  944.       if (isTouching = e.type === 'touchstart') {
  945.         _ref = e.changedTouches, e = _ref[_ref.length - 1];
  946.       }
  947.       el = $.x('ancestor::div[contains(@class,"dialog")][1]', this);
  948.       rect = el.getBoundingClientRect();
  949.       screenHeight = doc.clientHeight;
  950.       screenWidth = doc.clientWidth;
  951.       o = {
  952.         id: el.id,
  953.         style: el.style,
  954.         dx: e.clientX - rect.left,
  955.         dy: e.clientY - rect.top,
  956.         height: screenHeight - rect.height,
  957.         width: screenWidth - rect.width,
  958.         screenHeight: screenHeight,
  959.         screenWidth: screenWidth,
  960.         isTouching: isTouching
  961.       };
  962.       if (isTouching) {
  963.         o.identifier = e.identifier;
  964.         o.move = touchmove.bind(o);
  965.         o.up = touchend.bind(o);
  966.         $.on(d, 'touchmove', o.move);
  967.         return $.on(d, 'touchend touchcancel', o.up);
  968.       } else {
  969.         o.move = drag.bind(o);
  970.         o.up = dragend.bind(o);
  971.         $.on(d, 'mousemove', o.move);
  972.         return $.on(d, 'mouseup', o.up);
  973.       }
  974.     };
  975.     touchmove = function(e) {
  976.       var touch, _i, _len, _ref;
  977.       _ref = e.changedTouches;
  978.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  979.         touch = _ref[_i];
  980.         if (touch.identifier === this.identifier) {
  981.           drag.call(this, touch);
  982.           return;
  983.         }
  984.       }
  985.     };
  986.     drag = function(e) {
  987.       var bottom, clientX, clientY, left, right, style, top;
  988.       clientX = e.clientX, clientY = e.clientY;
  989.       left = clientX - this.dx;
  990.       left = left < 10 ? 0 : this.width - left < 10 ? null : left / this.screenWidth * 100 + '%';
  991.       top = clientY - this.dy;
  992.       top = top < 10 ? 0 : this.height - top < 10 ? null : top / this.screenHeight * 100 + '%';
  993.       right = left === null ? 0 : null;
  994.       bottom = top === null ? 0 : null;
  995.       style = this.style;
  996.       style.left = left;
  997.       style.right = right;
  998.       style.top = top;
  999.       return style.bottom = bottom;
  1000.     };
  1001.     touchend = function(e) {
  1002.       var touch, _i, _len, _ref;
  1003.       _ref = e.changedTouches;
  1004.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  1005.         touch = _ref[_i];
  1006.         if (touch.identifier === this.identifier) {
  1007.           dragend.call(this);
  1008.           return;
  1009.         }
  1010.       }
  1011.     };
  1012.     dragend = function() {
  1013.       if (this.isTouching) {
  1014.         $.off(d, 'touchmove', this.move);
  1015.         $.off(d, 'touchend touchcancel', this.up);
  1016.       } else {
  1017.         $.off(d, 'mousemove', this.move);
  1018.         $.off(d, 'mouseup', this.up);
  1019.       }
  1020.       return $.set("" + this.id + ".position", this.style.cssText);
  1021.     };
  1022.     hoverstart = function(_arg) {
  1023.       var asapTest, cb, el, endEvents, latestEvent, o, offsetX, offsetY, root;
  1024.       root = _arg.root, el = _arg.el, latestEvent = _arg.latestEvent, endEvents = _arg.endEvents, asapTest = _arg.asapTest, cb = _arg.cb, offsetX = _arg.offsetX, offsetY = _arg.offsetY;
  1025.       o = {
  1026.         root: root,
  1027.         el: el,
  1028.         style: el.style,
  1029.         cb: cb,
  1030.         endEvents: endEvents,
  1031.         latestEvent: latestEvent,
  1032.         clientHeight: doc.clientHeight,
  1033.         clientWidth: doc.clientWidth,
  1034.         offsetX: offsetX || 45,
  1035.         offsetY: offsetY || -120
  1036.       };
  1037.       o.hover = hover.bind(o);
  1038.       o.hoverend = hoverend.bind(o);
  1039.       if (asapTest) {
  1040.         $.asap(function() {
  1041.           return !el.parentNode || asapTest();
  1042.         }, function() {
  1043.           if (el.parentNode) {
  1044.             return o.hover(o.latestEvent);
  1045.           }
  1046.         });
  1047.       }
  1048.       $.on(root, endEvents, o.hoverend);
  1049.       $.on(root, 'mousemove', o.hover);
  1050.       o.workaround = function(e) {
  1051.         if (!root.contains(e.target)) {
  1052.           return o.hoverend();
  1053.         }
  1054.       };
  1055.       return $.on(doc, 'mousemove', o.workaround);
  1056.     };
  1057.     hover = function(e) {
  1058.       var clientX, clientY, height, left, right, style, top, _ref;
  1059.       this.latestEvent = e;
  1060.       height = this.el.offsetHeight;
  1061.       clientX = e.clientX, clientY = e.clientY;
  1062.       top = clientY + this.offsetY;
  1063.       top = this.clientHeight <= height || top <= 0 ? 0 : top + height >= this.clientHeight ? this.clientHeight - height : top;
  1064.       _ref = clientX <= this.clientWidth / 2 ? [clientX + this.offsetX + 'px', null] : [null, this.clientWidth - clientX + this.offsetX + 'px'], left = _ref[0], right = _ref[1];
  1065.       style = this.style;
  1066.       style.top = top + 'px';
  1067.       style.left = left;
  1068.       return style.right = right;
  1069.     };
  1070.     hoverend = function() {
  1071.       $.rm(this.el);
  1072.       $.off(this.root, this.endEvents, this.hoverend);
  1073.       $.off(this.root, 'mousemove', this.hover);
  1074.       $.off(doc, 'mousemove', this.workaround);
  1075.       if (this.cb) {
  1076.         return this.cb.call(this);
  1077.       }
  1078.     };
  1079.     return {
  1080.       dialog: dialog,
  1081.       Menu: Menu,
  1082.       hover: hoverstart
  1083.     };
  1084.   })();
  1085.  
  1086.   Header = {
  1087.     init: function() {
  1088.       var barPositionToggler, botBoardToggler, customNavToggler, editCustomNav, headerEl, headerToggler, menuButton, scrollHeaderToggler, topBoardToggler;
  1089.       headerEl = $.el('div', {
  1090.         id: 'header',
  1091.         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>"
  1092.       });
  1093.       this.bar = $('#header-bar', headerEl);
  1094.       this.hitzone = $('#header-bar-hitzone', this.bar);
  1095.       this.noticesRoot = $('#notifications', headerEl);
  1096.       this.menu = new UI.Menu();
  1097.       menuButton = $.el('a', {
  1098.         className: 'menu-button',
  1099.         innerHTML: '<i class="fa fa-bars"></i>',
  1100.         href: 'javascript:;'
  1101.       });
  1102.       $.on(menuButton, 'click', this.menuToggle);
  1103.       this.addShortcut(menuButton, 0);
  1104.       $.on(window, 'load hashchange', Header.hashScroll);
  1105.       $.on(d, 'CreateNotification', this.createNotification);
  1106.       headerToggler = $.el('label', {
  1107.         innerHTML: '<input type=checkbox name="Header auto-hide"> Auto-hide header'
  1108.       });
  1109.       scrollHeaderToggler = $.el('label', {
  1110.         innerHTML: '<input type=checkbox name="Header auto-hide on scroll"> Auto-hide header on scroll'
  1111.       });
  1112.       barPositionToggler = $.el('label', {
  1113.         innerHTML: '<input type=checkbox name="Bottom header"> Bottom header'
  1114.       });
  1115.       topBoardToggler = $.el('label', {
  1116.         innerHTML: '<input type=checkbox name="Top Board List"> Top original board list'
  1117.       });
  1118.       botBoardToggler = $.el('label', {
  1119.         innerHTML: '<input type=checkbox name="Bottom Board List"> Bottom original board list'
  1120.       });
  1121.       customNavToggler = $.el('label', {
  1122.         innerHTML: '<input type=checkbox name="Custom Board Navigation"> Custom board navigation'
  1123.       });
  1124.       editCustomNav = $.el('a', {
  1125.         textContent: 'Edit custom board navigation',
  1126.         href: 'javascript:;'
  1127.       });
  1128.       this.headerToggler = headerToggler.firstElementChild;
  1129.       this.scrollHeaderToggler = scrollHeaderToggler.firstElementChild;
  1130.       this.barPositionToggler = barPositionToggler.firstElementChild;
  1131.       this.topBoardToggler = topBoardToggler.firstElementChild;
  1132.       this.botBoardToggler = botBoardToggler.firstElementChild;
  1133.       this.customNavToggler = customNavToggler.firstElementChild;
  1134.       $.on(this.headerToggler, 'change', this.toggleBarVisibility);
  1135.       $.on(this.scrollHeaderToggler, 'change', this.toggleHideBarOnScroll);
  1136.       $.on(this.barPositionToggler, 'change', this.toggleBarPosition);
  1137.       $.on(this.topBoardToggler, 'change', this.toggleOriginalBoardList);
  1138.       $.on(this.botBoardToggler, 'change', this.toggleOriginalBoardList);
  1139.       $.on(this.customNavToggler, 'change', this.toggleCustomNav);
  1140.       $.on(editCustomNav, 'click', this.editCustomNav);
  1141.       this.setBarVisibility(Conf['Header auto-hide']);
  1142.       this.setHideBarOnScroll(Conf['Header auto-hide on scroll']);
  1143.       this.setBarPosition(Conf['Bottom header']);
  1144.       this.setTopBoardList(Conf['Top Board List']);
  1145.       this.setBotBoardList(Conf['Bottom Board List']);
  1146.       $.sync('Header auto-hide', this.setBarVisibility);
  1147.       $.sync('Header auto-hide on scroll', this.setHideBarOnScroll);
  1148.       $.sync('Bottom header', this.setBarPosition);
  1149.       $.sync('Top Board List', this.setTopBoardList);
  1150.       $.sync('Bottom Board List', this.setBotBoardList);
  1151.       this.menu.addEntry({
  1152.         el: $.el('span', {
  1153.           textContent: 'Header'
  1154.         }),
  1155.         order: 105,
  1156.         subEntries: [
  1157.           {
  1158.             el: headerToggler
  1159.           }, {
  1160.             el: scrollHeaderToggler
  1161.           }, {
  1162.             el: barPositionToggler
  1163.           }, {
  1164.             el: topBoardToggler
  1165.           }, {
  1166.             el: botBoardToggler
  1167.           }, {
  1168.             el: customNavToggler
  1169.           }, {
  1170.             el: editCustomNav
  1171.           }
  1172.         ]
  1173.       });
  1174.       $.asap((function() {
  1175.         return d.body;
  1176.       }), function() {
  1177.         if (!Main.isThisPageLegit()) {
  1178.           return;
  1179.         }
  1180.         $.asap((function() {
  1181.           return $.id('boardNavMobile') || d.readyState !== 'loading';
  1182.         }), Header.setBoardList);
  1183.         return $.prepend(d.body, headerEl);
  1184.       });
  1185.       $.ready(function() {
  1186.         var a;
  1187.         if (a = $("a[href*='/" + g.BOARD + "/']", $.id('boardNavDesktopFoot'))) {
  1188.           return a.className = 'current';
  1189.         }
  1190.       });
  1191.       return this.enableDesktopNotifications();
  1192.     },
  1193.     setBoardList: function() {
  1194.       var a, btn, fullBoardList, nav;
  1195.       nav = $.id('boardNavDesktop');
  1196.       if (a = $("a[href*='/" + g.BOARD + "/']", nav)) {
  1197.         a.className = 'current';
  1198.       }
  1199.       fullBoardList = $('#full-board-list', Header.bar);
  1200.       fullBoardList.innerHTML = nav.innerHTML;
  1201.       $.rm($('#navtopright', fullBoardList));
  1202.       btn = $.el('span', {
  1203.         className: 'hide-board-list-button brackets-wrap',
  1204.         innerHTML: '<a href=javascript:;> - </a>'
  1205.       });
  1206.       $.on(btn, 'click', Header.toggleBoardList);
  1207.       $.add(fullBoardList, btn);
  1208.       Header.setCustomNav(Conf['Custom Board Navigation']);
  1209.       Header.generateBoardList(Conf['boardnav']);
  1210.       $.sync('Custom Board Navigation', Header.setCustomNav);
  1211.       return $.sync('boardnav', Header.generateBoardList);
  1212.     },
  1213.     generateBoardList: function(text) {
  1214.       var as, list, nodes, re;
  1215.       list = $('#custom-board-list', Header.bar);
  1216.       $.rmAll(list);
  1217.       if (!text) {
  1218.         return;
  1219.       }
  1220.       as = $$('.boardList a[title]', Header.bar);
  1221.       re = /[\w@]+(-(all|title|replace|full|archive|(mode|sort|text):"[^"]+"))*|[^\w@]+/g;
  1222.       nodes = text.match(re).map(function(t) {
  1223.         var a, boardID, href, m, type, _i, _len;
  1224.         if (/^[^\w@]/.test(t)) {
  1225.           return $.tn(t);
  1226.         }
  1227.         if (/^toggle-all/.test(t)) {
  1228.           a = $.el('a', {
  1229.             className: 'show-board-list-button',
  1230.             textContent: (t.match(/-text:"(.+)"/) || [null, '+'])[1],
  1231.             href: 'javascript:;'
  1232.           });
  1233.           $.on(a, 'click', Header.toggleBoardList);
  1234.           return a;
  1235.         }
  1236.         boardID = t.split('-')[0];
  1237.         if (boardID === 'current') {
  1238.           boardID = g.BOARD.ID;
  1239.         }
  1240.         for (_i = 0, _len = as.length; _i < _len; _i++) {
  1241.           a = as[_i];
  1242.           if (!(a.textContent === boardID)) {
  1243.             continue;
  1244.           }
  1245.           a = a.cloneNode();
  1246.           break;
  1247.         }
  1248.         if (a.parentNode) {
  1249.           return $.tn(boardID);
  1250.         }
  1251.         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;
  1252.         if (/-archive/.test(t)) {
  1253.           if (href = Redirect.to('board', {
  1254.             boardID: boardID
  1255.           })) {
  1256.             a.href = href;
  1257.           } else {
  1258.             return a.firstChild;
  1259.           }
  1260.         }
  1261.         if (m = t.match(/-mode:"([^"]+)"/)) {
  1262.           type = m[1].toLowerCase();
  1263.           a.dataset.indexMode = (function() {
  1264.             switch (type) {
  1265.               case 'all threads':
  1266.                 return 'all pages';
  1267.               case 'paged':
  1268.               case 'catalog':
  1269.                 return type;
  1270.               default:
  1271.                 return 'paged';
  1272.             }
  1273.           })();
  1274.         }
  1275.         if (m = t.match(/-sort:"([^"]+)"/)) {
  1276.           type = m[1].toLowerCase();
  1277.           a.dataset.indexSort = (function() {
  1278.             switch (type) {
  1279.               case 'bump order':
  1280.                 return 'bump';
  1281.               case 'last reply':
  1282.                 return 'lastreply';
  1283.               case 'creation date':
  1284.                 return 'birth';
  1285.               case 'reply count':
  1286.                 return 'replycount';
  1287.               case 'file count':
  1288.                 return 'filecount';
  1289.               default:
  1290.                 return 'bump';
  1291.             }
  1292.           })();
  1293.         }
  1294.         if (boardID === '@') {
  1295.           $.addClass(a, 'navSmall');
  1296.         }
  1297.         return a;
  1298.       });
  1299.       return $.add(list, nodes);
  1300.     },
  1301.     toggleBoardList: function() {
  1302.       var bar, custom, full, showBoardList;
  1303.       bar = Header.bar;
  1304.       custom = $('#custom-board-list', bar);
  1305.       full = $('#full-board-list', bar);
  1306.       showBoardList = !full.hidden;
  1307.       custom.hidden = !showBoardList;
  1308.       return full.hidden = showBoardList;
  1309.     },
  1310.     setBarVisibility: function(hide) {
  1311.       Header.headerToggler.checked = hide;
  1312.       return (hide ? $.addClass : $.rmClass)(Header.bar, 'autohide');
  1313.     },
  1314.     toggleBarVisibility: function(e) {
  1315.       var hide;
  1316.       hide = this.checked;
  1317.       Conf['Header auto-hide'] = hide;
  1318.       $.set('Header auto-hide', hide);
  1319.       return Header.setBarVisibility(hide);
  1320.     },
  1321.     setHideBarOnScroll: function(hide) {
  1322.       Header.scrollHeaderToggler.checked = hide;
  1323.       if (hide) {
  1324.         $.on(window, 'scroll', Header.hideBarOnScroll);
  1325.         return;
  1326.       }
  1327.       $.off(window, 'scroll', Header.hideBarOnScroll);
  1328.       $.rmClass(Header.bar, 'scroll');
  1329.       if (!Conf['Header auto-hide']) {
  1330.         return $.rmClass(Header.bar, 'autohide');
  1331.       }
  1332.     },
  1333.     toggleHideBarOnScroll: function() {
  1334.       $.cb.checked.call(this);
  1335.       return Header.setHideBarOnScroll(this.checked);
  1336.     },
  1337.     hideBarOnScroll: function() {
  1338.       var offsetY;
  1339.       offsetY = window.pageYOffset;
  1340.       if (offsetY > (Header.previousOffset || 0)) {
  1341.         $.addClass(Header.bar, 'autohide', 'scroll');
  1342.       } else {
  1343.         $.rmClass(Header.bar, 'autohide', 'scroll');
  1344.       }
  1345.       return Header.previousOffset = offsetY;
  1346.     },
  1347.     setBarPosition: function(bottom) {
  1348.       Header.barPositionToggler.checked = bottom;
  1349.       $.event('CloseMenu');
  1350.       if (bottom) {
  1351.         $.addClass(doc, 'bottom-header');
  1352.         $.rmClass(doc, 'top-header');
  1353.         return Header.bar.parentNode.className = 'bottom';
  1354.       } else {
  1355.         $.addClass(doc, 'top-header');
  1356.         $.rmClass(doc, 'bottom-header');
  1357.         return Header.bar.parentNode.className = 'top';
  1358.       }
  1359.     },
  1360.     toggleBarPosition: function() {
  1361.       $.cb.checked.call(this);
  1362.       return Header.setBarPosition(this.checked);
  1363.     },
  1364.     setTopBoardList: function(show) {
  1365.       Header.topBoardToggler.checked = show;
  1366.       if (show) {
  1367.         return $.addClass(doc, 'show-original-top-board-list');
  1368.       } else {
  1369.         return $.rmClass(doc, 'show-original-top-board-list');
  1370.       }
  1371.     },
  1372.     setBotBoardList: function(show) {
  1373.       Header.botBoardToggler.checked = show;
  1374.       if (show) {
  1375.         return $.addClass(doc, 'show-original-bot-board-list');
  1376.       } else {
  1377.         return $.rmClass(doc, 'show-original-bot-board-list');
  1378.       }
  1379.     },
  1380.     toggleOriginalBoardList: function() {
  1381.       $.cb.checked.call(this);
  1382.       return (this.name === 'Top Board List' ? Header.setTopBoardList : Header.setBotBoardList)(this.checked);
  1383.     },
  1384.     setCustomNav: function(show) {
  1385.       var btn, cust, full, _ref;
  1386.       Header.customNavToggler.checked = show;
  1387.       cust = $('#custom-board-list', Header.bar);
  1388.       full = $('#full-board-list', Header.bar);
  1389.       btn = $('.hide-board-list-button', full);
  1390.       return _ref = show ? [false, true, false] : [true, false, true], cust.hidden = _ref[0], full.hidden = _ref[1], btn.hidden = _ref[2], _ref;
  1391.     },
  1392.     toggleCustomNav: function() {
  1393.       $.cb.checked.call(this);
  1394.       return Header.setCustomNav(this.checked);
  1395.     },
  1396.     editCustomNav: function() {
  1397.       var settings;
  1398.       Settings.open('Rice');
  1399.       settings = $.id('fourchanx-settings');
  1400.       return $('input[name=boardnav]', settings).focus();
  1401.     },
  1402.     hashScroll: function() {
  1403.       var hash, post;
  1404.       hash = this.location.hash.slice(1);
  1405.       if (!(/^p\d+$/.test(hash) && (post = $.id(hash)))) {
  1406.         return;
  1407.       }
  1408.       if ((Get.postFromNode(post)).isHidden) {
  1409.         return;
  1410.       }
  1411.       return Header.scrollTo(post);
  1412.     },
  1413.     scrollTo: function(root, down, needed) {
  1414.       var height, x;
  1415.       if (down) {
  1416.         x = Header.getBottomOf(root);
  1417.         if (Conf['Header auto-hide on scroll'] && Conf['Bottom header']) {
  1418.           height = Header.bar.getBoundingClientRect().height;
  1419.           if (x <= 0) {
  1420.             if (!Header.isHidden()) {
  1421.               x += height;
  1422.             }
  1423.           } else {
  1424.             if (Header.isHidden()) {
  1425.               x -= height;
  1426.             }
  1427.           }
  1428.         }
  1429.         if (!(needed && x >= 0)) {
  1430.           return window.scrollBy(0, -x);
  1431.         }
  1432.       } else {
  1433.         x = Header.getTopOf(root);
  1434.         if (Conf['Header auto-hide on scroll'] && !Conf['Bottom header']) {
  1435.           height = Header.bar.getBoundingClientRect().height;
  1436.           if (x >= 0) {
  1437.             if (!Header.isHidden()) {
  1438.               x += height;
  1439.             }
  1440.           } else {
  1441.             if (Header.isHidden()) {
  1442.               x -= height;
  1443.             }
  1444.           }
  1445.         }
  1446.         if (!(needed && x >= 0)) {
  1447.           return window.scrollBy(0, x);
  1448.         }
  1449.       }
  1450.     },
  1451.     scrollToIfNeeded: function(root, down) {
  1452.       return Header.scrollTo(root, down, true);
  1453.     },
  1454.     getTopOf: function(root) {
  1455.       var headRect, top;
  1456.       top = root.getBoundingClientRect().top;
  1457.       if (!Conf['Bottom header']) {
  1458.         headRect = ($.hasClass(Header.bar, 'autohide') ? Header.hitzone : Header.bar).getBoundingClientRect();
  1459.         top -= headRect.top + headRect.height;
  1460.       }
  1461.       return top;
  1462.     },
  1463.     getBottomOf: function(root) {
  1464.       var bottom, clientHeight, headRect;
  1465.       clientHeight = doc.clientHeight;
  1466.       bottom = clientHeight - root.getBoundingClientRect().bottom;
  1467.       if (Conf['Bottom header']) {
  1468.         headRect = ($.hasClass(Header.bar, 'autohide') ? Header.hitzone : Header.bar).getBoundingClientRect();
  1469.         bottom -= clientHeight - headRect.bottom + headRect.height;
  1470.       }
  1471.       return bottom;
  1472.     },
  1473.     isNodeVisible: function(node) {
  1474.       var height;
  1475.       height = node.getBoundingClientRect().height;
  1476.       return Header.getTopOf(node) + height >= 0 && Header.getBottomOf(node) + height >= 0;
  1477.     },
  1478.     isHidden: function() {
  1479.       var top;
  1480.       top = Header.bar.getBoundingClientRect().top;
  1481.       if (Conf['Bottom header']) {
  1482.         return top === doc.clientHeight;
  1483.       } else {
  1484.         return top < 0;
  1485.       }
  1486.     },
  1487.     addShortcut: function(el, index) {
  1488.       var shortcut, shortcuts;
  1489.       shortcut = $.el('span', {
  1490.         className: 'shortcut'
  1491.       });
  1492.       shortcut.dataset.index = index;
  1493.       $.add(shortcut, el);
  1494.       shortcuts = $('#shortcuts', Header.bar);
  1495.       return $.add(shortcuts, __slice.call(shortcuts.childNodes).concat(shortcut).sort(function(a, b) {
  1496.         return a.dataset.index - b.dataset.index;
  1497.       }));
  1498.     },
  1499.     menuToggle: function(e) {
  1500.       return Header.menu.toggle(e, this, g);
  1501.     },
  1502.     createNotification: function(e) {
  1503.       var content, lifetime, notice, type, _ref;
  1504.       _ref = e.detail, type = _ref.type, content = _ref.content, lifetime = _ref.lifetime;
  1505.       return notice = new Notice(type, content, lifetime);
  1506.     },
  1507.     areNotificationsEnabled: false,
  1508.     enableDesktopNotifications: function() {
  1509.       var authorize, disable, el, notice, _ref;
  1510.       if (!(window.Notification && Conf['Desktop Notifications'])) {
  1511.         return;
  1512.       }
  1513.       switch (Notification.permission) {
  1514.         case 'granted':
  1515.           Header.areNotificationsEnabled = true;
  1516.           return;
  1517.         case 'denied':
  1518.           return;
  1519.       }
  1520.       el = $.el('span', {
  1521.         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>"
  1522.       });
  1523.       _ref = $$('button', el), authorize = _ref[0], disable = _ref[1];
  1524.       $.on(authorize, 'click', function() {
  1525.         return Notification.requestPermission(function(status) {
  1526.           Header.areNotificationsEnabled = status === 'granted';
  1527.           if (status === 'default') {
  1528.             return;
  1529.           }
  1530.           return notice.close();
  1531.         });
  1532.       });
  1533.       $.on(disable, 'click', function() {
  1534.         $.set('Desktop Notifications', false);
  1535.         return notice.close();
  1536.       });
  1537.       return notice = new Notice('info', el);
  1538.     }
  1539.   };
  1540.  
  1541.   Notice = (function() {
  1542.     function Notice(type, content, timeout) {
  1543.       this.timeout = timeout;
  1544.       this.close = __bind(this.close, this);
  1545.       this.add = __bind(this.add, this);
  1546.       this.el = $.el('div', {
  1547.         innerHTML: '<a href=javascript:; class="close fa fa-times" title=Close></a><div class=message></div>'
  1548.       });
  1549.       this.el.style.opacity = 0;
  1550.       this.setType(type);
  1551.       $.on(this.el.firstElementChild, 'click', this.close);
  1552.       if (typeof content === 'string') {
  1553.         content = $.tn(content);
  1554.       }
  1555.       $.add(this.el.lastElementChild, content);
  1556.       $.ready(this.add);
  1557.     }
  1558.  
  1559.     Notice.prototype.setType = function(type) {
  1560.       return this.el.className = "notification " + type;
  1561.     };
  1562.  
  1563.     Notice.prototype.add = function() {
  1564.       if (d.hidden) {
  1565.         $.on(d, 'visibilitychange', this.add);
  1566.         return;
  1567.       }
  1568.       $.off(d, 'visibilitychange', this.add);
  1569.       $.add(Header.noticesRoot, this.el);
  1570.       this.el.clientHeight;
  1571.       this.el.style.opacity = 1;
  1572.       if (this.timeout) {
  1573.         return setTimeout(this.close, this.timeout * $.SECOND);
  1574.       }
  1575.     };
  1576.  
  1577.     Notice.prototype.close = function() {
  1578.       $.off(d, 'visibilitychange', this.add);
  1579.       return $.rm(this.el);
  1580.     };
  1581.  
  1582.     return Notice;
  1583.  
  1584.   })();
  1585.  
  1586.   Settings = {
  1587.     init: function() {
  1588.       var link, settings;
  1589.       link = $.el('a', {
  1590.         className: 'settings-link',
  1591.         textContent: '4chan X Settings',
  1592.         href: 'javascript:;'
  1593.       });
  1594.       $.on(link, 'click', Settings.open);
  1595.       Header.menu.addEntry({
  1596.         el: link,
  1597.         order: 111
  1598.       });
  1599.       Settings.addSection('Main', Settings.main);
  1600.       Settings.addSection('Filter', Settings.filter);
  1601.       Settings.addSection('QR', Settings.qr);
  1602.       Settings.addSection('Sauce', Settings.sauce);
  1603.       Settings.addSection('Rice', Settings.rice);
  1604.       Settings.addSection('Archives', Settings.archives);
  1605.       Settings.addSection('Keybinds', Settings.keybinds);
  1606.       $.on(d, 'OpenSettings', function(e) {
  1607.         return Settings.open(e.detail);
  1608.       });
  1609.       settings = JSON.parse(localStorage.getItem('4chan-settings')) || {};
  1610.       if (settings.disableAll) {
  1611.         return;
  1612.       }
  1613.       settings.disableAll = true;
  1614.       return localStorage.setItem('4chan-settings', JSON.stringify(settings));
  1615.     },
  1616.     open: function(openSection) {
  1617.       var html, link, links, overlay, section, sectionToOpen, _i, _len, _ref;
  1618.       if (Settings.dialog) {
  1619.         return;
  1620.       }
  1621.       $.event('CloseMenu');
  1622.       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>&nbsp;|&nbsp;<a href=\"https://github.com/MayhemYDG/4chan-x/blob/v3/CHANGELOG.md\" target=\"_blank\">" + g.VERSION + "</a>&nbsp;|&nbsp;<a href=\"https://github.com/MayhemYDG/4chan-x/blob/v3/CONTRIBUTING.md#reporting-bugs-and-suggestions\" target=\"_blank\">Issues</a>&nbsp;|&nbsp;<a href=\"javascript:;\" class=\"close fa fa-times\" title=\"Close\"></a></div></nav><section></section></div>";
  1623.       Settings.dialog = overlay = $.el('div', {
  1624.         id: 'overlay',
  1625.         innerHTML: html
  1626.       });
  1627.       links = [];
  1628.       _ref = Settings.sections;
  1629.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  1630.         section = _ref[_i];
  1631.         link = $.el('a', {
  1632.           className: "tab-" + section.hyphenatedTitle,
  1633.           textContent: section.title,
  1634.           href: 'javascript:;'
  1635.         });
  1636.         $.on(link, 'click', Settings.openSection.bind(section));
  1637.         links.push(link, $.tn(' | '));
  1638.         if (section.title === openSection) {
  1639.           sectionToOpen = link;
  1640.         }
  1641.       }
  1642.       links.pop();
  1643.       $.add($('.sections-list', overlay), links);
  1644.       (sectionToOpen ? sectionToOpen : links[0]).click();
  1645.       $.on($('.close', overlay), 'click', Settings.close);
  1646.       $.on(overlay, 'click', Settings.close);
  1647.       $.on(overlay.firstElementChild, 'click', function(e) {
  1648.         return e.stopPropagation();
  1649.       });
  1650.       d.body.style.width = "" + d.body.clientWidth + "px";
  1651.       $.addClass(d.body, 'unscroll');
  1652.       return $.add(d.body, overlay);
  1653.     },
  1654.     close: function() {
  1655.       if (!Settings.dialog) {
  1656.         return;
  1657.       }
  1658.       d.body.style.removeProperty('width');
  1659.       $.rmClass(d.body, 'unscroll');
  1660.       $.rm(Settings.dialog);
  1661.       return delete Settings.dialog;
  1662.     },
  1663.     sections: [],
  1664.     addSection: function(title, open) {
  1665.       var hyphenatedTitle;
  1666.       hyphenatedTitle = title.toLowerCase().replace(/\s+/g, '-');
  1667.       return Settings.sections.push({
  1668.         title: title,
  1669.         hyphenatedTitle: hyphenatedTitle,
  1670.         open: open
  1671.       });
  1672.     },
  1673.     openSection: function() {
  1674.       var section, selected;
  1675.       if (selected = $('.tab-selected', Settings.dialog)) {
  1676.         $.rmClass(selected, 'tab-selected');
  1677.       }
  1678.       $.addClass($(".tab-" + this.hyphenatedTitle, Settings.dialog), 'tab-selected');
  1679.       section = $('section', Settings.dialog);
  1680.       $.rmAll(section);
  1681.       section.className = "section-" + this.hyphenatedTitle;
  1682.       this.open(section, g);
  1683.       return section.scrollTop = 0;
  1684.     },
  1685.     main: function(section) {
  1686.       var arr, button, description, div, fs, input, inputs, items, key, obj, _ref;
  1687.       section.innerHTML = "<button class=\"export\">Export Settings</button><button class=\"import\">Import Settings</button><button class=\"reset\">Reset Settings</button><input type=\"file\" hidden>";
  1688.       $.on($('.export', section), 'click', Settings["export"]);
  1689.       $.on($('.import', section), 'click', Settings["import"]);
  1690.       $.on($('.reset', section), 'click', Settings.reset);
  1691.       $.on($('input', section), 'change', Settings.onImport);
  1692.       items = {};
  1693.       inputs = {};
  1694.       _ref = Config.main;
  1695.       for (key in _ref) {
  1696.         obj = _ref[key];
  1697.         fs = $.el('fieldset', {
  1698.           innerHTML: "<legend>" + key + "</legend>"
  1699.         });
  1700.         for (key in obj) {
  1701.           arr = obj[key];
  1702.           description = arr[1];
  1703.           div = $.el('div', {
  1704.             innerHTML: "<label><input type=checkbox name=\"" + key + "\">" + key + "</label><span class=description>: " + description + "</span>"
  1705.           });
  1706.           input = $('input', div);
  1707.           $.on(input, 'change', $.cb.checked);
  1708.           items[key] = Conf[key];
  1709.           inputs[key] = input;
  1710.           $.add(fs, div);
  1711.         }
  1712.         $.add(section, fs);
  1713.       }
  1714.       $.get(items, function(items) {
  1715.         var val;
  1716.         for (key in items) {
  1717.           val = items[key];
  1718.           inputs[key].checked = val;
  1719.         }
  1720.       });
  1721.       div = $.el('div', {
  1722.         innerHTML: "<button></button><span class=description>: Clear manually-hidden threads and posts on all boards. Reload the page to apply."
  1723.       });
  1724.       button = $('button', div);
  1725.       $.get('hiddenPosts', {}, function(_arg) {
  1726.         var ID, board, hiddenNum, hiddenPosts, thread, _ref1;
  1727.         hiddenPosts = _arg.hiddenPosts;
  1728.         hiddenNum = 0;
  1729.         _ref1 = hiddenPosts.boards;
  1730.         for (ID in _ref1) {
  1731.           board = _ref1[ID];
  1732.           for (ID in board) {
  1733.             thread = board[ID];
  1734.             hiddenNum += Object.keys(thread).length;
  1735.           }
  1736.         }
  1737.         return button.textContent = "Hidden: " + hiddenNum;
  1738.       });
  1739.       $.on(button, 'click', function() {
  1740.         this.textContent = 'Hidden: 0';
  1741.         return $["delete"]('hiddenPosts');
  1742.       });
  1743.       return $.after($('input[name="Recursive Hiding"]', section).parentNode.parentNode, div);
  1744.     },
  1745.     "export": function() {
  1746.       return $.get(Conf, function(Conf) {
  1747.         delete Conf['archives'];
  1748.         return Settings.downloadExport('Settings', {
  1749.           version: g.VERSION,
  1750.           date: Date.now(),
  1751.           Conf: Conf
  1752.         });
  1753.       });
  1754.     },
  1755.     downloadExport: function(title, data) {
  1756.       var a;
  1757.       a = $.el('a', {
  1758.         download: "4chan X v" + g.VERSION + " " + title + "." + data.date + ".json",
  1759.         href: "data:application/json;base64," + (btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2)))))
  1760.       });
  1761.       $.add(d.body, a);
  1762.       a.click();
  1763.       return $.rm(a);
  1764.     },
  1765.     "import": function() {
  1766.       return $('input[type=file]', this.parentNode).click();
  1767.     },
  1768.     onImport: function() {
  1769.       var file, reader;
  1770.       if (!(file = this.files[0])) {
  1771.         return;
  1772.       }
  1773.       if (!confirm('Your current settings will be entirely overwritten, are you sure?')) {
  1774.         return;
  1775.       }
  1776.       reader = new FileReader();
  1777.       reader.onload = function(e) {
  1778.         var err;
  1779.         try {
  1780.           Settings.loadSettings(JSON.parse(e.target.result));
  1781.         } catch (_error) {
  1782.           err = _error;
  1783.           alert('Import failed due to an error.');
  1784.           c.error(err.stack);
  1785.           return;
  1786.         }
  1787.         if (confirm('Import successful. Reload now?')) {
  1788.           return window.location.reload();
  1789.         }
  1790.       };
  1791.       return reader.readAsText(file);
  1792.     },
  1793.     loadSettings: function(data) {
  1794.       var convertSettings, key, val, version, _ref;
  1795.       version = data.version.split('.');
  1796.       if (version[0] === '2') {
  1797.         convertSettings = function(data, map) {
  1798.           var newKey, prevKey;
  1799.           for (prevKey in map) {
  1800.             newKey = map[prevKey];
  1801.             if (newKey) {
  1802.               data.Conf[newKey] = data.Conf[prevKey];
  1803.             }
  1804.             delete data.Conf[prevKey];
  1805.           }
  1806.           return data;
  1807.         };
  1808.         data = Settings.convertSettings(data, {
  1809.           'Disable 4chan\'s extension': '',
  1810.           'Catalog Links': '',
  1811.           'Reply Navigation': '',
  1812.           'Show Stubs': 'Stubs',
  1813.           'Image Auto-Gif': 'Auto-GIF',
  1814.           'Expand From Current': '',
  1815.           'Unread Favicon': 'Unread Tab Icon',
  1816.           'Post in Title': 'Thread Excerpt',
  1817.           'Auto Hide QR': '',
  1818.           'Open Reply in New Tab': '',
  1819.           'Remember QR size': '',
  1820.           'Quote Inline': 'Quote Inlining',
  1821.           'Quote Preview': 'Quote Previewing',
  1822.           'Indicate OP quote': '',
  1823.           'Indicate Cross-thread Quotes': '',
  1824.           'uniqueid': 'uniqueID',
  1825.           'mod': 'capcode',
  1826.           'country': 'flag',
  1827.           'md5': 'MD5',
  1828.           'openEmptyQR': 'Open empty QR',
  1829.           'openQR': 'Open QR',
  1830.           'openOptions': 'Open settings',
  1831.           'close': 'Close',
  1832.           'spoiler': 'Spoiler tags',
  1833.           'code': 'Code tags',
  1834.           'submit': 'Submit QR',
  1835.           'watch': 'Watch',
  1836.           'update': 'Update',
  1837.           'unreadCountTo0': '',
  1838.           'expandAllImages': 'Expand images',
  1839.           'expandImage': 'Expand image',
  1840.           'zero': 'Front page',
  1841.           'nextPage': 'Next page',
  1842.           'previousPage': 'Previous page',
  1843.           'nextThread': 'Next thread',
  1844.           'previousThread': 'Previous thread',
  1845.           'expandThread': 'Expand thread',
  1846.           'openThreadTab': 'Open thread',
  1847.           'openThread': 'Open thread tab',
  1848.           'nextReply': 'Next reply',
  1849.           'previousReply': 'Previous reply',
  1850.           'hide': 'Hide',
  1851.           'Scrolling': 'Auto Scroll',
  1852.           'Verbose': ''
  1853.         });
  1854.         data.Conf.sauces = data.Conf.sauces.replace(/\$\d/g, function(c) {
  1855.           switch (c) {
  1856.             case '$1':
  1857.               return '%TURL';
  1858.             case '$2':
  1859.               return '%URL';
  1860.             case '$3':
  1861.               return '%MD5';
  1862.             case '$4':
  1863.               return '%board';
  1864.             default:
  1865.               return c;
  1866.           }
  1867.         });
  1868.         _ref = Config.hotkeys;
  1869.         for (key in _ref) {
  1870.           val = _ref[key];
  1871.           if (key in data.Conf) {
  1872.             data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, function(s) {
  1873.               return "" + (s[0].toUpperCase()) + s.slice(1);
  1874.             }).replace(/(^|.+\+)[A-Z]$/g, function(s) {
  1875.               return "Shift+" + s.slice(0, -1) + (s.slice(-1).toLowerCase());
  1876.             });
  1877.           }
  1878.         }
  1879.         data.Conf['WatchedThreads'] = data.WatchedThreads;
  1880.       }
  1881.       if (data.Conf['WatchedThreads']) {
  1882.         data.Conf['watchedThreads'] = {
  1883.           boards: ThreadWatcher.convert(data.Conf['WatchedThreads'])
  1884.         };
  1885.         delete data.Conf['WatchedThreads'];
  1886.       }
  1887.       return $.clear(function() {
  1888.         return $.set(data.Conf);
  1889.       });
  1890.     },
  1891.     reset: function() {
  1892.       if (confirm('Your current settings will be entirely wiped, are you sure?')) {
  1893.         return $.clear(function() {
  1894.           if (confirm('Reset successful. Reload now?')) {
  1895.             return window.location.reload();
  1896.           }
  1897.         });
  1898.       }
  1899.     },
  1900.     filter: function(section) {
  1901.       var select;
  1902.       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>";
  1903.       select = $('select', section);
  1904.       $.on(select, 'change', Settings.selectFilter);
  1905.       return Settings.selectFilter.call(select);
  1906.     },
  1907.     selectFilter: function() {
  1908.       var div, name, ta;
  1909.       div = this.nextElementSibling;
  1910.       if ((name = this.value) !== 'guide') {
  1911.         $.rmAll(div);
  1912.         ta = $.el('textarea', {
  1913.           name: name,
  1914.           className: 'field',
  1915.           spellcheck: false
  1916.         });
  1917.         $.get(name, Conf[name], function(item) {
  1918.           return ta.value = item[name];
  1919.         });
  1920.         $.on(ta, 'change', $.cb.value);
  1921.         $.add(div, ta);
  1922.         return;
  1923.       }
  1924.       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>";
  1925.     },
  1926.     qr: function(section) {
  1927.       var ta;
  1928.       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>";
  1929.       ta = $('textarea', section);
  1930.       $.get('QR.personas', Conf['QR.personas'], function(item) {
  1931.         return ta.value = item['QR.personas'];
  1932.       });
  1933.       return $.on(ta, 'change', $.cb.value);
  1934.     },
  1935.     sauce: function(section) {
  1936.       var ta;
  1937.       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>";
  1938.       ta = $('textarea', section);
  1939.       $.get('sauces', Conf['sauces'], function(item) {
  1940.         return ta.value = item['sauces'];
  1941.       });
  1942.       return $.on(ta, 'change', $.cb.value);
  1943.     },
  1944.     rice: function(section) {
  1945.       var input, inputs, items, name, _i, _len, _ref;
  1946.       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>";
  1947.       items = {};
  1948.       inputs = {};
  1949.       _ref = ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss'];
  1950.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  1951.         name = _ref[_i];
  1952.         input = $("[name=" + name + "]", section);
  1953.         items[name] = Conf[name];
  1954.         inputs[name] = input;
  1955.         $.on(input, 'change', $.cb.value);
  1956.       }
  1957.       $.get(items, function(items) {
  1958.         var event, key, val;
  1959.         for (key in items) {
  1960.           val = items[key];
  1961.           input = inputs[key];
  1962.           input.value = val;
  1963.           if (key === 'usercss') {
  1964.             continue;
  1965.           }
  1966.           event = key === 'favicon' || key === 'usercss' ? 'change' : 'input';
  1967.           $.on(input, event, Settings[key]);
  1968.           Settings[key].call(input);
  1969.         }
  1970.       });
  1971.       $.on($('input[name="Custom CSS"]', section), 'change', Settings.togglecss);
  1972.       return $.on($('#apply-css', section), 'click', Settings.usercss);
  1973.     },
  1974.     boardnav: function() {
  1975.       return Header.generateBoardList(this.value);
  1976.     },
  1977.     time: function() {
  1978.       var funk;
  1979.       funk = Time.createFunc(this.value);
  1980.       return this.nextElementSibling.textContent = funk(Time, new Date());
  1981.     },
  1982.     backlink: function() {
  1983.       return this.nextElementSibling.textContent = this.value.replace(/%id/, '123456789');
  1984.     },
  1985.     fileInfo: function() {
  1986.       var data, funk;
  1987.       data = {
  1988.         isReply: true,
  1989.         file: {
  1990.           URL: '//i.4cdn.org/g/1334437723720.jpg',
  1991.           name: 'd9bb2efc98dd0df141a94399ff5880b7.jpg',
  1992.           size: '276 KB',
  1993.           sizeInBytes: 276 * 1024,
  1994.           dimensions: '1280x720',
  1995.           isImage: true,
  1996.           isVideo: false,
  1997.           isSpoiler: true
  1998.         }
  1999.       };
  2000.       funk = FileInfo.createFunc(this.value);
  2001.       return this.nextElementSibling.innerHTML = funk(FileInfo, data);
  2002.     },
  2003.     favicon: function() {
  2004.       Favicon["switch"]();
  2005.       if (g.VIEW === 'thread' && Conf['Unread Tab Icon']) {
  2006.         Unread.update();
  2007.       }
  2008.       return this.nextElementSibling.innerHTML = "<img src=" + Favicon["default"] + ">\n<img src=" + Favicon.unreadSFW + ">\n<img src=" + Favicon.unreadNSFW + ">\n<img src=" + Favicon.unreadDead + ">";
  2009.     },
  2010.     togglecss: function() {
  2011.       if ($('textarea[name=usercss]', $.x('ancestor::fieldset[1]', this)).disabled = !this.checked) {
  2012.         CustomCSS.rmStyle();
  2013.       } else {
  2014.         CustomCSS.addStyle();
  2015.       }
  2016.       return $.cb.checked.call(this);
  2017.     },
  2018.     usercss: function() {
  2019.       return CustomCSS.update();
  2020.     },
  2021.     archives: function(section) {
  2022.       var button;
  2023.       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>";
  2024.       button = $('button', section);
  2025.       $.on(button, 'click', function() {
  2026.         $["delete"]('lastarchivecheck');
  2027.         button.textContent = '...';
  2028.         button.disabled = true;
  2029.         return Redirect.update(function() {
  2030.           button.textContent = 'Updated';
  2031.           return Settings.addArchivesTable(section);
  2032.         });
  2033.       });
  2034.       return Settings.addArchivesTable(section);
  2035.     },
  2036.     addArchivesTable: function(section) {
  2037.       var archive, boardID, boards, data, row, rows, tbody, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2;
  2038.       boards = {};
  2039.       _ref = Conf['archives'];
  2040.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  2041.         archive = _ref[_i];
  2042.         _ref1 = archive.boards;
  2043.         for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
  2044.           boardID = _ref1[_j];
  2045.           data = boards[boardID] || (boards[boardID] = {
  2046.             thread: [],
  2047.             post: [],
  2048.             file: []
  2049.           });
  2050.           data.thread.push(archive);
  2051.           if (archive.software === 'foolfuuka') {
  2052.             data.post.push(archive);
  2053.           }
  2054.           if (__indexOf.call(archive.files, boardID) >= 0) {
  2055.             data.file.push(archive);
  2056.           }
  2057.         }
  2058.       }
  2059.       rows = [];
  2060.       _ref2 = Object.keys(boards).sort();
  2061.       for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
  2062.         boardID = _ref2[_k];
  2063.         row = $.el('tr');
  2064.         rows.push(row);
  2065.         $.add(row, $.el('th', {
  2066.           textContent: "/" + boardID + "/",
  2067.           className: boardID === g.BOARD.ID ? 'warning' : ''
  2068.         }));
  2069.         data = boards[boardID];
  2070.         Settings.addArchiveCell(row, boardID, data, 'thread');
  2071.         Settings.addArchiveCell(row, boardID, data, 'post');
  2072.         Settings.addArchiveCell(row, boardID, data, 'file');
  2073.       }
  2074.       tbody = $('tbody', section);
  2075.       $.rmAll(tbody);
  2076.       $.add(tbody, rows);
  2077.       return $.get({
  2078.         lastarchivecheck: 0,
  2079.         selectedArchives: Conf['selectedArchives']
  2080.       }, function(_arg) {
  2081.         var lastarchivecheck, option, selectedArchives, type, uid;
  2082.         lastarchivecheck = _arg.lastarchivecheck, selectedArchives = _arg.selectedArchives;
  2083.         for (boardID in selectedArchives) {
  2084.           data = selectedArchives[boardID];
  2085.           for (type in data) {
  2086.             uid = data[type];
  2087.             if (option = $("select[data-board-i-d='" + boardID + "'][data-type='" + type + "'] > option[value='" + uid + "']", section)) {
  2088.               option.selected = true;
  2089.             }
  2090.           }
  2091.         }
  2092.         return $('time', section).textContent = new Date(lastarchivecheck).toLocaleString();
  2093.       });
  2094.     },
  2095.     addArchiveCell: function(row, boardID, data, type) {
  2096.       var archive, length, options, select, td, _i, _len, _ref;
  2097.       options = [];
  2098.       _ref = data[type];
  2099.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  2100.         archive = _ref[_i];
  2101.         options.push($.el('option', {
  2102.           textContent: archive.name,
  2103.           value: archive.uid
  2104.         }));
  2105.       }
  2106.       td = $.el('td');
  2107.       length = options.length;
  2108.       if (length) {
  2109.         td.innerHTML = '<select></select>';
  2110.         select = td.firstElementChild;
  2111.         if (!(select.disabled = length === 1)) {
  2112.           $.extend(select.dataset, {
  2113.             boardID: boardID,
  2114.             type: type
  2115.           });
  2116.           $.on(select, 'change', Settings.saveSelectedArchive);
  2117.         }
  2118.         $.add(select, options);
  2119.       } else {
  2120.         td.textContent = 'N/A';
  2121.       }
  2122.       return $.add(row, td);
  2123.     },
  2124.     saveSelectedArchive: function() {
  2125.       return $.get('selectedArchives', Conf['selectedArchives'], (function(_this) {
  2126.         return function(_arg) {
  2127.           var selectedArchives, _name;
  2128.           selectedArchives = _arg.selectedArchives;
  2129.           (selectedArchives[_name = _this.dataset.boardID] || (selectedArchives[_name] = {}))[_this.dataset.type] = +_this.value;
  2130.           Conf['selectedArchives'] = selectedArchives;
  2131.           Redirect.selectArchives();
  2132.           return $.set('selectedArchives', selectedArchives);
  2133.         };
  2134.       })(this));
  2135.     },
  2136.     keybinds: function(section) {
  2137.       var arr, input, inputs, items, key, tbody, tr, _ref;
  2138.       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>";
  2139.       tbody = $('tbody', section);
  2140.       items = {};
  2141.       inputs = {};
  2142.       _ref = Config.hotkeys;
  2143.       for (key in _ref) {
  2144.         arr = _ref[key];
  2145.         tr = $.el('tr', {
  2146.           innerHTML: "<td>" + arr[1] + "</td><td><input class=field></td>"
  2147.         });
  2148.         input = $('input', tr);
  2149.         input.name = key;
  2150.         input.spellcheck = false;
  2151.         items[key] = Conf[key];
  2152.         inputs[key] = input;
  2153.         $.on(input, 'keydown', Settings.keybind);
  2154.         $.add(tbody, tr);
  2155.       }
  2156.       return $.get(items, function(items) {
  2157.         var val;
  2158.         for (key in items) {
  2159.           val = items[key];
  2160.           inputs[key].value = val;
  2161.         }
  2162.       });
  2163.     },
  2164.     keybind: function(e) {
  2165.       var key;
  2166.       if (e.keyCode === 9) {
  2167.         return;
  2168.       }
  2169.       e.preventDefault();
  2170.       e.stopPropagation();
  2171.       if ((key = Keybinds.keyCode(e)) == null) {
  2172.         return;
  2173.       }
  2174.       this.value = key;
  2175.       return $.cb.value.call(this);
  2176.     }
  2177.   };
  2178.  
  2179.   Index = {
  2180.     showHiddenThreads: false,
  2181.     init: function() {
  2182.       var input, label, name, refNavEntry, repliesEntry, select, targetEntry, threadNumEntry, threadsNumInput, _i, _j, _len, _len1, _ref, _ref1;
  2183.       if (g.VIEW !== 'index') {
  2184.         $.ready(this.setupNavLinks);
  2185.         return;
  2186.       }
  2187.       if (g.BOARD.ID === 'f') {
  2188.         return;
  2189.       }
  2190.       this.db = new DataBoard('pinnedThreads');
  2191.       Thread.callbacks.push({
  2192.         name: 'Thread Pinning',
  2193.         cb: this.threadNode
  2194.       });
  2195.       CatalogThread.callbacks.push({
  2196.         name: 'Catalog Features',
  2197.         cb: this.catalogNode
  2198.       });
  2199.       this.button = $.el('a', {
  2200.         className: 'index-refresh-shortcut fa fa-refresh',
  2201.         title: 'Refresh Index',
  2202.         href: 'javascript:;'
  2203.       });
  2204.       $.on(this.button, 'click', this.update);
  2205.       Header.addShortcut(this.button, 1);
  2206.       threadNumEntry = {
  2207.         el: $.el('span', {
  2208.           textContent: 'Threads per page'
  2209.         }),
  2210.         subEntries: [
  2211.           {
  2212.             el: $.el('label', {
  2213.               innerHTML: '<input type=number min=0 name="Threads per Page">',
  2214.               title: 'Use 0 for default value'
  2215.             })
  2216.           }
  2217.         ]
  2218.       };
  2219.       threadsNumInput = threadNumEntry.subEntries[0].el.firstChild;
  2220.       threadsNumInput.value = Conf['Threads per Page'];
  2221.       $.on(threadsNumInput, 'change', $.cb.value);
  2222.       $.on(threadsNumInput, 'change', this.cb.threadsNum);
  2223.       targetEntry = {
  2224.         el: $.el('label', {
  2225.           innerHTML: '<input type=checkbox name="Open threads in a new tab"> Open threads in a new tab',
  2226.           title: 'Catalog-only setting.'
  2227.         })
  2228.       };
  2229.       repliesEntry = {
  2230.         el: $.el('label', {
  2231.           innerHTML: '<input type=checkbox name="Show Replies"> Show replies'
  2232.         })
  2233.       };
  2234.       refNavEntry = {
  2235.         el: $.el('label', {
  2236.           innerHTML: '<input type=checkbox name="Refreshed Navigation"> Refreshed navigation',
  2237.           title: 'Refresh index when navigating through pages.'
  2238.         })
  2239.       };
  2240.       _ref = [targetEntry, repliesEntry, refNavEntry];
  2241.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  2242.         label = _ref[_i];
  2243.         input = label.el.firstChild;
  2244.         name = input.name;
  2245.         input.checked = Conf[name];
  2246.         $.on(input, 'change', $.cb.checked);
  2247.         switch (name) {
  2248.           case 'Open threads in a new tab':
  2249.             $.on(input, 'change', this.cb.target);
  2250.             break;
  2251.           case 'Show Replies':
  2252.             $.on(input, 'change', this.cb.replies);
  2253.         }
  2254.       }
  2255.       Header.menu.addEntry({
  2256.         el: $.el('span', {
  2257.           textContent: 'Index Navigation'
  2258.         }),
  2259.         order: 90,
  2260.         subEntries: [threadNumEntry, targetEntry, repliesEntry, refNavEntry]
  2261.       });
  2262.       $.addClass(doc, 'index-loading');
  2263.       this.update();
  2264.       this.navLinks = $.el('div', {
  2265.         id: 'nav-links',
  2266.         innerHTML: "<input type=\"search\" id=\"index-search\" class=\"field\" placeholder=\"Search\"><a id=\"index-search-clear\" class=\"fa fa-times-circle\" href=\"javascript:;\"></a>&nbsp;<time id=\"index-last-refresh\" title=\"Last index refresh\">...</time><span id=\"hidden-label\" hidden>&nbsp;&mdash; <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>"
  2267.       });
  2268.       this.searchInput = $('#index-search', this.navLinks);
  2269.       this.hideLabel = $('#hidden-label', this.navLinks);
  2270.       this.selectMode = $('#index-mode', this.navLinks);
  2271.       this.selectSort = $('#index-sort', this.navLinks);
  2272.       this.selectSize = $('#index-size', this.navLinks);
  2273.       $.on(this.searchInput, 'input', this.onSearchInput);
  2274.       $.on($('#index-search-clear', this.navLinks), 'click', this.clearSearch);
  2275.       $.on($('#hidden-toggle a', this.navLinks), 'click', this.cb.toggleHiddenThreads);
  2276.       _ref1 = [this.selectMode, this.selectSort, this.selectSize];
  2277.       for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
  2278.         select = _ref1[_j];
  2279.         select.value = Conf[select.name];
  2280.         $.on(select, 'change', $.cb.value);
  2281.       }
  2282.       $.on(this.selectMode, 'change', this.cb.mode);
  2283.       $.on(this.selectSort, 'change', this.cb.sort);
  2284.       $.on(this.selectSize, 'change', this.cb.size);
  2285.       this.root = $.el('div', {
  2286.         className: 'board'
  2287.       });
  2288.       this.pagelist = $.el('div', {
  2289.         className: 'pagelist',
  2290.         hidden: true,
  2291.         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>"
  2292.       });
  2293.       this.currentPage = this.getCurrentPage();
  2294.       $.on(window, 'popstate', this.cb.popstate);
  2295.       $.on(this.pagelist, 'click', this.cb.pageNav);
  2296.       $.on($('#custom-board-list', Header.bar), 'click', this.cb.headerNav);
  2297.       this.cb.toggleCatalogMode();
  2298.       return $.asap((function() {
  2299.         return $('.board', doc) || d.readyState !== 'loading';
  2300.       }), function() {
  2301.         var board, navLink, _k, _len2, _ref2;
  2302.         board = $('.board');
  2303.         $.replace(board, Index.root);
  2304.         d.implementation.createDocument(null, null, null).appendChild(board);
  2305.         _ref2 = $$('.navLinks');
  2306.         for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
  2307.           navLink = _ref2[_k];
  2308.           $.rm(navLink);
  2309.         }
  2310.         $.before($.x('child::form[@name="delform"]/preceding-sibling::hr[1]'), Index.navLinks);
  2311.         return $.asap((function() {
  2312.           return $('.pagelist') || d.readyState !== 'loading';
  2313.         }), function() {
  2314.           var pagelist;
  2315.           if (pagelist = $('.pagelist')) {
  2316.             $.replace(pagelist, Index.pagelist);
  2317.           }
  2318.           return $.rmClass(doc, 'index-loading');
  2319.         });
  2320.       });
  2321.     },
  2322.     menu: {
  2323.       init: function() {
  2324.         if (g.VIEW !== 'index' || !Conf['Menu'] || g.BOARD.ID === 'f') {
  2325.           return;
  2326.         }
  2327.         return Menu.menu.addEntry({
  2328.           el: $.el('a', {
  2329.             href: 'javascript:;'
  2330.           }),
  2331.           order: 19,
  2332.           open: function(_arg) {
  2333.             var thread;
  2334.             thread = _arg.thread;
  2335.             if (Conf['Index Mode'] !== 'catalog') {
  2336.               return false;
  2337.             }
  2338.             this.el.textContent = thread.isPinned ? 'Unpin thread' : 'Pin thread';
  2339.             if (this.cb) {
  2340.               $.off(this.el, 'click', this.cb);
  2341.             }
  2342.             this.cb = function() {
  2343.               $.event('CloseMenu');
  2344.               return Index.togglePin(thread);
  2345.             };
  2346.             $.on(this.el, 'click', this.cb);
  2347.             return true;
  2348.           }
  2349.         });
  2350.       }
  2351.     },
  2352.     threadNode: function() {
  2353.       if (!Index.db.get({
  2354.         boardID: this.board.ID,
  2355.         threadID: this.ID
  2356.       })) {
  2357.         return;
  2358.       }
  2359.       return this.pin();
  2360.     },
  2361.     catalogNode: function() {
  2362.       $.on(this.nodes.thumb, 'click', Index.onClick);
  2363.       if (Conf['Image Hover in Catalog']) {
  2364.         return;
  2365.       }
  2366.       return $.on(