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 + "
" + comment + "
" + innerHTML: "
" + postCount + " / " + fileCount + " / " + pageCount + "
" + subject + "
" + comment + "
" }); 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;', "
\uf00d
+
No selected fileSpoiler\uf0c1Post from URL+Dump\uf00dRemove File
") }; @@ -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 + "
" + comment + "
" + innerHTML: "
" + postCount + " / " + fileCount + " / " + pageCount + "
" + subject + "
" + comment + "
" }); 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;', "
\uf00d
+
No selected fileSpoiler\uf0c1Post from URL+Dump\uf00dRemove File
") }; @@ -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 ''} + + + + #{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