diff --git a/4chan_x.user.js b/4chan_x.user.js index 351807b3f..059db7818 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -20,7 +20,7 @@ // @icon https://github.com/MayhemYDG/4chan-x/raw/stable/img/icon.gif // ==/UserScript== -/* 4chan X Alpha - Version 3.0.0 - 2013-02-13 +/* 4chan X Alpha - Version 3.0.0 - 2013-02-14 * http://mayhemydg.github.com/4chan-x/ * * Copyright (c) 2009-2011 James Campos @@ -43,7 +43,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageHover, Main, Menu, Notification, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, doc, g, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Main, Menu, Notification, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, doc, g, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; @@ -74,7 +74,6 @@ Imaging: { 'Auto-GIF': [false, 'Animate GIF thumbnails.'], 'Image Expansion': [true, 'Expand images.'], - 'Expand From Position': [true, 'Expand all images only from current position to thread end.'], 'Image Hover': [false, 'Show full image on mouseover.'], 'Sauce': [true, 'Add sauce links to images.'], 'Reveal Spoilers': [false, 'Reveal spoiler thumbnails.'] @@ -168,8 +167,7 @@ 'Auto Update': [true, 'Automatically fetch new posts.'] }, 'Interval': 30 - }, - imageFit: 'fit width' + } }; if (!/^(boards|images|sys)\.4chan\.org$/.test(location.hostname)) { @@ -722,7 +720,11 @@ style = $.el('style', { textContent: css }); - $.add(d.head, style); + $.asap((function() { + return d.head; + }), function() { + return $.add(d.head, style); + }); return style; }, x: function(path, root) { @@ -955,7 +957,7 @@ $.on(toggleBar, 'click', this.toggleBar); $.prepend(headerBar, [menuButton, boardListButton, $.tn(' '), boardTitle, boardList, toggleBar]); catalogToggler = $.el('label', { - innerHTML: " Use catalog links" + innerHTML: " Use catalog links" }); $.on(catalogToggler.firstElementChild, 'change', this.toggleCatalogLinks); $.event('AddMenuEntry', { @@ -1902,7 +1904,7 @@ }, toggle: function(e) { var post; - post = this.dataset.clone ? Get.postFromRoot($.x('ancestor::div[contains(@class,"postContainer")][1]', this)) : g.posts[this.dataset.postid]; + post = this.dataset.clone ? Get.postFromNode(this) : g.posts[this.dataset.postid]; return Menu.menu.toggle(e, this, post); } }; @@ -2492,6 +2494,12 @@ return post; } }, + postFromNode: function(root) { + return Get.postFromRoot($.x('ancestor::div[contains(@class,"postContainer")][1]', root)); + }, + contextFromLink: function(quotelink) { + return Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', quotelink)); + }, postDataFromLink: function(link) { var board, path, postID, threadID; if (link.hostname === 'boards.4chan.org') { @@ -2544,9 +2552,6 @@ return board === post.board.ID && postID === post.ID; }); }, - contextFromLink: function(quotelink) { - return Get.postFromRoot($.x('ancestor::div[parent::div[@class="thread"]][1]', quotelink)); - }, postClone: function(board, threadID, postID, root, context) { var post, url; if (post = g.posts["" + board + "." + postID]) { @@ -3409,6 +3414,244 @@ } }; + ImageExpand = { + init: function() { + if (g.VIEW === 'catalog' || !Conf['Image Expansion']) { + return; + } + return Post.prototype.callbacks.push({ + name: 'Image Expansion', + cb: this.node + }); + }, + node: function() { + if (!(this.file && this.file.isImage)) { + return; + } + $.on(this.file.thumb.parentNode, 'click', ImageExpand.cb.toggle); + if (ImageExpand.on && !this.isHidden) { + return ImageExpand.expand(this); + } + }, + cb: { + toggle: function(e) { + if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) { + return; + } + e.preventDefault(); + return ImageExpand.toggle(Get.postFromNode(this)); + }, + all: function() { + var ID, file, post, thumb, _i, _len, _ref, _ref1; + $.event('CloseMenu'); + ImageExpand.on = this.checked; + _ref = g.posts; + for (ID in _ref) { + post = _ref[ID]; + _ref1 = [post].concat(post.clones); + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + post = _ref1[_i]; + file = post.file; + if (!(file && file.isImage)) { + continue; + } + thumb = file.thumb; + if (!doc.contains(thumb)) { + continue; + } + if (ImageExpand.on) { + if (!ImageExpand.spoilers && file.isSpoiler || ImageExpand.fromPosition && thumb.getBoundingClientRect().top < 0) { + continue; + } + ImageExpand.expand(post); + } else { + ImageExpand.contract(post); + } + } + } + }, + updateFitness: function() { + var checked; + checked = this.checked; + (checked ? $.addClass : $.rmClass)(doc, this.name.toLowerCase().replace(/\s+/g, '-')); + if (this.name !== 'Fit height') { + return; + } + if (checked) { + $.on(window, 'resize', ImageExpand.resize); + if (!ImageExpand.style) { + ImageExpand.style = $.addStyle(''); + } + return ImageExpand.resize(); + } else { + return $.off(window, 'resize', ImageExpand.resize); + } + }, + spoilers: function() { + return ImageExpand.spoilers = this.checked; + }, + position: function() { + return ImageExpand.fromPosition = this.checked; + } + }, + toggle: function(post) { + var headRect, postRect, rect, thumb, top; + thumb = post.file.thumb; + if (!thumb.hidden) { + ImageExpand.expand(post); + return; + } + rect = thumb.parentNode.getBoundingClientRect(); + if (rect.bottom > 0) { + postRect = post.nodes.root.getBoundingClientRect(); + headRect = $.id('header-bar').getBoundingClientRect(); + top = postRect.top - headRect.top - headRect.height - 2; + if ($.engine === 'webkit') { + if (rect.top < 0) { + d.body.scrollTop += top; + } + if (rect.left < 0) { + d.body.scrollLeft = 0; + } + } else { + if (rect.top < 0) { + d.documentElement.scrollTop += top; + } + if (rect.left < 0) { + d.documentElement.scrollLeft = 0; + } + } + } + return ImageExpand.contract(post); + }, + contract: function(post) { + var img, thumb; + thumb = post.file.thumb; + thumb.hidden = false; + if (img = $('.full-image', thumb.parentNode)) { + img.hidden = true; + } + return $.rmClass(post.nodes.root, 'expanded-image'); + }, + expand: function(post) { + var img, thumb; + thumb = post.file.thumb; + if (post.isHidden || thumb.hidden) { + return; + } + thumb.hidden = true; + $.addClass(post.nodes.root, 'expanded-image'); + if (img = $('.full-image', thumb.parentNode)) { + img.hidden = false; + return; + } + img = $.el('img', { + className: 'full-image', + src: post.file.URL + }); + $.on(img, 'error', ImageExpand.error); + return $.after(thumb, img); + }, + error: function() { + var URL, post, src, timeoutID; + post = Get.postFromNode(this); + ImageExpand.contract(post); + $.rm(this); + src = this.src.split('/'); + if (!(src[2] === 'images.4chan.org' && (URL = Redirect.image(src[3], src[5])))) { + if (g.DEAD) { + return; + } + URL = post.file.URL; + } + if ($.engine !== 'webkit' && URL.split('/')[2] === 'images.4chan.org') { + return; + } + timeoutID = setTimeout(ImageExpand.expand, 10000, post); + if ($.engine !== 'webkit' || URL.split('/')[2] !== 'images.4chan.org') { + return; + } + return $.ajax(URL, { + onreadystatechange: (function() { + if (this.status === 404) { + return clearTimeout(timeoutID); + } + }) + }, { + type: 'head' + }); + }, + menu: { + init: function() { + var createSubEntry, el, subEntries; + if (g.VIEW === 'catalog' || !Conf['Image Expansion']) { + return; + } + el = $.el('span', { + textContent: 'Image expansion' + }); + ImageExpand.menu.config = $.get('ImageExpansionConfig', { + 'Fit width': true, + 'Fit height': false, + 'Expand spoilers': false, + 'Expand from here': true + }); + createSubEntry = ImageExpand.menu.createSubEntry; + subEntries = []; + subEntries.push(createSubEntry('Expand all')); + subEntries.push(createSubEntry('Fit width', true)); + subEntries.push(createSubEntry('Fit height', true)); + subEntries.push(createSubEntry('Expand spoilers', true)); + subEntries.push(createSubEntry('Expand from here', true)); + return $.event('AddMenuEntry', { + type: 'header', + el: el, + order: 20, + subEntries: subEntries + }); + }, + createSubEntry: function(type, hasConfig) { + var input, label; + label = $.el('label', { + innerHTML: " " + type + }); + input = label.firstElementChild; + switch (type) { + case 'Expand all': + $.on(input, 'change', ImageExpand.cb.all); + break; + case 'Expand spoilers': + label.title = 'Expand all images along with spoilers.'; + $.on(input, 'change', ImageExpand.cb.spoilers); + break; + case 'Expand from here': + label.title = 'Expand all images only from current position to thread end.'; + $.on(input, 'change', ImageExpand.cb.position); + break; + default: + $.on(input, 'change', ImageExpand.cb.updateFitness); + } + if (hasConfig) { + input.checked = ImageExpand.menu.config[type]; + $.event('change', null, input); + $.on(input, 'change', ImageExpand.menu.saveConfig); + } + return { + el: label + }; + }, + saveConfig: function() { + var config; + config = ImageExpand.menu.config; + config[this.name] = this.checked; + return $.set('ImageExpansionConfig', config); + } + }, + resize: function() { + return ImageExpand.style.textContent = ":root.fit-height .full-image {max-height:" + doc.clientHeight + "px}"; + } + }; + RevealSpoilers = { init: function() { if (g.VIEW === 'catalog' || !Conf['Reveal Spoilers']) { @@ -3505,28 +3748,28 @@ return root.dispatchEvent(e); }, error: function() { - var src, timeoutID, url, + var URL, src, timeoutID, _this = this; if (!this.parentNode) { return; } src = this.src.split('/'); - if (!(src[2] === 'images.4chan.org' && (url = Redirect.image(src[3], src[5])))) { + if (!(src[2] === 'images.4chan.org' && (URL = Redirect.image(src[3], src[5])))) { if (g.DEAD) { return; } - url = "//images.4chan.org/" + src[3] + "/src/" + src[5]; + URL = post.file.URL; } - if ($.engine !== 'webkit' && url.split('/')[2] === 'images.4chan.org') { + if ($.engine !== 'webkit' && URL.split('/')[2] === 'images.4chan.org') { return; } timeoutID = setTimeout((function() { - return _this.src = url; + return _this.src = URL; }), 3000); - if ($.engine !== 'webkit' || url.split('/')[2] !== 'images.4chan.org') { + if ($.engine !== 'webkit' || URL.split('/')[2] !== 'images.4chan.org') { return; } - return $.ajax(url, { + return $.ajax(URL, { onreadystatechange: (function() { if (this.status === 404) { return clearTimeout(timeoutID); @@ -3968,7 +4211,7 @@ if (!QR.el) { return; } - if (g.dead) { + if (g.DEAD) { value = 404; disabled = true; QR.cooldown.auto = false; @@ -5155,6 +5398,8 @@ initFeature('Time Formatting', Time); initFeature('File Info Formatting', FileInfo); initFeature('Sauce', Sauce); + initFeature('Image Expansion', ImageExpand); + initFeature('Image Expansion (Menu)', ImageExpand.menu); initFeature('Reveal Spoilers', RevealSpoilers); initFeature('Auto-GIF', AutoGIF); initFeature('Image Hover', ImageHover); @@ -5326,7 +5571,7 @@ }); return [message, error]; }, - css: "/* general */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.field {\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\n-webkit-transition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field:-moz-placeholder,\n.field:hover:-moz-placeholder {\ncolor: #AAA !important;\n}\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.move {\ncursor: move;\n}\nlabel {\ncursor: pointer;\n}\na[href=\"javascript:;\"] {\ntext-decoration: none;\n}\n.warning {\ncolor: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\ndisplay: block !important;\n}\n.post {\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\nposition: fixed;\n}\n#notifications {\nz-index: 80;\n}\n#qp, #ihover {\nz-index: 70;\n}\n#menu {\nz-index: 60;\n}\n#updater, #stats {\nz-index: 50;\n}\n#header:hover {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#header {\nz-index: 20;\n}\n#watcher {\nz-index: 10;\n}\n\n/* Header */\n.fourchan-x body {\nmargin-top: 2em;\n}\n.fourchan-x #boardNavDesktop,\n.fourchan-x #navtopright,\n.fourchan-x #boardNavDesktopFoot {\ndisplay: none !important;\n}\n#header {\ntop: 0;\nright: 0;\nleft: 0;\n}\n#header-bar {\nborder-width: 0 0 1px;\npadding: 4px;\nposition: relative;\n-webkit-transition: all .1s ease-in-out;\ntransition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\nbox-shadow: none;\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n-webkit-transition: all .75s .25s ease-in-out;\ntransition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\ncursor: n-resize;\nleft: 0;\nright: 0;\nbottom: -8px;\nheight: 10px;\nposition: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a {\ntext-decoration: none;\npadding: 1px;\n}\n#header-bar > .menu-button {\nfloat: right;\npadding: 0;\n}\n\n/* notifications */\n#notifications {\ntext-align: center;\n}\n.notification {\ncolor: #FFF;\nfont-weight: 700;\ntext-shadow: 0 1px 2px rgba(0, 0, 0, .5);\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder-radius: 2px;\nmargin: 1px auto;\nwidth: 500px;\nmax-width: 100%;\nposition: relative;\n-webkit-transition: all .25s ease-in-out;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 40%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 40%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 40%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 40%, .9);\n}\n.notification > .close {\ncolor: white;\npadding: 4px 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 4px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* thread updater */\n#updater {\ntext-align: right;\n}\n#updater:not(:hover) {\nbackground: none;\nborder: none;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n.new {\ncolor: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\ntext-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\ntext-decoration: none !important;\n}\n.inlined {\nopacity: .5;\n}\n#qp input, .forwarded {\ndisplay: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\ntext-decoration: none;\nborder-bottom: 1px dashed;\n}\n.filtered {\ntext-decoration: underline line-through;\n}\n.inline {\nborder: 1px solid;\ndisplay: table;\nmargin: 2px 0;\n}\n.inline .post {\nborder: 0 !important;\nbackground-color: transparent !important;\ndisplay: table !important;\nmargin: 0 !important;\npadding: 1px 2px !important;\n}\n#qp {\npadding: 2px 2px 5px;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 0;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\nbox-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\ndisplay: none;\n}\n#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\nfloat: left;\nmargin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\ndisplay: none !important;\n}\n\n/* QR */\n.hide-original-post-form #postForm,\n.hide-original-post-form .postingMode {\ndisplay: none;\n}\n#qr > .move {\nmin-width: 300px;\noverflow: hidden;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 0 2px;\n}\n#qr > .move > span {\nfloat: right;\n}\n#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {\ncursor: pointer;\n}\n#qr select {\nmargin: 0;\n}\n#dump {\nbackground: -webkit-linear-gradient(#EEE, #CCC);\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\nwidth: 30px;\n}\n.gecko #dump {\npadding: 1px 0 2px;\nwidth: 10%;\n}\n#dump:hover, #dump:focus {\nbackground: -webkit-linear-gradient(#FFF, #DDD);\nbackground: linear-gradient(#FFF, #DDD);\n}\n#dump:active, .dump #dump:not(:hover):not(:focus) {\nbackground: -webkit-linear-gradient(#CCC, #DDD);\nbackground: linear-gradient(#CCC, #DDD);\n}\n#qr:not(.dump) #replies, .dump > form > label {\ndisplay: none;\n}\n#replies {\ndisplay: block;\nheight: 100px;\nposition: relative;\n-webkit-user-select: none;\n-moz-user-select: none;\n-o-user-select: none;\nuser-select: none;\n}\n#replies > div {\ncounter-reset: qrpreviews;\ntop: 0; right: 0; bottom: 0; left: 0;\nmargin: 0; padding: 0;\noverflow: hidden;\nposition: absolute;\nwhite-space: pre;\n}\n#replies > div:hover {\nbottom: -10px;\noverflow-x: auto;\nz-index: 1;\n}\n.qrpreview {\nbackground-position: 50% 20%;\nbackground-size: cover;\nborder: 1px solid #808080;\ncolor: #FFF !important;\nfont-size: 12px;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncursor: move;\ndisplay: inline-block;\nheight: 90px; width: 90px;\nmargin: 5px; padding: 2px;\nopacity: .6;\noutline: none;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-webkit-transition: opacity .25s ease-in-out;\ntransition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qrpreview:hover, .qrpreview:focus {\nopacity: .9;\ncolor: #FFF !important;\n}\n.qrpreview#selected {\nopacity: 1;\n}\n.qrpreview::before {\ncounter-increment: qrpreviews;\ncontent: counter(qrpreviews);\nfont-weight: 700;\ntext-shadow: 0 0 3px #000, 0 0 5px #000;\nposition: absolute;\ntop: 3px; right: 3px;\n}\n.qrpreview.drag {\nborder-color: red;\nborder-style: dashed;\n}\n.qrpreview.over {\nborder-color: #FFF;\nborder-style: dashed;\n}\n.remove {\ncolor: #E00 !important;\nfont-weight: 700;\npadding: 3px;\n}\n.remove:hover::after {\ncontent: ' Remove';\n}\n.qrpreview > label {\nbackground: rgba(0, 0, 0, .5);\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qrpreview > label > input {\nmargin: 1px 0;\nvertical-align: bottom;\n}\n#addReply {\nfont-size: 3.5em;\nline-height: 100px;\n}\n.persona {\ndisplay: -webkit-flex;\ndisplay: flex;\n}\n.persona .field {\n-webkit-flex: 1;\nflex: 1;\n}\n.gecko .persona .fieldĀ {\nwidth: 30%;\n}\n#qr textarea.field {\ndisplay: -webkit-box;\nmin-height: 160px;\nmin-width: 100%;\n}\n#qr.captcha textarea.field {\nmin-height: 120px;\n}\n.textarea {\nposition: relative;\n}\n#charCount {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nmargin: 1px;\nposition: absolute;\nbottom: 0;\nright: 0;\npointer-events: none;\n}\n#charCount.warning {\ncolor: red;\n}\n.captchainput > .field {\nmin-width: 100%;\n}\n.captchaimg {\nbackground: #FFF;\noutline: 1px solid #CCC;\noutline-offset: -1px;\ntext-align: center;\n}\n.captchaimg > img {\ndisplay: block;\nheight: 57px;\nwidth: 300px;\n}\n#qr [type=file] {\nmargin: 1px 0;\nwidth: 70%;\n}\n#qr [type=submit] {\nmargin: 1px 0;\npadding: 1px; /* not Gecko */\nwidth: 30%;\n}\n.gecko #qr [type=submit] {\npadding: 0 1px; /* Gecko does not respect box-sizing: border-box */\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\n}\n.menu-button > span {\nborder-top: 6px solid;\nborder-right: 4px solid transparent;\nborder-left: 4px solid transparent;\ndisplay: inline-block;\nmargin: 2px;\nvertical-align: middle;\n}\n#menu {\nborder-bottom: 0;\ndisplay: -webkit-flex;\ndisplay: flex;\n-webkit-flex-flow: column nowrap;\nflex-flow: column nowrap;\nposition: absolute;\noutline: none;\n}\n.entry {\ncursor: pointer;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry.has-submenu {\npadding-right: 20px;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: 6px solid;\nborder-top: 4px solid transparent;\nborder-bottom: 4px solid transparent;\ndisplay: inline-block;\nmargin: 4px;\nposition: absolute;\nright: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\ndisplay: none;\n}\n.submenu {\nborder-bottom: 0;\ndisplay: -webkit-flex;\ndisplay: flex;\n-webkit-flex-flow: column nowrap;\nflex-flow: column nowrap;\nposition: absolute;\nmargin: -1px 0;\n}\n.entry input {\nmargin: 0;\n}\n\n/* general */\n:root.yotsuba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.yotsuba #header-bar {\nfont-size: 9pt;\ncolor: #B86;\n}\n:root.yotsuba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.yotsuba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.yotsuba-b .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.yotsuba-b #header-bar {\nfont-size: 9pt;\ncolor: #89A;\n}\n:root.yotsuba-b #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba-b .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.yotsuba-b .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.futaba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.futaba #header-bar {\nfont-size: 11pt;\ncolor: #B86;\n}\n:root.futaba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.futaba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.futaba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.burichan .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.burichan #header-bar {\nfont-size: 11pt;\ncolor: #89A;\n}\n:root.burichan #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.burichan .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.burichan .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.tomorrow .dialog {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow .field:focus {\nborder-color: #000;\n}\n\n/* Header */\n:root.tomorrow #header-bar {\nfont-size: 9pt;\ncolor: #C5C8C6;\n}\n:root.tomorrow #header-bar a {\ncolor: #81A2BE;\n}\n\n/* quote */\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n:root.tomorrow .qrpreview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\n}\n:root.tomorrow .focused.entry {\nbackground: rgba(0, 0, 0, .33);\n}\n\n/* general */\n:root.photon .dialog {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.photon #header-bar {\nfont-size: 9pt;\ncolor: #333;\n}\n:root.photon #header-bar a {\ncolor: #FF6600;\n}\n\n/* quote */\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.photon .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" + css: "/* general */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.field {\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\n-webkit-transition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field:-moz-placeholder,\n.field:hover:-moz-placeholder {\ncolor: #AAA !important;\n}\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.move {\ncursor: move;\n}\nlabel {\ncursor: pointer;\n}\na[href=\"javascript:;\"] {\ntext-decoration: none;\n}\n.warning {\ncolor: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\ndisplay: block !important;\n}\n.post {\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\nposition: fixed;\n}\n#notifications {\nz-index: 80;\n}\n#qp, #ihover {\nz-index: 70;\n}\n#menu {\nz-index: 60;\n}\n#updater, #stats {\nz-index: 50;\n}\n#header:hover {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#header {\nz-index: 20;\n}\n#watcher {\nz-index: 10;\n}\n\n/* Header */\n.fourchan-x body {\nmargin-top: 2em;\n}\n.fourchan-x #boardNavDesktop,\n.fourchan-x #navtopright,\n.fourchan-x #boardNavDesktopFoot {\ndisplay: none !important;\n}\n#header {\ntop: 0;\nright: 0;\nleft: 0;\n}\n#header-bar {\nborder-width: 0 0 1px;\npadding: 4px;\nposition: relative;\n-webkit-transition: all .1s ease-in-out;\ntransition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\nbox-shadow: none;\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n-webkit-transition: all .75s .25s ease-in-out;\ntransition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\ncursor: n-resize;\nleft: 0;\nright: 0;\nbottom: -8px;\nheight: 10px;\nposition: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a {\ntext-decoration: none;\npadding: 1px;\n}\n#header-bar > .menu-button {\nfloat: right;\npadding: 0;\n}\n\n/* notifications */\n#notifications {\ntext-align: center;\n}\n.notification {\ncolor: #FFF;\nfont-weight: 700;\ntext-shadow: 0 1px 2px rgba(0, 0, 0, .5);\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder-radius: 2px;\nmargin: 1px auto;\nwidth: 500px;\nmax-width: 100%;\nposition: relative;\n-webkit-transition: all .25s ease-in-out;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 40%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 40%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 40%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 40%, .9);\n}\n.notification > .close {\ncolor: white;\npadding: 4px 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 4px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* thread updater */\n#updater {\ntext-align: right;\n}\n#updater:not(:hover) {\nbackground: none;\nborder: none;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n.new {\ncolor: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\ntext-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\ntext-decoration: none !important;\n}\n.inlined {\nopacity: .5;\n}\n#qp input, .forwarded {\ndisplay: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\ntext-decoration: none;\nborder-bottom: 1px dashed;\n}\n.filtered {\ntext-decoration: underline line-through;\n}\n.inline {\nborder: 1px solid;\ndisplay: table;\nmargin: 2px 0;\n}\n.inline .post {\nborder: 0 !important;\nbackground-color: transparent !important;\ndisplay: table !important;\nmargin: 0 !important;\npadding: 1px 2px !important;\n}\n#qp {\npadding: 2px 2px 5px;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 0;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\nbox-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\ndisplay: none;\n}\n:root.fit-width .full-image {\nmax-width: 100%;\n}\n:root.gecko.fit-width .full-image,\n:root.presto.fit-width .full-image {\nwidth: 100%;\n}\n.expanded-image > .op > .file > .fileThumb,\n.expanded-image > .op > .file > .fileThumb > .full-image {\nfloat: none;\n}\n#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\nfloat: left;\nmargin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\ndisplay: none !important;\n}\n\n/* QR */\n.hide-original-post-form #postForm,\n.hide-original-post-form .postingMode {\ndisplay: none;\n}\n#qr > .move {\nmin-width: 300px;\noverflow: hidden;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 0 2px;\n}\n#qr > .move > span {\nfloat: right;\n}\n#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {\ncursor: pointer;\n}\n#qr select {\nmargin: 0;\n}\n#dump {\nbackground: -webkit-linear-gradient(#EEE, #CCC);\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\nwidth: 30px;\n}\n.gecko #dump {\npadding: 1px 0 2px;\nwidth: 10%;\n}\n#dump:hover, #dump:focus {\nbackground: -webkit-linear-gradient(#FFF, #DDD);\nbackground: linear-gradient(#FFF, #DDD);\n}\n#dump:active, .dump #dump:not(:hover):not(:focus) {\nbackground: -webkit-linear-gradient(#CCC, #DDD);\nbackground: linear-gradient(#CCC, #DDD);\n}\n#qr:not(.dump) #replies, .dump > form > label {\ndisplay: none;\n}\n#replies {\ndisplay: block;\nheight: 100px;\nposition: relative;\n-webkit-user-select: none;\n-moz-user-select: none;\n-o-user-select: none;\nuser-select: none;\n}\n#replies > div {\ncounter-reset: qrpreviews;\ntop: 0; right: 0; bottom: 0; left: 0;\nmargin: 0; padding: 0;\noverflow: hidden;\nposition: absolute;\nwhite-space: pre;\n}\n#replies > div:hover {\nbottom: -10px;\noverflow-x: auto;\nz-index: 1;\n}\n.qrpreview {\nbackground-position: 50% 20%;\nbackground-size: cover;\nborder: 1px solid #808080;\ncolor: #FFF !important;\nfont-size: 12px;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncursor: move;\ndisplay: inline-block;\nheight: 90px; width: 90px;\nmargin: 5px; padding: 2px;\nopacity: .6;\noutline: none;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-webkit-transition: opacity .25s ease-in-out;\ntransition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qrpreview:hover, .qrpreview:focus {\nopacity: .9;\ncolor: #FFF !important;\n}\n.qrpreview#selected {\nopacity: 1;\n}\n.qrpreview::before {\ncounter-increment: qrpreviews;\ncontent: counter(qrpreviews);\nfont-weight: 700;\ntext-shadow: 0 0 3px #000, 0 0 5px #000;\nposition: absolute;\ntop: 3px; right: 3px;\n}\n.qrpreview.drag {\nborder-color: red;\nborder-style: dashed;\n}\n.qrpreview.over {\nborder-color: #FFF;\nborder-style: dashed;\n}\n.remove {\ncolor: #E00 !important;\nfont-weight: 700;\npadding: 3px;\n}\n.remove:hover::after {\ncontent: ' Remove';\n}\n.qrpreview > label {\nbackground: rgba(0, 0, 0, .5);\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qrpreview > label > input {\nmargin: 1px 0;\nvertical-align: bottom;\n}\n#addReply {\nfont-size: 3.5em;\nline-height: 100px;\n}\n.persona {\ndisplay: -webkit-flex;\ndisplay: flex;\n}\n.persona .field {\n-webkit-flex: 1;\nflex: 1;\n}\n.gecko .persona .fieldĀ {\nwidth: 30%;\n}\n#qr textarea.field {\ndisplay: -webkit-box;\nmin-height: 160px;\nmin-width: 100%;\n}\n#qr.captcha textarea.field {\nmin-height: 120px;\n}\n.textarea {\nposition: relative;\n}\n#charCount {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nmargin: 1px;\nposition: absolute;\nbottom: 0;\nright: 0;\npointer-events: none;\n}\n#charCount.warning {\ncolor: red;\n}\n.captchainput > .field {\nmin-width: 100%;\n}\n.captchaimg {\nbackground: #FFF;\noutline: 1px solid #CCC;\noutline-offset: -1px;\ntext-align: center;\n}\n.captchaimg > img {\ndisplay: block;\nheight: 57px;\nwidth: 300px;\n}\n#qr [type=file] {\nmargin: 1px 0;\nwidth: 70%;\n}\n#qr [type=submit] {\nmargin: 1px 0;\npadding: 1px; /* not Gecko */\nwidth: 30%;\n}\n.gecko #qr [type=submit] {\npadding: 0 1px; /* Gecko does not respect box-sizing: border-box */\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\n}\n.menu-button > span {\nborder-top: 6px solid;\nborder-right: 4px solid transparent;\nborder-left: 4px solid transparent;\ndisplay: inline-block;\nmargin: 2px;\nvertical-align: middle;\n}\n#menu {\nborder-bottom: 0;\ndisplay: -webkit-flex;\ndisplay: flex;\n-webkit-flex-flow: column nowrap;\nflex-flow: column nowrap;\nposition: absolute;\noutline: none;\n}\n.entry {\ncursor: pointer;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry.has-submenu {\npadding-right: 20px;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: 6px solid;\nborder-top: 4px solid transparent;\nborder-bottom: 4px solid transparent;\ndisplay: inline-block;\nmargin: 4px;\nposition: absolute;\nright: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\ndisplay: none;\n}\n.submenu {\nborder-bottom: 0;\ndisplay: -webkit-flex;\ndisplay: flex;\n-webkit-flex-flow: column nowrap;\nflex-flow: column nowrap;\nposition: absolute;\nmargin: -1px 0;\n}\n.entry input {\nmargin: 0;\n}\n\n/* general */\n:root.yotsuba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.yotsuba #header-bar {\nfont-size: 9pt;\ncolor: #B86;\n}\n:root.yotsuba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.yotsuba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.yotsuba-b .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.yotsuba-b #header-bar {\nfont-size: 9pt;\ncolor: #89A;\n}\n:root.yotsuba-b #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba-b .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.yotsuba-b .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.futaba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.futaba #header-bar {\nfont-size: 11pt;\ncolor: #B86;\n}\n:root.futaba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.futaba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.futaba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.burichan .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.burichan #header-bar {\nfont-size: 11pt;\ncolor: #89A;\n}\n:root.burichan #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.burichan .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.burichan .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.tomorrow .dialog {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow .field:focus {\nborder-color: #000;\n}\n\n/* Header */\n:root.tomorrow #header-bar {\nfont-size: 9pt;\ncolor: #C5C8C6;\n}\n:root.tomorrow #header-bar a {\ncolor: #81A2BE;\n}\n\n/* quote */\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n:root.tomorrow .qrpreview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\n}\n:root.tomorrow .focused.entry {\nbackground: rgba(0, 0, 0, .33);\n}\n\n/* general */\n:root.photon .dialog {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.photon #header-bar {\nfont-size: 9pt;\ncolor: #333;\n}\n:root.photon #header-bar a {\ncolor: #FF6600;\n}\n\n/* quote */\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.photon .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" }; Main.init(); diff --git a/changelog b/changelog index 24a2f9227..cd0bd18ac 100644 --- a/changelog +++ b/changelog @@ -6,11 +6,15 @@ alpha From the Header's menu, access to: Settings Quick Reply shortcut + Image Expansion Can be auto-hidden. QR changes: Creating threads outside of the index is now possible. Selection-to-quote also applies to selected text inside the post, not just inside the comment. Quoting the OP will not insert the >>opnumber anymore unless the QR was already opened. + Image Expansion changes: + Expanding OP images won't squish replies anymore. + There is now a setting to allow expanding spoilers. Added touch and multi-touch support for dragging windows. The Thread Updater will pause when offline, and resume when online. Added Thread & Post Hiding in the Menu, with individual settings. diff --git a/css/style.css b/css/style.css index 4ad273979..9c30b04f2 100644 --- a/css/style.css +++ b/css/style.css @@ -249,6 +249,17 @@ a[href="javascript:;"] { .fileText:not(:hover) .fnfull { display: none; } +:root.fit-width .full-image { + max-width: 100%; +} +:root.gecko.fit-width .full-image, +:root.presto.fit-width .full-image { + width: 100%; +} +.expanded-image > .op > .file > .fileThumb, +.expanded-image > .op > .file > .fileThumb > .full-image { + float: none; +} #ihover { -moz-box-sizing: border-box; box-sizing: border-box; diff --git a/lib/$.coffee b/lib/$.coffee index e2c9cb9bb..eb476c47f 100644 --- a/lib/$.coffee +++ b/lib/$.coffee @@ -86,7 +86,8 @@ $.extend $, addStyle: (css) -> style = $.el 'style', textContent: css - $.add d.head, style + $.asap (-> d.head), -> + $.add d.head, style style x: (path, root=d.body) -> # XPathResult.ANY_UNORDERED_NODE_TYPE === 8 diff --git a/src/config.coffee b/src/config.coffee index 2237ac603..c9b4b5cdb 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -22,7 +22,6 @@ Config = Imaging: 'Auto-GIF': [false, 'Animate GIF thumbnails.'] 'Image Expansion': [true, 'Expand images.'] - 'Expand From Position': [true, 'Expand all images only from current position to thread end.'] 'Image Hover': [false, 'Show full image on mouseover.'] 'Sauce': [true, 'Add sauce links to images.'] 'Reveal Spoilers': [false, 'Reveal spoiler thumbnails.'] @@ -161,4 +160,3 @@ Config = 'Scroll BG': [false, 'Auto-scroll background tabs.'] 'Auto Update': [true, 'Automatically fetch new posts.'] 'Interval': 30 - imageFit: 'fit width' diff --git a/src/features.coffee b/src/features.coffee index 5ccbeba15..f69e2d939 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -38,7 +38,7 @@ Header = $.prepend headerBar, [menuButton, boardListButton, $.tn(' '), boardTitle, boardList, toggleBar] catalogToggler = $.el 'label', - innerHTML: " Use catalog links" + innerHTML: " Use catalog links" $.on catalogToggler.firstElementChild, 'change', @toggleCatalogLinks $.event 'AddMenuEntry', type: 'header' @@ -811,7 +811,7 @@ Menu = toggle: (e) -> post = if @dataset.clone - Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', @ + Get.postFromNode @ else g.posts[@dataset.postid] Menu.menu.toggle e, @, post @@ -1377,6 +1377,10 @@ Get = index = root.dataset.clone post = g.posts["#{board}.#{postID}"] if index then post.clones[index] else post + postFromNode: (root) -> + Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', root + contextFromLink: (quotelink) -> + Get.postFromRoot $.x 'ancestor::div[parent::div[@class="thread"]][1]', quotelink postDataFromLink: (link) -> if link.hostname is 'boards.4chan.org' path = link.pathname.split '/' @@ -1418,8 +1422,6 @@ Get = quotelinks.filter (quotelink) -> {board, postID} = Get.postDataFromLink quotelink board is post.board.ID and postID is post.ID - contextFromLink: (quotelink) -> - Get.postFromRoot $.x 'ancestor::div[parent::div[@class="thread"]][1]', quotelink postClone: (board, threadID, postID, root, context) -> if post = g.posts["#{board}.#{postID}"] Get.insert post, root, context @@ -2085,6 +2087,171 @@ Sauce = nodes.push $.tn('\u00A0'), link @, Sauce.link.cloneNode true $.add @file.info, nodes +ImageExpand = + init: -> + return if g.VIEW is 'catalog' or !Conf['Image Expansion'] + + Post::callbacks.push + name: 'Image Expansion' + cb: @node + + node: -> + return unless @file and @file.isImage + $.on @file.thumb.parentNode, 'click', ImageExpand.cb.toggle + if ImageExpand.on and !@isHidden + ImageExpand.expand @ + cb: + toggle: (e) -> + return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 + e.preventDefault() + ImageExpand.toggle Get.postFromNode @ + all: -> + $.event 'CloseMenu' + ImageExpand.on = @checked + for ID, post of g.posts + for post in [post].concat post.clones + {file} = post + continue unless file and file.isImage + {thumb} = file + continue unless doc.contains thumb + if ImageExpand.on + if !ImageExpand.spoilers and file.isSpoiler or + ImageExpand.fromPosition and thumb.getBoundingClientRect().top < 0 + continue + ImageExpand.expand post + else + ImageExpand.contract post + return + updateFitness: -> + {checked} = @ + (if checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-' + return unless @name is 'Fit height' + if checked + $.on window, 'resize', ImageExpand.resize + unless ImageExpand.style + ImageExpand.style = $.addStyle '' + ImageExpand.resize() + else + $.off window, 'resize', ImageExpand.resize + spoilers: -> + ImageExpand.spoilers = @checked + position: -> + ImageExpand.fromPosition = @checked + + toggle: (post) -> + {thumb} = post.file + unless thumb.hidden + ImageExpand.expand post + return + rect = thumb.parentNode.getBoundingClientRect() + if rect.bottom > 0 # Should be at least partially visible. + # Scroll back to the thumbnail when contracting the image + # to avoid being left miles away from the relevant post. + postRect = post.nodes.root.getBoundingClientRect() + headRect = $.id('header-bar').getBoundingClientRect() + top = postRect.top - headRect.top - headRect.height - 2 + if $.engine is 'webkit' + d.body.scrollTop += top if rect.top < 0 + d.body.scrollLeft = 0 if rect.left < 0 + else + d.documentElement.scrollTop += top if rect.top < 0 + d.documentElement.scrollLeft = 0 if rect.left < 0 + ImageExpand.contract post + + contract: (post) -> + {thumb} = post.file + thumb.hidden = false + if img = $ '.full-image', thumb.parentNode + img.hidden = true + $.rmClass post.nodes.root, 'expanded-image' + + expand: (post) -> + # Do not expand images of hidden/filtered replies, or already expanded pictures. + {thumb} = post.file + return if post.isHidden or thumb.hidden + thumb.hidden = true + $.addClass post.nodes.root, 'expanded-image' + if img = $ '.full-image', thumb.parentNode + # Expand already loaded picture + img.hidden = false + return + img = $.el 'img', + className: 'full-image' + src: post.file.URL + $.on img, 'error', ImageExpand.error + $.after thumb, img + + error: -> + post = Get.postFromNode @ + ImageExpand.contract post + $.rm @ + src = @src.split '/' + unless src[2] is 'images.4chan.org' and URL = Redirect.image src[3], src[5] + return if g.DEAD + {URL} = post.file + return if $.engine isnt 'webkit' and URL.split('/')[2] is 'images.4chan.org' + timeoutID = setTimeout ImageExpand.expand, 10000, post + # Only Chrome let userscripts do cross domain requests. + # Don't check for 404'd status in the archivers. + return if $.engine isnt 'webkit' or URL.split('/')[2] isnt 'images.4chan.org' + $.ajax URL, onreadystatechange: (-> clearTimeout timeoutID if @status is 404), + type: 'head' + + menu: + init: -> + return if g.VIEW is 'catalog' or !Conf['Image Expansion'] + + el = $.el 'span', + textContent: 'Image expansion' + + ImageExpand.menu.config = $.get 'ImageExpansionConfig', + 'Fit width': true + 'Fit height': false + 'Expand spoilers': false + 'Expand from here': true + + {createSubEntry} = ImageExpand.menu + subEntries = [] + subEntries.push createSubEntry 'Expand all' + subEntries.push createSubEntry 'Fit width', true + subEntries.push createSubEntry 'Fit height', true + subEntries.push createSubEntry 'Expand spoilers', true + subEntries.push createSubEntry 'Expand from here', true + + $.event 'AddMenuEntry', + type: 'header' + el: el + order: 20 + subEntries: subEntries + + createSubEntry: (type, hasConfig) -> + label = $.el 'label', + innerHTML: " #{type}" + input = label.firstElementChild + switch type + when 'Expand all' + $.on input, 'change', ImageExpand.cb.all + when 'Expand spoilers' + label.title = 'Expand all images along with spoilers.' + $.on input, 'change', ImageExpand.cb.spoilers + when 'Expand from here' + label.title = 'Expand all images only from current position to thread end.' + $.on input, 'change', ImageExpand.cb.position + else + $.on input, 'change', ImageExpand.cb.updateFitness + if hasConfig + input.checked = ImageExpand.menu.config[type] + $.event 'change', null, input + $.on input, 'change', ImageExpand.menu.saveConfig + el: label + saveConfig: -> + {config} = ImageExpand.menu + config[@name] = @checked + $.set 'ImageExpansionConfig', config + + resize: -> + ImageExpand.style.textContent = ":root.fit-height .full-image {max-height:#{doc.clientHeight}px}" + RevealSpoilers = init: -> return if g.VIEW is 'catalog' or !Conf['Reveal Spoilers'] @@ -2148,15 +2315,15 @@ ImageHover = error: -> return unless @parentNode src = @src.split '/' - unless src[2] is 'images.4chan.org' and url = Redirect.image src[3], src[5] + unless src[2] is 'images.4chan.org' and URL = Redirect.image src[3], src[5] return if g.DEAD - url = "//images.4chan.org/#{src[3]}/src/#{src[5]}" - return if $.engine isnt 'webkit' and url.split('/')[2] is 'images.4chan.org' - timeoutID = setTimeout (=> @src = url), 3000 + {URL} = post.file + return if $.engine isnt 'webkit' and URL.split('/')[2] is 'images.4chan.org' + timeoutID = setTimeout (=> @src = URL), 3000 # Only Chrome let userscripts do cross domain requests. # Don't check for 404'd status in the archivers. - return if $.engine isnt 'webkit' or url.split('/')[2] isnt 'images.4chan.org' - $.ajax url, onreadystatechange: (-> clearTimeout timeoutID if @status is 404), + return if $.engine isnt 'webkit' or URL.split('/')[2] isnt 'images.4chan.org' + $.ajax URL, onreadystatechange: (-> clearTimeout timeoutID if @status is 404), type: 'head' ThreadUpdater = diff --git a/src/main.coffee b/src/main.coffee index 5ef3026cf..8a89e35a2 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -316,6 +316,8 @@ Main = initFeature 'Time Formatting', Time initFeature 'File Info Formatting', FileInfo initFeature 'Sauce', Sauce + initFeature 'Image Expansion', ImageExpand + initFeature 'Image Expansion (Menu)', ImageExpand.menu initFeature 'Reveal Spoilers', RevealSpoilers initFeature 'Auto-GIF', AutoGIF initFeature 'Image Hover', ImageHover diff --git a/src/qr.coffee b/src/qr.coffee index 09f4c6ea6..3accbbe9f 100644 --- a/src/qr.coffee +++ b/src/qr.coffee @@ -96,7 +96,7 @@ QR = status: (data={}) -> return unless QR.el - if g.dead # XXX + if g.DEAD value = 404 disabled = true QR.cooldown.auto = false