Add Filter.

This commit is contained in:
Nicolas Stepien 2013-01-27 15:09:24 +01:00
parent 7dbd481a6d
commit c54ddff83d
5 changed files with 642 additions and 56 deletions

View File

@ -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 <james.r.campos@gmail.com>
@ -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();

View File

@ -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;
}

View File

@ -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: [

View File

@ -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 =
"<span class=fntrunc>#{FileInfo.escape shortname}</span><span class=fnfull>#{FileInfo.escape fullname}</span>"
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 =

View File

@ -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()