diff --git a/4chan_x.user.js b/4chan_x.user.js index 991de23e7..6545fee5d 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -20,7 +20,7 @@ // @icon https://github.com/MayhemYDG/4chan-x/raw/stable/img/icon.gif // ==/UserScript== -/* 4chan X Alpha - Version 3.0.0 - 2013-01-26 +/* 4chan X Alpha - Version 3.0.0 - 2013-01-27 * http://mayhemydg.github.com/4chan-x/ * * Copyright (c) 2009-2011 James Campos @@ -43,7 +43,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Get, ImageHover, Main, Menu, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, g, _base, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, ImageHover, Main, Menu, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, g, _base, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; @@ -119,7 +119,7 @@ }, filter: { name: ['# Filter any namefags:', '#/^(?!Anonymous$)/'].join('\n'), - uniqueid: ['# Filter a specific ID:', '#/Txhvk1Tl/'].join('\n'), + uniqueID: ['# Filter a specific ID:', '#/Txhvk1Tl/'].join('\n'), tripcode: ['# Filter any tripfags', '#/^!/'].join('\n'), capcode: ['# Set a custom class for mods:', '#/Mod$/;highlight:mod;op:yes', '# Set a custom class for moot:', '#/Admin$/;highlight:moot;op:yes'].join('\n'), email: ['# Filter any e-mails that are not `sage` on /a/ and /jp/:', '#/^(?!sage$)/;boards:a,jp'].join('\n'), @@ -129,7 +129,7 @@ filename: [''].join('\n'), dimensions: ['# Highlight potential wallpapers:', '#/1920x1080/;op:yes;highlight;top:no;boards:w,wg'].join('\n'), filesize: [''].join('\n'), - md5: [''].join('\n') + MD5: [''].join('\n') }, sauces: ['http://iqdb.org/?url=%turl', 'http://www.google.com/searchbyimage?image_url=%turl', '#http://tineye.com/search?url=%turl', '#http://saucenao.com/search.php?db=999&url=%turl', '#http://3d.iqdb.org/?url=%turl', '#http://regex.info/exif.cgi?imgurl=%url', '# uploaders:', '#http://imgur.com/upload?url=%url;text:Upload to imgur', '#http://omploader.org/upload?url1=%url;text:Upload to omploader', '# "View Same" in archives:', '#//archive.foolz.us/_/search/image/%md5/;text:View same on foolz', '#//archive.foolz.us/%board/search/image/%md5/;text:View same on foolz /%board/', '#//archive.installgentoo.net/%board/image/%md5;text:View same on installgentoo /%board/'].join('\n'), time: '%m/%d/%y(%a)%H:%M:%S', @@ -679,6 +679,262 @@ } }); + Filter = { + filters: {}, + init: function() { + var boards, filter, hl, key, op, regexp, stub, top, _i, _len, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; + for (key in Config.filter) { + this.filters[key] = []; + _ref = Conf[key].split('\n'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + filter = _ref[_i]; + if (filter[0] === '#') { + continue; + } + if (!(regexp = filter.match(/\/(.+)\/(\w*)/))) { + continue; + } + filter = filter.replace(regexp[0], ''); + boards = ((_ref1 = filter.match(/boards:([^;]+)/)) != null ? _ref1[1].toLowerCase() : void 0) || 'global'; + if (boards !== 'global' && !(_ref2 = g.BOARD.ID, __indexOf.call(boards.split(','), _ref2) >= 0)) { + continue; + } + if (key === 'uniqueID' || key === 'MD5') { + regexp = regexp[1]; + } else { + try { + regexp = RegExp(regexp[1], regexp[2]); + } catch (err) { + alert(err.message); + continue; + } + } + op = ((_ref3 = filter.match(/[^t]op:(yes|no|only)/)) != null ? _ref3[1] : void 0) || 'no'; + stub = (function() { + var _ref4; + switch ((_ref4 = filter.match(/stub:(yes|no)/)) != null ? _ref4[1] : void 0) { + case 'yes': + return true; + case 'no': + return false; + default: + return Conf['Stubs']; + } + })(); + if (hl = /highlight/.test(filter)) { + hl = ((_ref4 = filter.match(/highlight:(\w+)/)) != null ? _ref4[1] : void 0) || 'filter-highlight'; + top = ((_ref5 = filter.match(/top:(yes|no)/)) != null ? _ref5[1] : void 0) || 'yes'; + top = top === 'yes'; + } + this.filters[key].push(this.createFilter(regexp, op, stub, hl, top)); + } + if (!this.filters[key].length) { + delete this.filters[key]; + } + } + if (!Object.keys(this.filters).length) { + return; + } + return Post.prototype.callbacks.push({ + name: 'Thread Hiding', + cb: this.node + }); + }, + createFilter: function(regexp, op, stub, hl, top) { + var settings, test; + test = typeof regexp === 'string' ? function(value) { + return regexp === value; + } : function(value) { + return regexp.test(value); + }; + settings = { + hide: !hl, + stub: stub, + "class": hl, + top: top + }; + return function(value, isReply) { + if (isReply && op === 'only' || !isReply && op === 'no') { + return false; + } + if (!test(value)) { + return false; + } + return settings; + }; + }, + node: function() { + var filter, firstThread, key, result, thisThread, value, _i, _len, _ref; + if (this.isClone) { + return; + } + for (key in Filter.filters) { + value = Filter[key](this); + if (value === false) { + continue; + } + _ref = Filter.filters[key]; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + filter = _ref[_i]; + if (!(result = filter(value, this.isReply))) { + continue; + } + if (result.hide) { + if (this.isReply) { + ReplyHiding.hide(this, result.stub); + } else if (g.VIEW === 'index') { + ThreadHiding.hide(this.thread, result.stub); + } else { + continue; + } + return; + } + $.addClass(this.nodes.root, result["class"]); + if (!this.isReply && result.top && g.VIEW === 'index') { + thisThread = this.nodes.root.parentNode; + if (firstThread = $('div[class="postContainer opContainer"]')) { + if (firstThread !== this.nodes.root) { + $.before(firstThread.parentNode, [thisThread, thisThread.nextElementSibling]); + } + } + } + } + } + }, + name: function(post) { + if ('name' in post.info) { + return post.info.name; + } + return false; + }, + uniqueID: function(post) { + if ('uniqueID' in post.info) { + return post.info.uniqueID; + } + return false; + }, + tripcode: function(post) { + if ('tripcode' in post.info) { + return post.info.tripcode; + } + return false; + }, + capcode: function(post) { + if ('capcode' in post.info) { + return post.info.capcode; + } + return false; + }, + email: function(post) { + if ('email' in post.info) { + return post.info.email; + } + return false; + }, + subject: function(post) { + if ('subject' in post.info) { + return post.info.subject || false; + } + return false; + }, + comment: function(post) { + if ('comment' in post.info) { + return post.info.comment; + } + return false; + }, + flag: function(post) { + if ('flag' in post.info) { + return post.info.flag; + } + return false; + }, + filename: function(post) { + if (post.file) { + return post.file.name; + } + return false; + }, + dimensions: function(post) { + if (post.file && post.file.isImage) { + return post.file.dimensions; + } + return false; + }, + filesize: function(post) { + if (post.file) { + return post.file.size; + } + return false; + }, + MD5: function(post) { + if (post.file) { + return post.file.MD5; + } + return false; + }, + menu: { + init: function() { + var div, entry, type, _i, _len, _ref; + div = $.el('div', { + textContent: 'Filter' + }); + entry = { + el: div, + open: function(post) { + Filter.menu.post = post; + return true; + }, + children: [] + }; + _ref = [['Name', 'name'], ['Unique ID', 'uniqueID'], ['Tripcode', 'tripcode'], ['Capcode', 'capcode'], ['E-mail', 'email'], ['Subject', 'subject'], ['Comment', 'comment'], ['Flag', 'flag'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'MD5']]; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + type = _ref[_i]; + entry.children.push(Filter.menu.createSubEntry(type[0], type[1])); + } + return Menu.addEntry(entry); + }, + createSubEntry: function(text, type) { + var el; + el = $.el('a', { + href: 'javascript:;', + textContent: text + }); + el.setAttribute('data-type', type); + $.on(el, 'click', Filter.menu.makeFilter); + return { + el: el, + open: function(post) { + var value; + value = Filter[type](post); + return value !== false; + } + }; + }, + makeFilter: function() { + var re, save, type, value; + type = this.dataset.type; + value = Filter[type](Filter.menu.post); + re = type === 'uniqueID' || type === 'MD5' ? value : value.replace(/\/|\\|\^|\$|\n|\.|\(|\)|\{|\}|\[|\]|\?|\*|\+|\|/g, function(c) { + if (c === '\n') { + return '\\n'; + } else if (c === '\\') { + return '\\\\'; + } else { + return "\\" + c; + } + }); + re = type === 'uniqueID' || type === 'MD5' ? "/" + re + "/" : "/^" + re + "$/"; + if (!Filter.menu.post.isReply) { + re += ';op:yes'; + } + save = $.get(type, ''); + save = save ? "" + save + "\n" + re : re; + return $.set(type, save); + } + } + }; + ThreadHiding = { init: function() { if (g.VIEW !== 'index') { @@ -1579,7 +1835,7 @@ }, children: [] }; - _ref = [['Post', 'post'], ['Name', 'name'], ['Tripcode', 'tripcode'], ['E-mail', 'email'], ['Subject', 'subject'], ['Filename', 'filename'], ['Image MD5', 'md5']]; + _ref = [['Post', 'post'], ['Name', 'name'], ['Tripcode', 'tripcode'], ['E-mail', 'email'], ['Subject', 'subject'], ['Filename', 'filename'], ['Image MD5', 'MD5']]; for (_i = 0, _len = _ref.length; _i < _len; _i++) { type = _ref[_i]; entry.children.push(this.createSubEntry(type[0], type[1])); @@ -1603,6 +1859,17 @@ }; } else { open = function(post) { + var value; + value = Filter[type](post); + if (!value) { + return false; + } + el.href = Redirect.to({ + board: post.board, + type: type, + value: value, + isSearch: true + }); return true; }; } @@ -1741,7 +2008,7 @@ var board, path, postID, threadID, type, value; if (data.isSearch) { board = data.board, type = data.type, value = data.value; - type = type === 'name' ? 'username' : type === 'md5' ? 'image' : type; + type = type === 'name' ? 'username' : type === 'MD5' ? 'image' : type; value = encodeURIComponent(value); if (archiver === 'foolfuuka') { return "" + base + "/" + board + "/search/" + type + "/" + value; @@ -1752,7 +2019,7 @@ } } board = data.board, threadID = data.threadID, postID = data.postID; - if (typeof postID === 'string') { + if (postID && typeof postID === 'string') { postID = postID.match(/\d+/)[0]; } path = threadID ? "" + board + "/thread/" + threadID : "" + board + "/post/" + postID; @@ -2213,14 +2480,14 @@ a.setAttribute('data-postid', ID); } } - if (a) { - $.replace(deadlink, a); - } else { - deadlink.textContent += "\u00A0(Dead)"; - } - if (this.quotes.indexOf(quoteID) === -1) { + if (__indexOf.call(this.quotes, quoteID) < 0) { this.quotes.push(quoteID); } + if (!a) { + deadlink.textContent += "\u00A0(Dead)"; + continue; + } + $.replace(deadlink, a); if ($.hasClass(a, 'quotelink')) { this.nodes.quotelinks.push(a); } @@ -2735,16 +3002,16 @@ } }, s: function() { - return $.bytesToString(this.file.size); + return this.file.size; }, B: function() { - return FileInfo.convertUnit(this.file.size, 'B'); + return FileInfo.convertUnit(this.file.sizeInBytes, 'B'); }, K: function() { - return FileInfo.convertUnit(this.file.size, 'KB'); + return FileInfo.convertUnit(this.file.sizeInBytes, 'KB'); }, M: function() { - return FileInfo.convertUnit(this.file.size, 'MB'); + return FileInfo.convertUnit(this.file.sizeInBytes, 'MB'); }, r: function() { if (this.file.isImage) { @@ -3319,7 +3586,7 @@ } if (uniqueID = $('.posteruid', info)) { this.nodes.uniqueID = uniqueID; - this.info.uniqueID = uniqueID.textContent; + this.info.uniqueID = uniqueID.firstElementChild.textContent; } if (capcode = $('.capcode', info)) { this.nodes.capcode = capcode; @@ -3376,15 +3643,16 @@ text: fileInfo.firstElementChild, thumb: thumb, URL: anchor.href, + size: alt.match(/[\d.]+\s\w+/)[0], MD5: thumb.dataset.md5, isSpoiler: $.hasClass(anchor, 'imgspoiler') }; - size = +alt.match(/\d+(\.\d+)?/)[0]; - unit = ['B', 'KB', 'MB', 'GB'].indexOf(alt.match(/\w+$/)[0]); - while (unit--) { + size = +this.file.size.match(/[\d.]+/)[0]; + unit = ['B', 'KB', 'MB', 'GB'].indexOf(this.file.size.match(/\w+$/)[0]); + while (unit-- > 0) { size *= 1024; } - this.file.size = size; + this.file.sizeInBytes = size; this.file.thumbURL = that.isArchived ? thumb.src : "" + location.protocol + "//thumbs.4chan.org/" + board + "/thumb/" + (this.file.URL.match(/(\d+)\./)[1]) + "s.jpg"; this.file.name = $('span[title]', fileInfo).title.replace(/%22/g, '"'); if (this.file.isImage = /(jpg|png|gif)$/i.test(this.file.name)) { @@ -3632,6 +3900,13 @@ $.log(err, 'Resurrect Quotes'); } } + if (Conf['Filter']) { + try { + Filter.init(); + } catch (err) { + $.log(err, 'Filter'); + } + } if (Conf['Thread Hiding']) { try { ThreadHiding.init(); @@ -3685,6 +3960,13 @@ $.log(err, 'Delete Link'); } } + if (Conf['Filter']) { + try { + Filter.menu.init(); + } catch (err) { + $.log(err, 'Filter - Menu'); + } + } if (Conf['Download Link']) { try { DownloadLink.init(); @@ -3853,7 +4135,7 @@ settings: function() { return alert('Here be settings'); }, - css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n[hidden] {\n display: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#boardNavDesktop.reply,\n#qr, #watcher {\n position: fixed;\n}\n#qp, #ihover {\n z-index: 100;\n}\n#updater, #stats {\n z-index: 90;\n}\n#boardNavDesktop.reply:hover {\n z-index: 80;\n}\n#qr {\n z-index: 50;\n}\n#watcher {\n z-index: 30;\n}\n#boardNavDesktop.reply {\n z-index: 10;\n}\n\n\n/* header */\nbody.fourchan_x {\n margin-top: 2.5em;\n}\n#boardNavDesktop.reply {\n border-width: 0 0 1px;\n padding: 4px;\n top: 0;\n right: 0;\n left: 0;\n transition: opacity .1s ease-in-out;\n -o-transition: opacity .1s ease-in-out;\n -moz-transition: opacity .1s ease-in-out;\n -webkit-transition: opacity .1s ease-in-out;\n}\n#boardNavDesktop.reply:not(:hover) {\n opacity: .4;\n transition: opacity 1.5s .5s ease-in-out;\n -o-transition: opacity 1.5s .5s ease-in-out;\n -moz-transition: opacity 1.5s .5s ease-in-out;\n -webkit-transition: opacity 1.5s .5s ease-in-out;\n}\n#boardNavDesktop.reply a {\n margin: -1px;\n}\n#settings {\n float: right;\n}\n\n/* thread updater */\n#updater {\n text-align: right;\n}\n#updater:not(:hover) {\n background: none;\n border: none;\n}\n#updater input[type=number] {\n width: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\n display: none;\n}\n.new {\n color: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\n text-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\n text-decoration: none !important;\n}\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.filtered {\n text-decoration: underline line-through;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n box-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n padding-bottom: 16px;\n}\n\n/* thread & reply hiding */\n.hide-thread-button,\n.hide-reply-button {\n float: left;\n margin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\n display: none !important;\n}\n\n/* Menu */\n.menu-button {\n display: inline-block;\n}\n.menu-button > span {\n border-top: 6px solid;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n display: inline-block;\n margin: 2px;\n vertical-align: middle;\n}\n#menu {\n position: absolute;\n outline: none;\n}\n.entry {\n border-bottom: 1px solid rgba(0, 0, 0, .25);\n cursor: pointer;\n display: block;\n outline: none;\n padding: 3px 7px;\n position: relative;\n text-decoration: none;\n white-space: nowrap;\n}\n.entry:last-child {\n border: none;\n}\n.focused.entry {\n background: rgba(255, 255, 255, .33);\n}\n.entry.has-submenu {\n padding-right: 20px;\n}\n.has-submenu::after {\n content: \"\";\n border-left: 6px solid;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n display: inline-block;\n margin: 4px;\n position: absolute;\n right: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\n display: none;\n}\n.submenu {\n position: absolute;\n margin: -1px 0;\n}" + css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n[hidden] {\n display: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#boardNavDesktop.reply,\n#qr, #watcher {\n position: fixed;\n}\n#qp, #ihover {\n z-index: 100;\n}\n#updater, #stats {\n z-index: 90;\n}\n#boardNavDesktop.reply:hover {\n z-index: 80;\n}\n#qr {\n z-index: 50;\n}\n#watcher {\n z-index: 30;\n}\n#boardNavDesktop.reply {\n z-index: 10;\n}\n\n\n/* header */\nbody.fourchan_x {\n margin-top: 2.5em;\n}\n#boardNavDesktop.reply {\n border-width: 0 0 1px;\n padding: 4px;\n top: 0;\n right: 0;\n left: 0;\n transition: opacity .1s ease-in-out;\n -o-transition: opacity .1s ease-in-out;\n -moz-transition: opacity .1s ease-in-out;\n -webkit-transition: opacity .1s ease-in-out;\n}\n#boardNavDesktop.reply:not(:hover) {\n opacity: .4;\n transition: opacity 1.5s .5s ease-in-out;\n -o-transition: opacity 1.5s .5s ease-in-out;\n -moz-transition: opacity 1.5s .5s ease-in-out;\n -webkit-transition: opacity 1.5s .5s ease-in-out;\n}\n#boardNavDesktop.reply a {\n margin: -1px;\n}\n#settings {\n float: right;\n}\n\n/* thread updater */\n#updater {\n text-align: right;\n}\n#updater:not(:hover) {\n background: none;\n border: none;\n}\n#updater input[type=number] {\n width: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\n display: none;\n}\n.new {\n color: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\n text-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\n text-decoration: none !important;\n}\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.filtered {\n text-decoration: underline line-through;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n box-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n padding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\n box-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\n box-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7)\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\n float: left;\n margin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\n display: none !important;\n}\n\n/* Menu */\n.menu-button {\n display: inline-block;\n}\n.menu-button > span {\n border-top: 6px solid;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n display: inline-block;\n margin: 2px;\n vertical-align: middle;\n}\n#menu {\n position: absolute;\n outline: none;\n}\n.entry {\n border-bottom: 1px solid rgba(0, 0, 0, .25);\n cursor: pointer;\n display: block;\n outline: none;\n padding: 3px 7px;\n position: relative;\n text-decoration: none;\n white-space: nowrap;\n}\n.entry:last-child {\n border: none;\n}\n.focused.entry {\n background: rgba(255, 255, 255, .33);\n}\n.entry.has-submenu {\n padding-right: 20px;\n}\n.has-submenu::after {\n content: \"\";\n border-left: 6px solid;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n display: inline-block;\n margin: 4px;\n position: absolute;\n right: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\n display: none;\n}\n.submenu {\n position: absolute;\n margin: -1px 0;\n}\n.entry input {\n margin: 0;\n}" }; Main.init(); diff --git a/css/style.css b/css/style.css index 5bbad357e..0056fb247 100644 --- a/css/style.css +++ b/css/style.css @@ -163,7 +163,23 @@ body.fourchan_x { padding-bottom: 16px; } -/* thread & reply hiding */ +/* Filter */ +.opContainer.filter-highlight { + box-shadow: inset 5px 0 rgba(255, 0, 0, .5); +} +.opContainer.filter-highlight.qphl { + box-shadow: inset 5px 0 rgba(255, 0, 0, .5), + 0 0 0 2px rgba(216, 94, 49, .7); +} +.filter-highlight > .reply { + box-shadow: -5px 0 rgba(255, 0, 0, .5); +} +.filter-highlight > .reply.qphl { + box-shadow: -5px 0 rgba(255, 0, 0, .5), + 0 0 0 2px rgba(216, 94, 49, .7) +} + +/* Thread & Reply Hiding */ .hide-thread-button, .hide-reply-button { float: left; @@ -227,3 +243,6 @@ body.fourchan_x { position: absolute; margin: -1px 0; } +.entry input { + margin: 0; +} diff --git a/src/config.coffee b/src/config.coffee index 84dfed134..e8aabbaa0 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -64,7 +64,7 @@ Config = '# Filter any namefags:' '#/^(?!Anonymous$)/' ].join '\n' - uniqueid: [ + uniqueID: [ '# Filter a specific ID:' '#/Txhvk1Tl/' ].join '\n' @@ -103,7 +103,7 @@ Config = filesize: [ '' ].join '\n' - md5: [ + MD5: [ '' ].join '\n' sauces: [ diff --git a/src/features.coffee b/src/features.coffee index 716f51935..a447e6fda 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -1,3 +1,273 @@ +Filter = + filters: {} + init: -> + for key of Config.filter + @filters[key] = [] + for filter in Conf[key].split '\n' + continue if filter[0] is '#' + + unless regexp = filter.match /\/(.+)\/(\w*)/ + continue + + # Don't mix up filter flags with the regular expression. + filter = filter.replace regexp[0], '' + + # Do not add this filter to the list if it's not a global one + # and it's not specifically applicable to the current board. + # Defaults to global. + boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global' + if boards isnt 'global' and not (g.BOARD.ID in boards.split ',') + continue + + if key in ['uniqueID', 'MD5'] + # MD5 filter will use strings instead of regular expressions. + regexp = regexp[1] + else + try + # Please, don't write silly regular expressions. + regexp = RegExp regexp[1], regexp[2] + catch err + # I warned you, bro. + # XXX handle error + alert err.message + continue + + # Filter OPs along with their threads, replies only, or both. + # Defaults to replies only. + op = filter.match(/[^t]op:(yes|no|only)/)?[1] or 'no' + + # Overrule the `Show Stubs` setting. + # Defaults to stub showing. + stub = switch filter.match(/stub:(yes|no)/)?[1] + when 'yes' + true + when 'no' + false + else + Conf['Stubs'] + + # Highlight the post, or hide it. + # If not specified, the highlight class will be filter-highlight. + # Defaults to post hiding. + if hl = /highlight/.test filter + hl = filter.match(/highlight:(\w+)/)?[1] or 'filter-highlight' + # Put highlighted OP's thread on top of the board page or not. + # Defaults to on top. + top = filter.match(/top:(yes|no)/)?[1] or 'yes' + top = top is 'yes' # Turn it into a boolean + + @filters[key].push @createFilter regexp, op, stub, hl, top + + # Only execute filter types that contain valid filters. + unless @filters[key].length + delete @filters[key] + + return unless Object.keys(@filters).length + Post::callbacks.push + name: 'Thread Hiding' + cb: @node + + createFilter: (regexp, op, stub, hl, top) -> + test = + if typeof regexp is 'string' + # MD5 checking + (value) -> regexp is value + else + (value) -> regexp.test value + settings = + hide: !hl + stub: stub + class: hl + top: top + (value, isReply) -> + if isReply and op is 'only' or !isReply and op is 'no' + return false + unless test value + return false + settings + + node: -> + return if @isClone + for key of Filter.filters + value = Filter[key] @ + # Continue if there's nothing to filter (no tripcode for example). + continue if value is false + + for filter in Filter.filters[key] + unless result = filter value, @isReply + continue + + # Hide + if result.hide + if @isReply + ReplyHiding.hide @, result.stub + else if g.VIEW is 'index' + ThreadHiding.hide @thread, result.stub + else + continue + return + + # Highlight + $.addClass @nodes.root, result.class + if !@isReply and result.top and g.VIEW is 'index' + # Put the highlighted OPs' thread on top of the board page... + thisThread = @nodes.root.parentNode + # ...before the first non highlighted thread. + if firstThread = $ 'div[class="postContainer opContainer"]' + unless firstThread is @nodes.root + $.before firstThread.parentNode, [thisThread, thisThread.nextElementSibling] + + name: (post) -> + if 'name' of post.info + return post.info.name + false + uniqueID: (post) -> + if 'uniqueID' of post.info + return post.info.uniqueID + false + tripcode: (post) -> + if 'tripcode' of post.info + return post.info.tripcode + false + capcode: (post) -> + if 'capcode' of post.info + return post.info.capcode + false + email: (post) -> + if 'email' of post.info + return post.info.email + false + subject: (post) -> + if 'subject' of post.info + return post.info.subject or false + false + comment: (post) -> + if 'comment' of post.info + return post.info.comment + false + flag: (post) -> + if 'flag' of post.info + return post.info.flag + false + filename: (post) -> + if post.file + return post.file.name + false + dimensions: (post) -> + if post.file and post.file.isImage + return post.file.dimensions + false + filesize: (post) -> + if post.file + return post.file.size + false + MD5: (post) -> + if post.file + return post.file.MD5 + false + + menu: + init: -> + div = $.el 'div', + textContent: 'Filter' + + entry = + el: div + open: (post) -> + Filter.menu.post = post + true + children: [] + + for type in [ + ['Name', 'name'] + ['Unique ID', 'uniqueID'] + ['Tripcode', 'tripcode'] + ['Capcode', 'capcode'] + ['E-mail', 'email'] + ['Subject', 'subject'] + ['Comment', 'comment'] + ['Flag', 'flag'] + ['Filename', 'filename'] + ['Image dimensions', 'dimensions'] + ['Filesize', 'filesize'] + ['Image MD5', 'MD5'] + ] + # Add a sub entry for each filter type. + entry.children.push Filter.menu.createSubEntry type[0], type[1] + + Menu.addEntry entry + + createSubEntry: (text, type) -> + el = $.el 'a', + href: 'javascript:;' + textContent: text + el.setAttribute 'data-type', type + $.on el, 'click', Filter.menu.makeFilter + + return { + el: el + open: (post) -> + value = Filter[type] post + value isnt false + } + + makeFilter: -> + {type} = @dataset + # Convert value -> regexp, unless type is MD5 + value = Filter[type] Filter.menu.post + re = if type in ['uniqueID', 'MD5'] then value else value.replace /// + / + | \\ + | \^ + | \$ + | \n + | \. + | \( + | \) + | \{ + | \} + | \[ + | \] + | \? + | \* + | \+ + | \| + ///g, (c) -> + if c is '\n' + '\\n' + else if c is '\\' + '\\\\' + else + "\\#{c}" + + re = + if type in ['uniqueID', 'MD5'] + "/#{re}/" + else + "/^#{re}$/" + unless Filter.menu.post.isReply + re += ';op:yes' + + # Add a new line before the regexp unless the text is empty. + save = $.get type, '' + save = + if save + "#{save}\n#{re}" + else + re + $.set type, save + + # Open the options and display & focus the relevant filter textarea. + # Options.dialog() + # select = $ 'select[name=filter]', $.id 'options' + # select.value = type + # $.event select, new Event 'change' + # $.id('filter_tab').checked = true + # ta = select.nextElementSibling + # tl = ta.textLength + # ta.setSelectionRange tl, tl + # ta.focus() + ThreadHiding = init: -> return if g.VIEW isnt 'index' @@ -687,7 +957,7 @@ ArchiveLink = ['E-mail', 'email'] ['Subject', 'subject'] ['Filename', 'filename'] - ['Image MD5', 'md5'] + ['Image MD5', 'MD5'] ] # Add a sub entry for each type. entry.children.push @createSubEntry type[0], type[1] @@ -708,14 +978,14 @@ ArchiveLink = true else open = (post) -> - # value = Filter[type] post - # # We want to parse the exact same stuff as the filter does already. - # return false unless value - # el.href = Redirect.to - # board: post.board - # type: type - # value: value - # isSearch: true + value = Filter[type] post + # We want to parse the exact same stuff as the filter does already. + return false unless value + el.href = Redirect.to + board: post.board + type: type + value: value + isSearch: true true return { @@ -783,7 +1053,7 @@ Redirect = type = if type is 'name' 'username' - else if type is 'md5' + else if type is 'MD5' 'image' else type @@ -797,7 +1067,7 @@ Redirect = {board, threadID, postID} = data # keep the number only if the location.hash was sent f.e. - postID = postID.match(/\d+/)[0] if typeof postID is 'string' + postID = postID.match(/\d+/)[0] if postID and typeof postID is 'string' path = if threadID "#{board}/thread/#{threadID}" @@ -1344,13 +1614,14 @@ Quotify = a.setAttribute 'data-board', board a.setAttribute 'data-postid', ID - if a - $.replace deadlink, a - else - deadlink.textContent += "\u00A0(Dead)" - - if @quotes.indexOf(quoteID) is -1 + unless quoteID in @quotes @quotes.push quoteID + + unless a + deadlink.textContent += "\u00A0(Dead)" + continue + + $.replace deadlink, a if $.hasClass a, 'quotelink' @nodes.quotelinks.push a return @@ -1718,10 +1989,10 @@ FileInfo = "#{FileInfo.escape shortname}#{FileInfo.escape fullname}" N: -> FileInfo.escape @file.name p: -> if @file.isSpoiler then 'Spoiler, ' else '' - s: -> $.bytesToString @file.size - B: -> FileInfo.convertUnit @file.size, 'B' - K: -> FileInfo.convertUnit @file.size, 'KB' - M: -> FileInfo.convertUnit @file.size, 'MB' + s: -> @file.size + B: -> FileInfo.convertUnit @file.sizeInBytes, 'B' + K: -> FileInfo.convertUnit @file.sizeInBytes, 'KB' + M: -> FileInfo.convertUnit @file.sizeInBytes, 'MB' r: -> if @file.isImage then @file.dimensions else 'PDF' Sauce = diff --git a/src/main.coffee b/src/main.coffee index aaffecf9e..b6f81ce61 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -57,7 +57,7 @@ class Post @info.tripcode = tripcode.textContent if uniqueID = $ '.posteruid', info @nodes.uniqueID = uniqueID - @info.uniqueID = uniqueID.textContent + @info.uniqueID = uniqueID.firstElementChild.textContent if capcode = $ '.capcode', info @nodes.capcode = capcode @info.capcode = capcode.textContent @@ -113,21 +113,21 @@ class Post if (file = $ '.file', post) and thumb = $ 'img[data-md5]', file # Supports JPG/PNG/GIF/PDF. # Flash files are not supported. - alt = thumb.alt - anchor = thumb.parentNode + alt = thumb.alt + anchor = thumb.parentNode fileInfo = file.firstElementChild - @file = + @file = info: fileInfo text: fileInfo.firstElementChild thumb: thumb URL: anchor.href + size: alt.match(/[\d.]+\s\w+/)[0] MD5: thumb.dataset.md5 isSpoiler: $.hasClass anchor, 'imgspoiler' - size = +alt.match(/\d+(\.\d+)?/)[0] - unit = ['B', 'KB', 'MB', 'GB'].indexOf alt.match(/\w+$/)[0] - while unit-- - size *= 1024 - @file.size = size + size = +@file.size.match(/[\d.]+/)[0] + unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0] + size *= 1024 while unit-- > 0 + @file.sizeInBytes = size @file.thumbURL = if that.isArchived thumb.src @@ -323,6 +323,13 @@ Main = # XXX handle error $.log err, 'Resurrect Quotes' + if Conf['Filter'] + try + Filter.init() + catch err + # XXX handle error + $.log err, 'Filter' + if Conf['Thread Hiding'] try ThreadHiding.init() @@ -378,6 +385,13 @@ Main = # XXX handle error $.log err, 'Delete Link' + if Conf['Filter'] + try + Filter.menu.init() + catch err + # XXX handle error + $.log err, 'Filter - Menu' + if Conf['Download Link'] try DownloadLink.init()