diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75af0f1f3..b090ad469 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,29 @@
+**ccd0**
+- Update due to more Recaptcha changes.
+- For single files, file errors are reported but no longer stop you from attempting to post. Files with errors are still removed when posting multiple files.
+- WebM files are checked for audio before posting (Firefox only).
+- Max resolution updated, now 10000x10000.
+- Check dimensions and duration of .webm files before posting.
+- Partly restore Mayhem's captcha changes reverted in last version. Captchas are now destroyed after posting instead of reloaded, unless `Auto-load captcha` is checked. Captcha caching is still enabled.
+- Thumbnails for .webm files in Quick Reply.
+- Revert captcha fixes of 1.4.2 as Google appears to have reverted the changes on its end. This restores captcha caching.
+- Quick fix for moot breaking captcha.
+- Restore `Comment Expansion`.
+- Another update to handle HTML changes.
+- Use new URLs.
+- Bugfixes.
+
+**fgts**
+- Update archive list.
+
+**MayhemYDG**
+- Update 4chan namespaces support.
+- Better handling of webm playback errors.
+- Bugfixes
+
+**woxxy**
+- Remove /v/ from stable Foolz archive.
+
### v2.9.20
*2014-04-20*
diff --git a/Gruntfile.coffee b/Gruntfile.coffee
index 00e7b52a9..0683e75b2 100755
--- a/Gruntfile.coffee
+++ b/Gruntfile.coffee
@@ -94,10 +94,6 @@ module.exports = (grunt) ->
push: false
shell:
- options:
- stdout: true
- stderr: true
- failOnError: true
checkout:
command: 'git checkout <%= pkg.meta.mainBranch %>'
commit:
diff --git a/LICENSE b/LICENSE
index 2798a6994..098c5c032 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
/*
-* appchan x - Version 2.9.20 - 2014-05-02
+* appchan x - Version 2.9.20 - 2014-05-03
*
* Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE
diff --git a/README.md b/README.md
index 0b1188f60..1f2568798 100755
--- a/README.md
+++ b/README.md
@@ -1,4 +1,11 @@
+<<<<<<< HEAD
# Get Appchan X [HERE](http://zixaphir.github.io/appchan-x/).
+=======
+Fork of [Spittie's 4chan X](https://github.com/Spittie/4chan-x) (itself a fork of [Seaweed's](https://github.com/seaweedchan/4chan-x)).
+
+Note: If you're looking for a maintained fork of OneeChan, try
+https://github.com/Nebukazar/OneeChan
+>>>>>>> v3
1. Make sure both your **browser** and **Appchan X** are up to date.
2. Disable your other extensions & scripts to identify conflicts.
diff --git a/builds/4chan-X.meta.js b/builds/4chan-X.meta.js
index fc3598040..37163a9bf 100755
--- a/builds/4chan-X.meta.js
+++ b/builds/4chan-X.meta.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X
-// @version 1.7.8
+// @version 1.7.27
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js
index 72b27bd57..fe0d8de1e 100644
--- a/builds/appchan-x.user.js
+++ b/builds/appchan-x.user.js
@@ -25,7 +25,7 @@
// ==/UserScript==
/*
-* appchan x - Version 2.9.20 - 2014-05-02
+* appchan x - Version 2.9.20 - 2014-05-03
*
* Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@@ -224,7 +224,7 @@
'Posting Success Notifications': [true, 'Show notifications on successful post creation or file uploading.'],
'Captcha Warning Notifications': [true, 'When disabled, shows a red border on the CAPTCHA input until a key is pressed instead of a notification.'],
'Dump List Before Comment': [false, 'Position of the QR\'s Dump List.'],
- 'Auto-load captcha': [false, 'Automatically load the captcha when you open a thread']
+ 'Auto-load captcha': [false, 'Automatically load the captcha when you open a thread, and reload it after you post.']
},
'Quote Links': {
'Quote Backlinks': [true, 'Add quote backlinks.'],
@@ -404,7 +404,7 @@
},
time: '%m/%d/%y(%a)%H:%M:%S',
backlink: '>>%id',
- fileInfo: '%L (%p%s, %r)',
+ fileInfo: '%l (%p%s, %r)',
favicon: 'ferongr',
usercss: "/* Tripcode Italics: */\n/*\nspan.postertrip {\nfont-style: italic;\n}\n*/\n\n/* Add a rounded border to thumbnails (but not expanded images): */\n/*\n.fileThumb > img:first-child {\nborder: solid 2px rgba(0,0,100,0.5);\nborder-radius: 10px;\n}\n*/\n\n/* Make highlighted posts look inset on the page: */\n/*\ndiv.post:target,\ndiv.post.highlight {\nbox-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);\n}\n*/",
hotkeys: {
@@ -428,17 +428,17 @@
'Open Gallery': ['g', 'Opens the gallery.'],
'fappeTyme': ['f', 'Fappe Tyme.'],
'werkTyme': ['Shift+w', 'Werk Tyme'],
- 'Front page': ['0', 'Jump to front page.'],
- 'Open front page': ['Shift+0', 'Open front page in a new tab.'],
- 'Next page': ['Shift+Right', 'Jump to the next page.'],
- 'Previous page': ['Shift+Left', 'Jump to the previous page.'],
+ 'Front page': ['1', 'Jump to front page.'],
+ 'Open front page': ['Shift+1', 'Open front page in a new tab.'],
+ 'Next page': ['Ctrl+Right', 'Jump to the next page.'],
+ 'Previous page': ['Ctrl+Left', 'Jump to the previous page.'],
'Search form': ['Ctrl+Alt+s', 'Focus the search field on the board index.'],
'Paged mode': ['Alt+1', 'Sets the index mode to paged.'],
'All pages mode': ['Alt+2', 'Sets the index mode to all threads.'],
'Catalog mode': ['Alt+3', 'Sets the index mode to catalog.'],
'Cycle sort type': ['Alt+x', 'Cycle through index sort types.'],
- 'Next thread': ['Shift+Down', 'See next thread.'],
- 'Previous thread': ['Shift+Up', 'See previous thread.'],
+ 'Next thread': ['Ctrl+Down', 'See next thread.'],
+ 'Previous thread': ['Ctrl+Up', 'See previous thread.'],
'Expand thread': ['Ctrl+e', 'Expand thread.'],
'Open thread': ['o', 'Open thread in current tab.'],
'Open thread tab': ['Shift+o', 'Open thread in new tab.'],
@@ -3165,7 +3165,7 @@
title: type,
className: "" + typeLC + "Icon"
});
- root = type === 'Closed' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : g.VIEW === 'index' ? $('.page-num', this.OP.nodes.info) : $('[title="Quote this post"]', this.OP.nodes.info);
+ root = type === 'Closed' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : g.VIEW === 'index' ? $('.page-num', this.OP.nodes.info) : $('[title="Reply to this post"]', this.OP.nodes.info);
$.after(root, [$.tn(' '), icon]);
if (!this.catalogView) {
return;
@@ -3364,7 +3364,7 @@
Post.prototype.parseQuote = function(quotelink) {
var fullID, match;
- if (!(match = quotelink.href.match(/boards\.4chan\.org\/([^\/]+)\/thread\/\d+#p(\d+)$/))) {
+ if (!(match = quotelink.href.match(/boards\.4chan\.org\/([^\/]+)\/(res|thread)\/\d+(.*)?\#p(\d+)$/))) {
return;
}
this.nodes.quotelinks.push(quotelink);
@@ -3399,12 +3399,12 @@
}
this.file.sizeInBytes = size;
this.file.thumbURL = that.isArchived ? thumb.src : "" + location.protocol + "//t.4cdn.org/" + this.board + "/" + (this.file.URL.match(/(\d+)\./)[1]) + "s.jpg";
- this.file.name = !this.file.isSpoiler && (nameNode = $('a', fileText)) ? nameNode.title || nameNode.textContent : fileText.title;
- this.file.isImage = /(jpg|png|gif)$/i.test(this.file.name);
- this.file.isVideo = /webm$/i.test(this.file.name);
+ this.file.isImage = /(jpg|png|gif)$/i.test(this.file.URL);
+ this.file.isVideo = /webm$/i.test(this.file.URL);
if (this.file.isImage || this.file.isVideo) {
- return this.file.dimensions = fileText.textContent.match(/\d+x\d+/)[0];
+ this.file.dimensions = fileText.childNodes[2].data.match(/\d+x\d+/)[0];
}
+ return this.file.name = !this.file.isSpoiler && (nameNode = $('a', fileText)) ? nameNode.title || nameNode.textContent : fileText.title;
};
Post.prototype.cleanup = function(root, post) {
@@ -3414,7 +3414,7 @@
node = _ref[_i];
$.rm(node);
}
- _ref1 = $$('[id]', post);
+ _ref1 = $$('[id]:not(.exif)', post);
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
node = _ref1[_j];
node.removeAttribute('id');
@@ -4821,12 +4821,15 @@
$.asap((function() {
return $('.board', doc) || d.readyState !== 'loading';
}), function() {
- var board, navLink, _l, _len3, _ref3;
+ var board, navLink, _l, _len3, _ref3, _ref4;
_ref3 = $$('.navLinks');
for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
navLink = _ref3[_l];
$.rm(navLink);
}
+ if ((_ref4 = $.id('search-box')) != null) {
+ _ref4.parentNode.remove();
+ }
$.after($.x('child::form/preceding-sibling::hr[1]'), Index.navLinks);
if (g.VIEW !== 'index') {
return;
@@ -5057,8 +5060,8 @@
toggleHiddenThreads: function() {
$('#hidden-toggle a', Index.navLinks).textContent = (Index.showHiddenThreads = !Index.showHiddenThreads) ? 'Hide' : 'Show';
Index.sort();
- if (Conf['Index Mode'] === 'paged' && Index.getCurrentPage() > 0) {
- return Index.pageNav(0);
+ if (Conf['Index Mode'] === 'paged' && Index.getCurrentPage() > 1) {
+ return Index.pageNav(1);
} else {
return Index.buildIndex();
}
@@ -5141,7 +5144,7 @@
if (Index.cb.indexNav(a, true)) {
return;
}
- return Index.userPageNav(+a.pathname.split('/')[2]);
+ return Index.userPageNav(+a.pathname.split('/')[2] || 1);
},
headerNav: function(e) {
var a, needChange, onSameIndex;
@@ -5194,10 +5197,10 @@
if (Conf['Index Mode'] === 'infinite' && Index.currentPage) {
return Index.currentPage;
}
- return +window.location.pathname.split('/')[2];
+ return +window.location.pathname.split('/')[2] || 1;
},
userPageNav: function(pageNum) {
- Navigate.pushState(pageNum === 0 ? './' : pageNum);
+ Navigate.pushState(pageNum === 1 ? './' : pageNum);
if (Conf['Refreshed Navigation'] && Conf['Index Mode'] !== 'all pages') {
return Index.update(pageNum);
} else {
@@ -5208,7 +5211,7 @@
if (Index.currentPage === pageNum && !Index.root.parentElement) {
return;
}
- Navigate.pushState(pageNum === 0 ? './' : pageNum);
+ Navigate.pushState(pageNum === 1 ? './' : pageNum);
return Index.pageLoad(pageNum);
},
pageLoad: function(pageNum) {
@@ -5230,7 +5233,14 @@
return Math.ceil(Index.sortedThreads.length / Index.getThreadsNumPerPage());
},
getMaxPageNum: function() {
- return Math.max(0, Index.getPagesNum() - 1);
+ var max, min;
+ min = 1;
+ max = +Index.getPagesNum();
+ if (min < max) {
+ return max;
+ } else {
+ return min;
+ }
},
togglePagelist: function() {
return Index.pagelist.hidden = Conf['Index Mode'] !== 'paged';
@@ -5239,12 +5249,12 @@
var a, i, maxPageNum, nodes, pagesRoot, _i;
pagesRoot = $('.pages', Index.pagelist);
maxPageNum = Index.getMaxPageNum();
- if (pagesRoot.childElementCount !== maxPageNum + 1) {
+ if (pagesRoot.childElementCount !== maxPageNum) {
nodes = [];
- for (i = _i = 0; _i <= maxPageNum; i = _i += 1) {
+ for (i = _i = 1; _i <= maxPageNum; i = _i += 1) {
a = $.el('a', {
textContent: i,
- href: i ? i : './'
+ href: i === 1 ? './' : i
});
nodes.push($.tn('['), a, $.tn('] '));
}
@@ -5263,11 +5273,11 @@
pagesRoot = $('.pages', Index.pagelist);
prev = pagesRoot.previousSibling.firstChild;
next = pagesRoot.nextSibling.firstChild;
- href = Math.max(pageNum - 1, 0);
- prev.href = href === 0 ? './' : href;
+ href = Math.max(pageNum - 1, 1);
+ prev.href = href === 1 ? './' : href;
prev.firstChild.disabled = href === pageNum;
href = Math.min(pageNum + 1, maxPageNum);
- next.href = href === 0 ? './' : href;
+ next.href = href === 1 ? './' : href;
next.firstChild.disabled = href === pageNum;
if (strong = $('strong', pagesRoot)) {
if (+strong.textContent === pageNum) {
@@ -5277,7 +5287,7 @@
} else {
strong = $.el('strong');
}
- if (!(a = pagesRoot.children[pageNum])) {
+ if (!(a = pagesRoot.children[pageNum - 1])) {
return;
}
$.before(a, strong);
@@ -5319,7 +5329,7 @@
if (!(d.readyState === 'loading' || Index.root.parentElement)) {
$.replace($('.board'), Index.root);
}
- Index.currentPage = 0;
+ Index.currentPage = 1;
if ((_ref = Index.req) != null) {
_ref.abort();
}
@@ -5380,7 +5390,7 @@
}
Navigate.title();
try {
- pageNum || (pageNum = 0);
+ pageNum || (pageNum = 1);
if (req.status === 200) {
Index.parse(req.response, pageNum);
} else if (req.status === 304) {
@@ -5446,7 +5456,7 @@
var err, thread, threadRoot;
threadRoot = Build.thread(g.BOARD, threadData);
if (thread = g.BOARD.threads[threadData.no]) {
- thread.setPage(Math.floor(i / Index.threadsNumPerPage));
+ thread.setPage(Math.floor(i / Index.threadsNumPerPage) + 1);
thread.setCount('post', threadData.replies + 1, threadData.bumplimit);
thread.setCount('file', threadData.images + !!threadData.ext, threadData.imagelimit);
thread.setStatus('Sticky', !!threadData.sticky);
@@ -5641,7 +5651,7 @@
switch (Conf['Index Mode']) {
case 'paged':
case 'infinite':
- pageNum = Index.getCurrentPage();
+ pageNum = Index.getCurrentPage() - 1;
threadsPerPage = Index.getThreadsNumPerPage();
threads = [];
i = threadsPerPage * pageNum;
@@ -5682,7 +5692,7 @@
if (!Index.searchInput.dataset.searching) {
Index.searchInput.dataset.searching = 1;
Index.pageBeforeSearch = Index.getCurrentPage();
- Index.setPage(pageNum = 0);
+ Index.setPage(pageNum = 1);
} else {
if (Conf['Index Mode'] !== 'infinite') {
pageNum = Index.getCurrentPage();
@@ -5769,6 +5779,17 @@
return n = (n + 1) % 3;
};
})(),
+ path: function(boardID, threadID, postID, fragment) {
+ var path;
+ path = "/" + boardID + "/thread/" + threadID;
+ if ((g.SLUG != null) && threadID === g.THREADID) {
+ path += "/" + g.SLUG;
+ }
+ if (postID) {
+ path += "#" + (fragment || 'p') + postID;
+ }
+ return path;
+ },
postFromObject: function(data, boardID) {
var o;
o = {
@@ -5883,28 +5904,26 @@
sticky = isSticky ? "
" : '';
closed = isClosed ? "
" : '';
if (isOP && g.VIEW === 'index') {
- pageNum = Math.floor(Index.liveThreadData.keys.indexOf("" + postID) / Index.threadsNumPerPage);
+ pageNum = Math.floor(Index.liveThreadData.keys.indexOf("" + postID) / Index.threadsNumPerPage) + 1;
pageIcon = " Page " + pageNum + "";
- replyLink = " [Reply]";
+ replyLink = " [Reply]";
} else {
- pageIcon = replyLink = '';
+ pageIcon = '';
+ replyLink = '';
}
container = $.el('div', {
id: "pc" + postID,
className: "postContainer " + (isOP ? 'op' : 'reply') + "Container",
- innerHTML: (isOP ? '' : "
>>
") + ("") + (isOP ? fileHTML : '') + "
" + ("
") + ("
" + (subject || '') + " ") + ("
") + emailStart + ("" + (name || '') + "") + tripcode + capcodeStart + emailEnd + capcodeIcon + userID + flag + ' ' + ("
" + date + " ") + "
" + ("No.") + ("" + postID + "") + pageIcon + sticky + closed + replyLink + '' + '
' + (isOP ? '' : fileHTML) + ("
" + (comment || '') + "
") + '
'
+ innerHTML: (isOP ? '' : ">>
") + ("") + (isOP ? fileHTML : '') + "
" + ("
") + ("
" + (subject || '') + " ") + ("
") + emailStart + ("" + (name || '') + "") + tripcode + capcodeStart + emailEnd + capcodeIcon + userID + flag + ' ' + ("
" + date + " ") + "
" + ("No.") + ("" + postID + "") + pageIcon + sticky + closed + replyLink + '' + '
' + (isOP ? '' : fileHTML) + ("
" + (comment || '') + "
") + '
'
});
_ref = $$('.quotelink', container);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
quote = _ref[_i];
href = quote.getAttribute('href');
- if (href[0] === '/') {
+ if (href[0] !== '#') {
continue;
}
- if (href[0] === '#') {
- href = "" + threadID + href;
- }
- quote.href = "/" + boardID + "/thread/" + href;
+ quote.href = Build.path(boardID, threadID, href.slice(2));
}
return container;
},
@@ -5919,7 +5938,7 @@
return $.el('a', {
className: 'summary',
textContent: text.join(' '),
- href: "/" + boardID + "/thread/" + threadID
+ href: Build.path(boardID, threadID)
});
},
thread: function(board, data, full) {
@@ -5955,12 +5974,12 @@
data = Index.liveThreadData[thread.ID];
postCount = data.replies + 1;
fileCount = data.images + !!data.ext;
- pageCount = Math.floor(Index.liveThreadData.keys.indexOf("" + thread.ID) / Index.threadsNumPerPage);
+ pageCount = Math.floor(Index.liveThreadData.keys.indexOf("" + thread.ID) / Index.threadsNumPerPage) + 1;
subject = thread.OP.info.subject ? "" + thread.OP.info.subject + "
" : '';
comment = thread.OP.nodes.comment.innerHTML.replace(/(
\s*){2,}/g, '
');
root = $.el('div', {
className: 'catalog-thread',
- innerHTML: "" + postCount + " / " + fileCount + " / " + pageCount + "
" + subject + ""
+ innerHTML: "" + postCount + " / " + fileCount + " / " + pageCount + "
" + subject + ""
});
root.dataset.fullID = thread.fullID;
if (thread.isPinned) {
@@ -7431,7 +7450,7 @@
var a, frag, hash, text;
frag = QuoteBacklink.frag.cloneNode(true);
a = frag.lastElementChild;
- a.href = "/" + quoter.board + "/thread/" + quoter.thread + "#p" + quoter;
+ a.href = Build.path(quoter.board.ID, quoter.thread.ID, quoter.ID);
a.textContent = text = QuoteBacklink.funk(quoter.ID);
if (quoter.isDead) {
$.addClass(a, 'deadlink');
@@ -7912,7 +7931,7 @@
}
if (post = posts[post.ID]) {
posts.after(post, posts[this.ID]);
- } else {
+ } else if (posts[this.ID]) {
posts.prepend(posts[this.ID]);
}
return true;
@@ -8003,7 +8022,7 @@
quoteID = "" + boardID + "." + postID;
if (post = g.posts[quoteID]) {
a = $.el('a', {
- href: "/" + boardID + "/thread/" + post.thread + "#p" + postID,
+ href: Build.path(boardID, post.thread.ID, postID),
className: post.isDead ? 'quotelink deadlink' : 'quotelink',
textContent: quote
});
@@ -8280,9 +8299,9 @@
return $.toggleClass(this, 'embedded');
},
embed: function(a) {
- var el, style, type;
+ var el, type;
el = (type = Linkify.types[a.dataset.key]).el(a);
- el.style.cssText = (style = type.style) ? style : "border: 0; width: 640px; height: 390px";
+ el.style.cssText = type.style != null ? type.style : "border: 0; width: 640px; height: 390px";
return el;
},
unembed: function(a) {
@@ -8327,9 +8346,10 @@
{
key: 'audio',
regExp: /(.*\.(mp3|ogg|wav))$/,
+ style: '',
el: function(a) {
return $.el('audio', {
- controls: 'controls',
+ controls: true,
preload: 'auto',
src: a.dataset.uid
});
@@ -8523,10 +8543,12 @@
}, {
key: 'Vocaroo',
regExp: /.*(?:vocaroo.com\/)([^#\&\?]*).*/,
- style: 'border: 0; width: 150px; height: 45px;',
+ style: '',
el: function(a) {
- return $.el('object', {
- innerHTML: ""
+ return $.el('audio', {
+ controls: true,
+ preload: 'auto',
+ src: "http://vocaroo.com/media_command.php?media=" + (a.dataset.uid.replace(/^i\//, '')) + "&command=download_ogg"
});
}
}, {
@@ -8584,6 +8606,7 @@
}, {
key: 'video',
regExp: /(.*\.(ogv|webm|mp4))$/,
+ style: 'border: 0; width: auto; height: auto;',
el: function(a) {
return $.el('video', {
controls: 'controls',
@@ -8596,6 +8619,7 @@
};
QR = {
+ mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'],
init: function() {
var con, sc;
this.db = new DataBoard('yourPosts');
@@ -8688,7 +8712,7 @@
})) {
$.addClass(this.nodes.root, 'your-post');
}
- return $.on($('a[title="Quote this post"]', this.nodes.info), 'click', QR.quote);
+ return $.on($('a[title="Reply to this post"]', this.nodes.info), 'click', QR.quote);
},
persist: function() {
if (!QR.postingIsEnabled) {
@@ -8742,7 +8766,10 @@
post["delete"]();
}
QR.cooldown.auto = false;
- return QR.status();
+ QR.status();
+ if (QR.captcha.isEnabled && !Conf['Auto-load captcha']) {
+ return QR.captcha.destroy();
+ }
},
focusin: function() {
return $.addClass(QR.nodes.el, 'focus');
@@ -8776,8 +8803,10 @@
el.removeAttribute('style');
}
if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) {
- QR.captcha.nodes.input.focus();
- QR.captcha.setup();
+ if (QR.captcha.captchas.length === 0) {
+ QR.captcha.nodes.input.focus();
+ QR.captcha.setup();
+ }
if (Conf['Captcha Warning Notifications'] && !d.hidden) {
QR.notify(el);
} else {
@@ -8862,7 +8891,7 @@
_ref = $$('br', frag);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
node = _ref[_i];
- if (node !== frag.lastElementChild) {
+ if (node !== frag.lastChild) {
$.replace(node, $.tn('\n>'));
}
}
@@ -8948,29 +8977,18 @@
QR.handleFiles(files);
return $.addClass(QR.nodes.el, 'dump');
},
- handleBlob: function(urlBlob, header, url) {
- var blob, end, endnl, endsc, mime, name, name_end, name_start, start;
- name = url.substr(url.lastIndexOf('/') + 1, url.length);
- start = header.indexOf("Content-Type: ") + 14;
- endsc = header.substr(start, header.length).indexOf(";");
- endnl = header.substr(start, header.length).indexOf("\n") - 1;
- end = endnl;
- if (endsc !== -1 && endsc < endnl) {
- end = endsc;
+ handleBlob: function(urlBlob, contentType, contentDisposition, url) {
+ var blob, match, mime, name, _ref, _ref1, _ref2;
+ name = (_ref = url.match(/([^\/]+)\/*$/)) != null ? _ref[1] : void 0;
+ mime = (contentType != null ? contentType.match(/[^;]*/)[0] : void 0) || 'application/octet-stream';
+ match = (contentDisposition != null ? (_ref1 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? _ref1[1] : void 0 : void 0) || (contentType != null ? (_ref2 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? _ref2[1] : void 0 : void 0);
+ if (match) {
+ name = match.replace(/\\"/g, '"');
}
- mime = header.substr(start, end);
blob = new Blob([urlBlob], {
type: mime
});
- blob.name = url.substr(url.lastIndexOf('/') + 1, url.length);
- name_start = header.indexOf('name="') + 6;
- if (name_start - 6 !== -1) {
- name_end = header.substr(name_start, header.length).indexOf('"');
- blob.name = header.substr(name_start, name_end);
- }
- if (blob.type === null) {
- return QR.error("Unsupported file type.");
- }
+ blob.name = name;
return QR.handleFiles([blob]);
},
handleUrl: function() {
@@ -8979,12 +8997,12 @@
if (url === null) {
return;
}
- GM_xmlhttpRequest({
+ return GM_xmlhttpRequest({
method: "GET",
url: url,
overrideMimeType: "text/plain; charset=x-user-defined",
onload: function(xhr) {
- var data, i, r;
+ var contentDisposition, contentType, data, i, r, _ref, _ref1;
r = xhr.responseText;
data = new Uint8Array(r.length);
i = 0;
@@ -8992,18 +9010,17 @@
data[i] = r.charCodeAt(i);
i++;
}
- QR.handleBlob(data, xhr.responseHeaders, url);
- return;
- return {
- onerror: function(xhr) {
- return QR.error("Can't load image.");
- }
- };
+ contentType = (_ref = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)) != null ? _ref[1] : void 0;
+ contentDisposition = (_ref1 = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)) != null ? _ref1[1] : void 0;
+ return QR.handleBlob(data, contentType, contentDisposition, url);
+ },
+ onerror: function(xhr) {
+ return QR.error("Can't load image.");
}
});
},
handleFiles: function(files) {
- var file, isSingle, max, _i, _len;
+ var file, i, _i, _len;
if (this !== QR) {
files = __slice.call(this.files);
this.value = null;
@@ -9011,52 +9028,46 @@
if (!files.length) {
return;
}
- max = QR.nodes.fileInput.max;
- isSingle = files.length === 1;
QR.cleanNotifications();
- for (_i = 0, _len = files.length; _i < _len; _i++) {
- file = files[_i];
- if (file.type === 'application/x-shockwave-flash') {
- QR.handleFile(file, isSingle, max);
- } else {
- QR.checkDimensions(file, isSingle, max);
- }
+ for (i = _i = 0, _len = files.length; _i < _len; i = ++_i) {
+ file = files[i];
+ QR.handleFile(file, i, files.length);
}
- if (!isSingle) {
+ if (files.length !== 1) {
return $.addClass(QR.nodes.el, 'dump');
}
},
- checkDimensions: function(file, isSingle, max) {
- var img;
- if (/^image\//.test(file.type)) {
- img = new Image();
- img.onload = (function(_this) {
- return function() {
- var height, width;
- height = img.height, width = img.width;
- if (height > QR.max_heigth || width > QR.max_heigth) {
- return QR.error("" + file.name + ": Image too large (image: " + img.height + "x" + img.width + "px, max: " + QR.max_heigth + "x" + QR.max_width + "px)");
- }
- if (height < QR.min_heigth || width < QR.min_heigth) {
- return QR.error("" + file.name + ": Image too small (image: " + img.height + "x" + img.width + "px, min: " + QR.min_heigth + "x" + QR.min_width + "px)");
- }
- return QR.handleFile(file, isSingle, max);
- };
- })(this);
- return img.src = URL.createObjectURL(file);
- } else {
- return QR.handleFile(file, isSingle, max);
+ handleFile: function(file, index, nfiles) {
+ var isSingle, max, post, _ref;
+ isSingle = nfiles === 1;
+ if (/^text\//.test(file.type)) {
+ if (isSingle) {
+ post = QR.selected;
+ } else if (index !== 0 || (post = QR.posts[QR.posts.length - 1]).com) {
+ post = new QR.post();
+ }
+ post.pasteText(file);
+ return;
+ }
+ if (_ref = file.type, __indexOf.call(QR.mimeTypes, _ref) < 0) {
+ QR.error("" + file.name + ": Unsupported file type.");
+ if (!isSingle) {
+ return;
+ }
+ }
+ max = QR.nodes.fileInput.max;
+ if (/^video\//.test(file.type)) {
+ max = Math.min(max, QR.max_size_video);
}
- },
- handleFile: function(file, isSingle, max) {
- var post;
if (file.size > max) {
QR.error("" + file.name + ": File too large (file: " + ($.bytesToString(file.size)) + ", max: " + ($.bytesToString(max)) + ").");
- return;
+ if (!isSingle) {
+ return;
+ }
}
if (isSingle) {
post = QR.selected;
- } else if ((post = QR.posts[QR.posts.length - 1]).file) {
+ } else if (index !== 0 || (post = QR.posts[QR.posts.length - 1]).file) {
post = new QR.post();
}
if (/^text/.test(file.type)) {
@@ -9110,7 +9121,7 @@
}
},
dialog: function() {
- var dialog, elm, event, i, items, name, node, nodes, rules, save, setNode, _, _ref, _ref1;
+ var dialog, elm, event, i, items, name, node, nodes, prop, rules, save, setNode, _, _i, _len, _ref, _ref1, _ref2;
QR.nodes = nodes = {
el: dialog = UI.dialog('qr', 'top:0;right:0;', "")
};
@@ -9143,15 +9154,23 @@
setNode('status', '[type=submit]');
setNode('fileInput', '[type=file]');
rules = $('ul.rules').textContent.trim();
- QR.min_width = QR.min_heigth = 1;
- QR.max_width = QR.max_heigth = 5000;
+ QR.min_width = QR.min_height = 1;
+ QR.max_width = QR.max_height = 10000;
try {
- _ref = rules.match(/.+smaller than (\d+)x(\d+).+/), _ = _ref[0], QR.min_width = _ref[1], QR.min_heigth = _ref[2];
- _ref1 = rules.match(/.+greater than (\d+)x(\d+).+/), _ = _ref1[0], QR.max_width = _ref1[1], QR.max_heigth = _ref1[2];
+ _ref = rules.match(/.+smaller than (\d+)x(\d+).+/), _ = _ref[0], QR.min_width = _ref[1], QR.min_height = _ref[2];
+ _ref1 = rules.match(/.+greater than (\d+)x(\d+).+/), _ = _ref1[0], QR.max_width = _ref1[1], QR.max_height = _ref1[2];
+ _ref2 = ['min_width', 'min_height', 'max_width', 'max_height'];
+ for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
+ prop = _ref2[_i];
+ QR[prop] = parseInt(QR[prop], 10);
+ }
} catch (_error) {
null;
}
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value;
+ QR.max_size_video = 3145728;
+ QR.max_width_video = QR.max_height_video = 2048;
+ QR.max_duration_video = 120;
QR.spoiler = !!$('input[name=spoiler]');
if (QR.spoiler) {
$.addClass(QR.nodes.el, 'has-spoiler');
@@ -9379,10 +9398,6 @@
onload: QR.response,
onerror: function() {
delete QR.req;
- if (QR.captcha.isEnabled) {
- QR.captcha.destroy();
- QR.captcha.setup();
- }
post.unlock();
QR.cooldown.auto = false;
QR.status();
@@ -9412,12 +9427,9 @@
return QR.status();
},
response: function() {
- var URL, ban, board, err, h1, isReply, m, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1;
+ var URL, ban, board, captchasCount, err, h1, isReply, m, notif, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1;
req = QR.req;
delete QR.req;
- if (QR.captcha.isEnabled) {
- QR.captcha.destroy();
- }
post = QR.posts[0];
post.unlock();
resDoc = req.response;
@@ -9442,12 +9454,12 @@
} else if (/expired/i.test(err.textContent)) {
err = 'This CAPTCHA is no longer valid because it has expired.';
}
- QR.cooldown.auto = false;
+ QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : err === 'Connection error with sys.4chan.org.' ? true : false;
QR.cooldown.set({
delay: 2
});
} else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) {
- QR.cooldown.auto = !QR.captcha.isEnabled;
+ QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : true;
QR.cooldown.set({
delay: m[1]
});
@@ -9489,13 +9501,26 @@
});
postsCount = QR.posts.length - 1;
QR.cooldown.auto = postsCount && isReply;
- if (QR.captcha.isEnabled && QR.cooldown.auto) {
- QR.captcha.setup();
+ if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) {
+ notif = new Notification('Quick reply warning', {
+ body: "You are running low on cached captchas. Cache count: " + captchasCount + ".",
+ icon: Favicon.logo
+ });
+ notif.onclick = function() {
+ QR.open();
+ QR.captcha.nodes.input.focus();
+ return window.focus();
+ };
+ notif.onshow = function() {
+ return setTimeout(function() {
+ return notif.close();
+ }, 7 * $.SECOND);
+ };
}
if (!(Conf['Persistent QR'] || QR.cooldown.auto)) {
QR.close();
} else {
- if (QR.posts.length > 1) {
+ if (QR.posts.length > 1 && QR.captcha.isEnabled && QR.captcha.captchas.length === 0) {
QR.captcha.setup();
}
post.rm();
@@ -9506,7 +9531,7 @@
isReply: isReply,
threadID: threadID
});
- URL = !isReply ? "/" + g.BOARD + "/res/" + threadID : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? "/" + g.BOARD + "/res/" + threadID + "#p" + postID : void 0;
+ URL = threadID === postID ? Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? Build.path(g.BOARD.ID, threadID, postID) : void 0;
if (URL) {
if (Conf['Open Post in New Tab']) {
$.open(URL);
@@ -9575,8 +9600,21 @@
};
$.on(input, 'blur', QR.focusout);
$.on(input, 'focus', QR.focusin);
+ $.on(input, 'keydown', QR.captcha.keydown.bind(QR.captcha));
+ $.on(this.nodes.img.parentNode, 'click', QR.captcha.reload.bind(QR.captcha));
$.addClass(QR.nodes.el, 'has-captcha');
$.after(QR.nodes.com.parentNode, [imgContainer, input]);
+ this.captchas = [];
+ $.get('captchas', [], function(_arg) {
+ var captchas;
+ captchas = _arg.captchas;
+ QR.captcha.sync(captchas);
+ return QR.captcha.clear();
+ });
+ $.sync('captchas', this.sync);
+ new MutationObserver(this.afterSetup).observe($.id('captchaContainer'), {
+ childList: true
+ });
this.beforeSetup();
return this.afterSetup();
},
@@ -9586,28 +9624,31 @@
img.parentNode.parentNode.hidden = true;
input.value = '';
input.placeholder = 'Focus to load reCAPTCHA';
- $.on(input, 'focus', this.setup);
- this.setupObserver = new MutationObserver(this.afterSetup);
- return this.setupObserver.observe($.id('captchaContainer'), {
- childList: true
- });
+ this.count();
+ return $.on(input, 'focus', this.setup);
},
setup: function() {
return $.globalEval('loadRecaptcha()');
},
afterSetup: function() {
- var challenge, img, input, _ref;
+ var challenge, img, input, setLifetime, _ref;
if (!(challenge = $.id('recaptcha_challenge_field_holder'))) {
return;
}
- QR.captcha.setupObserver.disconnect();
- delete QR.captcha.setupObserver;
+ if (challenge === QR.captcha.nodes.challenge) {
+ return;
+ }
+ setLifetime = function(e) {
+ return QR.captcha.lifetime = e.detail;
+ };
+ $.on(window, 'captcha:timeout', setLifetime);
+ $.globalEval('window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))');
+ $.off(window, 'captcha:timeout', setLifetime);
_ref = QR.captcha.nodes, img = _ref.img, input = _ref.input;
img.parentNode.parentNode.hidden = false;
input.placeholder = 'Verification';
+ QR.captcha.count();
$.off(input, 'focus', QR.captcha.setup);
- $.on(input, 'keydown', QR.captcha.keydown.bind(QR.captcha));
- $.on(img.parentNode, 'click', QR.captcha.reload.bind(QR.captcha));
QR.captcha.nodes.challenge = challenge;
new MutationObserver(QR.captcha.load.bind(QR.captcha)).observe(challenge, {
childList: true,
@@ -9620,30 +9661,112 @@
$.globalEval('Recaptcha.destroy()');
return this.beforeSetup();
},
+ sync: function(captchas) {
+ QR.captcha.captchas = captchas;
+ return QR.captcha.count();
+ },
getOne: function() {
- var challenge, response;
- challenge = this.nodes.img.alt;
- response = this.nodes.input.value.trim();
- if (response && !/\s/.test(response)) {
- response = "" + response + " " + response;
+ var captcha, challenge, response;
+ this.clear();
+ if (captcha = this.captchas.shift()) {
+ challenge = captcha.challenge, response = captcha.response;
+ this.count();
+ $.set('captchas', this.captchas);
+ } else {
+ challenge = this.nodes.img.alt;
+ if (response = this.nodes.input.value) {
+ if (Conf['Auto-load captcha']) {
+ this.reload();
+ } else {
+ this.destroy();
+ }
+ }
+ }
+ if (response) {
+ response = response.trim();
+ if (!/\s/.test(response)) {
+ response = "" + response + " " + response;
+ }
}
return {
challenge: challenge,
response: response
};
},
+ save: function() {
+ var response;
+ if (!(response = this.nodes.input.value.trim())) {
+ return;
+ }
+ this.nodes.input.value = '';
+ this.captchas.push({
+ challenge: this.nodes.img.alt,
+ response: response,
+ timeout: this.timeout
+ });
+ this.count();
+ this.reload();
+ return $.set('captchas', this.captchas);
+ },
+ clear: function() {
+ var captcha, i, now, _i, _len, _ref;
+ if (!this.captchas.length) {
+ return;
+ }
+ now = Date.now();
+ _ref = this.captchas;
+ for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
+ captcha = _ref[i];
+ if (captcha.timeout > now) {
+ break;
+ }
+ }
+ if (!i) {
+ return;
+ }
+ this.captchas = this.captchas.slice(i);
+ this.count();
+ return $.set('captchas', this.captchas);
+ },
load: function() {
- var challenge;
+ var challenge, challenge_image;
if (!this.nodes.challenge.firstChild) {
return;
}
+ if (!(challenge_image = $.id('recaptcha_challenge_image'))) {
+ return;
+ }
+ this.timeout = Date.now() + this.lifetime * $.SECOND - $.MINUTE;
challenge = this.nodes.challenge.firstChild.value;
this.nodes.img.alt = challenge;
- this.nodes.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge;
- return this.nodes.input.value = null;
+ this.nodes.img.src = challenge_image.src;
+ this.nodes.input.value = null;
+ return this.clear();
+ },
+ count: function() {
+ var count, placeholder;
+ count = this.captchas ? this.captchas.length : 0;
+ placeholder = this.nodes.input.placeholder.replace(/\ \(.*\)$/, '');
+ placeholder += (function() {
+ switch (count) {
+ case 0:
+ if (placeholder === 'Verification') {
+ return ' (Shift + Enter to cache)';
+ } else {
+ return '';
+ }
+ break;
+ case 1:
+ return ' (1 cached captcha)';
+ default:
+ return " (" + count + " cached captchas)";
+ }
+ })();
+ this.nodes.input.placeholder = placeholder;
+ return this.nodes.input.alt = count;
},
reload: function(focus) {
- $.globalEval('Recaptcha.reload("t")');
+ $.globalEval('Recaptcha.reload(); Recaptcha.should_focus = false;');
if (focus) {
return this.nodes.input.focus();
}
@@ -9651,6 +9774,8 @@
keydown: function(e) {
if (e.keyCode === 8 && !this.nodes.input.value) {
this.reload();
+ } else if (e.keyCode === 13 && e.shiftKey) {
+ this.save();
} else {
return;
}
@@ -9982,9 +10107,6 @@
node.disabled = lock;
}
}
- if (QR.captcha.isEnabled) {
- QR.captcha.nodes.input.disabled = lock;
- }
this.nodes.rm.style.visibility = lock ? 'hidden' : '';
(lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput);
this.nodes.spoiler.disabled = lock;
@@ -10084,7 +10206,7 @@
} else {
this.updateFilename();
}
- if (!/^image/.test(file.type)) {
+ if (!/^(image|video)\//.test(file.type)) {
this.nodes.el.style.backgroundImage = null;
return;
}
@@ -10092,20 +10214,35 @@
};
_Class.prototype.setThumbnail = function() {
- var fileURL, img;
- img = $.el('img');
- img.onload = (function(_this) {
+ var el, fileURL, isVideo;
+ isVideo = /^video\//.test(this.file.type);
+ el = $.el((isVideo ? 'video' : 'img'));
+ $.on(el, (isVideo ? 'loadeddata' : 'load'), (function(_this) {
return function() {
- var cv, height, s, width;
+ var cv, error, errors, height, s, width, _i, _len;
+ errors = _this.checkDimensions(el, isVideo);
+ if (errors.length) {
+ for (_i = 0, _len = errors.length; _i < _len; _i++) {
+ error = errors[_i];
+ QR.error(error);
+ }
+ _this.URL = fileURL;
+ return _this.rmFile();
+ }
s = 90 * 2 * window.devicePixelRatio;
if (_this.file.type === 'image/gif') {
s *= 3;
}
- height = img.height, width = img.width;
- if (height < s || width < s) {
- _this.URL = fileURL;
- _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")";
- return;
+ if (isVideo) {
+ height = el.videoHeight;
+ width = el.videoWidth;
+ } else {
+ height = el.height, width = el.width;
+ if (height < s || width < s) {
+ _this.URL = fileURL;
+ _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")";
+ return;
+ }
}
if (height <= width) {
width = s / height * width;
@@ -10115,18 +10252,52 @@
width = s;
}
cv = $.el('canvas');
- cv.height = img.height = height;
- cv.width = img.width = width;
- cv.getContext('2d').drawImage(img, 0, 0, width, height);
+ cv.height = el.height = height;
+ cv.width = el.width = width;
+ cv.getContext('2d').drawImage(el, 0, 0, width, height);
URL.revokeObjectURL(fileURL);
return cv.toBlob(function(blob) {
_this.URL = URL.createObjectURL(blob);
return _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")";
});
};
- })(this);
+ })(this));
fileURL = URL.createObjectURL(this.file);
- return img.src = fileURL;
+ return el.src = fileURL;
+ };
+
+ _Class.prototype.checkDimensions = function(el, video) {
+ var duration, err, height, max_height, max_width, videoHeight, videoWidth, width;
+ err = [];
+ if (video) {
+ videoHeight = el.videoHeight, videoWidth = el.videoWidth, duration = el.duration;
+ max_height = QR.max_height < QR.max_height_video ? QR.max_height : QR.max_height_video;
+ max_width = QR.max_width < QR.max_width_video ? QR.max_width : QR.max_width_video;
+ if (videoHeight > max_height || videoWidth > max_width) {
+ err.push("" + this.file.name + ": Video too large (video: " + videoHeight + "x" + videoWidth + "px, max: " + max_height + "x" + max_width + "px)");
+ }
+ if (videoHeight < QR.min_height || videoWidth < QR.min_width) {
+ err.push("" + this.file.name + ": Video too small (video: " + videoHeight + "x" + videoWidth + "px, min: " + QR.min_height + "x" + QR.min_width + "px)");
+ }
+ if (!isFinite(el.duration)) {
+ err.push("" + file.name + ": Video lacks duration metadata (try remuxing)");
+ }
+ if (duration > QR.max_duration_video) {
+ err.push("" + this.file.name + ": Video too long (video: " + duration + "s, max: " + QR.max_duration_video + "s)");
+ }
+ if (el.mozHasAudio) {
+ err.push("" + file.name + ": Audio not allowed");
+ }
+ } else {
+ height = el.height, width = el.width;
+ if (height > QR.max_height || width > QR.max_width) {
+ err.push("" + this.file.name + ": Image too large (image: " + height + "x" + width + "px, max: " + QR.max_height + "x" + QR.max_width + "px)");
+ }
+ if (height < QR.min_height || width < QR.min_width) {
+ err.push("" + this.file.name + ": Image too small (image: " + height + "x" + width + "px, min: " + QR.min_height + "x" + QR.min_width + "px)");
+ }
+ }
+ return err;
};
_Class.prototype.rmFile = function() {
@@ -10488,7 +10659,7 @@
if (src[2] === 'i.4cdn.org') {
URL = Redirect.to('file', {
boardID: src[3],
- filename: src[5]
+ filename: src[src.length - 1]
});
if (URL) {
thumb.href = URL;
@@ -10502,7 +10673,7 @@
return;
}
}
- return $.ajax("//a.4cdn.org/" + post.board + "/res/" + post.thread + ".json", {
+ return $.ajax("//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", {
onload: function() {
var i, postObj, posts;
if (this.status !== 200) {
@@ -11802,6 +11973,7 @@
delete this.postCountEl;
delete this.fileCountEl;
delete this.pageCountEl;
+ delete this.dialog;
Thread.callbacks.disconnect('Thread Stats');
return $.off(d, 'ThreadUpdate', ThreadStats.onUpdate);
},
@@ -11852,7 +12024,7 @@
continue;
}
ThreadStats.pageCountEl.textContent = page.page;
- (page.page === this.response.length - 1 ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning');
+ (page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning');
return;
}
}
@@ -12978,7 +13150,7 @@
threadID: data.thread.ID,
postID: ID
})) {
- QuoteYou.lastRead = data.nodes.root;
+ QuoteMarkers.lastRead = data.nodes.root;
}
}
if (!ID) {
@@ -13073,7 +13245,7 @@
}
return Redirect.data = o;
},
- archives: [{"uid":0,"name":"Foolz","domain":"archive.foolz.us","http":true,"https":true,"software":"foolfuuka","boards":["a","biz","co","diy","gd","jp","m","sci","sp","tg","tv","v","vg","vp","vr","wsg"],"files":["a","biz","gd","diy","jp","m","sci","tg","vg","vp","vr","wsg"]},{"uid":1,"name":"NSFW Foolz","domain":"nsfw.foolz.us","http":true,"https":true,"software":"foolfuuka","boards":["u"],"files":["u"]},{"uid":2,"name":"The Dark Cave","domain":"archive.thedarkcave.org","http":true,"https":true,"software":"foolfuuka","boards":["c","int","out","po"],"files":["c","po"]},{"uid":3,"name":"4plebs Archive","domain":"archive.4plebs.org","http":true,"https":true,"software":"foolfuuka","boards":["adv","hr","o","pol","s4s","tg","trv","tv","x"],"files":["adv","hr","o","pol","s4s","tg","trv","tv","x"]},{"uid":18,"name":"4plebs Flash Archive","domain":"flash.4plebs.org","http":true,"https":true,"software":"foolfuuka","boards":["f"],"files":["f"]},{"uid":4,"name":"Nyafuu","domain":"archive.nyafuu.org","http":true,"https":true,"software":"foolfuuka","boards":["c","e","w","wg"],"files":["c","e","w","wg"]},{"uid":5,"name":"Love is Over","domain":"loveisover.me","http":true,"https":true,"software":"foolfuuka","boards":["d","i"],"files":["d","i"]},{"uid":8,"name":"Rebecca Black Tech","domain":"rbt.asia","http":true,"https":true,"software":"fuuka","boards":["cgl","g","mu","w"],"files":["cgl","g","mu","w"]},{"uid":9,"name":"Heinessen","domain":"archive.heinessen.com","http":true,"https":false,"software":"fuuka","boards":["an","fit","k","mlp","r9k","toy"],"files":["an","fit","k","r9k","toy"]},{"uid":10,"name":"warosu","domain":"fuuka.warosu.org","http":false,"https":true,"software":"fuuka","boards":["3","biz","cgl","ck","diy","fa","g","ic","jp","lit","sci","tg","vr"],"files":["3","biz","cgl","ck","diy","fa","ic","jp","lit","sci","tg","vr"]},{"uid":15,"name":"fgts","domain":"fgts.eu","http":true,"https":true,"software":"foolfuuka","boards":["cm","h","hc","hm","r","s","soc","y"],"files":["cm","h","hc","hm","r","s","soc","y"]},{"uid":16,"name":"maware","domain":"archive.mawa.re","http":true,"https":false,"software":"foolfuuka","boards":["t"],"files":["t"]},{"uid":17,"name":"installgentoo.com","domain":"chan.installgentoo.com","http":true,"https":false,"software":"foolfuuka","boards":["g","t"],"files":["g","t"]},{"uid":13,"name":"Foolz Beta","domain":"beta.foolz.us","http":true,"https":true,"withCredentials":true,"software":"foolfuuka","boards":["a","biz","co","d","diy","gd","jp","m","s4s","sci","sp","tg","tv","u","v","vg","vp","vr","wsg"],"files":["a","biz","d","diy","gd","jp","m","s4s","sci","tg","u","vg","vp","vr","wsg"]}],
+ archives: [{"uid":0,"name":"Foolz","domain":"archive.foolz.us","http":true,"https":true,"software":"foolfuuka","boards":["a","biz","co","diy","gd","jp","m","sci","sp","tg","tv","vg","vp","vr","wsg"],"files":["a","biz","gd","diy","jp","m","sci","tg","vg","vp","vr","wsg"]},{"uid":1,"name":"NSFW Foolz","domain":"nsfw.foolz.us","http":true,"https":true,"software":"foolfuuka","boards":["u"],"files":["u"]},{"uid":2,"name":"The Dark Cave","domain":"archive.thedarkcave.org","http":true,"https":true,"software":"foolfuuka","boards":["c","int","out","po"],"files":["c","po"]},{"uid":3,"name":"4plebs Archive","domain":"archive.4plebs.org","http":true,"https":true,"software":"foolfuuka","boards":["adv","hr","o","pol","s4s","tg","trv","tv","x"],"files":["adv","hr","o","pol","s4s","tg","trv","tv","x"]},{"uid":18,"name":"4plebs Flash Archive","domain":"flash.4plebs.org","http":true,"https":true,"software":"foolfuuka","boards":["f"],"files":["f"]},{"uid":4,"name":"Nyafuu","domain":"archive.nyafuu.org","http":true,"https":true,"software":"foolfuuka","boards":["c","e","w","wg"],"files":["c","e","w","wg"]},{"uid":5,"name":"Love is Over","domain":"loveisover.me","http":true,"https":true,"software":"foolfuuka","boards":["d","i"],"files":["d","i"]},{"uid":8,"name":"Rebecca Black Tech","domain":"rbt.asia","http":true,"https":true,"software":"fuuka","boards":["cgl","g","mu","w"],"files":["cgl","g","mu","w"]},{"uid":9,"name":"Heinessen","domain":"archive.heinessen.com","http":true,"https":false,"software":"fuuka","boards":["an","fit","k","mlp","r9k","toy"],"files":["an","fit","k","r9k","toy"]},{"uid":10,"name":"warosu","domain":"fuuka.warosu.org","http":false,"https":true,"software":"fuuka","boards":["3","biz","cgl","ck","diy","fa","g","ic","jp","lit","sci","tg","vr"],"files":["3","biz","cgl","ck","diy","fa","ic","jp","lit","sci","tg","vr"]},{"uid":15,"name":"fgts","domain":"fgts.eu","http":true,"https":true,"software":"foolfuuka","boards":["asp","cm","h","hc","hm","n","p","r","s","soc","y"],"files":["asp","cm","h","hc","hm","n","p","r","s","soc","y"]},{"uid":16,"name":"maware","domain":"archive.mawa.re","http":true,"https":false,"software":"foolfuuka","boards":["t"],"files":["t"]},{"uid":17,"name":"installgentoo.com","domain":"chan.installgentoo.com","http":true,"https":false,"software":"foolfuuka","boards":["g","t"],"files":["g","t"]},{"uid":13,"name":"Foolz Beta","domain":"beta.foolz.us","http":true,"https":true,"withCredentials":true,"software":"foolfuuka","boards":["a","biz","co","d","diy","gd","jp","m","s4s","sci","sp","tg","tv","u","vg","vp","vr","wsg"],"files":["a","biz","d","diy","gd","jp","m","s4s","sci","tg","u","vg","vp","vr","wsg"]}],
to: function(dest, data) {
var archive;
archive = (dest === 'search' || dest === 'board' ? Redirect.data.thread : Redirect.data[dest])[data.boardID];
@@ -13137,7 +13309,7 @@
"http": true,
"https": true,
"software": "foolfuuka",
- "boards": ["a", "biz", "co", "diy", "gd", "jp", "m", "sci", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
+ "boards": ["a", "biz", "co", "diy", "gd", "jp", "m", "sci", "sp", "tg", "tv", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "gd", "diy", "jp", "m", "sci", "tg", "vg", "vp", "vr", "wsg"]
}, {
"uid": 1,
@@ -13227,8 +13399,8 @@
"http": true,
"https": true,
"software": "foolfuuka",
- "boards": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"],
- "files": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"]
+ "boards": ["asp", "cm", "h", "hc", "hm", "n", "p", "r", "s", "soc", "y"],
+ "files": ["asp", "cm", "h", "hc", "hm", "n", "p", "r", "s", "soc", "y"]
}, {
"uid": 16,
"name": "maware",
@@ -13255,7 +13427,7 @@
"https": true,
"withCredentials": true,
"software": "foolfuuka",
- "boards": ["a", "biz", "co", "d", "diy", "gd", "jp", "m", "s4s", "sci", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
+ "boards": ["a", "biz", "co", "d", "diy", "gd", "jp", "m", "s4s", "sci", "sp", "tg", "tv", "u", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "d", "diy", "gd", "jp", "m", "s4s", "sci", "tg", "u", "vg", "vp", "vr", "wsg"]
}
];
@@ -13266,7 +13438,7 @@
return d.body;
}), function() {
return $.asap((function() {
- return $('.abovePostForm');
+ return $('hr');
}), Banner.ready);
});
},
@@ -13286,7 +13458,7 @@
alt: '4chan',
title: 'Click to change'
});
- $.on(img, 'click', Banner.cb.toggle);
+ $.on(img, 'click error', Banner.cb.toggle);
Banner.cb.toggle.call(img);
$.prepend(banner, img);
continue;
@@ -15090,7 +15262,7 @@
return;
}
a.textContent = "Post No." + post + " Loading...";
- return $.cache("//api.4chan.org" + a.pathname + ".json", function() {
+ return $.cache("//a.4cdn.org" + (a.pathname.split('/').splice(0, 4).join('/')) + ".json", function() {
return ExpandComment.parse(this, a, post);
});
},
@@ -15111,7 +15283,7 @@
a.textContent = "Error " + req.statusText + " (" + status + ")";
return;
}
- posts = JSON.parse(req.response).posts;
+ posts = req.response.posts;
if (spoilerRange = posts[0].custom_spoiler) {
Build.spoilerRange[g.BOARD] = spoilerRange;
}
@@ -15135,7 +15307,11 @@
if (href[0] === '/') {
continue;
}
- quote.href = "/" + post.board + "/res/" + href;
+ if (href[0] === '#') {
+ quote.href = "" + (a.pathname.split('/').splice(0, 4).join('/')) + href;
+ } else {
+ quote.href = "" + (a.pathname.split('/').splice(0, 3).join('/')) + "/" + href;
+ }
}
post.nodes.shortComment = comment;
$.replace(comment, clone);
@@ -15721,7 +15897,7 @@
return Conf[hotkey] = key;
},
keydown: function(e) {
- var form, key, notification, notifications, op, target, thread, threadRoot, _i, _len, _ref;
+ var form, key, notification, notifications, op, searchInput, target, thread, threadRoot, _i, _len, _ref;
if (!(key = Keybinds.keyCode(e))) {
return;
}
@@ -15731,9 +15907,11 @@
return;
}
}
- threadRoot = Nav.getThread();
- if (op = $('.op', threadRoot)) {
- thread = Get.postFromNode(op).thread;
+ if (g.VIEW !== 'catalog') {
+ threadRoot = Nav.getThread();
+ if (op = $('.op', threadRoot)) {
+ thread = Get.postFromNode(op).thread;
+ }
}
switch (key) {
case Conf['Toggle board list']:
@@ -15745,10 +15923,13 @@
Header.toggleBarVisibility();
break;
case Conf['Open empty QR']:
- Keybinds.qr(threadRoot);
+ Keybinds.qr();
break;
case Conf['Open QR']:
- Keybinds.qr(threadRoot, true);
+ if (g.VIEW === 'catalog') {
+ return;
+ }
+ Keybinds.qr(threadRoot);
break;
case Conf['Open settings']:
Settings.open();
@@ -15821,30 +16002,48 @@
}
break;
case Conf['Watch']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
ThreadWatcher.toggle(thread);
break;
case Conf['Expand image']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.img(threadRoot);
break;
case Conf['Expand images']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.img(threadRoot, true);
break;
case Conf['Open Gallery']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Gallery.cb.toggle();
break;
case Conf['fappeTyme']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
FappeTyme.cb.toggle.call({
name: 'fappe'
});
break;
case Conf['werkTyme']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
FappeTyme.cb.toggle.call({
name: 'werk'
});
break;
case Conf['Front page']:
if (Conf['JSON Navigation'] && g.VIEW === 'index') {
- Index.userPageNav(0);
+ Index.userPageNav(1);
} else {
window.location = "/" + g.BOARD + "/";
}
@@ -15881,11 +16080,13 @@
}
break;
case Conf['Search form']:
- if (Conf['JSON Navigation']) {
- Index.searchInput.focus();
- } else {
- $.id('search-btn').click();
+ if (g.VIEW !== 'index') {
+ return;
}
+ searchInput = Conf['JSON Navigation'] ? Index.searchInput : $.id('search-box');
+ Header.scrollToIfNeeded(searchInput);
+ searchInput.click();
+ searchInput.focus();
break;
case Conf['Paged mode']:
if (!(g.VIEW === 'index' && Conf['Index Mode'] !== 'paged')) {
@@ -15937,21 +16138,39 @@
Nav.scroll(-1);
break;
case Conf['Expand thread']:
+ if (g.VIEW !== 'index') {
+ return;
+ }
ExpandThread.toggle(thread);
break;
case Conf['Open thread']:
+ if (g.VIEW !== 'index') {
+ return;
+ }
Keybinds.open(thread);
break;
case Conf['Open thread tab']:
+ if (g.VIEW !== 'index') {
+ return;
+ }
Keybinds.open(thread, true);
break;
case Conf['Next reply']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.hl(+1, threadRoot);
break;
case Conf['Previous reply']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.hl(-1, threadRoot);
break;
case Conf['Deselect reply']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.hl(0, threadRoot);
break;
case Conf['Hide']:
@@ -16011,12 +16230,12 @@
}
return key;
},
- qr: function(thread, quote) {
- if (!QR.postingIsEnabled) {
+ qr: function(thread) {
+ if (!(Conf['Quick Reply'] && QR.postingIsEnabled)) {
return;
}
QR.open();
- if (quote) {
+ if (thread != null) {
QR.quote.call($('input', $('.post.highlight', thread) || thread));
}
QR.nodes.com.focus();
@@ -16056,7 +16275,7 @@
if (g.VIEW !== 'index') {
return;
}
- url = "/" + thread.board + "/thread/" + thread;
+ url = Build.path(thread.board.ID, thread.ID);
if (tab) {
return $.open(url);
} else {
@@ -16545,10 +16764,14 @@
return g.VIEW = view;
},
updateBoard: function(boardID) {
- var fullBoardList;
+ var current, fullBoardList;
fullBoardList = $('#full-board-list', Header.boardList);
- $.rmClass($('.current', fullBoardList), 'current');
- $.addClass($("a[href*='/" + boardID + "/']", fullBoardList), 'current');
+ if (current = $('.current', fullBoardList)) {
+ $.rmClass(current, 'current');
+ }
+ if (current = $("a[href*='/" + boardID + "/']", fullBoardList)) {
+ $.addClass(current, 'current');
+ }
Header.generateBoardList(Conf['boardnav'].replace(/(\r\n|\n|\r)/g, ' '));
Index.catalogLink.href = "//boards.4chan.org/" + boardID + "/";
QR.flagsInput();
@@ -16674,7 +16897,7 @@
if (threadID) {
view = 'thread';
} else {
- pageNum = +view;
+ pageNum = +view || 1;
view = 'index';
}
path = this.pathname;
@@ -16709,7 +16932,7 @@
return Index.update(pageNum);
}
load = Navigate.load;
- Navigate.req = $.ajax("//a.4cdn.org/" + boardID + "/res/" + threadID + ".json", {
+ Navigate.req = $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", {
onabort: load,
onloadend: load
});
@@ -17913,6 +18136,13 @@
}
if (g.VIEW === 'thread') {
g.THREADID = +pathname[3];
+ if (pathname[4] != null) {
+ g.SLUG = pathname[4];
+ }
+ if (pathname[2] !== 'thread') {
+ pathname[2] = 'thread';
+ history.replaceState(null, '', pathname.slice(0, 4).join('/') + location.hash);
+ }
}
flatten = function(parent, obj) {
var key, val;
@@ -18112,14 +18342,15 @@
return window.open('//sys.4chan.org/auth', 'This will steal your data.', 'left=0,top=0,width=500,height=255,toolbar=0,resizable=0');
});
$.before(styleSelector.previousSibling, [$.tn('['), passLink, $.tn(']\u00A0\u00A0')]);
+ $('link[href*="mobile"', d.head).disabled = true;
}
if (!Conf['JSON Navigation'] || g.VIEW === 'thread') {
Main.initThread();
+ $.add(d.head, $.el('link', {
+ href: "//s.4cdn.org/css/flags.556.css",
+ rel: "stylesheet"
+ }));
}
- $.add(d.head, $.el('link', {
- href: "//s.4cdn.org/css/flags.556.css",
- rel: "stylesheet"
- }));
$.event('4chanXInitFinished');
test = $.el('span');
test.classList.add('a', 'b');
diff --git a/builds/crx/script.js b/builds/crx/script.js
index d9bf3a5c8..488034d11 100644
--- a/builds/crx/script.js
+++ b/builds/crx/script.js
@@ -1,6 +1,6 @@
// Generated by CoffeeScript
/*
-* appchan x - Version 2.9.20 - 2014-05-02
+* appchan x - Version 2.9.20 - 2014-05-03
*
* Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE
@@ -200,7 +200,7 @@
'Posting Success Notifications': [true, 'Show notifications on successful post creation or file uploading.'],
'Captcha Warning Notifications': [true, 'When disabled, shows a red border on the CAPTCHA input until a key is pressed instead of a notification.'],
'Dump List Before Comment': [false, 'Position of the QR\'s Dump List.'],
- 'Auto-load captcha': [false, 'Automatically load the captcha when you open a thread']
+ 'Auto-load captcha': [false, 'Automatically load the captcha when you open a thread, and reload it after you post.']
},
'Quote Links': {
'Quote Backlinks': [true, 'Add quote backlinks.'],
@@ -379,7 +379,7 @@
},
time: '%m/%d/%y(%a)%H:%M:%S',
backlink: '>>%id',
- fileInfo: '%L (%p%s, %r)',
+ fileInfo: '%l (%p%s, %r)',
favicon: 'ferongr',
usercss: "/* Tripcode Italics: */\n/*\nspan.postertrip {\nfont-style: italic;\n}\n*/\n\n/* Add a rounded border to thumbnails (but not expanded images): */\n/*\n.fileThumb > img:first-child {\nborder: solid 2px rgba(0,0,100,0.5);\nborder-radius: 10px;\n}\n*/\n\n/* Make highlighted posts look inset on the page: */\n/*\ndiv.post:target,\ndiv.post.highlight {\nbox-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);\n}\n*/",
hotkeys: {
@@ -403,17 +403,17 @@
'Open Gallery': ['g', 'Opens the gallery.'],
'fappeTyme': ['f', 'Fappe Tyme.'],
'werkTyme': ['Shift+w', 'Werk Tyme'],
- 'Front page': ['0', 'Jump to front page.'],
- 'Open front page': ['Shift+0', 'Open front page in a new tab.'],
- 'Next page': ['Shift+Right', 'Jump to the next page.'],
- 'Previous page': ['Shift+Left', 'Jump to the previous page.'],
+ 'Front page': ['1', 'Jump to front page.'],
+ 'Open front page': ['Shift+1', 'Open front page in a new tab.'],
+ 'Next page': ['Ctrl+Right', 'Jump to the next page.'],
+ 'Previous page': ['Ctrl+Left', 'Jump to the previous page.'],
'Search form': ['Ctrl+Alt+s', 'Focus the search field on the board index.'],
'Paged mode': ['Alt+1', 'Sets the index mode to paged.'],
'All pages mode': ['Alt+2', 'Sets the index mode to all threads.'],
'Catalog mode': ['Alt+3', 'Sets the index mode to catalog.'],
'Cycle sort type': ['Alt+x', 'Cycle through index sort types.'],
- 'Next thread': ['Shift+Down', 'See next thread.'],
- 'Previous thread': ['Shift+Up', 'See previous thread.'],
+ 'Next thread': ['Ctrl+Down', 'See next thread.'],
+ 'Previous thread': ['Ctrl+Up', 'See previous thread.'],
'Expand thread': ['Ctrl+e', 'Expand thread.'],
'Open thread': ['o', 'Open thread in current tab.'],
'Open thread tab': ['Shift+o', 'Open thread in new tab.'],
@@ -3220,7 +3220,7 @@
title: type,
className: "" + typeLC + "Icon"
});
- root = type === 'Closed' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : g.VIEW === 'index' ? $('.page-num', this.OP.nodes.info) : $('[title="Quote this post"]', this.OP.nodes.info);
+ root = type === 'Closed' && this.isSticky ? $('.stickyIcon', this.OP.nodes.info) : g.VIEW === 'index' ? $('.page-num', this.OP.nodes.info) : $('[title="Reply to this post"]', this.OP.nodes.info);
$.after(root, [$.tn(' '), icon]);
if (!this.catalogView) {
return;
@@ -3419,7 +3419,7 @@
Post.prototype.parseQuote = function(quotelink) {
var fullID, match;
- if (!(match = quotelink.href.match(/boards\.4chan\.org\/([^\/]+)\/thread\/\d+#p(\d+)$/))) {
+ if (!(match = quotelink.href.match(/boards\.4chan\.org\/([^\/]+)\/(res|thread)\/\d+(.*)?\#p(\d+)$/))) {
return;
}
this.nodes.quotelinks.push(quotelink);
@@ -3433,7 +3433,7 @@
};
Post.prototype.parseFile = function(that) {
- var anchor, fileEl, fileText, nameNode, size, thumb, unit;
+ var anchor, fileEl, fileText, nameNode, size, thumb, unit, _ref;
if (!((fileEl = $('.file', this.nodes.post)) && (thumb = $('img[data-md5]', fileEl)))) {
return;
}
@@ -3454,13 +3454,13 @@
}
this.file.sizeInBytes = size;
this.file.thumbURL = that.isArchived ? thumb.src : "" + location.protocol + "//t.4cdn.org/" + this.board + "/" + (this.file.URL.match(/(\d+)\./)[1]) + "s.jpg";
- this.file.name = !this.file.isSpoiler && (nameNode = $('a', fileText)) ? nameNode.title || nameNode.textContent : fileText.title;
- this.file.name = this.file.name.replace(/%22/g, '"');
- this.file.isImage = /(jpg|png|gif)$/i.test(this.file.name);
- this.file.isVideo = /webm$/i.test(this.file.name);
+ this.file.isImage = /(jpg|png|gif)$/i.test(this.file.URL);
+ this.file.isVideo = /webm$/i.test(this.file.URL);
if (this.file.isImage || this.file.isVideo) {
- return this.file.dimensions = fileText.textContent.match(/\d+x\d+/)[0];
+ this.file.dimensions = fileText.childNodes[2].data.match(/\d+x\d+/)[0];
}
+ this.file.name = !this.file.isSpoiler && (nameNode = $('a', fileText)) ? nameNode.title || nameNode.textContent : fileText.title;
+ return this.file.name = (_ref = this.file.name) != null ? _ref.replace(/%22/g, '"') : void 0;
};
Post.prototype.cleanup = function(root, post) {
@@ -3470,7 +3470,7 @@
node = _ref[_i];
$.rm(node);
}
- _ref1 = $$('[id]', post);
+ _ref1 = $$('[id]:not(.exif)', post);
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
node = _ref1[_j];
node.removeAttribute('id');
@@ -4879,12 +4879,15 @@
$.asap((function() {
return $('.board', doc) || d.readyState !== 'loading';
}), function() {
- var board, navLink, _l, _len3, _ref3;
+ var board, navLink, _l, _len3, _ref3, _ref4;
_ref3 = $$('.navLinks');
for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
navLink = _ref3[_l];
$.rm(navLink);
}
+ if ((_ref4 = $.id('search-box')) != null) {
+ _ref4.parentNode.remove();
+ }
$.after($.x('child::form/preceding-sibling::hr[1]'), Index.navLinks);
if (g.VIEW !== 'index') {
return;
@@ -5115,8 +5118,8 @@
toggleHiddenThreads: function() {
$('#hidden-toggle a', Index.navLinks).textContent = (Index.showHiddenThreads = !Index.showHiddenThreads) ? 'Hide' : 'Show';
Index.sort();
- if (Conf['Index Mode'] === 'paged' && Index.getCurrentPage() > 0) {
- return Index.pageNav(0);
+ if (Conf['Index Mode'] === 'paged' && Index.getCurrentPage() > 1) {
+ return Index.pageNav(1);
} else {
return Index.buildIndex();
}
@@ -5199,7 +5202,7 @@
if (Index.cb.indexNav(a, true)) {
return;
}
- return Index.userPageNav(+a.pathname.split('/')[2]);
+ return Index.userPageNav(+a.pathname.split('/')[2] || 1);
},
headerNav: function(e) {
var a, needChange, onSameIndex;
@@ -5252,10 +5255,10 @@
if (Conf['Index Mode'] === 'infinite' && Index.currentPage) {
return Index.currentPage;
}
- return +window.location.pathname.split('/')[2];
+ return +window.location.pathname.split('/')[2] || 1;
},
userPageNav: function(pageNum) {
- Navigate.pushState(pageNum === 0 ? './' : pageNum);
+ Navigate.pushState(pageNum === 1 ? './' : pageNum);
if (Conf['Refreshed Navigation'] && Conf['Index Mode'] !== 'all pages') {
return Index.update(pageNum);
} else {
@@ -5266,7 +5269,7 @@
if (Index.currentPage === pageNum && !Index.root.parentElement) {
return;
}
- Navigate.pushState(pageNum === 0 ? './' : pageNum);
+ Navigate.pushState(pageNum === 1 ? './' : pageNum);
return Index.pageLoad(pageNum);
},
pageLoad: function(pageNum) {
@@ -5288,7 +5291,14 @@
return Math.ceil(Index.sortedThreads.length / Index.getThreadsNumPerPage());
},
getMaxPageNum: function() {
- return Math.max(0, Index.getPagesNum() - 1);
+ var max, min;
+ min = 1;
+ max = +Index.getPagesNum();
+ if (min < max) {
+ return max;
+ } else {
+ return min;
+ }
},
togglePagelist: function() {
return Index.pagelist.hidden = Conf['Index Mode'] !== 'paged';
@@ -5297,12 +5307,12 @@
var a, i, maxPageNum, nodes, pagesRoot, _i;
pagesRoot = $('.pages', Index.pagelist);
maxPageNum = Index.getMaxPageNum();
- if (pagesRoot.childElementCount !== maxPageNum + 1) {
+ if (pagesRoot.childElementCount !== maxPageNum) {
nodes = [];
- for (i = _i = 0; _i <= maxPageNum; i = _i += 1) {
+ for (i = _i = 1; _i <= maxPageNum; i = _i += 1) {
a = $.el('a', {
textContent: i,
- href: i ? i : './'
+ href: i === 1 ? './' : i
});
nodes.push($.tn('['), a, $.tn('] '));
}
@@ -5321,11 +5331,11 @@
pagesRoot = $('.pages', Index.pagelist);
prev = pagesRoot.previousSibling.firstChild;
next = pagesRoot.nextSibling.firstChild;
- href = Math.max(pageNum - 1, 0);
- prev.href = href === 0 ? './' : href;
+ href = Math.max(pageNum - 1, 1);
+ prev.href = href === 1 ? './' : href;
prev.firstChild.disabled = href === pageNum;
href = Math.min(pageNum + 1, maxPageNum);
- next.href = href === 0 ? './' : href;
+ next.href = href === 1 ? './' : href;
next.firstChild.disabled = href === pageNum;
if (strong = $('strong', pagesRoot)) {
if (+strong.textContent === pageNum) {
@@ -5335,7 +5345,7 @@
} else {
strong = $.el('strong');
}
- if (!(a = pagesRoot.children[pageNum])) {
+ if (!(a = pagesRoot.children[pageNum - 1])) {
return;
}
$.before(a, strong);
@@ -5377,7 +5387,7 @@
if (!(d.readyState === 'loading' || Index.root.parentElement)) {
$.replace($('.board'), Index.root);
}
- Index.currentPage = 0;
+ Index.currentPage = 1;
if ((_ref = Index.req) != null) {
_ref.abort();
}
@@ -5438,7 +5448,7 @@
}
Navigate.title();
try {
- pageNum || (pageNum = 0);
+ pageNum || (pageNum = 1);
if (req.status === 200) {
Index.parse(req.response, pageNum);
} else if (req.status === 304) {
@@ -5504,7 +5514,7 @@
var err, thread, threadRoot;
threadRoot = Build.thread(g.BOARD, threadData);
if (thread = g.BOARD.threads[threadData.no]) {
- thread.setPage(Math.floor(i / Index.threadsNumPerPage));
+ thread.setPage(Math.floor(i / Index.threadsNumPerPage) + 1);
thread.setCount('post', threadData.replies + 1, threadData.bumplimit);
thread.setCount('file', threadData.images + !!threadData.ext, threadData.imagelimit);
thread.setStatus('Sticky', !!threadData.sticky);
@@ -5699,7 +5709,7 @@
switch (Conf['Index Mode']) {
case 'paged':
case 'infinite':
- pageNum = Index.getCurrentPage();
+ pageNum = Index.getCurrentPage() - 1;
threadsPerPage = Index.getThreadsNumPerPage();
threads = [];
i = threadsPerPage * pageNum;
@@ -5740,7 +5750,7 @@
if (!Index.searchInput.dataset.searching) {
Index.searchInput.dataset.searching = 1;
Index.pageBeforeSearch = Index.getCurrentPage();
- Index.setPage(pageNum = 0);
+ Index.setPage(pageNum = 1);
} else {
if (Conf['Index Mode'] !== 'infinite') {
pageNum = Index.getCurrentPage();
@@ -5827,6 +5837,17 @@
return n = (n + 1) % 3;
};
})(),
+ path: function(boardID, threadID, postID, fragment) {
+ var path;
+ path = "/" + boardID + "/thread/" + threadID;
+ if ((g.SLUG != null) && threadID === g.THREADID) {
+ path += "/" + g.SLUG;
+ }
+ if (postID) {
+ path += "#" + (fragment || 'p') + postID;
+ }
+ return path;
+ },
postFromObject: function(data, boardID) {
var o;
o = {
@@ -5941,28 +5962,26 @@
sticky = isSticky ? "
" : '';
closed = isClosed ? "
" : '';
if (isOP && g.VIEW === 'index') {
- pageNum = Math.floor(Index.liveThreadData.keys.indexOf("" + postID) / Index.threadsNumPerPage);
+ pageNum = Math.floor(Index.liveThreadData.keys.indexOf("" + postID) / Index.threadsNumPerPage) + 1;
pageIcon = " Page " + pageNum + "";
- replyLink = " [Reply]";
+ replyLink = " [Reply]";
} else {
- pageIcon = replyLink = '';
+ pageIcon = '';
+ replyLink = '';
}
container = $.el('div', {
id: "pc" + postID,
className: "postContainer " + (isOP ? 'op' : 'reply') + "Container",
- innerHTML: (isOP ? '' : ">>
") + ("") + (isOP ? fileHTML : '') + "
" + ("
") + ("
" + (subject || '') + " ") + ("
") + emailStart + ("" + (name || '') + "") + tripcode + capcodeStart + emailEnd + capcodeIcon + userID + flag + ' ' + ("
" + date + " ") + "
" + ("No.") + ("" + postID + "") + pageIcon + sticky + closed + replyLink + '' + '
' + (isOP ? '' : fileHTML) + ("
" + (comment || '') + "
") + '
'
+ innerHTML: (isOP ? '' : ">>
") + ("") + (isOP ? fileHTML : '') + "
" + ("
") + ("
" + (subject || '') + " ") + ("
") + emailStart + ("" + (name || '') + "") + tripcode + capcodeStart + emailEnd + capcodeIcon + userID + flag + ' ' + ("
" + date + " ") + "
" + ("No.") + ("" + postID + "") + pageIcon + sticky + closed + replyLink + '' + '
' + (isOP ? '' : fileHTML) + ("
" + (comment || '') + "
") + '
'
});
_ref = $$('.quotelink', container);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
quote = _ref[_i];
href = quote.getAttribute('href');
- if (href[0] === '/') {
+ if (href[0] !== '#') {
continue;
}
- if (href[0] === '#') {
- href = "" + threadID + href;
- }
- quote.href = "/" + boardID + "/thread/" + href;
+ quote.href = Build.path(boardID, threadID, href.slice(2));
}
return container;
},
@@ -5977,7 +5996,7 @@
return $.el('a', {
className: 'summary',
textContent: text.join(' '),
- href: "/" + boardID + "/thread/" + threadID
+ href: Build.path(boardID, threadID)
});
},
thread: function(board, data, full) {
@@ -6013,12 +6032,12 @@
data = Index.liveThreadData[thread.ID];
postCount = data.replies + 1;
fileCount = data.images + !!data.ext;
- pageCount = Math.floor(Index.liveThreadData.keys.indexOf("" + thread.ID) / Index.threadsNumPerPage);
+ pageCount = Math.floor(Index.liveThreadData.keys.indexOf("" + thread.ID) / Index.threadsNumPerPage) + 1;
subject = thread.OP.info.subject ? "" + thread.OP.info.subject + "
" : '';
comment = thread.OP.nodes.comment.innerHTML.replace(/(
\s*){2,}/g, '
');
root = $.el('div', {
className: 'catalog-thread',
- innerHTML: "" + postCount + " / " + fileCount + " / " + pageCount + "
" + subject + ""
+ innerHTML: "" + postCount + " / " + fileCount + " / " + pageCount + "
" + subject + ""
});
root.dataset.fullID = thread.fullID;
if (thread.isPinned) {
@@ -7482,7 +7501,7 @@
var a, frag, hash, text;
frag = QuoteBacklink.frag.cloneNode(true);
a = frag.lastElementChild;
- a.href = "/" + quoter.board + "/thread/" + quoter.thread + "#p" + quoter;
+ a.href = Build.path(quoter.board.ID, quoter.thread.ID, quoter.ID);
a.textContent = text = QuoteBacklink.funk(quoter.ID);
if (quoter.isDead) {
$.addClass(a, 'deadlink');
@@ -7963,7 +7982,7 @@
}
if (post = posts[post.ID]) {
posts.after(post, posts[this.ID]);
- } else {
+ } else if (posts[this.ID]) {
posts.prepend(posts[this.ID]);
}
return true;
@@ -8054,7 +8073,7 @@
quoteID = "" + boardID + "." + postID;
if (post = g.posts[quoteID]) {
a = $.el('a', {
- href: "/" + boardID + "/thread/" + post.thread + "#p" + postID,
+ href: Build.path(boardID, post.thread.ID, postID),
className: post.isDead ? 'quotelink deadlink' : 'quotelink',
textContent: quote
});
@@ -8331,9 +8350,9 @@
return $.toggleClass(this, 'embedded');
},
embed: function(a) {
- var el, style, type;
+ var el, type;
el = (type = Linkify.types[a.dataset.key]).el(a);
- el.style.cssText = (style = type.style) ? style : "border: 0; width: 640px; height: 390px";
+ el.style.cssText = type.style != null ? type.style : "border: 0; width: 640px; height: 390px";
return el;
},
unembed: function(a) {
@@ -8378,9 +8397,10 @@
{
key: 'audio',
regExp: /(.*\.(mp3|ogg|wav))$/,
+ style: '',
el: function(a) {
return $.el('audio', {
- controls: 'controls',
+ controls: true,
preload: 'auto',
src: a.dataset.uid
});
@@ -8574,10 +8594,12 @@
}, {
key: 'Vocaroo',
regExp: /.*(?:vocaroo.com\/)([^#\&\?]*).*/,
- style: 'border: 0; width: 150px; height: 45px;',
+ style: '',
el: function(a) {
- return $.el('object', {
- innerHTML: ""
+ return $.el('audio', {
+ controls: true,
+ preload: 'auto',
+ src: "http://vocaroo.com/media_command.php?media=" + (a.dataset.uid.replace(/^i\//, '')) + "&command=download_ogg"
});
}
}, {
@@ -8635,6 +8657,7 @@
}, {
key: 'video',
regExp: /(.*\.(ogv|webm|mp4))$/,
+ style: 'border: 0; width: auto; height: auto;',
el: function(a) {
return $.el('video', {
controls: 'controls',
@@ -8647,6 +8670,7 @@
};
QR = {
+ mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'],
init: function() {
var con, sc;
this.db = new DataBoard('yourPosts');
@@ -8740,7 +8764,7 @@
})) {
$.addClass(this.nodes.root, 'your-post');
}
- return $.on($('a[title="Quote this post"]', this.nodes.info), 'click', QR.quote);
+ return $.on($('a[title="Reply to this post"]', this.nodes.info), 'click', QR.quote);
},
persist: function() {
if (!QR.postingIsEnabled) {
@@ -8794,7 +8818,10 @@
post["delete"]();
}
QR.cooldown.auto = false;
- return QR.status();
+ QR.status();
+ if (QR.captcha.isEnabled && !Conf['Auto-load captcha']) {
+ return QR.captcha.destroy();
+ }
},
focusin: function() {
return $.addClass(QR.nodes.el, 'focus');
@@ -8828,8 +8855,10 @@
el.removeAttribute('style');
}
if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) {
- QR.captcha.nodes.input.focus();
- QR.captcha.setup();
+ if (QR.captcha.captchas.length === 0) {
+ QR.captcha.nodes.input.focus();
+ QR.captcha.setup();
+ }
if (Conf['Captcha Warning Notifications'] && !d.hidden) {
QR.notify(el);
} else {
@@ -8923,7 +8952,7 @@
_ref = $$('br', frag);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
node = _ref[_i];
- if (node !== frag.lastElementChild) {
+ if (node !== frag.lastChild) {
$.replace(node, $.tn('\n>'));
}
}
@@ -9009,29 +9038,18 @@
QR.handleFiles(files);
return $.addClass(QR.nodes.el, 'dump');
},
- handleBlob: function(urlBlob, header, url) {
- var blob, end, endnl, endsc, mime, name, name_end, name_start, start;
- name = url.substr(url.lastIndexOf('/') + 1, url.length);
- start = header.indexOf("Content-Type: ") + 14;
- endsc = header.substr(start, header.length).indexOf(";");
- endnl = header.substr(start, header.length).indexOf("\n") - 1;
- end = endnl;
- if (endsc !== -1 && endsc < endnl) {
- end = endsc;
+ handleBlob: function(urlBlob, contentType, contentDisposition, url) {
+ var blob, match, mime, name, _ref, _ref1, _ref2;
+ name = (_ref = url.match(/([^\/]+)\/*$/)) != null ? _ref[1] : void 0;
+ mime = (contentType != null ? contentType.match(/[^;]*/)[0] : void 0) || 'application/octet-stream';
+ match = (contentDisposition != null ? (_ref1 = contentDisposition.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)) != null ? _ref1[1] : void 0 : void 0) || (contentType != null ? (_ref2 = contentType.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)) != null ? _ref2[1] : void 0 : void 0);
+ if (match) {
+ name = match.replace(/\\"/g, '"');
}
- mime = header.substr(start, end);
blob = new Blob([urlBlob], {
type: mime
});
- blob.name = url.substr(url.lastIndexOf('/') + 1, url.length);
- name_start = header.indexOf('name="') + 6;
- if (name_start - 6 !== -1) {
- name_end = header.substr(name_start, header.length).indexOf('"');
- blob.name = header.substr(name_start, name_end);
- }
- if (blob.type === null) {
- return QR.error("Unsupported file type.");
- }
+ blob.name = name;
return QR.handleFiles([blob]);
},
handleUrl: function() {
@@ -9044,19 +9062,22 @@
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
+ var contentDisposition, contentType;
if (this.readyState === this.DONE && xhr.status === 200) {
- QR.handleBlob(this.response, this.getResponseHeader('Content-Type'), url);
+ contentType = this.getResponseHeader('Content-Type');
+ contentDisposition = this.getResponseHeader('Content-Disposition');
+ return QR.handleBlob(this.response, contentType, contentDisposition, url);
} else {
- QR.error("Can't load image.");
+ return QR.error("Can't load image.");
}
};
xhr.onerror = function(e) {
- QR.error("Can't load image.");
+ return QR.error("Can't load image.");
};
- xhr.send();
+ return xhr.send();
},
handleFiles: function(files) {
- var file, isSingle, max, _i, _len;
+ var file, i, _i, _len;
if (this !== QR) {
files = __slice.call(this.files);
this.value = null;
@@ -9064,52 +9085,46 @@
if (!files.length) {
return;
}
- max = QR.nodes.fileInput.max;
- isSingle = files.length === 1;
QR.cleanNotifications();
- for (_i = 0, _len = files.length; _i < _len; _i++) {
- file = files[_i];
- if (file.type === 'application/x-shockwave-flash') {
- QR.handleFile(file, isSingle, max);
- } else {
- QR.checkDimensions(file, isSingle, max);
- }
+ for (i = _i = 0, _len = files.length; _i < _len; i = ++_i) {
+ file = files[i];
+ QR.handleFile(file, i, files.length);
}
- if (!isSingle) {
+ if (files.length !== 1) {
return $.addClass(QR.nodes.el, 'dump');
}
},
- checkDimensions: function(file, isSingle, max) {
- var img;
- if (/^image\//.test(file.type)) {
- img = new Image();
- img.onload = (function(_this) {
- return function() {
- var height, width;
- height = img.height, width = img.width;
- if (height > QR.max_heigth || width > QR.max_heigth) {
- return QR.error("" + file.name + ": Image too large (image: " + img.height + "x" + img.width + "px, max: " + QR.max_heigth + "x" + QR.max_width + "px)");
- }
- if (height < QR.min_heigth || width < QR.min_heigth) {
- return QR.error("" + file.name + ": Image too small (image: " + img.height + "x" + img.width + "px, min: " + QR.min_heigth + "x" + QR.min_width + "px)");
- }
- return QR.handleFile(file, isSingle, max);
- };
- })(this);
- return img.src = URL.createObjectURL(file);
- } else {
- return QR.handleFile(file, isSingle, max);
+ handleFile: function(file, index, nfiles) {
+ var isSingle, max, post, _ref;
+ isSingle = nfiles === 1;
+ if (/^text\//.test(file.type)) {
+ if (isSingle) {
+ post = QR.selected;
+ } else if (index !== 0 || (post = QR.posts[QR.posts.length - 1]).com) {
+ post = new QR.post();
+ }
+ post.pasteText(file);
+ return;
+ }
+ if (_ref = file.type, __indexOf.call(QR.mimeTypes, _ref) < 0) {
+ QR.error("" + file.name + ": Unsupported file type.");
+ if (!isSingle) {
+ return;
+ }
+ }
+ max = QR.nodes.fileInput.max;
+ if (/^video\//.test(file.type)) {
+ max = Math.min(max, QR.max_size_video);
}
- },
- handleFile: function(file, isSingle, max) {
- var post;
if (file.size > max) {
QR.error("" + file.name + ": File too large (file: " + ($.bytesToString(file.size)) + ", max: " + ($.bytesToString(max)) + ").");
- return;
+ if (!isSingle) {
+ return;
+ }
}
if (isSingle) {
post = QR.selected;
- } else if ((post = QR.posts[QR.posts.length - 1]).file) {
+ } else if (index !== 0 || (post = QR.posts[QR.posts.length - 1]).file) {
post = new QR.post();
}
if (/^text/.test(file.type)) {
@@ -9163,7 +9178,7 @@
}
},
dialog: function() {
- var dialog, elm, event, i, items, name, node, nodes, rules, save, setNode, _, _ref, _ref1;
+ var dialog, elm, event, i, items, name, node, nodes, prop, rules, save, setNode, _, _i, _len, _ref, _ref1, _ref2;
QR.nodes = nodes = {
el: dialog = UI.dialog('qr', 'top:0;right:0;', "")
};
@@ -9196,15 +9211,23 @@
setNode('status', '[type=submit]');
setNode('fileInput', '[type=file]');
rules = $('ul.rules').textContent.trim();
- QR.min_width = QR.min_heigth = 1;
- QR.max_width = QR.max_heigth = 5000;
+ QR.min_width = QR.min_height = 1;
+ QR.max_width = QR.max_height = 10000;
try {
- _ref = rules.match(/.+smaller than (\d+)x(\d+).+/), _ = _ref[0], QR.min_width = _ref[1], QR.min_heigth = _ref[2];
- _ref1 = rules.match(/.+greater than (\d+)x(\d+).+/), _ = _ref1[0], QR.max_width = _ref1[1], QR.max_heigth = _ref1[2];
+ _ref = rules.match(/.+smaller than (\d+)x(\d+).+/), _ = _ref[0], QR.min_width = _ref[1], QR.min_height = _ref[2];
+ _ref1 = rules.match(/.+greater than (\d+)x(\d+).+/), _ = _ref1[0], QR.max_width = _ref1[1], QR.max_height = _ref1[2];
+ _ref2 = ['min_width', 'min_height', 'max_width', 'max_height'];
+ for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
+ prop = _ref2[_i];
+ QR[prop] = parseInt(QR[prop], 10);
+ }
} catch (_error) {
null;
}
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value;
+ QR.max_size_video = 3145728;
+ QR.max_width_video = QR.max_height_video = 2048;
+ QR.max_duration_video = 120;
QR.spoiler = !!$('input[name=spoiler]');
if (QR.spoiler) {
$.addClass(QR.nodes.el, 'has-spoiler');
@@ -9421,10 +9444,6 @@
onload: QR.response,
onerror: function() {
delete QR.req;
- if (QR.captcha.isEnabled) {
- QR.captcha.destroy();
- QR.captcha.setup();
- }
post.unlock();
QR.cooldown.auto = false;
QR.status();
@@ -9454,12 +9473,9 @@
return QR.status();
},
response: function() {
- var URL, ban, board, err, h1, isReply, m, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1;
+ var URL, ban, board, captchasCount, err, h1, isReply, m, notif, post, postID, postsCount, req, resDoc, threadID, _, _ref, _ref1;
req = QR.req;
delete QR.req;
- if (QR.captcha.isEnabled) {
- QR.captcha.destroy();
- }
post = QR.posts[0];
post.unlock();
resDoc = req.response;
@@ -9484,12 +9500,12 @@
} else if (/expired/i.test(err.textContent)) {
err = 'This CAPTCHA is no longer valid because it has expired.';
}
- QR.cooldown.auto = false;
+ QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : err === 'Connection error with sys.4chan.org.' ? true : false;
QR.cooldown.set({
delay: 2
});
} else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) {
- QR.cooldown.auto = !QR.captcha.isEnabled;
+ QR.cooldown.auto = QR.captcha.isEnabled ? !!QR.captcha.captchas.length : true;
QR.cooldown.set({
delay: m[1]
});
@@ -9531,13 +9547,26 @@
});
postsCount = QR.posts.length - 1;
QR.cooldown.auto = postsCount && isReply;
- if (QR.captcha.isEnabled && QR.cooldown.auto) {
- QR.captcha.setup();
+ if (QR.cooldown.auto && QR.captcha.isEnabled && (captchasCount = QR.captcha.captchas.length) < 3 && captchasCount < postsCount) {
+ notif = new Notification('Quick reply warning', {
+ body: "You are running low on cached captchas. Cache count: " + captchasCount + ".",
+ icon: Favicon.logo
+ });
+ notif.onclick = function() {
+ QR.open();
+ QR.captcha.nodes.input.focus();
+ return window.focus();
+ };
+ notif.onshow = function() {
+ return setTimeout(function() {
+ return notif.close();
+ }, 7 * $.SECOND);
+ };
}
if (!(Conf['Persistent QR'] || QR.cooldown.auto)) {
QR.close();
} else {
- if (QR.posts.length > 1) {
+ if (QR.posts.length > 1 && QR.captcha.isEnabled && QR.captcha.captchas.length === 0) {
QR.captcha.setup();
}
post.rm();
@@ -9548,7 +9577,7 @@
isReply: isReply,
threadID: threadID
});
- URL = !isReply ? "/" + g.BOARD + "/res/" + threadID : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? "/" + g.BOARD + "/res/" + threadID + "#p" + postID : void 0;
+ URL = threadID === postID ? Build.path(g.BOARD.ID, threadID) : g.VIEW === 'index' && !QR.cooldown.auto && Conf['Open Post in New Tab'] ? Build.path(g.BOARD.ID, threadID, postID) : void 0;
if (URL) {
if (Conf['Open Post in New Tab']) {
$.open(URL);
@@ -9617,8 +9646,21 @@
};
$.on(input, 'blur', QR.focusout);
$.on(input, 'focus', QR.focusin);
+ $.on(input, 'keydown', QR.captcha.keydown.bind(QR.captcha));
+ $.on(this.nodes.img.parentNode, 'click', QR.captcha.reload.bind(QR.captcha));
$.addClass(QR.nodes.el, 'has-captcha');
$.after(QR.nodes.com.parentNode, [imgContainer, input]);
+ this.captchas = [];
+ $.get('captchas', [], function(_arg) {
+ var captchas;
+ captchas = _arg.captchas;
+ QR.captcha.sync(captchas);
+ return QR.captcha.clear();
+ });
+ $.sync('captchas', this.sync);
+ new MutationObserver(this.afterSetup).observe($.id('captchaContainer'), {
+ childList: true
+ });
this.beforeSetup();
return this.afterSetup();
},
@@ -9628,28 +9670,31 @@
img.parentNode.parentNode.hidden = true;
input.value = '';
input.placeholder = 'Focus to load reCAPTCHA';
- $.on(input, 'focus', this.setup);
- this.setupObserver = new MutationObserver(this.afterSetup);
- return this.setupObserver.observe($.id('captchaContainer'), {
- childList: true
- });
+ this.count();
+ return $.on(input, 'focus', this.setup);
},
setup: function() {
return $.globalEval('loadRecaptcha()');
},
afterSetup: function() {
- var challenge, img, input, _ref;
+ var challenge, img, input, setLifetime, _ref;
if (!(challenge = $.id('recaptcha_challenge_field_holder'))) {
return;
}
- QR.captcha.setupObserver.disconnect();
- delete QR.captcha.setupObserver;
+ if (challenge === QR.captcha.nodes.challenge) {
+ return;
+ }
+ setLifetime = function(e) {
+ return QR.captcha.lifetime = e.detail;
+ };
+ $.on(window, 'captcha:timeout', setLifetime);
+ $.globalEval('window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))');
+ $.off(window, 'captcha:timeout', setLifetime);
_ref = QR.captcha.nodes, img = _ref.img, input = _ref.input;
img.parentNode.parentNode.hidden = false;
input.placeholder = 'Verification';
+ QR.captcha.count();
$.off(input, 'focus', QR.captcha.setup);
- $.on(input, 'keydown', QR.captcha.keydown.bind(QR.captcha));
- $.on(img.parentNode, 'click', QR.captcha.reload.bind(QR.captcha));
QR.captcha.nodes.challenge = challenge;
new MutationObserver(QR.captcha.load.bind(QR.captcha)).observe(challenge, {
childList: true,
@@ -9662,30 +9707,112 @@
$.globalEval('Recaptcha.destroy()');
return this.beforeSetup();
},
+ sync: function(captchas) {
+ QR.captcha.captchas = captchas;
+ return QR.captcha.count();
+ },
getOne: function() {
- var challenge, response;
- challenge = this.nodes.img.alt;
- response = this.nodes.input.value.trim();
- if (response && !/\s/.test(response)) {
- response = "" + response + " " + response;
+ var captcha, challenge, response;
+ this.clear();
+ if (captcha = this.captchas.shift()) {
+ challenge = captcha.challenge, response = captcha.response;
+ this.count();
+ $.set('captchas', this.captchas);
+ } else {
+ challenge = this.nodes.img.alt;
+ if (response = this.nodes.input.value) {
+ if (Conf['Auto-load captcha']) {
+ this.reload();
+ } else {
+ this.destroy();
+ }
+ }
+ }
+ if (response) {
+ response = response.trim();
+ if (!/\s/.test(response)) {
+ response = "" + response + " " + response;
+ }
}
return {
challenge: challenge,
response: response
};
},
+ save: function() {
+ var response;
+ if (!(response = this.nodes.input.value.trim())) {
+ return;
+ }
+ this.nodes.input.value = '';
+ this.captchas.push({
+ challenge: this.nodes.img.alt,
+ response: response,
+ timeout: this.timeout
+ });
+ this.count();
+ this.reload();
+ return $.set('captchas', this.captchas);
+ },
+ clear: function() {
+ var captcha, i, now, _i, _len, _ref;
+ if (!this.captchas.length) {
+ return;
+ }
+ now = Date.now();
+ _ref = this.captchas;
+ for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
+ captcha = _ref[i];
+ if (captcha.timeout > now) {
+ break;
+ }
+ }
+ if (!i) {
+ return;
+ }
+ this.captchas = this.captchas.slice(i);
+ this.count();
+ return $.set('captchas', this.captchas);
+ },
load: function() {
- var challenge;
+ var challenge, challenge_image;
if (!this.nodes.challenge.firstChild) {
return;
}
+ if (!(challenge_image = $.id('recaptcha_challenge_image'))) {
+ return;
+ }
+ this.timeout = Date.now() + this.lifetime * $.SECOND - $.MINUTE;
challenge = this.nodes.challenge.firstChild.value;
this.nodes.img.alt = challenge;
- this.nodes.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge;
- return this.nodes.input.value = null;
+ this.nodes.img.src = challenge_image.src;
+ this.nodes.input.value = null;
+ return this.clear();
+ },
+ count: function() {
+ var count, placeholder;
+ count = this.captchas ? this.captchas.length : 0;
+ placeholder = this.nodes.input.placeholder.replace(/\ \(.*\)$/, '');
+ placeholder += (function() {
+ switch (count) {
+ case 0:
+ if (placeholder === 'Verification') {
+ return ' (Shift + Enter to cache)';
+ } else {
+ return '';
+ }
+ break;
+ case 1:
+ return ' (1 cached captcha)';
+ default:
+ return " (" + count + " cached captchas)";
+ }
+ })();
+ this.nodes.input.placeholder = placeholder;
+ return this.nodes.input.alt = count;
},
reload: function(focus) {
- $.globalEval('Recaptcha.reload("t")');
+ $.globalEval('Recaptcha.reload(); Recaptcha.should_focus = false;');
if (focus) {
return this.nodes.input.focus();
}
@@ -9693,6 +9820,8 @@
keydown: function(e) {
if (e.keyCode === 8 && !this.nodes.input.value) {
this.reload();
+ } else if (e.keyCode === 13 && e.shiftKey) {
+ this.save();
} else {
return;
}
@@ -10018,9 +10147,6 @@
node.disabled = lock;
}
}
- if (QR.captcha.isEnabled) {
- QR.captcha.nodes.input.disabled = lock;
- }
this.nodes.rm.style.visibility = lock ? 'hidden' : '';
(lock ? $.off : $.on)(QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput);
this.nodes.spoiler.disabled = lock;
@@ -10120,7 +10246,7 @@
} else {
this.updateFilename();
}
- if (!/^image/.test(file.type)) {
+ if (!/^(image|video)\//.test(file.type)) {
this.nodes.el.style.backgroundImage = null;
return;
}
@@ -10128,20 +10254,35 @@
};
_Class.prototype.setThumbnail = function() {
- var fileURL, img;
- img = $.el('img');
- img.onload = (function(_this) {
+ var el, fileURL, isVideo;
+ isVideo = /^video\//.test(this.file.type);
+ el = $.el((isVideo ? 'video' : 'img'));
+ $.on(el, (isVideo ? 'loadeddata' : 'load'), (function(_this) {
return function() {
- var cv, height, s, width;
+ var cv, error, errors, height, s, width, _i, _len;
+ errors = _this.checkDimensions(el, isVideo);
+ if (errors.length) {
+ for (_i = 0, _len = errors.length; _i < _len; _i++) {
+ error = errors[_i];
+ QR.error(error);
+ }
+ _this.URL = fileURL;
+ return _this.rmFile();
+ }
s = 90 * 2 * window.devicePixelRatio;
if (_this.file.type === 'image/gif') {
s *= 3;
}
- height = img.height, width = img.width;
- if (height < s || width < s) {
- _this.URL = fileURL;
- _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")";
- return;
+ if (isVideo) {
+ height = el.videoHeight;
+ width = el.videoWidth;
+ } else {
+ height = el.height, width = el.width;
+ if (height < s || width < s) {
+ _this.URL = fileURL;
+ _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")";
+ return;
+ }
}
if (height <= width) {
width = s / height * width;
@@ -10151,18 +10292,49 @@
width = s;
}
cv = $.el('canvas');
- cv.height = img.height = height;
- cv.width = img.width = width;
- cv.getContext('2d').drawImage(img, 0, 0, width, height);
+ cv.height = el.height = height;
+ cv.width = el.width = width;
+ cv.getContext('2d').drawImage(el, 0, 0, width, height);
URL.revokeObjectURL(fileURL);
return cv.toBlob(function(blob) {
_this.URL = URL.createObjectURL(blob);
return _this.nodes.el.style.backgroundImage = "url(" + _this.URL + ")";
});
};
- })(this);
+ })(this));
fileURL = URL.createObjectURL(this.file);
- return img.src = fileURL;
+ return el.src = fileURL;
+ };
+
+ _Class.prototype.checkDimensions = function(el, video) {
+ var duration, err, height, max_height, max_width, videoHeight, videoWidth, width;
+ err = [];
+ if (video) {
+ videoHeight = el.videoHeight, videoWidth = el.videoWidth, duration = el.duration;
+ max_height = QR.max_height < QR.max_height_video ? QR.max_height : QR.max_height_video;
+ max_width = QR.max_width < QR.max_width_video ? QR.max_width : QR.max_width_video;
+ if (videoHeight > max_height || videoWidth > max_width) {
+ err.push("" + this.file.name + ": Video too large (video: " + videoHeight + "x" + videoWidth + "px, max: " + max_height + "x" + max_width + "px)");
+ }
+ if (videoHeight < QR.min_height || videoWidth < QR.min_width) {
+ err.push("" + this.file.name + ": Video too small (video: " + videoHeight + "x" + videoWidth + "px, min: " + QR.min_height + "x" + QR.min_width + "px)");
+ }
+ if (!isFinite(el.duration)) {
+ err.push("" + file.name + ": Video lacks duration metadata (try remuxing)");
+ }
+ if (duration > QR.max_duration_video) {
+ err.push("" + this.file.name + ": Video too long (video: " + duration + "s, max: " + QR.max_duration_video + "s)");
+ }
+ } else {
+ height = el.height, width = el.width;
+ if (height > QR.max_height || width > QR.max_width) {
+ err.push("" + this.file.name + ": Image too large (image: " + height + "x" + width + "px, max: " + QR.max_height + "x" + QR.max_width + "px)");
+ }
+ if (height < QR.min_height || width < QR.min_width) {
+ err.push("" + this.file.name + ": Image too small (image: " + height + "x" + width + "px, min: " + QR.min_height + "x" + QR.min_width + "px)");
+ }
+ }
+ return err;
};
_Class.prototype.rmFile = function() {
@@ -10524,7 +10696,7 @@
if (src[2] === 'i.4cdn.org') {
URL = Redirect.to('file', {
boardID: src[3],
- filename: src[5]
+ filename: src[src.length - 1]
});
if (URL) {
thumb.href = URL;
@@ -10538,7 +10710,7 @@
return;
}
}
- return $.ajax("//a.4cdn.org/" + post.board + "/res/" + post.thread + ".json", {
+ return $.ajax("//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", {
onload: function() {
var i, postObj, posts;
if (this.status !== 200) {
@@ -11816,6 +11988,7 @@
delete this.postCountEl;
delete this.fileCountEl;
delete this.pageCountEl;
+ delete this.dialog;
Thread.callbacks.disconnect('Thread Stats');
return $.off(d, 'ThreadUpdate', ThreadStats.onUpdate);
},
@@ -11866,7 +12039,7 @@
continue;
}
ThreadStats.pageCountEl.textContent = page.page;
- (page.page === this.response.length - 1 ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning');
+ (page.page === this.response.length ? $.addClass : $.rmClass)(ThreadStats.pageCountEl, 'warning');
return;
}
}
@@ -12992,7 +13165,7 @@
threadID: data.thread.ID,
postID: ID
})) {
- QuoteYou.lastRead = data.nodes.root;
+ QuoteMarkers.lastRead = data.nodes.root;
}
}
if (!ID) {
@@ -13086,7 +13259,7 @@
}
return Redirect.data = o;
},
- archives: [{"uid":0,"name":"Foolz","domain":"archive.foolz.us","http":true,"https":true,"software":"foolfuuka","boards":["a","biz","co","diy","gd","jp","m","sci","sp","tg","tv","v","vg","vp","vr","wsg"],"files":["a","biz","gd","diy","jp","m","sci","tg","vg","vp","vr","wsg"]},{"uid":1,"name":"NSFW Foolz","domain":"nsfw.foolz.us","http":true,"https":true,"software":"foolfuuka","boards":["u"],"files":["u"]},{"uid":2,"name":"The Dark Cave","domain":"archive.thedarkcave.org","http":true,"https":true,"software":"foolfuuka","boards":["c","int","out","po"],"files":["c","po"]},{"uid":3,"name":"4plebs Archive","domain":"archive.4plebs.org","http":true,"https":true,"software":"foolfuuka","boards":["adv","hr","o","pol","s4s","tg","trv","tv","x"],"files":["adv","hr","o","pol","s4s","tg","trv","tv","x"]},{"uid":18,"name":"4plebs Flash Archive","domain":"flash.4plebs.org","http":true,"https":true,"software":"foolfuuka","boards":["f"],"files":["f"]},{"uid":4,"name":"Nyafuu","domain":"archive.nyafuu.org","http":true,"https":true,"software":"foolfuuka","boards":["c","e","w","wg"],"files":["c","e","w","wg"]},{"uid":5,"name":"Love is Over","domain":"loveisover.me","http":true,"https":true,"software":"foolfuuka","boards":["d","i"],"files":["d","i"]},{"uid":8,"name":"Rebecca Black Tech","domain":"rbt.asia","http":true,"https":true,"software":"fuuka","boards":["cgl","g","mu","w"],"files":["cgl","g","mu","w"]},{"uid":9,"name":"Heinessen","domain":"archive.heinessen.com","http":true,"https":false,"software":"fuuka","boards":["an","fit","k","mlp","r9k","toy"],"files":["an","fit","k","r9k","toy"]},{"uid":10,"name":"warosu","domain":"fuuka.warosu.org","http":false,"https":true,"software":"fuuka","boards":["3","biz","cgl","ck","diy","fa","g","ic","jp","lit","sci","tg","vr"],"files":["3","biz","cgl","ck","diy","fa","ic","jp","lit","sci","tg","vr"]},{"uid":15,"name":"fgts","domain":"fgts.eu","http":true,"https":true,"software":"foolfuuka","boards":["cm","h","hc","hm","r","s","soc","y"],"files":["cm","h","hc","hm","r","s","soc","y"]},{"uid":16,"name":"maware","domain":"archive.mawa.re","http":true,"https":false,"software":"foolfuuka","boards":["t"],"files":["t"]},{"uid":17,"name":"installgentoo.com","domain":"chan.installgentoo.com","http":true,"https":false,"software":"foolfuuka","boards":["g","t"],"files":["g","t"]},{"uid":13,"name":"Foolz Beta","domain":"beta.foolz.us","http":true,"https":true,"withCredentials":true,"software":"foolfuuka","boards":["a","biz","co","d","diy","gd","jp","m","s4s","sci","sp","tg","tv","u","v","vg","vp","vr","wsg"],"files":["a","biz","d","diy","gd","jp","m","s4s","sci","tg","u","vg","vp","vr","wsg"]}],
+ archives: [{"uid":0,"name":"Foolz","domain":"archive.foolz.us","http":true,"https":true,"software":"foolfuuka","boards":["a","biz","co","diy","gd","jp","m","sci","sp","tg","tv","vg","vp","vr","wsg"],"files":["a","biz","gd","diy","jp","m","sci","tg","vg","vp","vr","wsg"]},{"uid":1,"name":"NSFW Foolz","domain":"nsfw.foolz.us","http":true,"https":true,"software":"foolfuuka","boards":["u"],"files":["u"]},{"uid":2,"name":"The Dark Cave","domain":"archive.thedarkcave.org","http":true,"https":true,"software":"foolfuuka","boards":["c","int","out","po"],"files":["c","po"]},{"uid":3,"name":"4plebs Archive","domain":"archive.4plebs.org","http":true,"https":true,"software":"foolfuuka","boards":["adv","hr","o","pol","s4s","tg","trv","tv","x"],"files":["adv","hr","o","pol","s4s","tg","trv","tv","x"]},{"uid":18,"name":"4plebs Flash Archive","domain":"flash.4plebs.org","http":true,"https":true,"software":"foolfuuka","boards":["f"],"files":["f"]},{"uid":4,"name":"Nyafuu","domain":"archive.nyafuu.org","http":true,"https":true,"software":"foolfuuka","boards":["c","e","w","wg"],"files":["c","e","w","wg"]},{"uid":5,"name":"Love is Over","domain":"loveisover.me","http":true,"https":true,"software":"foolfuuka","boards":["d","i"],"files":["d","i"]},{"uid":8,"name":"Rebecca Black Tech","domain":"rbt.asia","http":true,"https":true,"software":"fuuka","boards":["cgl","g","mu","w"],"files":["cgl","g","mu","w"]},{"uid":9,"name":"Heinessen","domain":"archive.heinessen.com","http":true,"https":false,"software":"fuuka","boards":["an","fit","k","mlp","r9k","toy"],"files":["an","fit","k","r9k","toy"]},{"uid":10,"name":"warosu","domain":"fuuka.warosu.org","http":false,"https":true,"software":"fuuka","boards":["3","biz","cgl","ck","diy","fa","g","ic","jp","lit","sci","tg","vr"],"files":["3","biz","cgl","ck","diy","fa","ic","jp","lit","sci","tg","vr"]},{"uid":15,"name":"fgts","domain":"fgts.eu","http":true,"https":true,"software":"foolfuuka","boards":["asp","cm","h","hc","hm","n","p","r","s","soc","y"],"files":["asp","cm","h","hc","hm","n","p","r","s","soc","y"]},{"uid":16,"name":"maware","domain":"archive.mawa.re","http":true,"https":false,"software":"foolfuuka","boards":["t"],"files":["t"]},{"uid":17,"name":"installgentoo.com","domain":"chan.installgentoo.com","http":true,"https":false,"software":"foolfuuka","boards":["g","t"],"files":["g","t"]},{"uid":13,"name":"Foolz Beta","domain":"beta.foolz.us","http":true,"https":true,"withCredentials":true,"software":"foolfuuka","boards":["a","biz","co","d","diy","gd","jp","m","s4s","sci","sp","tg","tv","u","vg","vp","vr","wsg"],"files":["a","biz","d","diy","gd","jp","m","s4s","sci","tg","u","vg","vp","vr","wsg"]}],
to: function(dest, data) {
var archive;
archive = (dest === 'search' || dest === 'board' ? Redirect.data.thread : Redirect.data[dest])[data.boardID];
@@ -13150,7 +13323,7 @@
"http": true,
"https": true,
"software": "foolfuuka",
- "boards": ["a", "biz", "co", "diy", "gd", "jp", "m", "sci", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
+ "boards": ["a", "biz", "co", "diy", "gd", "jp", "m", "sci", "sp", "tg", "tv", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "gd", "diy", "jp", "m", "sci", "tg", "vg", "vp", "vr", "wsg"]
}, {
"uid": 1,
@@ -13240,8 +13413,8 @@
"http": true,
"https": true,
"software": "foolfuuka",
- "boards": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"],
- "files": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"]
+ "boards": ["asp", "cm", "h", "hc", "hm", "n", "p", "r", "s", "soc", "y"],
+ "files": ["asp", "cm", "h", "hc", "hm", "n", "p", "r", "s", "soc", "y"]
}, {
"uid": 16,
"name": "maware",
@@ -13268,7 +13441,7 @@
"https": true,
"withCredentials": true,
"software": "foolfuuka",
- "boards": ["a", "biz", "co", "d", "diy", "gd", "jp", "m", "s4s", "sci", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
+ "boards": ["a", "biz", "co", "d", "diy", "gd", "jp", "m", "s4s", "sci", "sp", "tg", "tv", "u", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "d", "diy", "gd", "jp", "m", "s4s", "sci", "tg", "u", "vg", "vp", "vr", "wsg"]
}
];
@@ -13279,7 +13452,7 @@
return d.body;
}), function() {
return $.asap((function() {
- return $('.abovePostForm');
+ return $('hr');
}), Banner.ready);
});
},
@@ -13299,7 +13472,7 @@
alt: '4chan',
title: 'Click to change'
});
- $.on(img, 'click', Banner.cb.toggle);
+ $.on(img, 'click error', Banner.cb.toggle);
Banner.cb.toggle.call(img);
$.prepend(banner, img);
continue;
@@ -15109,7 +15282,7 @@
return;
}
a.textContent = "Post No." + post + " Loading...";
- return $.cache("//api.4chan.org" + a.pathname + ".json", function() {
+ return $.cache("//a.4cdn.org" + (a.pathname.split('/').splice(0, 4).join('/')) + ".json", function() {
return ExpandComment.parse(this, a, post);
});
},
@@ -15130,7 +15303,7 @@
a.textContent = "Error " + req.statusText + " (" + status + ")";
return;
}
- posts = JSON.parse(req.response).posts;
+ posts = req.response.posts;
if (spoilerRange = posts[0].custom_spoiler) {
Build.spoilerRange[g.BOARD] = spoilerRange;
}
@@ -15154,7 +15327,11 @@
if (href[0] === '/') {
continue;
}
- quote.href = "/" + post.board + "/res/" + href;
+ if (href[0] === '#') {
+ quote.href = "" + (a.pathname.split('/').splice(0, 4).join('/')) + href;
+ } else {
+ quote.href = "" + (a.pathname.split('/').splice(0, 3).join('/')) + "/" + href;
+ }
}
post.nodes.shortComment = comment;
$.replace(comment, clone);
@@ -15740,7 +15917,7 @@
return Conf[hotkey] = key;
},
keydown: function(e) {
- var form, key, notification, notifications, op, target, thread, threadRoot, _i, _len, _ref;
+ var form, key, notification, notifications, op, searchInput, target, thread, threadRoot, _i, _len, _ref;
if (!(key = Keybinds.keyCode(e))) {
return;
}
@@ -15750,9 +15927,11 @@
return;
}
}
- threadRoot = Nav.getThread();
- if (op = $('.op', threadRoot)) {
- thread = Get.postFromNode(op).thread;
+ if (g.VIEW !== 'catalog') {
+ threadRoot = Nav.getThread();
+ if (op = $('.op', threadRoot)) {
+ thread = Get.postFromNode(op).thread;
+ }
}
switch (key) {
case Conf['Toggle board list']:
@@ -15764,10 +15943,13 @@
Header.toggleBarVisibility();
break;
case Conf['Open empty QR']:
- Keybinds.qr(threadRoot);
+ Keybinds.qr();
break;
case Conf['Open QR']:
- Keybinds.qr(threadRoot, true);
+ if (g.VIEW === 'catalog') {
+ return;
+ }
+ Keybinds.qr(threadRoot);
break;
case Conf['Open settings']:
Settings.open();
@@ -15840,30 +16022,48 @@
}
break;
case Conf['Watch']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
ThreadWatcher.toggle(thread);
break;
case Conf['Expand image']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.img(threadRoot);
break;
case Conf['Expand images']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.img(threadRoot, true);
break;
case Conf['Open Gallery']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Gallery.cb.toggle();
break;
case Conf['fappeTyme']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
FappeTyme.cb.toggle.call({
name: 'fappe'
});
break;
case Conf['werkTyme']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
FappeTyme.cb.toggle.call({
name: 'werk'
});
break;
case Conf['Front page']:
if (Conf['JSON Navigation'] && g.VIEW === 'index') {
- Index.userPageNav(0);
+ Index.userPageNav(1);
} else {
window.location = "/" + g.BOARD + "/";
}
@@ -15900,11 +16100,13 @@
}
break;
case Conf['Search form']:
- if (Conf['JSON Navigation']) {
- Index.searchInput.focus();
- } else {
- $.id('search-btn').click();
+ if (g.VIEW !== 'index') {
+ return;
}
+ searchInput = Conf['JSON Navigation'] ? Index.searchInput : $.id('search-box');
+ Header.scrollToIfNeeded(searchInput);
+ searchInput.click();
+ searchInput.focus();
break;
case Conf['Paged mode']:
if (!(g.VIEW === 'index' && Conf['Index Mode'] !== 'paged')) {
@@ -15956,21 +16158,39 @@
Nav.scroll(-1);
break;
case Conf['Expand thread']:
+ if (g.VIEW !== 'index') {
+ return;
+ }
ExpandThread.toggle(thread);
break;
case Conf['Open thread']:
+ if (g.VIEW !== 'index') {
+ return;
+ }
Keybinds.open(thread);
break;
case Conf['Open thread tab']:
+ if (g.VIEW !== 'index') {
+ return;
+ }
Keybinds.open(thread, true);
break;
case Conf['Next reply']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.hl(+1, threadRoot);
break;
case Conf['Previous reply']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.hl(-1, threadRoot);
break;
case Conf['Deselect reply']:
+ if (g.VIEW === 'catalog') {
+ return;
+ }
Keybinds.hl(0, threadRoot);
break;
case Conf['Hide']:
@@ -16030,12 +16250,12 @@
}
return key;
},
- qr: function(thread, quote) {
- if (!QR.postingIsEnabled) {
+ qr: function(thread) {
+ if (!(Conf['Quick Reply'] && QR.postingIsEnabled)) {
return;
}
QR.open();
- if (quote) {
+ if (thread != null) {
QR.quote.call($('input', $('.post.highlight', thread) || thread));
}
QR.nodes.com.focus();
@@ -16075,7 +16295,7 @@
if (g.VIEW !== 'index') {
return;
}
- url = "/" + thread.board + "/thread/" + thread;
+ url = Build.path(thread.board.ID, thread.ID);
if (tab) {
return $.open(url);
} else {
@@ -16569,10 +16789,14 @@
return g.VIEW = view;
},
updateBoard: function(boardID) {
- var fullBoardList;
+ var current, fullBoardList;
fullBoardList = $('#full-board-list', Header.boardList);
- $.rmClass($('.current', fullBoardList), 'current');
- $.addClass($("a[href*='/" + boardID + "/']", fullBoardList), 'current');
+ if (current = $('.current', fullBoardList)) {
+ $.rmClass(current, 'current');
+ }
+ if (current = $("a[href*='/" + boardID + "/']", fullBoardList)) {
+ $.addClass(current, 'current');
+ }
Header.generateBoardList(Conf['boardnav'].replace(/(\r\n|\n|\r)/g, ' '));
Index.catalogLink.href = "//boards.4chan.org/" + boardID + "/";
QR.flagsInput();
@@ -16698,7 +16922,7 @@
if (threadID) {
view = 'thread';
} else {
- pageNum = +view;
+ pageNum = +view || 1;
view = 'index';
}
path = this.pathname;
@@ -16733,7 +16957,7 @@
return Index.update(pageNum);
}
load = Navigate.load;
- Navigate.req = $.ajax("//a.4cdn.org/" + boardID + "/res/" + threadID + ".json", {
+ Navigate.req = $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", {
onabort: load,
onloadend: load
});
@@ -17929,6 +18153,13 @@
}
if (g.VIEW === 'thread') {
g.THREADID = +pathname[3];
+ if (pathname[4] != null) {
+ g.SLUG = pathname[4];
+ }
+ if (pathname[2] !== 'thread') {
+ pathname[2] = 'thread';
+ history.replaceState(null, '', pathname.slice(0, 4).join('/') + location.hash);
+ }
}
flatten = function(parent, obj) {
var key, val;
@@ -18128,14 +18359,15 @@
return window.open('//sys.4chan.org/auth', 'This will steal your data.', 'left=0,top=0,width=500,height=255,toolbar=0,resizable=0');
});
$.before(styleSelector.previousSibling, [$.tn('['), passLink, $.tn(']\u00A0\u00A0')]);
+ $('link[href*="mobile"', d.head).disabled = true;
}
if (!Conf['JSON Navigation'] || g.VIEW === 'thread') {
Main.initThread();
+ $.add(d.head, $.el('link', {
+ href: "//s.4cdn.org/css/flags.556.css",
+ rel: "stylesheet"
+ }));
}
- $.add(d.head, $.el('link', {
- href: "//s.4cdn.org/css/flags.556.css",
- rel: "stylesheet"
- }));
$.event('4chanXInitFinished');
try {
return localStorage.getItem('4chan-settings');
diff --git a/builds/updates.xml b/builds/updates.xml
index 12f1e8175..e4881e51d 100644
--- a/builds/updates.xml
+++ b/builds/updates.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/css/style.css b/css/style.css
index c65b2076c..545a7422b 100644
--- a/css/style.css
+++ b/css/style.css
@@ -105,7 +105,7 @@ a[href="javascript:;"] {
:root.bottom-header body {
margin-bottom: 2em;
}
-body > .desktop:not(#boardNavDesktop):not(#boardNavDesktopFoot),
+body > .desktop:not(hr):not(.navLinks):not(#boardNavDesktop):not(#boardNavDesktopFoot),
:root.fourchan-x #navtopright,
:root.fourchan-x #navbotright,
:root.fourchan-x:not(.show-original-top-board-list) #boardNavDesktop,
@@ -725,8 +725,7 @@ a.hide-announcement {
/* QR */
:root.hide-original-post-form #postForm,
-:root.hide-original-post-form .postingMode,
-:root.hide-original-post-form #togglePostForm,
+:root.hide-original-post-form #togglePostFormLink,
#qr.autohide:not(.has-focus):not(:hover) > form {
display: none;
}
diff --git a/package.json b/package.json
index 1f5cb0d07..a4ae96a9e 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
"grunt-contrib-concat": "~0.4.0",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-watch": "~0.6.1",
- "grunt-shell": "~0.6.4",
+ "grunt-shell": "~0.7.0",
"load-grunt-tasks": "~0.4.0"
},
"repository": {
diff --git a/src/Archive/archives.json b/src/Archive/archives.json
index a5d3039bb..ca1cabd60 100644
--- a/src/Archive/archives.json
+++ b/src/Archive/archives.json
@@ -5,7 +5,7 @@
"http": true,
"https": true,
"software": "foolfuuka",
- "boards": ["a", "biz", "co", "diy", "gd", "jp", "m", "sci", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
+ "boards": ["a", "biz", "co", "diy", "gd", "jp", "m", "sci", "sp", "tg", "tv", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "gd", "diy", "jp", "m", "sci", "tg", "vg", "vp", "vr", "wsg"]
}, {
"uid": 1,
@@ -95,8 +95,8 @@
"http": true,
"https": true,
"software": "foolfuuka",
- "boards": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"],
- "files": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"]
+ "boards": ["asp", "cm", "h", "hc", "hm", "n", "p", "r", "s", "soc", "y"],
+ "files": ["asp", "cm", "h", "hc", "hm", "n", "p", "r", "s", "soc", "y"]
}, {
"uid": 16,
"name": "maware",
@@ -123,6 +123,6 @@
"https": true,
"withCredentials": true,
"software": "foolfuuka",
- "boards": ["a", "biz", "co", "d", "diy", "gd", "jp", "m", "s4s", "sci", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
+ "boards": ["a", "biz", "co", "d", "diy", "gd", "jp", "m", "s4s", "sci", "sp", "tg", "tv", "u", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "d", "diy", "gd", "jp", "m", "s4s", "sci", "tg", "u", "vg", "vp", "vr", "wsg"]
}]
diff --git a/src/General/Build.coffee b/src/General/Build.coffee
index 86bbdeb22..ee3023c23 100755
--- a/src/General/Build.coffee
+++ b/src/General/Build.coffee
@@ -15,6 +15,11 @@ Build =
thumbRotate: do ->
n = 0
-> n = (n + 1) % 3
+ path: (boardID, threadID, postID, fragment) ->
+ path = "/#{boardID}/thread/#{threadID}"
+ path += "/#{g.SLUG}" if g.SLUG? and threadID is g.THREADID
+ path += "##{fragment or 'p'}#{postID}" if postID
+ path
postFromObject: (data, boardID) ->
o =
# id
@@ -178,11 +183,12 @@ Build =
''
if isOP and g.VIEW is 'index'
- pageNum = Index.liveThreadData.keys.indexOf("#{postID}") // Index.threadsNumPerPage
+ pageNum = Index.liveThreadData.keys.indexOf("#{postID}") // Index.threadsNumPerPage + 1
pageIcon = " Page #{pageNum}"
- replyLink = " [Reply]"
+ replyLink = " [Reply]"
else
- pageIcon = replyLink = ''
+ pageIcon = ''
+ replyLink = ''
container = $.el 'div',
id: "pc#{postID}"
@@ -208,13 +214,13 @@ Build =
' ' +
"#{date} " +
"" +
- "No." +
+ "No." +
"#{postID}" +
+ Build.path boardID, threadID, postID, 'q'
+ }' title='Reply to this post'>#{postID}" +
pageIcon + sticky + closed + replyLink +
'' +
'' +
@@ -225,11 +231,11 @@ Build =
''
+ # Fix quote pathnames in index or cross-{board,thread} posts
for quote in $$ '.quotelink', container
href = quote.getAttribute 'href'
- continue if href[0] is '/' # Cross-board quote, or board link
- href = "#{threadID}#{href}" if href[0] is '#'
- quote.href = "/#{boardID}/thread/#{href}" # Fix pathnames
+ continue unless href[0] is '#'
+ quote.href = Build.path boardID, threadID, href[2..]
container
@@ -241,7 +247,7 @@ Build =
$.el 'a',
className: 'summary'
textContent: text.join ' '
- href: "/#{boardID}/thread/#{threadID}"
+ href: Build.path boardID, threadID
thread: (board, data, full) ->
Build.spoilerRange[board] = data.custom_spoiler
@@ -275,7 +281,7 @@ Build =
postCount = data.replies + 1
fileCount = data.images + !!data.ext
- pageCount = Index.liveThreadData.keys.indexOf("#{thread.ID}") // Index.threadsNumPerPage
+ pageCount = Index.liveThreadData.keys.indexOf("#{thread.ID}") // Index.threadsNumPerPage + 1
subject = if thread.OP.info.subject
"#{thread.OP.info.subject}
"
diff --git a/src/General/Config.coffee b/src/General/Config.coffee
index c6d0197a1..a114b51c6 100644
--- a/src/General/Config.coffee
+++ b/src/General/Config.coffee
@@ -317,7 +317,7 @@ Config =
]
'Auto-load captcha': [
false
- 'Automatically load the captcha when you open a thread'
+ 'Automatically load the captcha when you open a thread, and reload it after you post.'
]
'Quote Links':
@@ -913,7 +913,7 @@ Config =
backlink: '>>%id'
- fileInfo: '%L (%p%s, %r)'
+ fileInfo: '%l (%p%s, %r)'
favicon: 'ferongr'
@@ -1028,19 +1028,19 @@ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
]
# Board Navigation
'Front page': [
- '0'
+ '1'
'Jump to front page.'
]
'Open front page': [
- 'Shift+0'
+ 'Shift+1'
'Open front page in a new tab.'
]
'Next page': [
- 'Shift+Right'
+ 'Ctrl+Right'
'Jump to the next page.'
]
'Previous page': [
- 'Shift+Left'
+ 'Ctrl+Left'
'Jump to the previous page.'
]
'Search form': [
@@ -1065,11 +1065,11 @@ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
]
# Thread Navigation
'Next thread': [
- 'Shift+Down'
+ 'Ctrl+Down'
'See next thread.'
]
'Previous thread': [
- 'Shift+Up'
+ 'Ctrl+Up'
'See previous thread.'
]
'Expand thread': [
diff --git a/src/General/Index.coffee b/src/General/Index.coffee
index 986f9c1fa..9f1f3702c 100644
--- a/src/General/Index.coffee
+++ b/src/General/Index.coffee
@@ -147,6 +147,7 @@ Index =
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
$.rm navLink for navLink in $$ '.navLinks'
+ $.id('search-box')?.parentNode.remove()
$.after $.x('child::form/preceding-sibling::hr[1]'), Index.navLinks
return if g.VIEW isnt 'index'
@@ -172,9 +173,7 @@ Index =
scroll: ->
return if Index.req or Conf['Index Mode'] isnt 'infinite' or (window.scrollY <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread'
Index.currentPage = (Index.currentPage or Index.getCurrentPage()) + 1 # Avoid having to pushState to keep track of the current page
-
return Index.endNotice() if Index.currentPage >= Index.pagesNum
-
Index.buildIndex true
endNotice: do ->
@@ -322,8 +321,8 @@ Index =
else
'Show'
Index.sort()
- if Conf['Index Mode'] is 'paged' and Index.getCurrentPage() > 0
- Index.pageNav 0
+ if Conf['Index Mode'] is 'paged' and Index.getCurrentPage() > 1
+ Index.pageNav 1
else
Index.buildIndex()
@@ -381,7 +380,7 @@ Index =
return
e.preventDefault()
return if Index.cb.indexNav a, true
- Index.userPageNav +a.pathname.split('/')[2]
+ Index.userPageNav +a.pathname.split('/')[2] or 1
headerNav: (e) ->
a = e.target
@@ -421,10 +420,10 @@ Index =
getCurrentPage: ->
if Conf['Index Mode'] is 'infinite' and Index.currentPage
return Index.currentPage
- +window.location.pathname.split('/')[2]
+ +window.location.pathname.split('/')[2] or 1
userPageNav: (pageNum) ->
- Navigate.pushState if pageNum is 0 then './' else pageNum
+ Navigate.pushState if pageNum is 1 then './' else pageNum
if Conf['Refreshed Navigation'] and Conf['Index Mode'] isnt 'all pages'
Index.update pageNum
else
@@ -432,7 +431,7 @@ Index =
pageNav: (pageNum) ->
return if Index.currentPage is pageNum and not Index.root.parentElement
- Navigate.pushState if pageNum is 0 then './' else pageNum
+ Navigate.pushState if pageNum is 1 then './' else pageNum
Index.pageLoad pageNum
pageLoad: (pageNum) ->
@@ -451,20 +450,22 @@ Index =
Math.ceil Index.sortedThreads.length / Index.getThreadsNumPerPage()
getMaxPageNum: ->
- Math.max 0, Index.getPagesNum() - 1
-
+ min = 1
+ max = +Index.getPagesNum()
+ if min < max then max else
+ min
togglePagelist: ->
Index.pagelist.hidden = Conf['Index Mode'] isnt 'paged'
buildPagelist: ->
pagesRoot = $ '.pages', Index.pagelist
maxPageNum = Index.getMaxPageNum()
- if pagesRoot.childElementCount isnt maxPageNum + 1
+ if pagesRoot.childElementCount isnt maxPageNum
nodes = []
- for i in [0..maxPageNum] by 1
+ for i in [1..maxPageNum] by 1
a = $.el 'a',
textContent: i
- href: if i then i else './'
+ href: if i is 1 then './' else i
nodes.push $.tn('['), a, $.tn '] '
$.rmAll pagesRoot
$.add pagesRoot, nodes
@@ -477,11 +478,11 @@ Index =
# Previous/Next buttons
prev = pagesRoot.previousSibling.firstChild
next = pagesRoot.nextSibling.firstChild
- href = Math.max pageNum - 1, 0
- prev.href = if href is 0 then './' else href
+ href = Math.max pageNum - 1, 1
+ prev.href = if href is 1 then './' else href
prev.firstChild.disabled = href is pageNum
href = Math.min pageNum + 1, maxPageNum
- next.href = if href is 0 then './' else href
+ next.href = if href is 1 then './' else href
next.firstChild.disabled = href is pageNum
# current page
if strong = $ 'strong', pagesRoot
@@ -489,7 +490,7 @@ Index =
$.replace strong, strong.firstChild
else
strong = $.el 'strong'
- return unless a = pagesRoot.children[pageNum] # If coming in from a Navigate.navigate, this could break.
+ return unless a = pagesRoot.children[pageNum - 1] # If coming in from a Navigate.navigate, this could break.
$.before a, strong
$.add strong, a
@@ -514,7 +515,7 @@ Index =
return
unless d.readyState is 'loading' or Index.root.parentElement
$.replace $('.board'), Index.root
- Index.currentPage = 0
+ Index.currentPage = 1
Index.req?.abort()
Index.notice?.close()
@@ -565,7 +566,7 @@ Index =
Navigate.title()
try
- pageNum or= 0
+ pageNum or= 1
if req.status is 200
Index.parse req.response, pageNum
else if req.status is 304
@@ -622,7 +623,7 @@ Index =
Index.liveThreadData.forEach (threadData) ->
threadRoot = Build.thread g.BOARD, threadData
if thread = g.BOARD.threads[threadData.no]
- thread.setPage i // Index.threadsNumPerPage
+ thread.setPage i // Index.threadsNumPerPage + 1
thread.setCount 'post', threadData.replies + 1, threadData.bumplimit
thread.setCount 'file', threadData.images + !!threadData.ext, threadData.imagelimit
thread.setStatus 'Sticky', !!threadData.sticky
@@ -760,7 +761,7 @@ Index =
nodes = []
switch Conf['Index Mode']
when 'paged', 'infinite'
- pageNum = Index.getCurrentPage()
+ pageNum = Index.getCurrentPage() - 1
threadsPerPage = Index.getThreadsNumPerPage()
threads = []
@@ -799,7 +800,7 @@ Index =
unless Index.searchInput.dataset.searching
Index.searchInput.dataset.searching = 1
Index.pageBeforeSearch = Index.getCurrentPage()
- Index.setPage pageNum = 0
+ Index.setPage pageNum = 1
else
unless Conf['Index Mode'] is 'infinite'
pageNum = Index.getCurrentPage()
diff --git a/src/General/Main.coffee b/src/General/Main.coffee
index f9f4f106d..c5a7c9725 100644
--- a/src/General/Main.coffee
+++ b/src/General/Main.coffee
@@ -18,6 +18,10 @@ Main =
return Index.catalogSwitch()
if g.VIEW is 'thread'
g.THREADID = +pathname[3]
+ g.SLUG = pathname[4] if pathname[4]?
+ if pathname[2] isnt 'thread'
+ pathname[2] = 'thread'
+ history.replaceState null, '', pathname.slice(0,4).join('/') + location.hash
# flatten Config into Conf
# and get saved or default values
@@ -199,14 +203,17 @@ Main =
'This will steal your data.'
'left=0,top=0,width=500,height=255,toolbar=0,resizable=0'
$.before styleSelector.previousSibling, [$.tn '['; passLink, $.tn ']\u00A0\u00A0']
+ # Completely disable the mobile layout
+ $('link[href*="mobile"', d.head).disabled = true
# Parse HTML or skip it and start building from JSON.
if !Conf['JSON Navigation'] or g.VIEW is 'thread'
Main.initThread()
-
- $.add d.head, $.el 'link',
- href: "//s.4cdn.org/css/flags.556.css"
- rel: "stylesheet"
+
+ # JSON Navigation may not load on a page that has flags, so force their CSS to always be available.
+ $.add d.head, $.el 'link',
+ href: "//s.4cdn.org/css/flags.556.css"
+ rel: "stylesheet"
$.event '4chanXInitFinished'
diff --git a/src/General/Navigate.coffee b/src/General/Navigate.coffee
index 0ed52dfd9..5d1e86494 100644
--- a/src/General/Navigate.coffee
+++ b/src/General/Navigate.coffee
@@ -128,8 +128,8 @@ Navigate =
updateBoard: (boardID) ->
fullBoardList = $ '#full-board-list', Header.boardList
- $.rmClass $('.current', fullBoardList), 'current'
- $.addClass $("a[href*='/#{boardID}/']", fullBoardList), 'current'
+ $.rmClass current, 'current' if current = $ '.current', fullBoardList
+ $.addClass current, 'current' if current = $ "a[href*='/#{boardID}/']", fullBoardList
Header.generateBoardList Conf['boardnav'].replace /(\r\n|\n|\r)/g, ' '
Index.catalogLink.href = "//boards.4chan.org/#{boardID}/"
@@ -226,7 +226,7 @@ Navigate =
if threadID
view = 'thread'
else
- pageNum = +view
+ pageNum = +view or 1 # string to number, '' to 1
view = 'index' # path is "/boardID/". See the problem?
path = @pathname
@@ -257,7 +257,7 @@ Navigate =
# Moving from index to thread or thread to thread
{load} = Navigate
- Navigate.req = $.ajax "//a.4cdn.org/#{boardID}/res/#{threadID}.json",
+ Navigate.req = $.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json",
onabort: load
onloadend: load
diff --git a/src/General/css/style.css b/src/General/css/style.css
index 67089d6a8..dd4425a8b 100755
--- a/src/General/css/style.css
+++ b/src/General/css/style.css
@@ -56,9 +56,12 @@ a[href="javascript:;"] {
.warning {
color: red;
}
-#boardNavDesktop {
+#boardNavDesktop, #boardNavMobile {
display: none !important;
}
+body.hasDropDownNav{
+ margin-top: 5px;
+}
a {
outline: none !important;
}
@@ -66,14 +69,17 @@ a {
border-radius: 3px;
padding: 0px 2px;
}
-body>hr, .ad-plea-bottom + hr {
+body > hr,
+#blotter hr,
+.desktop > hr,
+#delform > hr,
+#content > hr {
display: none;
}
-.board > hr:last-of-type {
- border-top-color: transparent !important;
-}
-div.navLinks {
- margin-bottom: -10px !important;
+:root.index .board > hr:last-of-type,
+:root.thread .board > hr {
+ border: 0px;
+ margin: 0px;
}
.ad-plea {
display: none;
@@ -485,9 +491,7 @@ div.center:not(.ad-cnt) {
:root.index-loading .navLinks,
:root.index-loading .board,
:root.index-loading .pagelist,
-:root.thread .pagelist {
- display: none;
-}
+:root.thread .pagelist,
:root:not(.catalog-mode) #index-size,
.index:not(.catalog-mode) #returnlink {
display: none;
@@ -902,7 +906,7 @@ span.hide-announcement {
:root.hide-original-post-form #postForm,
:root.hide-original-post-form .postingMode,
:root.hide-original-post-form #togglePostForm,
-#qr.autohide:not(.focus):not(:hover) > form,
+#qr.autohide:not(.focus):not(:hover):not(:active) > form,
.thread #qr select[data-name=thread],
#file-n-submit:not(.has-file) #qr-filerm {
display: none;
diff --git a/src/General/html/Build/post.html b/src/General/html/Build/post.html
new file mode 100755
index 000000000..5fabd2f5a
--- /dev/null
+++ b/src/General/html/Build/post.html
@@ -0,0 +1,36 @@
+"""#{if isOP then '' else ">>
"}
+
+
+ #{if isOP then fileHTML else ''}
+
+
+
+ #{' '}
#{subject or ''}#{' '}
+
+ #{emailStart}
+ #{name or ''}
+ #{tripcode + capcodeStart + emailEnd + capcodeIcon + userID + flag}
+ #{" "}
+
#{date}#{' '}
+
+ No.
+ #{postID}
+ #{pageIcon + sticky + closed + replyLink}
+
+
+
+ #{if isOP then '' else fileHTML}
+
+
#{comment or ''}
#{' '}
+
+
"""
diff --git a/src/General/html/Features/Index-navlinks.html b/src/General/html/Features/Index-navlinks.html
index c2b3ae90d..0639dd74f 100644
--- a/src/General/html/Features/Index-navlinks.html
+++ b/src/General/html/Features/Index-navlinks.html
@@ -25,4 +25,4 @@
-
\ No newline at end of file
+
diff --git a/src/General/html/Features/Thread-catalog-view.html b/src/General/html/Features/Thread-catalog-view.html
index 1df9afccb..2a18fa150 100644
--- a/src/General/html/Features/Thread-catalog-view.html
+++ b/src/General/html/Features/Thread-catalog-view.html
@@ -1,4 +1,4 @@
-
+
#{postCount} / #{fileCount} / #{pageCount}
diff --git a/src/General/lib/post.class b/src/General/lib/post.class
index f1e8e0b00..fbcb4e49d 100755
--- a/src/General/lib/post.class
+++ b/src/General/lib/post.class
@@ -101,8 +101,9 @@ class Post
return unless match = quotelink.href.match ///
boards\.4chan\.org/
([^/]+) # boardID
- /thread/\d+#p
- (\d+) # postID
+ /(res|thread)/\d+
+ (.*)? # thread slug
+ \#p(\d+) # postID
$
///
@@ -135,6 +136,10 @@ class Post
thumb.src
else
"#{location.protocol}//t.4cdn.org/#{@board}/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
+ @file.isImage = /(jpg|png|gif)$/i.test @file.URL
+ @file.isVideo = /webm$/i.test @file.URL
+ if @file.isImage or @file.isVideo
+ @file.dimensions = fileText.childNodes[2].data.match(/\d+x\d+/)[0]
@file.name = if !@file.isSpoiler and nameNode = $ 'a', fileText
nameNode.title or nameNode.textContent
else
@@ -145,17 +150,13 @@ class Post
# webk.it/62107
# https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909
# http://www.whatwg.org/specs/web-apps/current-work/#multipart-form-data
- @file.name = @file.name.replace /%22/g, '"'
+ @file.name = @file.name?.replace /%22/g, '"'
<% } %>
- @file.isImage = /(jpg|png|gif)$/i.test @file.name
- @file.isVideo = /webm$/i.test @file.name
- if @file.isImage or @file.isVideo
- @file.dimensions = fileText.textContent.match(/\d+x\d+/)[0]
cleanup: (root, post) ->
for node in $$ '.mobile', root
$.rm node
- for node in $$ '[id]', post
+ for node in $$ '[id]:not(.exif)', post
node.removeAttribute 'id'
for node in $$ '.desktop', root
$.rmClass node, 'desktop'
diff --git a/src/General/lib/thread.class b/src/General/lib/thread.class
index aa1f1c19a..da2bdb4cf 100755
--- a/src/General/lib/thread.class
+++ b/src/General/lib/thread.class
@@ -49,7 +49,7 @@ class Thread
else if g.VIEW is 'index'
$ '.page-num', @OP.nodes.info
else
- $ '[title="Quote this post"]', @OP.nodes.info
+ $ '[title="Reply to this post"]', @OP.nodes.info
$.after root, [$.tn(' '), icon]
return unless @catalogView
diff --git a/src/Images/Gallery.coffee b/src/Images/Gallery.coffee
index aee0c66e8..ce60a4fc4 100644
--- a/src/Images/Gallery.coffee
+++ b/src/Images/Gallery.coffee
@@ -193,7 +193,7 @@ Gallery =
if src[2] is 'i.4cdn.org'
URL = Redirect.to 'file',
boardID: src[3]
- filename: src[5]
+ filename: src[src.length - 1]
if URL
thumb.href = URL
return unless Gallery.nodes.current is img
@@ -202,8 +202,8 @@ Gallery =
if g.DEAD or post.isDead or post.file.isDead
return
- # XXX CORS for images.4chan.org WHEN?
- $.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: ->
+ # XXX CORS for i.4cdn.org WHEN?
+ $.ajax "//a.4cdn.org/#{post.board}/thread/#{post.thread}.json", onload: ->
return if @status isnt 200
i = 0
{posts} = @response
diff --git a/src/Linkification/Linkify.coffee b/src/Linkification/Linkify.coffee
index a9663fa68..4544d15f6 100755
--- a/src/Linkification/Linkify.coffee
+++ b/src/Linkification/Linkify.coffee
@@ -207,8 +207,8 @@ Linkify =
el = (type = Linkify.types[a.dataset.key]).el a
# Set style values.
- el.style.cssText = if style = type.style
- style
+ el.style.cssText = if type.style?
+ type.style
else
"border: 0; width: 640px; height: 390px"
@@ -249,9 +249,10 @@ Linkify =
ordered_types: [
key: 'audio'
regExp: /(.*\.(mp3|ogg|wav))$/
+ style: ''
el: (a) ->
$.el 'audio',
- controls: 'controls'
+ controls: true
preload: 'auto'
src: a.dataset.uid
,
@@ -391,10 +392,12 @@ Linkify =
,
key: 'Vocaroo'
regExp: /.*(?:vocaroo.com\/)([^#\&\?]*).*/
- style: 'border: 0; width: 150px; height: 45px;'
+ style: ''
el: (a) ->
- $.el 'object',
- innerHTML: ""
+ $.el 'audio',
+ controls: true
+ preload: 'auto'
+ src: "http://vocaroo.com/media_command.php?media=#{a.dataset.uid.replace /^i\//, ''}&command=download_ogg"
,
key: 'Vimeo'
regExp: /.*(?:vimeo.com\/)([^#\&\?]*).*/
@@ -434,6 +437,7 @@ Linkify =
,
key: 'video'
regExp: /(.*\.(ogv|webm|mp4))$/
+ style: 'border: 0; width: auto; height: auto;'
el: (a) ->
$.el 'video',
controls: 'controls'
diff --git a/src/Miscellaneous/ExpandComment.coffee b/src/Miscellaneous/ExpandComment.coffee
index 0cd435c47..f1ce43aa1 100755
--- a/src/Miscellaneous/ExpandComment.coffee
+++ b/src/Miscellaneous/ExpandComment.coffee
@@ -26,8 +26,8 @@ ExpandComment =
return
return unless a = $ '.abbr > a', post.nodes.comment
a.textContent = "Post No.#{post} Loading..."
- $.cache "//api.4chan.org#{a.pathname}.json", -> ExpandComment.parse @, a, post
-
+ $.cache "//a.4cdn.org#{a.pathname.split('/').splice(0,4).join('/')}.json", -> ExpandComment.parse @, a, post
+
contract: (post) ->
return unless post.nodes.shortComment
a = $ '.abbr > a', post.nodes.shortComment
@@ -41,7 +41,7 @@ ExpandComment =
a.textContent = "Error #{req.statusText} (#{status})"
return
- posts = JSON.parse(req.response).posts
+ posts = req.response.posts
if spoilerRange = posts[0].custom_spoiler
Build.spoilerRange[g.BOARD] = spoilerRange
@@ -54,10 +54,14 @@ ExpandComment =
{comment} = post.nodes
clone = comment.cloneNode false
clone.innerHTML = postObj.com
+ # Fix pathnames
for quote in $$ '.quotelink', clone
href = quote.getAttribute 'href'
continue if href[0] is '/' # Cross-board quote, or board link
- quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
+ if href[0] is '#'
+ quote.href = "#{a.pathname.split('/').splice(0,4).join('/')}#{href}"
+ else
+ quote.href = "#{a.pathname.split('/').splice(0,3).join('/')}/#{href}"
post.nodes.shortComment = comment
$.replace comment, clone
post.nodes.comment = post.nodes.longComment = clone
diff --git a/src/Miscellaneous/Keybinds.coffee b/src/Miscellaneous/Keybinds.coffee
index 38de66ec2..4cb0e3632 100755
--- a/src/Miscellaneous/Keybinds.coffee
+++ b/src/Miscellaneous/Keybinds.coffee
@@ -21,9 +21,10 @@ Keybinds =
{target} = e
if target.nodeName in ['INPUT', 'TEXTAREA']
return unless /(Esc|Alt|Ctrl|Meta|Shift\+\w{2,})/.test key
- threadRoot = Nav.getThread()
- if op = $ '.op', threadRoot
- thread = Get.postFromNode(op).thread
+ unless g.VIEW is 'catalog'
+ threadRoot = Nav.getThread()
+ if op = $ '.op', threadRoot
+ thread = Get.postFromNode(op).thread
switch key
# QR & Options
when Conf['Toggle board list']
@@ -32,9 +33,10 @@ Keybinds =
when Conf['Toggle header']
Header.toggleBarVisibility()
when Conf['Open empty QR']
- Keybinds.qr threadRoot
+ Keybinds.qr()
when Conf['Open QR']
- Keybinds.qr threadRoot, true
+ return if g.VIEW is 'catalog'
+ Keybinds.qr threadRoot
when Conf['Open settings']
Settings.open()
when Conf['Close']
@@ -76,22 +78,28 @@ Keybinds =
when 'index'
if Conf['JSON Navigation'] then Index.update()
when Conf['Watch']
+ return if g.VIEW is 'catalog'
ThreadWatcher.toggle thread
# Images
when Conf['Expand image']
+ return if g.VIEW is 'catalog'
Keybinds.img threadRoot
when Conf['Expand images']
+ return if g.VIEW is 'catalog'
Keybinds.img threadRoot, true
when Conf['Open Gallery']
+ return if g.VIEW is 'catalog'
Gallery.cb.toggle()
when Conf['fappeTyme']
+ return if g.VIEW is 'catalog'
FappeTyme.cb.toggle.call {name: 'fappe'}
when Conf['werkTyme']
+ return if g.VIEW is 'catalog'
FappeTyme.cb.toggle.call {name: 'werk'}
# Board Navigation
when Conf['Front page']
if Conf['JSON Navigation'] and g.VIEW is 'index'
- Index.userPageNav 0
+ Index.userPageNav 1
else
window.location = "/#{g.BOARD}/"
when Conf['Open front page']
@@ -113,10 +121,11 @@ Keybinds =
if form = $ '.prev form'
window.location = form.action
when Conf['Search form']
- if Conf['JSON Navigation']
- Index.searchInput.focus()
- else
- $.id('search-btn').click()
+ return unless g.VIEW is 'index'
+ searchInput = if Conf['JSON Navigation'] then Index.searchInput else $.id('search-box')
+ Header.scrollToIfNeeded searchInput
+ searchInput.click()
+ searchInput.focus()
when Conf['Paged mode']
return unless g.VIEW is 'index' and Conf['Index Mode'] isnt 'paged'
Index.setIndexMode 'paged'
@@ -144,17 +153,23 @@ Keybinds =
return if g.VIEW isnt 'index'
Nav.scroll -1
when Conf['Expand thread']
+ return if g.VIEW isnt 'index'
ExpandThread.toggle thread
when Conf['Open thread']
+ return if g.VIEW isnt 'index'
Keybinds.open thread
when Conf['Open thread tab']
+ return if g.VIEW isnt 'index'
Keybinds.open thread, true
# Reply Navigation
when Conf['Next reply']
+ return if g.VIEW is 'catalog'
Keybinds.hl +1, threadRoot
when Conf['Previous reply']
+ return if g.VIEW is 'catalog'
Keybinds.hl -1, threadRoot
when Conf['Deselect reply']
+ return if g.VIEW is 'catalog'
Keybinds.hl 0, threadRoot
when Conf['Hide']
PostHiding.toggle thread.OP
@@ -195,10 +210,10 @@ Keybinds =
if e.shiftKey then key = 'Shift+' + key
key
- qr: (thread, quote) ->
- return unless QR.postingIsEnabled
- do QR.open
- if quote
+ qr: (thread) ->
+ return unless Conf['Quick Reply'] and QR.postingIsEnabled
+ QR.open()
+ if thread?
QR.quote.call $ 'input', $('.post.highlight', thread) or thread
do QR.nodes.com.focus
@@ -239,7 +254,7 @@ Keybinds =
open: (thread, tab) ->
return if g.VIEW isnt 'index'
- url = "/#{thread.board}/thread/#{thread}"
+ url = Build.path thread.board.ID, thread.ID
if tab
$.open url
else
diff --git a/src/Monitoring/ThreadStats.coffee b/src/Monitoring/ThreadStats.coffee
index ddc00b842..750f87146 100755
--- a/src/Monitoring/ThreadStats.coffee
+++ b/src/Monitoring/ThreadStats.coffee
@@ -49,6 +49,7 @@ ThreadStats =
delete @postCountEl
delete @fileCountEl
delete @pageCountEl
+ delete @dialog
Thread.callbacks.disconnect 'Thread Stats'
$.off d, 'ThreadUpdate', ThreadStats.onUpdate
@@ -80,5 +81,5 @@ ThreadStats =
for page in @response
for thread in page.threads when thread.no is ThreadStats.thread.ID
ThreadStats.pageCountEl.textContent = page.page
- (if page.page is @response.length - 1 then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
+ (if page.page is @response.length then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
return
diff --git a/src/Monitoring/Unread.coffee b/src/Monitoring/Unread.coffee
index 4214248df..6d17b4eba 100755
--- a/src/Monitoring/Unread.coffee
+++ b/src/Monitoring/Unread.coffee
@@ -160,7 +160,7 @@ Unread =
threadID: data.thread.ID
postID: ID
}
- QuoteYou.lastRead = data.nodes.root
+ QuoteMarkers.lastRead = data.nodes.root
return unless ID
diff --git a/src/Posting/QR.captcha.coffee b/src/Posting/QR.captcha.coffee
index 42173511d..44e80b25a 100644
--- a/src/Posting/QR.captcha.coffee
+++ b/src/Posting/QR.captcha.coffee
@@ -21,10 +21,20 @@ QR.captcha =
$.on input, 'blur', QR.focusout
$.on input, 'focus', QR.focusin
+ $.on input, 'keydown', QR.captcha.keydown.bind QR.captcha
+ $.on @nodes.img.parentNode, 'click', QR.captcha.reload.bind QR.captcha
$.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.com.parentNode, [imgContainer, input]
+ @captchas = []
+ $.get 'captchas', [], ({captchas}) ->
+ QR.captcha.sync captchas
+ QR.captcha.clear()
+ $.sync 'captchas', @sync
+
+ new MutationObserver(@afterSetup).observe $.id('captchaContainer'), childList: true
+
@beforeSetup()
@afterSetup() # reCAPTCHA might have loaded before the QR.
@@ -34,24 +44,26 @@ QR.captcha =
img.parentNode.parentNode.hidden = true
input.value = ''
input.placeholder = 'Focus to load reCAPTCHA'
+ @count()
$.on input, 'focus', @setup
- @setupObserver = new MutationObserver @afterSetup
- @setupObserver.observe $.id('captchaContainer'), childList: true
setup: ->
$.globalEval 'loadRecaptcha()'
afterSetup: ->
return unless challenge = $.id 'recaptcha_challenge_field_holder'
- QR.captcha.setupObserver.disconnect()
- delete QR.captcha.setupObserver
+ return if challenge is QR.captcha.nodes.challenge
+
+ setLifetime = (e) -> QR.captcha.lifetime = e.detail
+ $.on window, 'captcha:timeout', setLifetime
+ $.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
+ $.off window, 'captcha:timeout', setLifetime
{img, input} = QR.captcha.nodes
img.parentNode.parentNode.hidden = false
input.placeholder = 'Verification'
- $.off input, 'focus', QR.captcha.setup
- $.on input, 'keydown', QR.captcha.keydown.bind QR.captcha
- $.on img.parentNode, 'click', QR.captcha.reload.bind QR.captcha
+ QR.captcha.count()
+ $.off input, 'focus', QR.captcha.setup
QR.captcha.nodes.challenge = challenge
new MutationObserver(QR.captcha.load.bind QR.captcha).observe challenge,
@@ -64,33 +76,84 @@ QR.captcha =
$.globalEval 'Recaptcha.destroy()'
@beforeSetup()
+ sync: (captchas) ->
+ QR.captcha.captchas = captchas
+ QR.captcha.count()
+
getOne: ->
- challenge = @nodes.img.alt
- response = @nodes.input.value.trim()
- if response and !/\s/.test response
+ @clear()
+ if captcha = @captchas.shift()
+ {challenge, response} = captcha
+ @count()
+ $.set 'captchas', @captchas
+ else
+ challenge = @nodes.img.alt
+ if response = @nodes.input.value
+ if Conf['Auto-load captcha'] then @reload() else @destroy()
+ if response
+ response = response.trim()
# one-word-captcha:
# If there's only one word, duplicate it.
- response = "#{response} #{response}"
+ response = "#{response} #{response}" unless /\s/.test response
{challenge, response}
+ save: ->
+ return unless response = @nodes.input.value.trim()
+ @nodes.input.value = ''
+ @captchas.push
+ challenge: @nodes.img.alt
+ response: response
+ timeout: @timeout
+ @count()
+ @reload()
+ $.set 'captchas', @captchas
+
+ clear: ->
+ return unless @captchas.length
+ now = Date.now()
+ for captcha, i in @captchas
+ break if captcha.timeout > now
+ return unless i
+ @captchas = @captchas[i..]
+ @count()
+ $.set 'captchas', @captchas
+
load: ->
return unless @nodes.challenge.firstChild
+ return unless challenge_image = $.id 'recaptcha_challenge_image'
# -1 minute to give upload some time.
+ @timeout = Date.now() + @lifetime * $.SECOND - $.MINUTE
challenge = @nodes.challenge.firstChild.value
@nodes.img.alt = challenge
- @nodes.img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}"
+ @nodes.img.src = challenge_image.src
@nodes.input.value = null
+ @clear()
+
+ count: ->
+ count = if @captchas then @captchas.length else 0
+ placeholder = @nodes.input.placeholder.replace /\ \(.*\)$/, ''
+ placeholder += switch count
+ when 0
+ if placeholder is 'Verification' then ' (Shift + Enter to cache)' else ''
+ when 1
+ ' (1 cached captcha)'
+ else
+ " (#{count} cached captchas)"
+ @nodes.input.placeholder = placeholder
+ @nodes.input.alt = count # For XTRM RICE.
reload: (focus) ->
- # the 't' argument prevents the input from being focused
- $.globalEval 'Recaptcha.reload("t")'
+ # Hack to prevent the input from being focused
+ $.globalEval 'Recaptcha.reload(); Recaptcha.should_focus = false;'
# Focus if we meant to.
@nodes.input.focus() if focus
keydown: (e) ->
if e.keyCode is 8 and not @nodes.input.value
@reload()
+ else if e.keyCode is 13 and e.shiftKey
+ @save()
else
return
e.preventDefault()
diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee
index 9d27d4ddc..ef4f787ce 100644
--- a/src/Posting/QR.coffee
+++ b/src/Posting/QR.coffee
@@ -1,4 +1,6 @@
QR =
+ mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm']
+
init: ->
@db = new DataBoard 'yourPosts'
@posts = []
@@ -73,7 +75,7 @@ QR =
node: ->
if QR.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID}
$.addClass @nodes.root, 'your-post'
- $.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
+ $.on $('a[title="Reply to this post"]', @nodes.info), 'click', QR.quote
persist: ->
return unless QR.postingIsEnabled
@@ -111,8 +113,13 @@ QR =
QR.cooldown.auto = false
QR.status()
- focusin: -> $.addClass QR.nodes.el, 'focus'
- focusout: -> $.rmClass QR.nodes.el, 'focus'
+ if QR.captcha.isEnabled and not Conf['Auto-load captcha']
+ QR.captcha.destroy()
+ focusin: ->
+ $.addClass QR.nodes.el, 'focus'
+
+ focusout: ->
+ $.rmClass QR.nodes.el, 'focus'
hide: ->
d.activeElement.blur()
@@ -137,9 +144,10 @@ QR =
el = err
el.removeAttribute 'style'
if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
- # Focus the captcha input on captcha error.
- QR.captcha.nodes.input.focus()
- QR.captcha.setup()
+ if QR.captcha.captchas.length is 0
+ # Focus the captcha input on captcha error.
+ QR.captcha.nodes.input.focus()
+ QR.captcha.setup()
if Conf['Captcha Warning Notifications'] and !d.hidden
QR.notify el
else
@@ -219,7 +227,7 @@ QR =
$.prepend frag, $.tn '[code]'
$.add frag, $.tn '[/code]'
for node in $$ 'br', frag
- $.replace node, $.tn '\n>' unless node is frag.lastElementChild
+ $.replace node, $.tn '\n>' unless node is frag.lastChild
for node in $$ 's', frag
$.replace node, [$.tn('[spoiler]'), node.childNodes..., $.tn '[/spoiler]']
for node in $$ '.prettyprint', frag
@@ -285,55 +293,43 @@ QR =
QR.handleFiles files
$.addClass QR.nodes.el, 'dump'
- handleBlob: (urlBlob, header, url) ->
- name = url.substr(url.lastIndexOf('/')+1, url.length)
- #QUALITY coding at work
- start = header.indexOf("Content-Type: ") + 14
- endsc = header.substr(start, header.length).indexOf(";")
- endnl = header.substr(start, header.length).indexOf("\n") - 1
- end = endnl
- if (endsc != -1 and endsc < endnl)
- end = endsc
- mime = header.substr(start, end)
+ handleBlob: (urlBlob, contentType, contentDisposition, url) ->
+ name = url.match(/([^\/]+)\/*$/)?[1]
+ mime = contentType?.match(/[^;]*/)[0] or 'application/octet-stream'
+ match =
+ contentDisposition?.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)?[1] or
+ contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1]
+ if match
+ name = match.replace /\\"/g, '"'
blob = new Blob([urlBlob], {type: mime})
- blob.name = url.substr(url.lastIndexOf('/')+1, url.length)
- name_start = header.indexOf('name="') + 6
- if (name_start - 6 != -1)
- name_end = header.substr(name_start, header.length).indexOf('"')
- blob.name = header.substr(name_start, name_end)
-
- return if blob.type is null
- QR.error "Unsupported file type."
+ blob.name = name
QR.handleFiles([blob])
handleUrl: ->
url = prompt("Insert an url:")
return if url is null
+
<% if (type === 'crx') { %>
xhr = new XMLHttpRequest();
xhr.open('GET', url, true)
xhr.responseType = 'blob'
xhr.onload = (e) ->
if @readyState is @DONE && xhr.status is 200
- QR.handleBlob(@response, @getResponseHeader('Content-Type'), url)
- return
+ contentType = @getResponseHeader('Content-Type')
+ contentDisposition = @getResponseHeader('Content-Disposition')
+ QR.handleBlob @response, contentType, contentDisposition, url
else
QR.error "Can't load image."
- return
-
xhr.onerror = (e) ->
QR.error "Can't load image."
- return
-
xhr.send()
- return
<% } %>
<% if (type === 'userscript') { %>
- GM_xmlhttpRequest {
- method: "GET",
- url: url,
- overrideMimeType: "text/plain; charset=x-user-defined",
+ GM_xmlhttpRequest
+ method: "GET"
+ url: url
+ overrideMimeType: "text/plain; charset=x-user-defined"
onload: (xhr) ->
r = xhr.responseText
data = new Uint8Array(r.length)
@@ -341,14 +337,11 @@ QR =
while i < r.length
data[i] = r.charCodeAt(i)
i++
-
- QR.handleBlob(data, xhr.responseHeaders, url)
- return
-
- onerror: (xhr) ->
- QR.error "Can't load image."
- }
- return
+ contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1]
+ contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1]
+ QR.handleBlob data, contentType, contentDisposition, url
+ onerror: (xhr) ->
+ QR.error "Can't load image."
<% } %>
handleFiles: (files) ->
@@ -356,40 +349,37 @@ QR =
files = [@files...]
@value = null
return unless files.length
- max = QR.nodes.fileInput.max
- isSingle = files.length is 1
QR.cleanNotifications()
- for file in files
- if file.type is 'application/x-shockwave-flash'
- QR.handleFile(file, isSingle, max)
- else
- QR.checkDimensions file, isSingle, max
- $.addClass QR.nodes.el, 'dump' unless isSingle
+ for file, i in files
+ QR.handleFile file, i, files.length
+ $.addClass QR.nodes.el, 'dump' unless files.length is 1
- checkDimensions: (file, isSingle, max) ->
- if /^image\//.test file.type
- img = new Image()
- img.onload = =>
- {height, width} = img
- return QR.error "#{file.name}: Image too large (image: #{img.height}x#{img.width}px, max: #{QR.max_heigth}x#{QR.max_width}px)" if height > QR.max_heigth or width > QR.max_heigth
- return QR.error "#{file.name}: Image too small (image: #{img.height}x#{img.width}px, min: #{QR.min_heigth}x#{QR.min_width}px)" if height < QR.min_heigth or width < QR.min_heigth
- QR.handleFile file, isSingle, max
- img.src = URL.createObjectURL file
- else
- QR.handleFile file, isSingle, max
-
- handleFile: (file, isSingle, max) ->
+ handleFile: (file, index, nfiles) ->
+ isSingle = nfiles is 1
+ if /^text\//.test file.type
+ if isSingle
+ post = QR.selected
+ else if index isnt 0 or (post = QR.posts[QR.posts.length - 1]).com
+ post = new QR.post()
+ post.pasteText file
+ return
+ unless file.type in QR.mimeTypes
+ QR.error "#{file.name}: Unsupported file type."
+ return unless isSingle
+ max = QR.nodes.fileInput.max
+ max = Math.min(max, QR.max_size_video) if /^video\//.test file.type
if file.size > max
QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
- return
+ return unless isSingle
if isSingle
post = QR.selected
- else if (post = QR.posts[QR.posts.length - 1]).file
+ else if index isnt 0 or (post = QR.posts[QR.posts.length - 1]).file
post = new QR.post()
if /^text/.test file.type
- post.pasteText file
+ return post.pasteText file
else
post.setFile file
+
openFileInput: (e) ->
e.stopPropagation()
if e.shiftKey and e.type is 'click'
@@ -457,16 +447,22 @@ QR =
setNode 'fileInput', '[type=file]'
rules = $('ul.rules').textContent.trim()
- QR.min_width = QR.min_heigth = 1
- QR.max_width = QR.max_heigth = 5000
+ QR.min_width = QR.min_height = 1
+ QR.max_width = QR.max_height = 10000
try
- [_, QR.min_width, QR.min_heigth] = rules.match(/.+smaller than (\d+)x(\d+).+/)
- [_, QR.max_width, QR.max_heigth] = rules.match(/.+greater than (\d+)x(\d+).+/)
+ [_, QR.min_width, QR.min_height] = rules.match(/.+smaller than (\d+)x(\d+).+/)
+ [_, QR.max_width, QR.max_height] = rules.match(/.+greater than (\d+)x(\d+).+/)
+ for prop in ['min_width', 'min_height', 'max_width', 'max_height']
+ QR[prop] = parseInt QR[prop], 10
catch
null
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
+ QR.max_size_video = 3145728
+ QR.max_width_video = QR.max_height_video = 2048
+ QR.max_duration_video = 120
+
QR.spoiler = !!$ 'input[name=spoiler]'
if QR.spoiler
$.addClass QR.nodes.el, 'has-spoiler'
@@ -697,9 +693,6 @@ QR =
onerror: ->
# Connection error, or www.4chan.org/banned
delete QR.req
- if QR.captcha.isEnabled
- QR.captcha.destroy()
- QR.captcha.setup()
post.unlock()
QR.cooldown.auto = false
QR.status()
@@ -733,7 +726,6 @@ QR =
{req} = QR
delete QR.req
- QR.captcha.destroy() if QR.captcha.isEnabled
post = QR.posts[0]
post.unlock()
@@ -765,12 +757,23 @@ QR =
err = 'You seem to have mistyped the CAPTCHA.'
else if /expired/i.test err.textContent
err = 'This CAPTCHA is no longer valid because it has expired.'
- QR.cooldown.auto = false
+ # Enable auto-post if we have some cached captchas.
+ QR.cooldown.auto = if QR.captcha.isEnabled
+ !!QR.captcha.captchas.length
+ else if err is 'Connection error with sys.4chan.org.'
+ true
+ else
+ # Something must've gone terribly wrong if you get captcha errors without captchas.
+ # Don't auto-post indefinitely in that case.
+ false
# Too many frequent mistyped captchas will auto-ban you!
# On connection error, the post most likely didn't go through.
QR.cooldown.set delay: 2
else if err.textContent and m = err.textContent.match /wait\s+(\d+)\s+second/i
- QR.cooldown.auto = !QR.captcha.isEnabled
+ QR.cooldown.auto = if QR.captcha.isEnabled
+ !!QR.captcha.captchas.length
+ else
+ true
QR.cooldown.set delay: m[1]
else # stop auto-posting
QR.cooldown.auto = false
@@ -811,21 +814,32 @@ QR =
# Enable auto-posting if we have stuff left to post, disable it otherwise.
postsCount = QR.posts.length - 1
QR.cooldown.auto = postsCount and isReply
- QR.captcha.setup() if QR.captcha.isEnabled and QR.cooldown.auto
+ if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount
+ notif = new Notification 'Quick reply warning',
+ body: "You are running low on cached captchas. Cache count: #{captchasCount}."
+ icon: Favicon.logo
+ notif.onclick = ->
+ QR.open()
+ QR.captcha.nodes.input.focus()
+ window.focus()
+ notif.onshow = ->
+ setTimeout ->
+ notif.close()
+ , 7 * $.SECOND
unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close()
else
- if QR.posts.length > 1
+ if QR.posts.length > 1 and QR.captcha.isEnabled and QR.captcha.captchas.length is 0
QR.captcha.setup()
post.rm()
QR.cooldown.set {req, post, isReply, threadID}
- URL = unless isReply # new thread
- "/#{g.BOARD}/res/#{threadID}"
+ URL = if threadID is postID # new thread
+ Build.path g.BOARD.ID, threadID
else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index
- "/#{g.BOARD}/res/#{threadID}#p#{postID}"
+ Build.path g.BOARD.ID, threadID, postID
if URL
if Conf['Open Post in New Tab']
diff --git a/src/Posting/QR.post.coffee b/src/Posting/QR.post.coffee
index 799c17e9f..846b7e59f 100644
--- a/src/Posting/QR.post.coffee
+++ b/src/Posting/QR.post.coffee
@@ -91,8 +91,6 @@ QR.post = class
return unless @ is QR.selected
for name in ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'] when node = QR.nodes[name]
node.disabled = lock
- if QR.captcha.isEnabled
- QR.captcha.nodes.input.disabled = lock
@nodes.rm.style.visibility = if lock then 'hidden' else ''
(if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
@nodes.spoiler.disabled = lock
@@ -171,27 +169,39 @@ QR.post = class
@showFileData()
else
@updateFilename()
- unless /^image/.test file.type
+ unless /^(image|video)\//.test file.type
@nodes.el.style.backgroundImage = null
return
@setThumbnail()
setThumbnail: ->
# Create a redimensioned thumbnail.
- img = $.el 'img'
+ isVideo = /^video\//.test @file.type
+ el = $.el (if isVideo then 'video' else 'img')
+
+ $.on el, (if isVideo then 'loadeddata' else 'load'), =>
+ # Verify element dimensions.
+ errors = @checkDimensions el, isVideo
+ if errors.length
+ QR.error error for error in errors
+ @URL = fileURL # this.removeFile will revoke this proper.
+ return @rmFile()
- img.onload = =>
# Generate thumbnails only if they're really big.
# Resized pictures through canvases look like ass,
# so we generate thumbnails `s` times bigger then expected
# to avoid crappy resized quality.
s = 90 * 2 * window.devicePixelRatio
s *= 3 if @file.type is 'image/gif' # let them animate
- {height, width} = img
- if height < s or width < s
- @URL = fileURL
- @nodes.el.style.backgroundImage = "url(#{@URL})"
- return
+ if isVideo
+ height = el.videoHeight
+ width = el.videoWidth
+ else
+ {height, width} = el
+ if height < s or width < s
+ @URL = fileURL
+ @nodes.el.style.backgroundImage = "url(#{@URL})"
+ return
if height <= width
width = s / height * width
height = s
@@ -199,16 +209,42 @@ QR.post = class
height = s / width * height
width = s
cv = $.el 'canvas'
- cv.height = img.height = height
- cv.width = img.width = width
- cv.getContext('2d').drawImage img, 0, 0, width, height
+ cv.height = el.height = height
+ cv.width = el.width = width
+ cv.getContext('2d').drawImage el, 0, 0, width, height
URL.revokeObjectURL fileURL
cv.toBlob (blob) =>
@URL = URL.createObjectURL blob
@nodes.el.style.backgroundImage = "url(#{@URL})"
fileURL = URL.createObjectURL @file
- img.src = fileURL
+ el.src = fileURL
+
+ checkDimensions: (el, video) ->
+ err = []
+ if video
+ {videoHeight, videoWidth, duration} = el
+ max_height = if QR.max_height < QR.max_height_video then QR.max_height else QR.max_height_video
+ max_width = if QR.max_width < QR.max_width_video then QR.max_width else QR.max_width_video
+ if videoHeight > max_height or videoWidth > max_width
+ err.push "#{@file.name}: Video too large (video: #{videoHeight}x#{videoWidth}px, max: #{max_height}x#{max_width}px)"
+ if videoHeight < QR.min_height or videoWidth < QR.min_width
+ err.push "#{@file.name}: Video too small (video: #{videoHeight}x#{videoWidth}px, min: #{QR.min_height}x#{QR.min_width}px)"
+ unless isFinite el.duration
+ err.push "#{file.name}: Video lacks duration metadata (try remuxing)"
+ if duration > QR.max_duration_video
+ err.push "#{@file.name}: Video too long (video: #{duration}s, max: #{QR.max_duration_video}s)"
+ <% if (type === 'userscript') { %>
+ if el.mozHasAudio
+ err.push "#{file.name}: Audio not allowed"
+ <% } %>
+ else
+ {height, width} = el
+ if height > QR.max_height or width > QR.max_width
+ err.push "#{@file.name}: Image too large (image: #{height}x#{width}px, max: #{QR.max_height}x#{QR.max_width}px)"
+ if height < QR.min_height or width < QR.min_width
+ err.push "#{@file.name}: Image too small (image: #{height}x#{width}px, min: #{QR.min_height}x#{QR.min_width}px)"
+ err
rmFile: ->
return if @isLocked
diff --git a/src/Quotelinks/QuoteBacklink.coffee b/src/Quotelinks/QuoteBacklink.coffee
index ff4d3c5e8..839dc7efd 100755
--- a/src/Quotelinks/QuoteBacklink.coffee
+++ b/src/Quotelinks/QuoteBacklink.coffee
@@ -56,7 +56,7 @@ QuoteBacklink =
buildBacklink: (quoted, quoter) ->
frag = QuoteBacklink.frag.cloneNode true
a = frag.lastElementChild
- a.href = "/#{quoter.board}/thread/#{quoter.thread}#p#{quoter}"
+ a.href = Build.path quoter.board.ID, quoter.thread.ID, quoter.ID
a.textContent = text = QuoteBacklink.funk quoter.ID
if quoter.isDead
$.addClass a, 'deadlink'
diff --git a/src/Quotelinks/QuoteThreading.coffee b/src/Quotelinks/QuoteThreading.coffee
index 7b77a13a3..2ac2d695f 100755
--- a/src/Quotelinks/QuoteThreading.coffee
+++ b/src/Quotelinks/QuoteThreading.coffee
@@ -97,7 +97,7 @@ QuoteThreading =
if post = posts[post.ID]
posts.after post, posts[@ID]
- else
+ else if posts[@ID]
posts.prepend posts[@ID]
return true
@@ -128,4 +128,4 @@ QuoteThreading =
kb: ->
control = $.id 'threadingControl'
control.checked = not control.checked
- QuoteThreading.toggle.call control
\ No newline at end of file
+ QuoteThreading.toggle.call control
diff --git a/src/Quotelinks/Quotify.coffee b/src/Quotelinks/Quotify.coffee
index 691312044..5186f024c 100755
--- a/src/Quotelinks/Quotify.coffee
+++ b/src/Quotelinks/Quotify.coffee
@@ -44,7 +44,7 @@ Quotify =
# Don't add 'deadlink' when quotifying in an archived post,
# and we don't know if the post died yet.
a = $.el 'a',
- href: "/#{boardID}/thread/#{post.thread}#p#{postID}"
+ href: Build.path boardID, post.thread.ID, postID
className: if post.isDead then 'quotelink deadlink' else 'quotelink'
textContent: quote
$.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
diff --git a/src/Theming/Banner.coffee b/src/Theming/Banner.coffee
index 89cb13d56..74179d6e1 100644
--- a/src/Theming/Banner.coffee
+++ b/src/Theming/Banner.coffee
@@ -1,7 +1,7 @@
Banner =
init: ->
$.asap (-> d.body), ->
- $.asap (-> $ '.abovePostForm'), Banner.ready
+ $.asap (-> $ 'hr'), Banner.ready
ready: ->
banner = $ ".boardBanner"
@@ -17,7 +17,7 @@ Banner =
alt: '4chan'
title: 'Click to change'
- $.on img, 'click', Banner.cb.toggle
+ $.on img, 'click error', Banner.cb.toggle
Banner.cb.toggle.call img
$.prepend banner, img
@@ -45,7 +45,7 @@ Banner =
->
type = Object.keys(types)[Math.floor 3 * Math.random()]
- num = Math.floor types[type] * Math.random()
+ num = Math.floor types[type] * Math.random()
@src = "//s.4cdn.org/image/title/#{num}.#{type}"
click: (e) ->
@@ -96,4 +96,4 @@ Banner =
else
$.set string, cachedTest
$.set string2, cachedTest
- child
\ No newline at end of file
+ child