Scroll back up to anchor.

This commit is contained in:
ahodesuka 2012-02-21 10:25:15 -06:00
parent 18898b825c
commit 15ee8ff6c5
3 changed files with 323 additions and 138 deletions

View File

@ -90,9 +90,8 @@
}, },
Filtering: { Filtering: {
'Anonymize': [false, 'Make everybody anonymous'], 'Anonymize': [false, 'Make everybody anonymous'],
'Filter': [false, 'Self-moderation placebo'], 'Filter': [true, 'Self-moderation placebo'],
'Filter OPs': [false, 'Filter OPs along with their threads'], 'Recursive Filtering': [true, 'Filter replies of filtered posts, recursively'],
'Recursive Filtering': [false, 'Filter replies of filtered posts, recursively'],
'Reply Hiding': [true, 'Hide single replies'], 'Reply Hiding': [true, 'Hide single replies'],
'Thread Hiding': [true, 'Hide entire threads'], 'Thread Hiding': [true, 'Hide entire threads'],
'Show Stubs': [true, 'Of hidden threads / replies'] 'Show Stubs': [true, 'Of hidden threads / replies']
@ -119,6 +118,7 @@
'Cooldown': [true, 'Prevent "flood detected" errors.'], 'Cooldown': [true, 'Prevent "flood detected" errors.'],
'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'], 'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'],
'Auto Hide QR': [true, 'Automatically hide the quick reply when posting.'], 'Auto Hide QR': [true, 'Automatically hide the quick reply when posting.'],
'Open Reply in New Tab': [false, 'Open replies in a new tab that are made from the main board.'],
'Remember QR size': [false, 'Remember the size of the Quick reply (Firefox only).'], 'Remember QR size': [false, 'Remember the size of the Quick reply (Firefox only).'],
'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.'], 'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.'],
'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'], 'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'],
@ -136,14 +136,15 @@
} }
}, },
filter: { filter: {
name: '', name: ['# Filter any namefags:', '#/^(?!Anonymous$)/'].join('\n'),
tripcode: '', tripcode: ['# Filter any tripfags', '#/^!/'].join('\n'),
email: '', email: ['# Filter any e-mails that are not `sage` on /a/ and /jp/:', '#/^(?!sage$)/;boards:a,jp'].join('\n'),
subject: '', subject: ['# Filter Generals on /v/:', '#/general/i;boards:v;op:only'].join('\n'),
comment: '', comment: ['# Filter Stallman copypasta on /g/:', '#/what you\'re refer+ing to as linux/i;boards:g'].join('\n'),
filename: '', filename: [''].join('\n'),
filesize: '', dimensions: ['# Highlight potential wallpapers:', '#/1920x1080/;op:yes;highlight;top:no;boards:w,wg'].join('\n'),
md5: '' filesize: [''].join('\n'),
md5: [''].join('\n')
}, },
sauces: ['http://iqdb.org/?url=$1', 'http://www.google.com/searchbyimage?image_url=$1', '#http://tineye.com/search?url=$1', '#http://saucenao.com/search.php?db=999&url=$1', '#http://3d.iqdb.org/?url=$1', '#http://regex.info/exif.cgi?imgurl=$2', '# uploaders:', '#http://imgur.com/upload?url=$2', '#http://omploader.org/upload?url1=$2', '# "View Same" in archives:', '#http://archive.foolz.us/a/image/$3/', '#http://archive.installgentoo.net/g/image/$3'].join('\n'), sauces: ['http://iqdb.org/?url=$1', 'http://www.google.com/searchbyimage?image_url=$1', '#http://tineye.com/search?url=$1', '#http://saucenao.com/search.php?db=999&url=$1', '#http://3d.iqdb.org/?url=$1', '#http://regex.info/exif.cgi?imgurl=$2', '# uploaders:', '#http://imgur.com/upload?url=$2', '#http://omploader.org/upload?url1=$2', '# "View Same" in archives:', '#http://archive.foolz.us/a/image/$3/', '#http://archive.installgentoo.net/g/image/$3'].join('\n'),
time: '%m/%d/%y(%a)%H:%M', time: '%m/%d/%y(%a)%H:%M',
@ -532,86 +533,126 @@
}; };
filter = { filter = {
regexps: {}, filters: {},
callbacks: [],
init: function() { init: function() {
var f, filter, key, m, _i, _len; var boards, filter, hl, key, op, regexp, top, _i, _len, _ref, _ref2, _ref3, _ref4, _ref5;
for (key in config.filter) { for (key in config.filter) {
if (!(m = conf[key].match(/^\/.+\/\w*$/gm))) continue; this.filters[key] = [];
this.regexps[key] = []; _ref = conf[key].split('\n');
for (_i = 0, _len = m.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
filter = m[_i]; filter = _ref[_i];
f = filter.match(/^\/(.+)\/(\w*)$/); if (filter[0] === '#') continue;
if (!(regexp = filter.match(/\/(.+)\/(\w*)/))) continue;
filter = filter.replace(regexp[0], '');
boards = ((_ref2 = filter.match(/boards:([^;]+)/)) != null ? _ref2[1].toLowerCase() : void 0) || 'global';
if (boards !== 'global' && boards.split(',').indexOf(g.BOARD) === -1) {
continue;
}
try { try {
this.regexps[key].push(RegExp(f[1], f[2])); regexp = RegExp(regexp[1], regexp[2]);
} catch (e) { } catch (e) {
alert(e.message); alert(e.message);
continue;
} }
op = ((_ref3 = filter.match(/[^t]op:(yes|no|only)/)) != null ? _ref3[1].toLowerCase() : void 0) || 'no';
if (hl = /highlight/.test(filter)) {
hl = ((_ref4 = filter.match(/highlight:(\w+)/)) != null ? _ref4[1].toLowerCase() : void 0) || 'filter_highlight';
top = ((_ref5 = filter.match(/top:(yes|no)/)) != null ? _ref5[1].toLowerCase() : void 0) || 'yes';
top = top === 'yes';
}
this.filters[key].push(this.createFilter(regexp, op, hl, top));
} }
this.callbacks.push(this[key]); if (!this.filters[key].length) delete this.filters[key];
} }
return g.callbacks.push(this.node); if (Object.keys(this.filters).length) return g.callbacks.push(this.node);
},
createFilter: function(regexp, op, hl, top) {
return function(root, value, isOP) {
var firstThread, thisThread;
if (isOP && op === 'no' || !isOP && op === 'only') return false;
if (!regexp.test(value)) return false;
if (hl) {
$.addClass(root, hl);
if (isOP && top && !g.REPLY) {
thisThread = root.parentNode;
if (firstThread = $('div[class=op]')) {
$.before(firstThread.parentNode, [thisThread, thisThread.nextElementSibling]);
}
}
return false;
}
if (isOP) {
if (!g.REPLY) threadHiding.hideHide(root.parentNode);
} else {
replyHiding.hideHide(root.previousSibling);
}
return true;
};
}, },
node: function(root) { node: function(root) {
if (!root.className) { var isOP, key, klass;
if (filter.callbacks.some(function(callback) { klass = root.className;
return callback(root); if (/\binlined\b/.test(klass)) return;
})) { if (!(isOP = klass === 'op')) root = $('td[id]', root);
return replyHiding.hideHide($('td:not([nowrap])', root)); for (key in filter.filters) {
} if (filter.test(root, key, isOP)) return;
} else if (root.className === 'op' && !g.REPLY && conf['Filter OPs']) {
if (filter.callbacks.some(function(callback) {
return callback(root);
})) {
return threadHiding.hideHide(root.parentNode);
}
} }
}, },
test: function(key, value) { test: function(root, key, isOP) {
return filter.regexps[key].some(function(regexp) { var filter, value, _i, _len, _ref;
return regexp.test(value); value = this[key](root, isOP);
}); if (value === false) return false;
_ref = this.filters[key];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
filter = _ref[_i];
if (filter(root, value, isOP)) return true;
}
return false;
}, },
name: function(root) { name: function(root, isOP) {
var name; var name;
name = root.className === 'op' ? $('.postername', root) : $('.commentpostername', root); name = isOP ? $('.postername', root) : $('.commentpostername', root);
return filter.test('name', name.textContent); return name.textContent;
}, },
tripcode: function(root) { tripcode: function(root) {
var trip; var trip;
if (trip = $('.postertrip', root)) { if (trip = $('.postertrip', root)) return trip.textContent;
return filter.test('tripcode', trip.textContent); return false;
}
}, },
email: function(root) { email: function(root) {
var mail; var mail;
if (mail = $('.linkmail', root)) return filter.test('email', mail.href); if (!(mail = $('.linkmail', root))) return mail.href;
return false;
}, },
subject: function(root) { subject: function(root, isOP) {
var sub; var sub;
sub = root.className === 'op' ? $('.filetitle', root) : $('.replytitle', root); sub = isOP ? $('.filetitle', root) : $('.replytitle', root);
return filter.test('subject', sub.textContent); return sub.textContent;
}, },
comment: function(root) { comment: function(root) {
return filter.test('comment', ($.el('a', { return ($.el('a', {
innerHTML: $('blockquote', root).innerHTML.replace(/<br>/g, '\n') innerHTML: $('blockquote', root).innerHTML.replace(/<br>/g, '\n')
})).textContent); })).textContent;
}, },
filename: function(root) { filename: function(root) {
var file; var file;
if (file = $('.filesize span', root)) { if (file = $('.filesize > span', root)) return file.title;
return filter.test('filename', file.title); return false;
} },
dimensions: function(root) {
var span;
if (span = $('.filesize', root)) return span.textContent.match(/\d+x\d+/)[0];
return false;
}, },
filesize: function(root) { filesize: function(root) {
var img; var img;
if (img = $('img[md5]', root)) return filter.test('filesize', img.alt); if (img = $('img[md5]', root)) return img.alt;
return false;
}, },
md5: function(root) { md5: function(root) {
var img; var img;
if (img = $('img[md5]', root)) { if (img = $('img[md5]', root)) return img.getAttribute('md5');
return filter.test('md5', img.getAttribute('md5')); return false;
}
} }
}; };
@ -1223,7 +1264,7 @@
$.before(form, link); $.before(form, link);
} }
g.callbacks.push(this.node); g.callbacks.push(this.node);
if (engine === 'webkit') { if (/chrome/i.test(navigator.userAgent)) {
qr.status({ qr.status({
ready: true ready: true
}); });
@ -1379,7 +1420,7 @@
if (e != null) e.preventDefault(); if (e != null) e.preventDefault();
qr.open(); qr.open();
if (!g.REPLY) { if (!g.REPLY) {
$('select', qr.el).value = $.x('ancestor::div', this).firstChild.id; $('select', qr.el).value = $.x('ancestor::div[@class="thread"]', this).firstChild.id;
} }
id = this.previousElementSibling.hash.slice(1); id = this.previousElementSibling.hash.slice(1);
text = ">>" + id + "\n"; text = ">>" + id + "\n";
@ -1678,7 +1719,7 @@
}); });
ta.style.cssText = $.get('qr.size', ''); ta.style.cssText = $.get('qr.size', '');
} }
mimeTypes = $('.rules').textContent.match(/: (.+) /)[1].toLowerCase().replace(/\w+/g, function(type) { mimeTypes = $('.rules').firstChild.textContent.match(/: (.+) /)[1].toLowerCase().replace(/\w+/g, function(type) {
switch (type) { switch (type) {
case 'jpg': case 'jpg':
return 'image/jpeg'; return 'image/jpeg';
@ -1832,14 +1873,14 @@
reader.readAsBinaryString(reply.file); reader.readAsBinaryString(reply.file);
return; return;
} }
if (engine === 'webkit') { if (/chrome/i.test(navigator.userAgent)) {
qr.message.post(post); qr.message.post(post);
return; return;
} }
return qr.message.send(post); return qr.message.send(post);
}, },
response: function(html) { response: function(html) {
var b, doc, err, node, persona, postNumber, reply, thread, _, _ref; var b, doc, err, node, open, persona, postNumber, reply, thread, _, _ref;
doc = $.el('a', { doc = $.el('a', {
innerHTML: html innerHTML: html
}); });
@ -1887,6 +1928,10 @@
} else { } else {
qr.cooldown.auto = qr.replies.length > 1; qr.cooldown.auto = qr.replies.length > 1;
qr.cooldown.set(/sage/i.test(reply.email) ? 60 : 30); qr.cooldown.set(/sage/i.test(reply.email) ? 60 : 30);
if (conf['Open Reply in New Tab'] && !g.REPLY && !qr.cooldown.auto) {
open = GM_openInTab || window.open;
open("http://boards.4chan.org/" + g.BOARD + "/res/" + thread + "#" + postNumber, "_blank");
}
} }
if (conf['Persistent QR'] || qr.cooldown.auto) { if (conf['Persistent QR'] || qr.cooldown.auto) {
reply.rm(); reply.rm();
@ -1899,7 +1944,7 @@
message: { message: {
send: function(data) { send: function(data) {
var host, window; var host, window;
if (engine === 'webkit') { if (/chrome/i.test(navigator.userAgent)) {
qr.message.receive(data); qr.message.receive(data);
return; return;
} }
@ -2071,7 +2116,7 @@
<input type=radio name=tab hidden id=sauces_tab>\ <input type=radio name=tab hidden id=sauces_tab>\
<div>\ <div>\
<div class=warning><code>Sauce</code> is disabled.</div>\ <div class=warning><code>Sauce</code> is disabled.</div>\
<div>Lines starting with a <code>#</code> will be ignored.</div>\ Lines starting with a <code>#</code> will be ignored.\
<ul>These variables will be replaced by the corresponding url:\ <ul>These variables will be replaced by the corresponding url:\
<li>$1: Thumbnail.</li>\ <li>$1: Thumbnail.</li>\
<li>$2: Full image.</li>\ <li>$2: Full image.</li>\
@ -2083,13 +2128,21 @@
<div>\ <div>\
<div class=warning><code>Filter</code> is disabled.</div>\ <div class=warning><code>Filter</code> is disabled.</div>\
Use <a href=https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions>regular expressions</a>, one per line.<br>\ Use <a href=https://developer.mozilla.org/en/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 `weeaboo` case-insensitive.\ For example, <code>/weeaboo/i</code> will filter posts containing `weeaboo` case-insensitive.\
<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`, this is default), or both (`yes`).<br>For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</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>hightlight:wallpaper;</code>.</li>\
<li>Highlighted OPs will have their threads put on top of board pages by default.<br>For example: <code>top:yes</code> or <code>top:no</code>.</li>\
</ul>\
<p>Name:<br><textarea name=name></textarea></p>\ <p>Name:<br><textarea name=name></textarea></p>\
<p>Tripcode:<br><textarea name=tripcode></textarea></p>\ <p>Tripcode:<br><textarea name=tripcode></textarea></p>\
<p>E-mail:<br><textarea name=email></textarea></p>\ <p>E-mail:<br><textarea name=email></textarea></p>\
<p>Subject:<br><textarea name=subject></textarea></p>\ <p>Subject:<br><textarea name=subject></textarea></p>\
<p>Comment:<br><textarea name=comment></textarea></p>\ <p>Comment:<br><textarea name=comment></textarea></p>\
<p>Filename:<br><textarea name=filename></textarea></p>\ <p>Filename:<br><textarea name=filename></textarea></p>\
<p>Image dimensions:<br><textarea name=dimensions></textarea></p>\
<p>Filesize:<br><textarea name=filesize></textarea></p>\ <p>Filesize:<br><textarea name=filesize></textarea></p>\
<p>Image MD5:<br><textarea name=md5></textarea></p>\ <p>Image MD5:<br><textarea name=md5></textarea></p>\
</div>\ </div>\
@ -3099,18 +3152,19 @@
return g.callbacks.push(this.node); return g.callbacks.push(this.node);
}, },
node: function(root) { node: function(root) {
var path, quote, tid, _i, _len, _ref; var hash, path, quote, tid, _i, _len, _ref;
if (root.className === 'inline') return; if (root.className === 'inline') return;
tid = g.THREAD_ID || $.x('ancestor::div[contains(@class,"thread")]', root).firstChild.id; tid = g.THREAD_ID || $.x('ancestor::div[contains(@class,"thread")]', root).firstChild.id;
_ref = $$('.quotelink', root); _ref = $$('.quotelink', root);
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
quote = _ref[_i]; quote = _ref[_i];
if (conf['Indicate OP quote'] && quote.hash.slice(1) === tid) { hash = quote.hash.slice(1);
if (conf['Indicate OP quote'] && hash === tid) {
$.add(quote, $.tn('\u00A0(OP)')); $.add(quote, $.tn('\u00A0(OP)'));
return; return;
} }
path = quote.pathname; path = quote.pathname;
if (conf['Indicate Cross-thread Quotes'] && path.lastIndexOf("/" + tid) === -1 && path.indexOf("/" + g.BOARD + "/") === 0) { if (conf['Indicate Cross-thread Quotes'] && hash && path.lastIndexOf("/" + tid) === -1 && path.indexOf("/" + g.BOARD + "/") === 0) {
$.add(quote, $.tn('\u00A0(Cross-thread)')); $.add(quote, $.tn('\u00A0(Cross-thread)'));
} }
} }
@ -3431,8 +3485,8 @@
var rect, thumb; var rect, thumb;
thumb = a.firstChild; thumb = a.firstChild;
if (thumb.hidden) { if (thumb.hidden) {
rect = a.parentNode.getBoundingClientRect(); rect = a.getBoundingClientRect();
if (rect.top < 0) d.body.scrollTop += rect.top; if (rect.top < 0) d.body.scrollTop += rect.top - 42;
if (rect.left < 0) d.body.scrollLeft += rect.left; if (rect.left < 0) d.body.scrollLeft += rect.left;
return imgExpand.contract(thumb); return imgExpand.contract(thumb);
} else { } else {
@ -3547,6 +3601,10 @@
return; return;
} }
$.ready(options.init); $.ready(options.init);
if (conf['Quick Reply'] && conf['Hide Original Post Form']) {
Main.css += 'form[name=post] { display: none; }';
}
Main.addStyle();
now = Date.now(); now = Date.now();
if (conf['Check for Updates'] && $.get('lastUpdate', 0) < now - 6 * HOUR) { if (conf['Check for Updates'] && $.get('lastUpdate', 0) < now - 6 * HOUR) {
$.ready(function() { $.ready(function() {
@ -3590,10 +3648,6 @@
quoteIndicators.init(); quoteIndicators.init();
} }
if (conf['Fix XXX\'d Post Numbers']) unxify.init(); if (conf['Fix XXX\'d Post Numbers']) unxify.init();
if (conf['Quick Reply'] && conf['Hide Original Post Form']) {
Main.css += 'form[name=post] { display: none; }';
}
Main.addStyle();
return $.ready(Main.ready); return $.ready(Main.ready);
}, },
ready: function() { ready: function() {
@ -4031,13 +4085,16 @@ img[md5], img[md5] + img {\
.inlined {\ .inlined {\
opacity: .5;\ opacity: .5;\
}\ }\
.inline td.reply {\ .inline .reply {\
background-color: rgba(255, 255, 255, 0.15);\ background-color: rgba(255, 255, 255, 0.15);\
border: 1px solid rgba(128, 128, 128, 0.5);\ border: 1px solid rgba(128, 128, 128, 0.5);\
}\ }\
.filetitle, .replytitle, .postername, .commentpostername, .postertrip {\ .filetitle, .replytitle, .postername, .commentpostername, .postertrip {\
background: none;\ background: none;\
}\ }\
.filter_highlight {\
box-shadow: -5px 0 rgba(255,0,0,0.5);\
}\
.filtered {\ .filtered {\
text-decoration: line-through;\ text-decoration: line-through;\
}\ }\

View File

@ -1,4 +1,15 @@
master master
- ahodesuka
Add Open Reply in New Tab option for replies made from the main board (not dumping).
Scroll back up (top of anchor - 42px) when unexpanding images.
- Mayhem
The Filter now has per filter settings:
- Filter the OP only along its thread, replies only, or both.
- Per boards, or global.
- Highlight, or hide. Highlighted OPs will have their threads put on top of the board page.
New filter group: Image dimensions.
Fix posting on Safari.
Fix rare case where the QR would not accept certain image types.
2.26.4 2.26.4
- Mayhem - Mayhem
@ -93,7 +104,7 @@ master
see https://github.com/MayhemYDG/4chan-x/issues/136 see https://github.com/MayhemYDG/4chan-x/issues/136
2.24.4 2.24.4
- ahokadesuka - ahodesuka
Scroll back up when unexpanding images. Scroll back up when unexpanding images.
- e000 - e000
Prevent absurd cooldown durations. Prevent absurd cooldown durations.

View File

@ -13,9 +13,8 @@ config =
'Check for Updates': [true, 'Check for updated versions of 4chan X'] 'Check for Updates': [true, 'Check for updated versions of 4chan X']
Filtering: Filtering:
'Anonymize': [false, 'Make everybody anonymous'] 'Anonymize': [false, 'Make everybody anonymous']
'Filter': [false, 'Self-moderation placebo'] 'Filter': [true, 'Self-moderation placebo']
'Filter OPs': [false, 'Filter OPs along with their threads'] 'Recursive Filtering': [true, 'Filter replies of filtered posts, recursively']
'Recursive Filtering': [false, 'Filter replies of filtered posts, recursively']
'Reply Hiding': [true, 'Hide single replies'] 'Reply Hiding': [true, 'Hide single replies']
'Thread Hiding': [true, 'Hide entire threads'] 'Thread Hiding': [true, 'Hide entire threads']
'Show Stubs': [true, 'Of hidden threads / replies'] 'Show Stubs': [true, 'Of hidden threads / replies']
@ -39,6 +38,7 @@ config =
'Cooldown': [true, 'Prevent "flood detected" errors.'] 'Cooldown': [true, 'Prevent "flood detected" errors.']
'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'] 'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.']
'Auto Hide QR': [true, 'Automatically hide the quick reply when posting.'] 'Auto Hide QR': [true, 'Automatically hide the quick reply when posting.']
'Open Reply in New Tab': [false, 'Open replies in a new tab that are made from the main board.']
'Remember QR size': [false, 'Remember the size of the Quick reply (Firefox only).'] 'Remember QR size': [false, 'Remember the size of the Quick reply (Firefox only).']
'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.'] 'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.']
'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'] 'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.']
@ -53,14 +53,39 @@ config =
'Indicate Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes'] 'Indicate Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes']
'Forward Hiding': [true, 'Hide original posts of inlined backlinks'] 'Forward Hiding': [true, 'Hide original posts of inlined backlinks']
filter: filter:
name: '' name: [
tripcode: '' '# Filter any namefags:'
email: '' '#/^(?!Anonymous$)/'
subject: '' ].join '\n'
comment: '' tripcode: [
filename: '' '# Filter any tripfags'
filesize: '' '#/^!/'
md5: '' ].join '\n'
email: [
'# Filter any e-mails that are not `sage` on /a/ and /jp/:'
'#/^(?!sage$)/;boards:a,jp'
].join '\n'
subject: [
'# Filter Generals on /v/:'
'#/general/i;boards:v;op:only'
].join '\n'
comment: [
'# Filter Stallman copypasta on /g/:'
'#/what you\'re refer+ing to as linux/i;boards:g'
].join '\n'
filename: [
''
].join '\n'
dimensions: [
'# Highlight potential wallpapers:'
'#/1920x1080/;op:yes;highlight;top:no;boards:w,wg'
].join '\n'
filesize: [
''
].join '\n'
md5: [
''
].join '\n'
sauces: [ sauces: [
'http://iqdb.org/?url=$1' 'http://iqdb.org/?url=$1'
'http://www.google.com/searchbyimage?image_url=$1' 'http://www.google.com/searchbyimage?image_url=$1'
@ -408,58 +433,132 @@ $$ = (selector, root=d.body) ->
Array::slice.call root.querySelectorAll selector Array::slice.call root.querySelectorAll selector
filter = filter =
regexps: {} filters: {}
callbacks: []
init: -> init: ->
for key of config.filter for key of config.filter
unless m = conf[key].match /^\/.+\/\w*$/gm @filters[key] = []
continue for filter in conf[key].split('\n')
@regexps[key] = [] continue if filter[0] is '#'
for filter in m
f = filter.match /^\/(.+)\/(\w*)$/
try
@regexps[key].push RegExp f[1], f[2]
catch e
alert e.message
#only execute what's filterable
@callbacks.push @[key]
g.callbacks.push @node 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 boards.split(',').indexOf(g.BOARD) is -1
continue
try
# Please, don't write silly regular expressions.
regexp = RegExp regexp[1], regexp[2]
catch e
# I warned you, bro.
alert e.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].toLowerCase() or 'no'
# 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].toLowerCase() 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].toLowerCase() or 'yes'
top = top is 'yes' # Turn it into a boolean
@filters[key].push @createFilter regexp, op, hl, top
# Only execute filter types that contain valid filters.
unless @filters[key].length
delete @filters[key]
if Object.keys(@filters).length
g.callbacks.push @node
createFilter: (regexp, op, hl, top) ->
(root, value, isOP) ->
if isOP and op is 'no' or !isOP and op is 'only'
return false
unless regexp.test value
return false
if hl
$.addClass root, hl
if isOP and top and not g.REPLY
# Put the highlighted OPs' threads on top of the board pages...
thisThread = root.parentNode
# ...before the first non highlighted thread.
if firstThread = $ 'div[class=op]'
$.before firstThread.parentNode, [thisThread, thisThread.nextElementSibling]
# Continue the filter lookup to add more classes or hide it.
return false
if isOP
unless g.REPLY
threadHiding.hideHide root.parentNode
else
replyHiding.hideHide root.previousSibling
true
node: (root) -> node: (root) ->
unless root.className klass = root.className
if filter.callbacks.some((callback) -> callback root) return if /\binlined\b/.test klass
replyHiding.hideHide $ 'td:not([nowrap])', root unless isOP = klass is 'op'
else if root.className is 'op' and not g.REPLY and conf['Filter OPs'] root = $ 'td[id]', root
if filter.callbacks.some((callback) -> callback root) for key of filter.filters
threadHiding.hideHide root.parentNode if filter.test root, key, isOP
return
test: (key, value) -> test: (root, key, isOP) ->
filter.regexps[key].some (regexp) -> regexp.test value value = @[key] root, isOP
if value is false
# Return if there's nothing to filter (no tripcode for example).
return false
name: (root) -> for filter in @filters[key]
name = if root.className is 'op' then $ '.postername', root else $ '.commentpostername', root if filter root, value, isOP
filter.test 'name', name.textContent return true
false
name: (root, isOP) ->
name = if isOP then $ '.postername', root else $ '.commentpostername', root
name.textContent
tripcode: (root) -> tripcode: (root) ->
if trip = $ '.postertrip', root if trip = $ '.postertrip', root
filter.test 'tripcode', trip.textContent return trip.textContent
false
email: (root) -> email: (root) ->
if mail = $ '.linkmail', root unless mail = $ '.linkmail', root
filter.test 'email', mail.href return mail.href
subject: (root) -> false
sub = if root.className is 'op' then $ '.filetitle', root else $ '.replytitle', root subject: (root, isOP) ->
filter.test 'subject', sub.textContent sub = if isOP then $ '.filetitle', root else $ '.replytitle', root
sub.textContent
comment: (root) -> comment: (root) ->
filter.test 'comment', ($.el 'a', innerHTML: $('blockquote', root).innerHTML.replace /<br>/g, '\n').textContent ($.el 'a', innerHTML: $('blockquote', root).innerHTML.replace /<br>/g, '\n').textContent
filename: (root) -> filename: (root) ->
if file = $ '.filesize span', root if file = $ '.filesize > span', root
filter.test 'filename', file.title return file.title
false
dimensions: (root) ->
if span = $ '.filesize', root
return span.textContent.match(/\d+x\d+/)[0]
return false
filesize: (root) -> filesize: (root) ->
if img = $ 'img[md5]', root if img = $ 'img[md5]', root
filter.test 'filesize', img.alt return img.alt
false
md5: (root) -> md5: (root) ->
if img = $ 'img[md5]', root if img = $ 'img[md5]', root
filter.test 'md5', img.getAttribute('md5') return img.getAttribute 'md5'
false
strikethroughQuotes = strikethroughQuotes =
init: -> init: ->
@ -900,7 +999,8 @@ qr =
$.before form, link $.before form, link
g.callbacks.push @node g.callbacks.push @node
if engine is 'webkit' # CORS is ignored for content script on Chrome, but not Safari/Oprah/Firefox.
if /chrome/i.test navigator.userAgent
qr.status ready: true qr.status ready: true
else else
iframe = $.el 'iframe', iframe = $.el 'iframe',
@ -1023,7 +1123,7 @@ qr =
e?.preventDefault() e?.preventDefault()
qr.open() qr.open()
unless g.REPLY unless g.REPLY
$('select', qr.el).value = $.x('ancestor::div', @).firstChild.id $('select', qr.el).value = $.x('ancestor::div[@class="thread"]', @).firstChild.id
# Make sure we get the correct number, even with XXX censors # Make sure we get the correct number, even with XXX censors
id = @previousElementSibling.hash[1..] id = @previousElementSibling.hash[1..]
@ -1265,7 +1365,7 @@ qr =
ta.style.cssText = $.get 'qr.size', '' ta.style.cssText = $.get 'qr.size', ''
# Allow only this board's supported files. # Allow only this board's supported files.
mimeTypes = $('.rules').textContent.match(/: (.+) /)[1].toLowerCase().replace /\w+/g, (type) -> mimeTypes = $('.rules').firstChild.textContent.match(/: (.+) /)[1].toLowerCase().replace /\w+/g, (type) ->
switch type switch type
when 'jpg' when 'jpg'
'image/jpeg' 'image/jpeg'
@ -1405,7 +1505,8 @@ qr =
reader.readAsBinaryString reply.file reader.readAsBinaryString reply.file
return return
if engine is 'webkit' # CORS is ignored for content script on Chrome, but not Safari/Oprah/Firefox.
if /chrome/i.test navigator.userAgent
qr.message.post post qr.message.post post
return return
qr.message.send post qr.message.send post
@ -1456,6 +1557,9 @@ qr =
# Enable auto-posting if we have stuff to post, disable it otherwise. # Enable auto-posting if we have stuff to post, disable it otherwise.
qr.cooldown.auto = qr.replies.length > 1 qr.cooldown.auto = qr.replies.length > 1
qr.cooldown.set if /sage/i.test reply.email then 60 else 30 qr.cooldown.set if /sage/i.test reply.email then 60 else 30
if conf['Open Reply in New Tab'] && !g.REPLY && !qr.cooldown.auto
open = GM_openInTab or window.open
open "http://boards.4chan.org/#{g.BOARD}/res/#{thread}##{postNumber}", "_blank"
if conf['Persistent QR'] or qr.cooldown.auto if conf['Persistent QR'] or qr.cooldown.auto
reply.rm() reply.rm()
@ -1467,7 +1571,8 @@ qr =
message: message:
send: (data) -> send: (data) ->
if engine is 'webkit' # CORS is ignored for content script on Chrome, but not Safari/Oprah/Firefox.
if /chrome/i.test navigator.userAgent
qr.message.receive data qr.message.receive data
return return
data.qr = true data.qr = true
@ -1607,7 +1712,7 @@ options =
<input type=radio name=tab hidden id=sauces_tab> <input type=radio name=tab hidden id=sauces_tab>
<div> <div>
<div class=warning><code>Sauce</code> is disabled.</div> <div class=warning><code>Sauce</code> is disabled.</div>
<div>Lines starting with a <code>#</code> will be ignored.</div> Lines starting with a <code>#</code> will be ignored.
<ul>These variables will be replaced by the corresponding url: <ul>These variables will be replaced by the corresponding url:
<li>$1: Thumbnail.</li> <li>$1: Thumbnail.</li>
<li>$2: Full image.</li> <li>$2: Full image.</li>
@ -1619,13 +1724,21 @@ options =
<div> <div>
<div class=warning><code>Filter</code> is disabled.</div> <div class=warning><code>Filter</code> is disabled.</div>
Use <a href=https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions>regular expressions</a>, one per line.<br> Use <a href=https://developer.mozilla.org/en/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 `weeaboo` case-insensitive. For example, <code>/weeaboo/i</code> will filter posts containing `weeaboo` case-insensitive.
<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`, this is default), or both (`yes`).<br>For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</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>hightlight:wallpaper;</code>.</li>
<li>Highlighted OPs will have their threads put on top of board pages by default.<br>For example: <code>top:yes</code> or <code>top:no</code>.</li>
</ul>
<p>Name:<br><textarea name=name></textarea></p> <p>Name:<br><textarea name=name></textarea></p>
<p>Tripcode:<br><textarea name=tripcode></textarea></p> <p>Tripcode:<br><textarea name=tripcode></textarea></p>
<p>E-mail:<br><textarea name=email></textarea></p> <p>E-mail:<br><textarea name=email></textarea></p>
<p>Subject:<br><textarea name=subject></textarea></p> <p>Subject:<br><textarea name=subject></textarea></p>
<p>Comment:<br><textarea name=comment></textarea></p> <p>Comment:<br><textarea name=comment></textarea></p>
<p>Filename:<br><textarea name=filename></textarea></p> <p>Filename:<br><textarea name=filename></textarea></p>
<p>Image dimensions:<br><textarea name=dimensions></textarea></p>
<p>Filesize:<br><textarea name=filesize></textarea></p> <p>Filesize:<br><textarea name=filesize></textarea></p>
<p>Image MD5:<br><textarea name=md5></textarea></p> <p>Image MD5:<br><textarea name=md5></textarea></p>
</div> </div>
@ -2441,13 +2554,14 @@ quoteIndicators =
# We use contains() so that it works with hidden threads # We use contains() so that it works with hidden threads
tid = g.THREAD_ID or $.x('ancestor::div[contains(@class,"thread")]', root).firstChild.id tid = g.THREAD_ID or $.x('ancestor::div[contains(@class,"thread")]', root).firstChild.id
for quote in $$ '.quotelink', root for quote in $$ '.quotelink', root
if conf['Indicate OP quote'] and quote.hash[1..] is tid hash = quote.hash[1..]
if conf['Indicate OP quote'] and hash is tid
# \u00A0 is nbsp # \u00A0 is nbsp
$.add quote, $.tn '\u00A0(OP)' $.add quote, $.tn '\u00A0(OP)'
return return
path = quote.pathname path = quote.pathname
#if quote leads to a different thread id and is located on the same board (index 0) #if quote leads to a different thread id and is located on the same board (index 0)
if conf['Indicate Cross-thread Quotes'] and path.lastIndexOf("/#{tid}") is -1 and path.indexOf("/#{g.BOARD}/") is 0 if conf['Indicate Cross-thread Quotes'] and hash and path.lastIndexOf("/#{tid}") is -1 and path.indexOf("/#{g.BOARD}/") is 0
# \u00A0 is nbsp # \u00A0 is nbsp
$.add quote, $.tn '\u00A0(Cross-thread)' $.add quote, $.tn '\u00A0(Cross-thread)'
return return
@ -2690,8 +2804,8 @@ imgExpand =
toggle: (a) -> toggle: (a) ->
thumb = a.firstChild thumb = a.firstChild
if thumb.hidden if thumb.hidden
rect = a.parentNode.getBoundingClientRect() rect = a.getBoundingClientRect()
d.body.scrollTop += rect.top if rect.top < 0 d.body.scrollTop += rect.top - 42 if rect.top < 0
d.body.scrollLeft += rect.left if rect.left < 0 d.body.scrollLeft += rect.left if rect.left < 0
imgExpand.contract thumb imgExpand.contract thumb
else else
@ -2784,6 +2898,11 @@ Main =
$.ready options.init $.ready options.init
if conf['Quick Reply'] and conf['Hide Original Post Form']
Main.css += 'form[name=post] { display: none; }'
Main.addStyle()
now = Date.now() now = Date.now()
if conf['Check for Updates'] and $.get('lastUpdate', 0) < now - 6*HOUR if conf['Check for Updates'] and $.get('lastUpdate', 0) < now - 6*HOUR
$.ready -> $.add d.head, $.el 'script', src: 'https://raw.github.com/mayhemydg/4chan-x/master/latest.js' $.ready -> $.add d.head, $.el 'script', src: 'https://raw.github.com/mayhemydg/4chan-x/master/latest.js'
@ -2854,11 +2973,6 @@ Main =
if conf['Fix XXX\'d Post Numbers'] if conf['Fix XXX\'d Post Numbers']
unxify.init() unxify.init()
if conf['Quick Reply'] and conf['Hide Original Post Form']
Main.css += 'form[name=post] { display: none; }'
Main.addStyle()
$.ready Main.ready $.ready Main.ready
ready: -> ready: ->
@ -3302,13 +3416,16 @@ img[md5], img[md5] + img {
.inlined { .inlined {
opacity: .5; opacity: .5;
} }
.inline td.reply { .inline .reply {
background-color: rgba(255, 255, 255, 0.15); background-color: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(128, 128, 128, 0.5); border: 1px solid rgba(128, 128, 128, 0.5);
} }
.filetitle, .replytitle, .postername, .commentpostername, .postertrip { .filetitle, .replytitle, .postername, .commentpostername, .postertrip {
background: none; background: none;
} }
.filter_highlight {
box-shadow: -5px 0 rgba(255,0,0,0.5);
}
.filtered { .filtered {
text-decoration: line-through; text-decoration: line-through;
} }