Release 4chan X v1.11.21.0.

This commit is contained in:
ccd0 2015-12-13 20:32:11 -08:00
parent 30adbd6fa2
commit 743723c35b
13 changed files with 299 additions and 285 deletions

View File

@ -2,6 +2,14 @@
Sometimes the changelog has notes (not comprehensive) acknowledging people's work. This does not mean the changes are their fault, only that their code was used. All changes to the script are chosen by and the fault of the maintainer (ccd0).
### v1.11.21
**v1.11.21.0** *(2015-12-13)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.21.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.21.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.20.3.
- (human) Add `exclude:` option to filters to apply filter to all boards except the specified ones.
- The dashed underlining on the matching quotelink within a post previewed from a backlink (or vice versa) has been extended to links in inlined posts.
- Various minor quotelink-related bugfixes.
### v1.11.20
**v1.11.20.3** *(2015-12-11)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.20.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.20.3/builds/4chan-X-noupdate.crx "Chromium version")]

Binary file not shown.

View File

@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X beta
// @version 1.11.20.3
// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X

View File

@ -1,7 +1,7 @@
// Generated by CoffeeScript
// ==UserScript==
// @name 4chan X beta
// @version 1.11.20.3
// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
@ -433,7 +433,7 @@
doc = d.documentElement;
g = {
VERSION: '1.11.20.3',
VERSION: '1.11.21.0',
NAMESPACE: '4chan X.',
boards: {}
};
@ -1404,6 +1404,8 @@
this.board = board1;
this.ID = +root.id.slice(2);
this.fullID = this.board + "." + this.ID;
this.context = this;
root.dataset.fullID = this.fullID;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@ -1543,7 +1545,8 @@
Post.prototype.parseQuote = function(quotelink) {
var fullID, match;
if (!(match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/))) {
match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/);
if (!(match || (this.isClone && quotelink.dataset.postID))) {
return;
}
this.nodes.quotelinks.push(quotelink);
@ -1696,8 +1699,10 @@
Clone = (function(superClass) {
extend(Clone, superClass);
Clone.prototype.isClone = true;
function Clone(origin1, context1, contractThumb) {
var file, info, inline, inlined, k, key, len1, len2, len3, nodes, post, q, ref, ref1, ref2, ref3, ref4, root, u, val;
var file, info, inline, inlined, k, key, len1, len2, len3, len4, node, nodes, post, q, ref, ref1, ref2, ref3, ref4, ref5, root, u, v, val;
this.origin = origin1;
this.context = context1;
ref = ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply'];
@ -1707,6 +1712,13 @@
}
nodes = this.origin.nodes;
root = contractThumb ? this.cloneWithoutVideo(nodes.root) : nodes.root.cloneNode(true);
Clone.prefix || (Clone.prefix = 0);
ref1 = [root].concat(slice.call($$('[id]', root)));
for (q = 0, len2 = ref1.length; q < len2; q++) {
node = ref1[q];
node.id = Clone.prefix + node.id;
}
Clone.prefix++;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@ -1729,14 +1741,14 @@
} else {
this.nodes.backlinks = info.getElementsByClassName('backlink');
}
ref1 = $$('.inline', post);
for (q = 0, len2 = ref1.length; q < len2; q++) {
inline = ref1[q];
ref2 = $$('.inline', post);
for (u = 0, len3 = ref2.length; u < len3; u++) {
inline = ref2[u];
$.rm(inline);
}
ref2 = $$('.inlined', post);
for (u = 0, len3 = ref2.length; u < len3; u++) {
inlined = ref2[u];
ref3 = $$('.inlined', post);
for (v = 0, len4 = ref3.length; v < len4; v++) {
inlined = ref3[v];
$.rmClass(inlined, 'inlined');
}
root.hidden = false;
@ -1767,11 +1779,12 @@
this.nodes.date = $('.dateTime', info);
}
this.parseQuotes();
this.quotes = slice.call(this.origin.quotes);
if (this.origin.file) {
this.file = {};
ref3 = this.origin.file;
for (key in ref3) {
val = ref3[key];
ref4 = this.origin.file;
for (key in ref4) {
val = ref4[key];
this.file[key] = val;
}
file = $('.file', post);
@ -1783,7 +1796,7 @@
if (this.file.videoThumb) {
this.file.thumb.muted = true;
}
if ((ref4 = this.file.thumb) != null ? ref4.dataset.src : void 0) {
if ((ref5 = this.file.thumb) != null ? ref5.dataset.src : void 0) {
this.file.thumb.src = this.file.thumb.dataset.src;
this.file.thumb.removeAttribute('data-src');
}
@ -1794,7 +1807,6 @@
if (this.origin.isDead) {
this.isDead = true;
}
this.isClone = true;
root.dataset.clone = this.origin.clones.push(this) - 1;
}
@ -2295,13 +2307,13 @@
})();
Fetcher = (function() {
function Fetcher(boardID1, threadID1, postID1, root1, context1) {
function Fetcher(boardID1, threadID1, postID1, root1, quoter1) {
var post;
this.boardID = boardID1;
this.threadID = threadID1;
this.postID = postID1;
this.root = root1;
this.context = context1;
this.quoter = quoter1;
if (post = g.posts[this.boardID + "." + this.postID]) {
this.insert(post);
return;
@ -2319,15 +2331,23 @@
}
Fetcher.prototype.insert = function(post) {
var clone, nodes;
var boardID, clone, k, len1, nodes, postID, quote, ref, ref1;
if (!this.root.parentNode) {
return;
}
clone = post.addClone(this.context, $.hasClass(this.root, 'dialog'));
clone = post.addClone(this.quoter.context, $.hasClass(this.root, 'dialog'));
Main.callbackNodes(Clone, [clone]);
nodes = clone.nodes;
$.rmAll(nodes.root);
$.add(nodes.root, nodes.post);
ref = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
for (k = 0, len1 = ref.length; k < len1; k++) {
quote = ref[k];
ref1 = Get.postDataFromLink(quote), boardID = ref1.boardID, postID = ref1.postID;
if (postID === this.quoter.ID && boardID === this.quoter.board.ID) {
$.addClass(quote, 'forwardlink');
}
}
$.rmAll(this.root);
$.add(this.root, nodes.root);
return $.event('PostsInserted');
@ -3089,10 +3109,10 @@
history.replaceState({}, '');
}
hash = this.location.hash.slice(1);
if (!(/^p\d+$/.test(hash) && (post = $.id(hash)))) {
if (!(/^\d*p\d+$/.test(hash) && (post = $.id(hash)))) {
return;
}
if ((Get.postFromRoot(post)).isHidden) {
if (!post.getBoundingClientRect().height) {
return;
}
return $.queueTask(function() {
@ -4547,15 +4567,12 @@
return Get.threadFromRoot($.x('ancestor::div[@class="thread"]', node));
},
postFromRoot: function(root) {
var boardID, index, link, post, postID;
var index, post;
if (root == null) {
return null;
}
link = $('.postNum > a[href*="#"]', root);
boardID = link.pathname.split(/\/+/)[1];
postID = link.hash.slice(2);
post = g.posts[root.dataset.fullID];
index = root.dataset.clone;
post = g.posts[boardID + "." + postID];
if (index) {
return post.clones[index];
} else {
@ -4565,9 +4582,6 @@
postFromNode: function(root) {
return Get.postFromRoot($.x('(ancestor::div[contains(@class,"postContainer")][1]|following::div[contains(@class,"postContainer")][1])', root));
},
contextFromNode: function(node) {
return Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', node));
},
postDataFromLink: function(link) {
var boardID, path, postID, ref, threadID;
if (link.hostname === 'boards.4chan.org') {
@ -4996,8 +5010,8 @@
return $.set(this.id + ".position", this.style.cssText);
};
hoverstart = function(arg) {
var asapTest, cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, asapTest = arg.asapTest, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
o = {
root: root,
el: el,
@ -5013,12 +5027,13 @@
};
o.hover = hover.bind(o);
o.hoverend = hoverend.bind(o);
$.asap(function() {
return !el.parentNode || asapTest();
}, function() {
o.hover(o.latestEvent);
new MutationObserver(function() {
if (el.parentNode) {
return o.hover(o.latestEvent);
}
}).observe(el, {
childList: true
});
$.on(root, endEvents, o.hoverend);
if ($.x('ancestor::div[contains(@class,"inline")][1]', root)) {
@ -5032,10 +5047,11 @@
};
return $.on(doc, 'mousemove', o.workaround);
};
hoverstart.padding = 25;
hover = function(e) {
var clientX, clientY, height, left, ref, right, style, threshold, top;
this.latestEvent = e;
height = this.height || this.el.offsetHeight;
height = (this.height || this.el.offsetHeight) + hoverstart.padding;
clientX = e.clientX, clientY = e.clientY;
top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120));
threshold = this.clientWidth / 2;
@ -5249,7 +5265,7 @@
Filter = {
filters: {},
init: function() {
var boards, err, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, regexp, stub, top;
var boards, err, excludes, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, stub, top;
if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) {
return;
}
@ -5270,6 +5286,9 @@
filter = line.replace(regexp[0], '');
boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global';
boards = boards === 'global' ? null : boards.split(',');
if (boards === null) {
excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase().split(',') : void 0) || null;
}
if (key === 'uniqueID' || key === 'MD5') {
regexp = regexp[1];
} else {
@ -5281,10 +5300,10 @@
continue;
}
}
op = ((ref3 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref3[1] : void 0) || 'yes';
op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes';
stub = (function() {
var ref4;
switch ((ref4 = filter.match(/stub:(yes|no)/)) != null ? ref4[1] : void 0) {
var ref5;
switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) {
case 'yes':
return true;
case 'no':
@ -5294,11 +5313,11 @@
}
})();
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';
hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight';
top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes';
top = top === 'yes';
}
this.filters[key].push(this.createFilter(regexp, boards, op, stub, hl, top));
this.filters[key].push(this.createFilter(regexp, boards, excludes, op, stub, hl, top));
}
if (!this.filters[key].length) {
delete this.filters[key];
@ -5312,7 +5331,7 @@
cb: this.node
});
},
createFilter: function(regexp, boards, op, stub, hl, top) {
createFilter: function(regexp, boards, excludes, op, stub, hl, top) {
var settings, test;
test = typeof regexp === 'string' ? function(value) {
return regexp === value;
@ -5329,6 +5348,9 @@
if (boards && indexOf.call(boards, boardID) < 0) {
return false;
}
if (excludes && indexOf.call(excludes, boardID) >= 0) {
return false;
}
if (isReply && op === 'only' || !isReply && op === 'no') {
return false;
}
@ -6259,7 +6281,7 @@
if (this.isClone && this.thread === this.context.thread) {
return;
}
ref = this.isClone ? this.context : this, board = ref.board, thread = ref.thread;
ref = this.context, board = ref.board, thread = ref.thread;
ref1 = this.nodes.quotelinks;
for (k = 0, len1 = ref1.length; k < len1; k++) {
quotelink = ref1[k];
@ -6330,20 +6352,21 @@
});
},
toggle: function(e) {
var boardID, context, postID, ref, threadID;
var boardID, context, postID, quoter, ref, threadID;
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
return;
}
e.preventDefault();
ref = Get.postDataFromLink(this), boardID = ref.boardID, threadID = ref.threadID, postID = ref.postID;
context = Get.contextFromNode(this);
quoter = Get.postFromNode(this);
context = quoter.context;
if ($.hasClass(this, 'inlined')) {
QuoteInline.rm(this, boardID, threadID, postID, context);
} else {
if ($.x("ancestor::div[@id='pc" + postID + "']", this)) {
if ($.x("ancestor::div[@data-full-i-d='" + boardID + "." + postID + "']", this)) {
return;
}
QuoteInline.add(this, boardID, threadID, postID, context);
QuoteInline.add(this, boardID, threadID, postID, context, quoter);
}
return this.classList.toggle('inlined');
},
@ -6354,18 +6377,17 @@
return $.x('ancestor-or-self::*[parent::blockquote][1]', quotelink);
}
},
add: function(quotelink, boardID, threadID, postID, context) {
add: function(quotelink, boardID, threadID, postID, context, quoter) {
var inline, isBacklink, post, qroot, root;
isBacklink = $.hasClass(quotelink, 'backlink');
inline = $.el('div', {
id: "i" + postID,
className: 'inline'
});
root = QuoteInline.findRoot(quotelink, isBacklink);
$.after(root, inline);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.addClass(qroot, 'hasInline');
new Fetcher(boardID, threadID, postID, inline, context);
new Fetcher(boardID, threadID, postID, inline, quoter);
if (!((post = g.posts[boardID + "." + postID]) && context.thread === post.thread)) {
return;
}
@ -6382,7 +6404,7 @@
var el, inlined, isBacklink, post, qroot, ref, root;
isBacklink = $.hasClass(quotelink, 'backlink');
root = QuoteInline.findRoot(quotelink, isBacklink);
root = $.x("following-sibling::div[@id='i" + postID + "'][1]", root);
root = $.x("following-sibling::div[div/@data-full-i-d='" + boardID + "." + postID + "'][1]", root);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.rm(root);
if (!$('.inline', qroot)) {
@ -6435,7 +6457,7 @@
quotelink.textContent = quotelink.textContent.replace(QuoteOP.text, '');
}
}
fullID = (this.isClone ? this.context : this).thread.fullID;
fullID = this.context.thread.fullID;
if (indexOf.call(quotes, fullID) < 0) {
return;
}
@ -6472,7 +6494,7 @@
}
},
mouseover: function(e) {
var boardID, clone, k, len1, len2, origin, post, postID, posts, q, qp, quote, quoterID, ref, ref1, threadID;
var boardID, k, len1, origin, post, postID, posts, qp, ref, threadID;
if ($.hasClass(this, 'inlined') || !d.contains(this)) {
return;
}
@ -6482,21 +6504,15 @@
className: 'dialog'
});
$.add(Header.hover, qp);
new Fetcher(boardID, threadID, postID, qp, Get.contextFromNode(this));
new Fetcher(boardID, threadID, postID, qp, Get.postFromNode(this));
UI.hover({
root: this,
el: qp,
latestEvent: e,
endEvents: 'mouseout click',
cb: QuotePreview.mouseout,
asapTest: function() {
return qp.firstElementChild;
}
cb: QuotePreview.mouseout
});
if (!(origin = g.posts[boardID + "." + postID])) {
return;
}
if (Conf['Quote Highlighting']) {
if (Conf['Quote Highlighting'] && (origin = g.posts[boardID + "." + postID])) {
posts = [origin].concat(origin.clones);
posts.pop();
for (k = 0, len1 = posts.length; k < len1; k++) {
@ -6504,15 +6520,6 @@
$.addClass(post.nodes.post, 'qphl');
}
}
quoterID = $.x('ancestor::*[@id][1]', this).id.match(/\d+$/)[0];
clone = Get.postFromRoot(qp.firstChild);
ref1 = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
for (q = 0, len2 = ref1.length; q < len2; q++) {
quote = ref1[q];
if (quote.hash.slice(2) === quoterID) {
$.addClass(quote, 'forwardlink');
}
}
},
mouseout: function() {
var clone, k, len1, post, ref, root;
@ -6802,7 +6809,7 @@
if (highlight = $('.highlight')) {
$.rmClass(highlight, 'highlight');
}
if (!QuoteYou.lastRead) {
if (!(QuoteYou.lastRead && doc.contains(QuoteYou.lastRead) && $.hasClass(QuoteYou.lastRead, 'quotesYou'))) {
if (!(post = QuoteYou.lastRead = $('.quotesYou'))) {
new Notice('warning', 'No posts are currently quoting you, loser.', 20);
return;
@ -6814,7 +6821,7 @@
post = QuoteYou.lastRead;
}
str = type + "::div[contains(@class,'quotesYou')]";
while (post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0)) {
while ((post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0))) {
if (QuoteYou.cb.scroll(post)) {
return;
}
@ -6822,14 +6829,16 @@
posts = $$('.quotesYou');
return QuoteYou.cb.scroll(posts[type === 'following' ? 0 : posts.length - 1]);
},
scroll: function(post) {
if (Get.postFromRoot(post).isHidden) {
scroll: function(root) {
var post;
post = $('.post', root);
if (!post.getBoundingClientRect().height) {
return false;
} else {
QuoteYou.lastRead = post;
QuoteYou.lastRead = root;
window.location = "#" + post.id;
Header.scrollTo(post);
$.addClass($('.post', post), 'highlight');
$.addClass(post, 'highlight');
return true;
}
}
@ -6852,16 +6861,13 @@
},
node: function() {
var deadlink, k, len1, ref;
if (this.isClone) {
return;
}
ref = $$('.deadlink', this.nodes.comment);
for (k = 0, len1 = ref.length; k < len1; k++) {
deadlink = ref[k];
if (this.isClone) {
if ($.hasClass(deadlink, 'quotelink')) {
this.nodes.quotelinks.push(deadlink);
}
} else {
Quotify.parseDeadlink.call(this, deadlink);
}
Quotify.parseDeadlink.call(this, deadlink);
}
},
parseDeadlink: function(deadlink) {
@ -11107,7 +11113,7 @@
},
mouseover: function(post) {
return function(e) {
var el, error, file, height, isVideo, left, maxHeight, maxWidth, padding, ref, ref1, ref2, right, scale, width, x;
var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x;
if (!doc.contains(this)) {
return;
}
@ -11151,9 +11157,8 @@
return results;
})(), width = ref1[0], height = ref1[1];
ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right;
padding = 25;
maxWidth = Math.max(left, doc.clientWidth - right);
maxHeight = doc.clientHeight - padding;
maxHeight = doc.clientHeight - UI.hover.padding;
scale = Math.min(1, maxWidth / width, maxHeight / height);
el.style.maxWidth = (scale * width) + "px";
el.style.maxHeight = (scale * height) + "px";
@ -11162,10 +11167,7 @@
el: el,
latestEvent: e,
endEvents: 'mouseout click',
asapTest: function() {
return true;
},
height: scale * height + padding,
height: scale * height,
noRemove: true,
cb: function() {
$.off(el, 'error', error);
@ -17286,7 +17288,7 @@
return;
}
$.extend(div, {
innerHTML: "<div class=\"warning\"><code>Filter</code> is disabled.</div><p>Use <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions\" target=\"_blank\">regular expressions</a>, one per line.<br>Lines starting with a <code>#</code> will be ignored.<br>For example, <code>/weeaboo/i</code> will filter posts containing the string \`<code>weeaboo</code>\`, case-insensitive.<br>MD5 filtering uses exact string matching, not regular expressions.</p><ul>You can use these settings with each regular expression, separate them with semicolons:<li>Per boards, separate them with commas. It is global if not specified.<br>For example: <code>boards:a,jp;</code>.</li><li>Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).<br>For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.</li><li>Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).<br>For example: <code>stub:yes;</code> or <code>stub:no;</code>.</li><li>Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.</li><li>Highlighted OPs will have their threads put on top of the board index by default.<br>For example: <code>top:yes;</code> or <code>top:no;</code>.</li></ul><p>Note: If you&#039;re using the native catalog rather than 4chan X&#039;s catalog, 4chan X&#039;s filters do not apply there.<br>The native catalog has its own separate filter list.</p>"
innerHTML: "<div class=\"warning\"><code>Filter</code> is disabled.</div><p>Use <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions\" target=\"_blank\">regular expressions</a>, one per line.<br>Lines starting with a <code>#</code> will be ignored.<br>For example, <code>/weeaboo/i</code> will filter posts containing the string \`<code>weeaboo</code>\`, case-insensitive.<br>MD5 filtering uses exact string matching, not regular expressions.</p><ul>You can use these settings with each regular expression, separate them with semicolons:<li>Per boards, separate them with commas. It is global if not specified.<br>For example: <code>boards:a,jp;</code>.</li><li>In case of a global rule, select boards to be excluded from the filter.<br>For example: <code>exclude:vg,v;</code>.</li><li>Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).<br>For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.</li><li>Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).<br>For example: <code>stub:yes;</code> or <code>stub:no;</code>.</li><li>Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.</li><li>Highlighted OPs will have their threads put on top of the board index by default.<br>For example: <code>top:yes;</code> or <code>top:no;</code>.</li></ul><p>Note: If you&#039;re using the native catalog rather than 4chan X&#039;s catalog, 4chan X&#039;s filters do not apply there.<br>The native catalog has its own separate filter list.</p>"
});
return $('.warning', div).hidden = Conf['Filter'];
},

Binary file not shown.

View File

@ -1,7 +1,7 @@
// Generated by CoffeeScript
// ==UserScript==
// @name 4chan X
// @version 1.11.20.3
// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
@ -433,7 +433,7 @@
doc = d.documentElement;
g = {
VERSION: '1.11.20.3',
VERSION: '1.11.21.0',
NAMESPACE: '4chan X.',
boards: {}
};
@ -1404,6 +1404,8 @@
this.board = board1;
this.ID = +root.id.slice(2);
this.fullID = this.board + "." + this.ID;
this.context = this;
root.dataset.fullID = this.fullID;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@ -1543,7 +1545,8 @@
Post.prototype.parseQuote = function(quotelink) {
var fullID, match;
if (!(match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/))) {
match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/);
if (!(match || (this.isClone && quotelink.dataset.postID))) {
return;
}
this.nodes.quotelinks.push(quotelink);
@ -1696,8 +1699,10 @@
Clone = (function(superClass) {
extend(Clone, superClass);
Clone.prototype.isClone = true;
function Clone(origin1, context1, contractThumb) {
var file, info, inline, inlined, k, key, len1, len2, len3, nodes, post, q, ref, ref1, ref2, ref3, ref4, root, u, val;
var file, info, inline, inlined, k, key, len1, len2, len3, len4, node, nodes, post, q, ref, ref1, ref2, ref3, ref4, ref5, root, u, v, val;
this.origin = origin1;
this.context = context1;
ref = ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply'];
@ -1707,6 +1712,13 @@
}
nodes = this.origin.nodes;
root = contractThumb ? this.cloneWithoutVideo(nodes.root) : nodes.root.cloneNode(true);
Clone.prefix || (Clone.prefix = 0);
ref1 = [root].concat(slice.call($$('[id]', root)));
for (q = 0, len2 = ref1.length; q < len2; q++) {
node = ref1[q];
node.id = Clone.prefix + node.id;
}
Clone.prefix++;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@ -1729,14 +1741,14 @@
} else {
this.nodes.backlinks = info.getElementsByClassName('backlink');
}
ref1 = $$('.inline', post);
for (q = 0, len2 = ref1.length; q < len2; q++) {
inline = ref1[q];
ref2 = $$('.inline', post);
for (u = 0, len3 = ref2.length; u < len3; u++) {
inline = ref2[u];
$.rm(inline);
}
ref2 = $$('.inlined', post);
for (u = 0, len3 = ref2.length; u < len3; u++) {
inlined = ref2[u];
ref3 = $$('.inlined', post);
for (v = 0, len4 = ref3.length; v < len4; v++) {
inlined = ref3[v];
$.rmClass(inlined, 'inlined');
}
root.hidden = false;
@ -1767,11 +1779,12 @@
this.nodes.date = $('.dateTime', info);
}
this.parseQuotes();
this.quotes = slice.call(this.origin.quotes);
if (this.origin.file) {
this.file = {};
ref3 = this.origin.file;
for (key in ref3) {
val = ref3[key];
ref4 = this.origin.file;
for (key in ref4) {
val = ref4[key];
this.file[key] = val;
}
file = $('.file', post);
@ -1783,7 +1796,7 @@
if (this.file.videoThumb) {
this.file.thumb.muted = true;
}
if ((ref4 = this.file.thumb) != null ? ref4.dataset.src : void 0) {
if ((ref5 = this.file.thumb) != null ? ref5.dataset.src : void 0) {
this.file.thumb.src = this.file.thumb.dataset.src;
this.file.thumb.removeAttribute('data-src');
}
@ -1794,7 +1807,6 @@
if (this.origin.isDead) {
this.isDead = true;
}
this.isClone = true;
root.dataset.clone = this.origin.clones.push(this) - 1;
}
@ -2295,13 +2307,13 @@
})();
Fetcher = (function() {
function Fetcher(boardID1, threadID1, postID1, root1, context1) {
function Fetcher(boardID1, threadID1, postID1, root1, quoter1) {
var post;
this.boardID = boardID1;
this.threadID = threadID1;
this.postID = postID1;
this.root = root1;
this.context = context1;
this.quoter = quoter1;
if (post = g.posts[this.boardID + "." + this.postID]) {
this.insert(post);
return;
@ -2319,15 +2331,23 @@
}
Fetcher.prototype.insert = function(post) {
var clone, nodes;
var boardID, clone, k, len1, nodes, postID, quote, ref, ref1;
if (!this.root.parentNode) {
return;
}
clone = post.addClone(this.context, $.hasClass(this.root, 'dialog'));
clone = post.addClone(this.quoter.context, $.hasClass(this.root, 'dialog'));
Main.callbackNodes(Clone, [clone]);
nodes = clone.nodes;
$.rmAll(nodes.root);
$.add(nodes.root, nodes.post);
ref = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
for (k = 0, len1 = ref.length; k < len1; k++) {
quote = ref[k];
ref1 = Get.postDataFromLink(quote), boardID = ref1.boardID, postID = ref1.postID;
if (postID === this.quoter.ID && boardID === this.quoter.board.ID) {
$.addClass(quote, 'forwardlink');
}
}
$.rmAll(this.root);
$.add(this.root, nodes.root);
return $.event('PostsInserted');
@ -3089,10 +3109,10 @@
history.replaceState({}, '');
}
hash = this.location.hash.slice(1);
if (!(/^p\d+$/.test(hash) && (post = $.id(hash)))) {
if (!(/^\d*p\d+$/.test(hash) && (post = $.id(hash)))) {
return;
}
if ((Get.postFromRoot(post)).isHidden) {
if (!post.getBoundingClientRect().height) {
return;
}
return $.queueTask(function() {
@ -4547,15 +4567,12 @@
return Get.threadFromRoot($.x('ancestor::div[@class="thread"]', node));
},
postFromRoot: function(root) {
var boardID, index, link, post, postID;
var index, post;
if (root == null) {
return null;
}
link = $('.postNum > a[href*="#"]', root);
boardID = link.pathname.split(/\/+/)[1];
postID = link.hash.slice(2);
post = g.posts[root.dataset.fullID];
index = root.dataset.clone;
post = g.posts[boardID + "." + postID];
if (index) {
return post.clones[index];
} else {
@ -4565,9 +4582,6 @@
postFromNode: function(root) {
return Get.postFromRoot($.x('(ancestor::div[contains(@class,"postContainer")][1]|following::div[contains(@class,"postContainer")][1])', root));
},
contextFromNode: function(node) {
return Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', node));
},
postDataFromLink: function(link) {
var boardID, path, postID, ref, threadID;
if (link.hostname === 'boards.4chan.org') {
@ -4996,8 +5010,8 @@
return $.set(this.id + ".position", this.style.cssText);
};
hoverstart = function(arg) {
var asapTest, cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, asapTest = arg.asapTest, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
o = {
root: root,
el: el,
@ -5013,12 +5027,13 @@
};
o.hover = hover.bind(o);
o.hoverend = hoverend.bind(o);
$.asap(function() {
return !el.parentNode || asapTest();
}, function() {
o.hover(o.latestEvent);
new MutationObserver(function() {
if (el.parentNode) {
return o.hover(o.latestEvent);
}
}).observe(el, {
childList: true
});
$.on(root, endEvents, o.hoverend);
if ($.x('ancestor::div[contains(@class,"inline")][1]', root)) {
@ -5032,10 +5047,11 @@
};
return $.on(doc, 'mousemove', o.workaround);
};
hoverstart.padding = 25;
hover = function(e) {
var clientX, clientY, height, left, ref, right, style, threshold, top;
this.latestEvent = e;
height = this.height || this.el.offsetHeight;
height = (this.height || this.el.offsetHeight) + hoverstart.padding;
clientX = e.clientX, clientY = e.clientY;
top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120));
threshold = this.clientWidth / 2;
@ -5249,7 +5265,7 @@
Filter = {
filters: {},
init: function() {
var boards, err, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, regexp, stub, top;
var boards, err, excludes, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, stub, top;
if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) {
return;
}
@ -5270,6 +5286,9 @@
filter = line.replace(regexp[0], '');
boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global';
boards = boards === 'global' ? null : boards.split(',');
if (boards === null) {
excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase().split(',') : void 0) || null;
}
if (key === 'uniqueID' || key === 'MD5') {
regexp = regexp[1];
} else {
@ -5281,10 +5300,10 @@
continue;
}
}
op = ((ref3 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref3[1] : void 0) || 'yes';
op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes';
stub = (function() {
var ref4;
switch ((ref4 = filter.match(/stub:(yes|no)/)) != null ? ref4[1] : void 0) {
var ref5;
switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) {
case 'yes':
return true;
case 'no':
@ -5294,11 +5313,11 @@
}
})();
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';
hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight';
top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes';
top = top === 'yes';
}
this.filters[key].push(this.createFilter(regexp, boards, op, stub, hl, top));
this.filters[key].push(this.createFilter(regexp, boards, excludes, op, stub, hl, top));
}
if (!this.filters[key].length) {
delete this.filters[key];
@ -5312,7 +5331,7 @@
cb: this.node
});
},
createFilter: function(regexp, boards, op, stub, hl, top) {
createFilter: function(regexp, boards, excludes, op, stub, hl, top) {
var settings, test;
test = typeof regexp === 'string' ? function(value) {
return regexp === value;
@ -5329,6 +5348,9 @@
if (boards && indexOf.call(boards, boardID) < 0) {
return false;
}
if (excludes && indexOf.call(excludes, boardID) >= 0) {
return false;
}
if (isReply && op === 'only' || !isReply && op === 'no') {
return false;
}
@ -6259,7 +6281,7 @@
if (this.isClone && this.thread === this.context.thread) {
return;
}
ref = this.isClone ? this.context : this, board = ref.board, thread = ref.thread;
ref = this.context, board = ref.board, thread = ref.thread;
ref1 = this.nodes.quotelinks;
for (k = 0, len1 = ref1.length; k < len1; k++) {
quotelink = ref1[k];
@ -6330,20 +6352,21 @@
});
},
toggle: function(e) {
var boardID, context, postID, ref, threadID;
var boardID, context, postID, quoter, ref, threadID;
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
return;
}
e.preventDefault();
ref = Get.postDataFromLink(this), boardID = ref.boardID, threadID = ref.threadID, postID = ref.postID;
context = Get.contextFromNode(this);
quoter = Get.postFromNode(this);
context = quoter.context;
if ($.hasClass(this, 'inlined')) {
QuoteInline.rm(this, boardID, threadID, postID, context);
} else {
if ($.x("ancestor::div[@id='pc" + postID + "']", this)) {
if ($.x("ancestor::div[@data-full-i-d='" + boardID + "." + postID + "']", this)) {
return;
}
QuoteInline.add(this, boardID, threadID, postID, context);
QuoteInline.add(this, boardID, threadID, postID, context, quoter);
}
return this.classList.toggle('inlined');
},
@ -6354,18 +6377,17 @@
return $.x('ancestor-or-self::*[parent::blockquote][1]', quotelink);
}
},
add: function(quotelink, boardID, threadID, postID, context) {
add: function(quotelink, boardID, threadID, postID, context, quoter) {
var inline, isBacklink, post, qroot, root;
isBacklink = $.hasClass(quotelink, 'backlink');
inline = $.el('div', {
id: "i" + postID,
className: 'inline'
});
root = QuoteInline.findRoot(quotelink, isBacklink);
$.after(root, inline);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.addClass(qroot, 'hasInline');
new Fetcher(boardID, threadID, postID, inline, context);
new Fetcher(boardID, threadID, postID, inline, quoter);
if (!((post = g.posts[boardID + "." + postID]) && context.thread === post.thread)) {
return;
}
@ -6382,7 +6404,7 @@
var el, inlined, isBacklink, post, qroot, ref, root;
isBacklink = $.hasClass(quotelink, 'backlink');
root = QuoteInline.findRoot(quotelink, isBacklink);
root = $.x("following-sibling::div[@id='i" + postID + "'][1]", root);
root = $.x("following-sibling::div[div/@data-full-i-d='" + boardID + "." + postID + "'][1]", root);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.rm(root);
if (!$('.inline', qroot)) {
@ -6435,7 +6457,7 @@
quotelink.textContent = quotelink.textContent.replace(QuoteOP.text, '');
}
}
fullID = (this.isClone ? this.context : this).thread.fullID;
fullID = this.context.thread.fullID;
if (indexOf.call(quotes, fullID) < 0) {
return;
}
@ -6472,7 +6494,7 @@
}
},
mouseover: function(e) {
var boardID, clone, k, len1, len2, origin, post, postID, posts, q, qp, quote, quoterID, ref, ref1, threadID;
var boardID, k, len1, origin, post, postID, posts, qp, ref, threadID;
if ($.hasClass(this, 'inlined') || !d.contains(this)) {
return;
}
@ -6482,21 +6504,15 @@
className: 'dialog'
});
$.add(Header.hover, qp);
new Fetcher(boardID, threadID, postID, qp, Get.contextFromNode(this));
new Fetcher(boardID, threadID, postID, qp, Get.postFromNode(this));
UI.hover({
root: this,
el: qp,
latestEvent: e,
endEvents: 'mouseout click',
cb: QuotePreview.mouseout,
asapTest: function() {
return qp.firstElementChild;
}
cb: QuotePreview.mouseout
});
if (!(origin = g.posts[boardID + "." + postID])) {
return;
}
if (Conf['Quote Highlighting']) {
if (Conf['Quote Highlighting'] && (origin = g.posts[boardID + "." + postID])) {
posts = [origin].concat(origin.clones);
posts.pop();
for (k = 0, len1 = posts.length; k < len1; k++) {
@ -6504,15 +6520,6 @@
$.addClass(post.nodes.post, 'qphl');
}
}
quoterID = $.x('ancestor::*[@id][1]', this).id.match(/\d+$/)[0];
clone = Get.postFromRoot(qp.firstChild);
ref1 = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
for (q = 0, len2 = ref1.length; q < len2; q++) {
quote = ref1[q];
if (quote.hash.slice(2) === quoterID) {
$.addClass(quote, 'forwardlink');
}
}
},
mouseout: function() {
var clone, k, len1, post, ref, root;
@ -6802,7 +6809,7 @@
if (highlight = $('.highlight')) {
$.rmClass(highlight, 'highlight');
}
if (!QuoteYou.lastRead) {
if (!(QuoteYou.lastRead && doc.contains(QuoteYou.lastRead) && $.hasClass(QuoteYou.lastRead, 'quotesYou'))) {
if (!(post = QuoteYou.lastRead = $('.quotesYou'))) {
new Notice('warning', 'No posts are currently quoting you, loser.', 20);
return;
@ -6814,7 +6821,7 @@
post = QuoteYou.lastRead;
}
str = type + "::div[contains(@class,'quotesYou')]";
while (post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0)) {
while ((post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0))) {
if (QuoteYou.cb.scroll(post)) {
return;
}
@ -6822,14 +6829,16 @@
posts = $$('.quotesYou');
return QuoteYou.cb.scroll(posts[type === 'following' ? 0 : posts.length - 1]);
},
scroll: function(post) {
if (Get.postFromRoot(post).isHidden) {
scroll: function(root) {
var post;
post = $('.post', root);
if (!post.getBoundingClientRect().height) {
return false;
} else {
QuoteYou.lastRead = post;
QuoteYou.lastRead = root;
window.location = "#" + post.id;
Header.scrollTo(post);
$.addClass($('.post', post), 'highlight');
$.addClass(post, 'highlight');
return true;
}
}
@ -6852,16 +6861,13 @@
},
node: function() {
var deadlink, k, len1, ref;
if (this.isClone) {
return;
}
ref = $$('.deadlink', this.nodes.comment);
for (k = 0, len1 = ref.length; k < len1; k++) {
deadlink = ref[k];
if (this.isClone) {
if ($.hasClass(deadlink, 'quotelink')) {
this.nodes.quotelinks.push(deadlink);
}
} else {
Quotify.parseDeadlink.call(this, deadlink);
}
Quotify.parseDeadlink.call(this, deadlink);
}
},
parseDeadlink: function(deadlink) {
@ -11107,7 +11113,7 @@
},
mouseover: function(post) {
return function(e) {
var el, error, file, height, isVideo, left, maxHeight, maxWidth, padding, ref, ref1, ref2, right, scale, width, x;
var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x;
if (!doc.contains(this)) {
return;
}
@ -11151,9 +11157,8 @@
return results;
})(), width = ref1[0], height = ref1[1];
ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right;
padding = 25;
maxWidth = Math.max(left, doc.clientWidth - right);
maxHeight = doc.clientHeight - padding;
maxHeight = doc.clientHeight - UI.hover.padding;
scale = Math.min(1, maxWidth / width, maxHeight / height);
el.style.maxWidth = (scale * width) + "px";
el.style.maxHeight = (scale * height) + "px";
@ -11162,10 +11167,7 @@
el: el,
latestEvent: e,
endEvents: 'mouseout click',
asapTest: function() {
return true;
},
height: scale * height + padding,
height: scale * height,
noRemove: true,
cb: function() {
$.off(el, 'error', error);
@ -17286,7 +17288,7 @@
return;
}
$.extend(div, {
innerHTML: "<div class=\"warning\"><code>Filter</code> is disabled.</div><p>Use <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions\" target=\"_blank\">regular expressions</a>, one per line.<br>Lines starting with a <code>#</code> will be ignored.<br>For example, <code>/weeaboo/i</code> will filter posts containing the string \`<code>weeaboo</code>\`, case-insensitive.<br>MD5 filtering uses exact string matching, not regular expressions.</p><ul>You can use these settings with each regular expression, separate them with semicolons:<li>Per boards, separate them with commas. It is global if not specified.<br>For example: <code>boards:a,jp;</code>.</li><li>Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).<br>For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.</li><li>Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).<br>For example: <code>stub:yes;</code> or <code>stub:no;</code>.</li><li>Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.</li><li>Highlighted OPs will have their threads put on top of the board index by default.<br>For example: <code>top:yes;</code> or <code>top:no;</code>.</li></ul><p>Note: If you&#039;re using the native catalog rather than 4chan X&#039;s catalog, 4chan X&#039;s filters do not apply there.<br>The native catalog has its own separate filter list.</p>"
innerHTML: "<div class=\"warning\"><code>Filter</code> is disabled.</div><p>Use <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions\" target=\"_blank\">regular expressions</a>, one per line.<br>Lines starting with a <code>#</code> will be ignored.<br>For example, <code>/weeaboo/i</code> will filter posts containing the string \`<code>weeaboo</code>\`, case-insensitive.<br>MD5 filtering uses exact string matching, not regular expressions.</p><ul>You can use these settings with each regular expression, separate them with semicolons:<li>Per boards, separate them with commas. It is global if not specified.<br>For example: <code>boards:a,jp;</code>.</li><li>In case of a global rule, select boards to be excluded from the filter.<br>For example: <code>exclude:vg,v;</code>.</li><li>Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).<br>For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.</li><li>Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).<br>For example: <code>stub:yes;</code> or <code>stub:no;</code>.</li><li>Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.</li><li>Highlighted OPs will have their threads put on top of the board index by default.<br>For example: <code>top:yes;</code> or <code>top:no;</code>.</li></ul><p>Note: If you&#039;re using the native catalog rather than 4chan X&#039;s catalog, 4chan X&#039;s filters do not apply there.<br>The native catalog has its own separate filter list.</p>"
});
return $('.warning', div).hidden = Conf['Filter'];
},

Binary file not shown.

View File

@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X
// @version 1.11.20.3
// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X

View File

@ -1,7 +1,7 @@
// Generated by CoffeeScript
// ==UserScript==
// @name 4chan X
// @version 1.11.20.3
// @version 1.11.21.0
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
@ -433,7 +433,7 @@
doc = d.documentElement;
g = {
VERSION: '1.11.20.3',
VERSION: '1.11.21.0',
NAMESPACE: '4chan X.',
boards: {}
};
@ -1404,6 +1404,8 @@
this.board = board1;
this.ID = +root.id.slice(2);
this.fullID = this.board + "." + this.ID;
this.context = this;
root.dataset.fullID = this.fullID;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@ -1543,7 +1545,8 @@
Post.prototype.parseQuote = function(quotelink) {
var fullID, match;
if (!(match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/))) {
match = quotelink.href.match(/^https?:\/\/boards\.4chan\.org\/+([^\/]+)\/+(?:res|thread)\/+\d+(?:\/[^#]*)?#p(\d+)$/);
if (!(match || (this.isClone && quotelink.dataset.postID))) {
return;
}
this.nodes.quotelinks.push(quotelink);
@ -1696,8 +1699,10 @@
Clone = (function(superClass) {
extend(Clone, superClass);
Clone.prototype.isClone = true;
function Clone(origin1, context1, contractThumb) {
var file, info, inline, inlined, k, key, len1, len2, len3, nodes, post, q, ref, ref1, ref2, ref3, ref4, root, u, val;
var file, info, inline, inlined, k, key, len1, len2, len3, len4, node, nodes, post, q, ref, ref1, ref2, ref3, ref4, ref5, root, u, v, val;
this.origin = origin1;
this.context = context1;
ref = ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply'];
@ -1707,6 +1712,13 @@
}
nodes = this.origin.nodes;
root = contractThumb ? this.cloneWithoutVideo(nodes.root) : nodes.root.cloneNode(true);
Clone.prefix || (Clone.prefix = 0);
ref1 = [root].concat(slice.call($$('[id]', root)));
for (q = 0, len2 = ref1.length; q < len2; q++) {
node = ref1[q];
node.id = Clone.prefix + node.id;
}
Clone.prefix++;
post = $('.post', root);
info = $('.postInfo', post);
this.nodes = {
@ -1729,14 +1741,14 @@
} else {
this.nodes.backlinks = info.getElementsByClassName('backlink');
}
ref1 = $$('.inline', post);
for (q = 0, len2 = ref1.length; q < len2; q++) {
inline = ref1[q];
ref2 = $$('.inline', post);
for (u = 0, len3 = ref2.length; u < len3; u++) {
inline = ref2[u];
$.rm(inline);
}
ref2 = $$('.inlined', post);
for (u = 0, len3 = ref2.length; u < len3; u++) {
inlined = ref2[u];
ref3 = $$('.inlined', post);
for (v = 0, len4 = ref3.length; v < len4; v++) {
inlined = ref3[v];
$.rmClass(inlined, 'inlined');
}
root.hidden = false;
@ -1767,11 +1779,12 @@
this.nodes.date = $('.dateTime', info);
}
this.parseQuotes();
this.quotes = slice.call(this.origin.quotes);
if (this.origin.file) {
this.file = {};
ref3 = this.origin.file;
for (key in ref3) {
val = ref3[key];
ref4 = this.origin.file;
for (key in ref4) {
val = ref4[key];
this.file[key] = val;
}
file = $('.file', post);
@ -1783,7 +1796,7 @@
if (this.file.videoThumb) {
this.file.thumb.muted = true;
}
if ((ref4 = this.file.thumb) != null ? ref4.dataset.src : void 0) {
if ((ref5 = this.file.thumb) != null ? ref5.dataset.src : void 0) {
this.file.thumb.src = this.file.thumb.dataset.src;
this.file.thumb.removeAttribute('data-src');
}
@ -1794,7 +1807,6 @@
if (this.origin.isDead) {
this.isDead = true;
}
this.isClone = true;
root.dataset.clone = this.origin.clones.push(this) - 1;
}
@ -2295,13 +2307,13 @@
})();
Fetcher = (function() {
function Fetcher(boardID1, threadID1, postID1, root1, context1) {
function Fetcher(boardID1, threadID1, postID1, root1, quoter1) {
var post;
this.boardID = boardID1;
this.threadID = threadID1;
this.postID = postID1;
this.root = root1;
this.context = context1;
this.quoter = quoter1;
if (post = g.posts[this.boardID + "." + this.postID]) {
this.insert(post);
return;
@ -2319,15 +2331,23 @@
}
Fetcher.prototype.insert = function(post) {
var clone, nodes;
var boardID, clone, k, len1, nodes, postID, quote, ref, ref1;
if (!this.root.parentNode) {
return;
}
clone = post.addClone(this.context, $.hasClass(this.root, 'dialog'));
clone = post.addClone(this.quoter.context, $.hasClass(this.root, 'dialog'));
Main.callbackNodes(Clone, [clone]);
nodes = clone.nodes;
$.rmAll(nodes.root);
$.add(nodes.root, nodes.post);
ref = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
for (k = 0, len1 = ref.length; k < len1; k++) {
quote = ref[k];
ref1 = Get.postDataFromLink(quote), boardID = ref1.boardID, postID = ref1.postID;
if (postID === this.quoter.ID && boardID === this.quoter.board.ID) {
$.addClass(quote, 'forwardlink');
}
}
$.rmAll(this.root);
$.add(this.root, nodes.root);
return $.event('PostsInserted');
@ -3089,10 +3109,10 @@
history.replaceState({}, '');
}
hash = this.location.hash.slice(1);
if (!(/^p\d+$/.test(hash) && (post = $.id(hash)))) {
if (!(/^\d*p\d+$/.test(hash) && (post = $.id(hash)))) {
return;
}
if ((Get.postFromRoot(post)).isHidden) {
if (!post.getBoundingClientRect().height) {
return;
}
return $.queueTask(function() {
@ -4547,15 +4567,12 @@
return Get.threadFromRoot($.x('ancestor::div[@class="thread"]', node));
},
postFromRoot: function(root) {
var boardID, index, link, post, postID;
var index, post;
if (root == null) {
return null;
}
link = $('.postNum > a[href*="#"]', root);
boardID = link.pathname.split(/\/+/)[1];
postID = link.hash.slice(2);
post = g.posts[root.dataset.fullID];
index = root.dataset.clone;
post = g.posts[boardID + "." + postID];
if (index) {
return post.clones[index];
} else {
@ -4565,9 +4582,6 @@
postFromNode: function(root) {
return Get.postFromRoot($.x('(ancestor::div[contains(@class,"postContainer")][1]|following::div[contains(@class,"postContainer")][1])', root));
},
contextFromNode: function(node) {
return Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', node));
},
postDataFromLink: function(link) {
var boardID, path, postID, ref, threadID;
if (link.hostname === 'boards.4chan.org') {
@ -4996,8 +5010,8 @@
return $.set(this.id + ".position", this.style.cssText);
};
hoverstart = function(arg) {
var asapTest, cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, asapTest = arg.asapTest, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
var cb, el, endEvents, height, latestEvent, noRemove, o, ref, root;
root = arg.root, el = arg.el, latestEvent = arg.latestEvent, endEvents = arg.endEvents, height = arg.height, cb = arg.cb, noRemove = arg.noRemove;
o = {
root: root,
el: el,
@ -5013,12 +5027,13 @@
};
o.hover = hover.bind(o);
o.hoverend = hoverend.bind(o);
$.asap(function() {
return !el.parentNode || asapTest();
}, function() {
o.hover(o.latestEvent);
new MutationObserver(function() {
if (el.parentNode) {
return o.hover(o.latestEvent);
}
}).observe(el, {
childList: true
});
$.on(root, endEvents, o.hoverend);
if ($.x('ancestor::div[contains(@class,"inline")][1]', root)) {
@ -5032,10 +5047,11 @@
};
return $.on(doc, 'mousemove', o.workaround);
};
hoverstart.padding = 25;
hover = function(e) {
var clientX, clientY, height, left, ref, right, style, threshold, top;
this.latestEvent = e;
height = this.height || this.el.offsetHeight;
height = (this.height || this.el.offsetHeight) + hoverstart.padding;
clientX = e.clientX, clientY = e.clientY;
top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120));
threshold = this.clientWidth / 2;
@ -5249,7 +5265,7 @@
Filter = {
filters: {},
init: function() {
var boards, err, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, regexp, stub, top;
var boards, err, excludes, filter, hl, k, key, len1, line, op, ref, ref1, ref2, ref3, ref4, ref5, ref6, regexp, stub, top;
if (!(((ref = g.VIEW) === 'index' || ref === 'thread') && Conf['Filter'])) {
return;
}
@ -5270,6 +5286,9 @@
filter = line.replace(regexp[0], '');
boards = ((ref2 = filter.match(/boards:([^;]+)/)) != null ? ref2[1].toLowerCase() : void 0) || 'global';
boards = boards === 'global' ? null : boards.split(',');
if (boards === null) {
excludes = ((ref3 = filter.match(/exclude:([^;]+)/)) != null ? ref3[1].toLowerCase().split(',') : void 0) || null;
}
if (key === 'uniqueID' || key === 'MD5') {
regexp = regexp[1];
} else {
@ -5281,10 +5300,10 @@
continue;
}
}
op = ((ref3 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref3[1] : void 0) || 'yes';
op = ((ref4 = filter.match(/[^t]op:(yes|no|only)/)) != null ? ref4[1] : void 0) || 'yes';
stub = (function() {
var ref4;
switch ((ref4 = filter.match(/stub:(yes|no)/)) != null ? ref4[1] : void 0) {
var ref5;
switch ((ref5 = filter.match(/stub:(yes|no)/)) != null ? ref5[1] : void 0) {
case 'yes':
return true;
case 'no':
@ -5294,11 +5313,11 @@
}
})();
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';
hl = ((ref5 = filter.match(/highlight:([\w-]+)/)) != null ? ref5[1] : void 0) || 'filter-highlight';
top = ((ref6 = filter.match(/top:(yes|no)/)) != null ? ref6[1] : void 0) || 'yes';
top = top === 'yes';
}
this.filters[key].push(this.createFilter(regexp, boards, op, stub, hl, top));
this.filters[key].push(this.createFilter(regexp, boards, excludes, op, stub, hl, top));
}
if (!this.filters[key].length) {
delete this.filters[key];
@ -5312,7 +5331,7 @@
cb: this.node
});
},
createFilter: function(regexp, boards, op, stub, hl, top) {
createFilter: function(regexp, boards, excludes, op, stub, hl, top) {
var settings, test;
test = typeof regexp === 'string' ? function(value) {
return regexp === value;
@ -5329,6 +5348,9 @@
if (boards && indexOf.call(boards, boardID) < 0) {
return false;
}
if (excludes && indexOf.call(excludes, boardID) >= 0) {
return false;
}
if (isReply && op === 'only' || !isReply && op === 'no') {
return false;
}
@ -6259,7 +6281,7 @@
if (this.isClone && this.thread === this.context.thread) {
return;
}
ref = this.isClone ? this.context : this, board = ref.board, thread = ref.thread;
ref = this.context, board = ref.board, thread = ref.thread;
ref1 = this.nodes.quotelinks;
for (k = 0, len1 = ref1.length; k < len1; k++) {
quotelink = ref1[k];
@ -6330,20 +6352,21 @@
});
},
toggle: function(e) {
var boardID, context, postID, ref, threadID;
var boardID, context, postID, quoter, ref, threadID;
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
return;
}
e.preventDefault();
ref = Get.postDataFromLink(this), boardID = ref.boardID, threadID = ref.threadID, postID = ref.postID;
context = Get.contextFromNode(this);
quoter = Get.postFromNode(this);
context = quoter.context;
if ($.hasClass(this, 'inlined')) {
QuoteInline.rm(this, boardID, threadID, postID, context);
} else {
if ($.x("ancestor::div[@id='pc" + postID + "']", this)) {
if ($.x("ancestor::div[@data-full-i-d='" + boardID + "." + postID + "']", this)) {
return;
}
QuoteInline.add(this, boardID, threadID, postID, context);
QuoteInline.add(this, boardID, threadID, postID, context, quoter);
}
return this.classList.toggle('inlined');
},
@ -6354,18 +6377,17 @@
return $.x('ancestor-or-self::*[parent::blockquote][1]', quotelink);
}
},
add: function(quotelink, boardID, threadID, postID, context) {
add: function(quotelink, boardID, threadID, postID, context, quoter) {
var inline, isBacklink, post, qroot, root;
isBacklink = $.hasClass(quotelink, 'backlink');
inline = $.el('div', {
id: "i" + postID,
className: 'inline'
});
root = QuoteInline.findRoot(quotelink, isBacklink);
$.after(root, inline);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.addClass(qroot, 'hasInline');
new Fetcher(boardID, threadID, postID, inline, context);
new Fetcher(boardID, threadID, postID, inline, quoter);
if (!((post = g.posts[boardID + "." + postID]) && context.thread === post.thread)) {
return;
}
@ -6382,7 +6404,7 @@
var el, inlined, isBacklink, post, qroot, ref, root;
isBacklink = $.hasClass(quotelink, 'backlink');
root = QuoteInline.findRoot(quotelink, isBacklink);
root = $.x("following-sibling::div[@id='i" + postID + "'][1]", root);
root = $.x("following-sibling::div[div/@data-full-i-d='" + boardID + "." + postID + "'][1]", root);
qroot = $.x('ancestor::*[contains(@class,"postContainer")][1]', root);
$.rm(root);
if (!$('.inline', qroot)) {
@ -6435,7 +6457,7 @@
quotelink.textContent = quotelink.textContent.replace(QuoteOP.text, '');
}
}
fullID = (this.isClone ? this.context : this).thread.fullID;
fullID = this.context.thread.fullID;
if (indexOf.call(quotes, fullID) < 0) {
return;
}
@ -6472,7 +6494,7 @@
}
},
mouseover: function(e) {
var boardID, clone, k, len1, len2, origin, post, postID, posts, q, qp, quote, quoterID, ref, ref1, threadID;
var boardID, k, len1, origin, post, postID, posts, qp, ref, threadID;
if ($.hasClass(this, 'inlined') || !d.contains(this)) {
return;
}
@ -6482,21 +6504,15 @@
className: 'dialog'
});
$.add(Header.hover, qp);
new Fetcher(boardID, threadID, postID, qp, Get.contextFromNode(this));
new Fetcher(boardID, threadID, postID, qp, Get.postFromNode(this));
UI.hover({
root: this,
el: qp,
latestEvent: e,
endEvents: 'mouseout click',
cb: QuotePreview.mouseout,
asapTest: function() {
return qp.firstElementChild;
}
cb: QuotePreview.mouseout
});
if (!(origin = g.posts[boardID + "." + postID])) {
return;
}
if (Conf['Quote Highlighting']) {
if (Conf['Quote Highlighting'] && (origin = g.posts[boardID + "." + postID])) {
posts = [origin].concat(origin.clones);
posts.pop();
for (k = 0, len1 = posts.length; k < len1; k++) {
@ -6504,15 +6520,6 @@
$.addClass(post.nodes.post, 'qphl');
}
}
quoterID = $.x('ancestor::*[@id][1]', this).id.match(/\d+$/)[0];
clone = Get.postFromRoot(qp.firstChild);
ref1 = clone.nodes.quotelinks.concat(slice.call(clone.nodes.backlinks));
for (q = 0, len2 = ref1.length; q < len2; q++) {
quote = ref1[q];
if (quote.hash.slice(2) === quoterID) {
$.addClass(quote, 'forwardlink');
}
}
},
mouseout: function() {
var clone, k, len1, post, ref, root;
@ -6802,7 +6809,7 @@
if (highlight = $('.highlight')) {
$.rmClass(highlight, 'highlight');
}
if (!QuoteYou.lastRead) {
if (!(QuoteYou.lastRead && doc.contains(QuoteYou.lastRead) && $.hasClass(QuoteYou.lastRead, 'quotesYou'))) {
if (!(post = QuoteYou.lastRead = $('.quotesYou'))) {
new Notice('warning', 'No posts are currently quoting you, loser.', 20);
return;
@ -6814,7 +6821,7 @@
post = QuoteYou.lastRead;
}
str = type + "::div[contains(@class,'quotesYou')]";
while (post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0)) {
while ((post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0))) {
if (QuoteYou.cb.scroll(post)) {
return;
}
@ -6822,14 +6829,16 @@
posts = $$('.quotesYou');
return QuoteYou.cb.scroll(posts[type === 'following' ? 0 : posts.length - 1]);
},
scroll: function(post) {
if (Get.postFromRoot(post).isHidden) {
scroll: function(root) {
var post;
post = $('.post', root);
if (!post.getBoundingClientRect().height) {
return false;
} else {
QuoteYou.lastRead = post;
QuoteYou.lastRead = root;
window.location = "#" + post.id;
Header.scrollTo(post);
$.addClass($('.post', post), 'highlight');
$.addClass(post, 'highlight');
return true;
}
}
@ -6852,16 +6861,13 @@
},
node: function() {
var deadlink, k, len1, ref;
if (this.isClone) {
return;
}
ref = $$('.deadlink', this.nodes.comment);
for (k = 0, len1 = ref.length; k < len1; k++) {
deadlink = ref[k];
if (this.isClone) {
if ($.hasClass(deadlink, 'quotelink')) {
this.nodes.quotelinks.push(deadlink);
}
} else {
Quotify.parseDeadlink.call(this, deadlink);
}
Quotify.parseDeadlink.call(this, deadlink);
}
},
parseDeadlink: function(deadlink) {
@ -11107,7 +11113,7 @@
},
mouseover: function(post) {
return function(e) {
var el, error, file, height, isVideo, left, maxHeight, maxWidth, padding, ref, ref1, ref2, right, scale, width, x;
var el, error, file, height, isVideo, left, maxHeight, maxWidth, ref, ref1, ref2, right, scale, width, x;
if (!doc.contains(this)) {
return;
}
@ -11151,9 +11157,8 @@
return results;
})(), width = ref1[0], height = ref1[1];
ref2 = this.getBoundingClientRect(), left = ref2.left, right = ref2.right;
padding = 25;
maxWidth = Math.max(left, doc.clientWidth - right);
maxHeight = doc.clientHeight - padding;
maxHeight = doc.clientHeight - UI.hover.padding;
scale = Math.min(1, maxWidth / width, maxHeight / height);
el.style.maxWidth = (scale * width) + "px";
el.style.maxHeight = (scale * height) + "px";
@ -11162,10 +11167,7 @@
el: el,
latestEvent: e,
endEvents: 'mouseout click',
asapTest: function() {
return true;
},
height: scale * height + padding,
height: scale * height,
noRemove: true,
cb: function() {
$.off(el, 'error', error);
@ -17286,7 +17288,7 @@
return;
}
$.extend(div, {
innerHTML: "<div class=\"warning\"><code>Filter</code> is disabled.</div><p>Use <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions\" target=\"_blank\">regular expressions</a>, one per line.<br>Lines starting with a <code>#</code> will be ignored.<br>For example, <code>/weeaboo/i</code> will filter posts containing the string \`<code>weeaboo</code>\`, case-insensitive.<br>MD5 filtering uses exact string matching, not regular expressions.</p><ul>You can use these settings with each regular expression, separate them with semicolons:<li>Per boards, separate them with commas. It is global if not specified.<br>For example: <code>boards:a,jp;</code>.</li><li>Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).<br>For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.</li><li>Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).<br>For example: <code>stub:yes;</code> or <code>stub:no;</code>.</li><li>Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.</li><li>Highlighted OPs will have their threads put on top of the board index by default.<br>For example: <code>top:yes;</code> or <code>top:no;</code>.</li></ul><p>Note: If you&#039;re using the native catalog rather than 4chan X&#039;s catalog, 4chan X&#039;s filters do not apply there.<br>The native catalog has its own separate filter list.</p>"
innerHTML: "<div class=\"warning\"><code>Filter</code> is disabled.</div><p>Use <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions\" target=\"_blank\">regular expressions</a>, one per line.<br>Lines starting with a <code>#</code> will be ignored.<br>For example, <code>/weeaboo/i</code> will filter posts containing the string \`<code>weeaboo</code>\`, case-insensitive.<br>MD5 filtering uses exact string matching, not regular expressions.</p><ul>You can use these settings with each regular expression, separate them with semicolons:<li>Per boards, separate them with commas. It is global if not specified.<br>For example: <code>boards:a,jp;</code>.</li><li>In case of a global rule, select boards to be excluded from the filter.<br>For example: <code>exclude:vg,v;</code>.</li><li>Filter OPs only along with their threads (\`only\`), replies only (\`no\`), or both (\`yes\`, this is default).<br>For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.</li><li>Overrule the \`Show Stubs\` setting if specified: create a stub (\`yes\`) or not (\`no\`).<br>For example: <code>stub:yes;</code> or <code>stub:no;</code>.</li><li>Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.</li><li>Highlighted OPs will have their threads put on top of the board index by default.<br>For example: <code>top:yes;</code> or <code>top:no;</code>.</li></ul><p>Note: If you&#039;re using the native catalog rather than 4chan X&#039;s catalog, 4chan X&#039;s filters do not apply there.<br>The native catalog has its own separate filter list.</p>"
});
return $('.warning', div).hidden = Conf['Filter'];
},

Binary file not shown.

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.11.20.3' />
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.11.21.0' />
</app>
</gupdate>

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.11.20.3' />
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.11.21.0' />
</app>
</gupdate>

View File

@ -1,4 +1,4 @@
{
"version": "1.11.20.3",
"date": "2015-12-11T08:28:46.605Z"
"version": "1.11.21.0",
"date": "2015-12-14T04:31:24.432Z"
}