diff --git a/.gitignore b/.gitignore index cd3a58c54..ec9d683c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules/ -tmp/ -4chan_x.user.js -Cakefile -script.coffee *~ *.db +tmp-crx/ +tmp-userjs/ +tmp-userscript/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 81fb35c70..b25e5ed4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +- Added the option `Hide Unread Count at (0)`, disabled by default. + ### 3.1.4 - *2013-04-17* - Fix QR remembering the file spoiler state when it shouldn't, for real this time. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 685ca0727..5e386b1ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,6 @@ -## Reporting bugs +## Reporting bugs and suggestions + +Reporting bugs: 1. Make sure both your **browser** and **4chan X** are up to date. 2. Disable your other extensions & scripts to identify conflicts. @@ -13,6 +15,12 @@ Open your console with: - `Ctrl + Shift + K` on Firefox. - `Ctrl + Shift + O` on Opera. +Respect these guidelines: +- Describe the issue clearly, put some effort into it. A one-liner isn't a good enough description. +- If you want to get your suggestion implemented sooner, make it convincing. +- If you want to criticize, make it convincing and constructive. +- Be mature. Act like an idiot and you will be blocked without warning. + ## Development & Contribution ### Get started diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 26ef696dd..249fa3fce 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -5,6 +5,11 @@ module.exports = (grunt) -> process: data: pkg + shellOptions = + stdout: true + stderr: true + failOnError: true + # Project configuration. grunt.initConfig pkg: pkg @@ -68,27 +73,25 @@ module.exports = (grunt) -> 'build-userscript' ] - exec: + shell: commit: - command: -> - release = "#{pkg.meta.name} v#{pkg.version}" - return [ - 'git checkout ' + pkg.meta.mainBranch, - 'git commit -am "Release ' + release + '."', - 'git tag -a ' + pkg.version + ' -m "' + release + '."', - 'git tag -af stable-v3 -m "' + release + '."' - ].join(' && '); + options: shellOptions + command: [ + 'git checkout <%= pkg.meta.mainBranch %>', + 'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."', + 'git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."', + 'git tag -af stable-v3 -m "<%= pkg.meta.name %> v<%= pkg.version %>."' + ].join(' && ') stdout: true push: - command: 'git push origin --all && git push origin --tags' - stdout: true + options: shellOptions + command: 'git push origin --tags -f && git push origin --all' watch: all: options: interrupt: true - nospawn: true files: [ 'Gruntfile.coffee' 'package.json' @@ -120,7 +123,7 @@ module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-contrib-concat' grunt.loadNpmTasks 'grunt-contrib-copy' grunt.loadNpmTasks 'grunt-contrib-watch' - grunt.loadNpmTasks 'grunt-exec' + grunt.loadNpmTasks 'grunt-shell' grunt.registerTask 'default', [ 'build' @@ -161,8 +164,8 @@ module.exports = (grunt) -> grunt.registerTask 'release', [ 'default' - 'exec:commit' - 'exec:push' + 'shell:commit' + 'shell:push' ] grunt.registerTask 'patch', [ diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000..e40c4d9d9 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,194 @@ +module.exports = function(grunt) { + + var pkg = grunt.file.readJSON('package.json'); + var concatOptions = { + process: { + data: pkg + } + }; + var shellOptions = { + stdout: true, + stderr: true, + failOnError: true + }; + + // Project configuration. + grunt.initConfig({ + pkg: pkg, + concat: { + coffee: { + options: concatOptions, + src: [ + 'src/config.coffee', + 'src/globals.coffee', + 'lib/ui.coffee', + 'lib/$.coffee', + 'lib/polyfill.coffee', + 'src/features.coffee', + 'src/qr.coffee', + 'src/report.coffee', + 'src/databoard.coffee', + 'src/main.coffee' + ], + dest: 'tmp-<%= pkg.type %>/script.coffee' + }, + crx: { + options: concatOptions, + files: { + 'builds/crx/manifest.json': 'src/manifest.json', + 'builds/crx/script.js': [ + 'src/banner.js', + 'tmp-<%= pkg.type %>/script.js' + ] + } + }, + userjs: { + options: concatOptions, + src: [ + 'src/metadata.js', + 'src/banner.js', + 'tmp-<%= pkg.type %>/script.js' + ], + dest: 'builds/<%= pkg.name %>.js' + }, + userscript: { + options: concatOptions, + files: { + 'builds/<%= pkg.name %>.meta.js': 'src/metadata.js', + 'builds/<%= pkg.name %>.user.js': [ + 'src/metadata.js', + 'src/banner.js', + 'tmp-<%= pkg.type %>/script.js' + ] + } + } + }, + copy: { + crx: { + src: 'img/*.png', + dest: 'builds/crx/', + expand: true, + flatten: true + } + }, + coffee: { + script: { + src: 'tmp-<%= pkg.type %>/script.coffee', + dest: 'tmp-<%= pkg.type %>/script.js' + } + }, + concurrent: { + build: ['build-crx', 'build-userjs', 'build-userscript'] + }, + shell: { + commit: { + options: shellOptions, + command: [ + 'git checkout <%= pkg.meta.mainBranch %>', + 'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."', + 'git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."', + 'git tag -af stable-v3 -m "<%= pkg.meta.name %> v<%= pkg.version %>."' + ].join(' && ') + }, + push: { + options: shellOptions, + command: 'git push origin --tags -f && git push origin --all' + } + }, + watch: { + all: { + options: { + interrupt: true + }, + files: [ + 'Gruntfile.js', + 'package.json', + 'lib/**/*', + 'src/**/*', + 'css/**/*', + 'img/**/*' + ], + tasks: 'build' + } + }, + compress: { + crx: { + options: { + archive: 'builds/<%= pkg.name %>.zip', + level: 9, + pretty: true + }, + expand: true, + flatten: true, + src: 'builds/crx/*', + dest: '/' + } + }, + clean: { + builds: 'builds', + tmpcrx: 'tmp-crx', + tmpuserjs: 'tmp-userjs', + tmpuserscript: 'tmp-userscript' + } + }); + + grunt.loadNpmTasks('grunt-bump'); + grunt.loadNpmTasks('grunt-concurrent'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-coffee'); + grunt.loadNpmTasks('grunt-contrib-compress'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-shell'); + + grunt.registerTask('default', ['build']); + + grunt.registerTask('set-build', 'Set the build type variable', function(type) { + pkg.type = type; + grunt.log.ok('pkg.type = %s', type); + }); + grunt.registerTask('build', ['concurrent:build']); + grunt.registerTask('build-crx', [ + 'set-build:crx', + 'concat:coffee', + 'coffee:script', + 'concat:crx', + 'copy:crx', + 'clean:tmpcrx' + ]); + grunt.registerTask('build-userjs', [ + 'set-build:userjs', + 'concat:coffee', + 'coffee:script', + 'concat:userjs', + 'clean:tmpuserjs' + ]); + grunt.registerTask('build-userscript', [ + 'set-build:userscript', + 'concat:coffee', + 'coffee:script', + 'concat:userscript', + 'clean:tmpuserscript' + ]); + + grunt.registerTask('release', ['shell:commit', 'shell:push', 'build-crx', 'compress:crx']); + grunt.registerTask('patch', ['bump', 'reloadPkg', 'updcl:3', 'release']); + grunt.registerTask('minor', ['bump:minor', 'reloadPkg', 'updcl:2', 'release']); + grunt.registerTask('major', ['bump:major', 'reloadPkg', 'updcl:1', 'release']); + + grunt.registerTask('reloadPkg', 'Reload the package', function() { + // Update the `pkg` object with the new version. + pkg = grunt.file.readJSON('package.json'); + concatOptions.process.data = pkg; + grunt.log.ok('pkg reloaded.'); + }); + + grunt.registerTask('updcl', 'Update the changelog', function(i) { + // i is the number of #s for markdown. + var version = new Array(+i + 1).join('#') + ' ' + pkg.version + ' - *' + grunt.template.today('yyyy-mm-dd') + '*'; + grunt.file.write('CHANGELOG.md', version + '\n\n' + grunt.file.read('CHANGELOG.md')); + grunt.log.ok('Changelog updated for v' + pkg.version + '.'); + }); + +}; diff --git a/builds/4chan-X.js b/builds/4chan-X.js index 974c56f8f..17e8325c5 100644 --- a/builds/4chan-X.js +++ b/builds/4chan-X.js @@ -20,7 +20,7 @@ // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwAgMAAAAqbBEUAAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAHFJREFUKFOt0LENACEIBdBv4Qju4wgWanEj3D6OcIVMKaitYHEU/jwTCQj8W75kiVCSBvdQ5/AvfVHBin11BgdRq3ysBgfwBDRrj3MCIA+oAQaku/Q1cNctrAmyDl577tOThYt/Y1RBM4DgOHzM0HFTAyLukH/cmRnqAAAAAElFTkSuQmCC // ==/UserScript== -/* 4chan X - Version 3.1.4 - 2013-04-21 +/* 4chan X - Version 3.1.4 - 2013-04-22 * https://4chan-x.just-believe.in/ * * Copyright (c) 2009-2011 James Campos @@ -79,7 +79,6 @@ 'Recursive Hiding': [true, 'Hide replies of hidden posts, recursively.'], 'Thread Hiding': [true, 'Add buttons to hide entire threads.'], 'Reply Hiding': [true, 'Add buttons to hide single replies.'], - 'Hiding Buttons': [true, 'Add buttons to hide threads / replies, in addition to menu links.'], 'Stubs': [true, 'Show stubs of hidden threads / replies.'] }, 'Images': { @@ -93,18 +92,19 @@ }, 'Menu': { 'Menu': [true, 'Add a drop-down menu to posts.'], + 'Report Link': [true, 'Add a report link to the menu.'], 'Thread Hiding Link': [true, 'Add a link to hide entire threads.'], 'Reply Hiding Link': [true, 'Add a link to hide single replies.'], - 'Report Link': [true, 'Add a report link to the menu.'], 'Delete Link': [true, 'Add post and image deletion links to the menu.'], - 'Download Link': [true, 'Add a download with original filename link to the menu. Chrome-only currently.'], 'Archive Link': [true, 'Add an archive link to the menu.'] }, 'Monitoring': { 'Thread Updater': [true, 'Fetch and insert new replies. Has more options in its own dialog.'], 'Unread Count': [true, 'Show the unread posts count in the tab title.'], + 'Hide Unread Count at (0)': [false, 'Hide the unread posts count when it reaches 0.'], 'Unread Tab Icon': [true, 'Show a different favicon when there are unread posts.'], 'Unread Line': [true, 'Show a line to distinguish read posts from unread ones.'], + 'Scroll to Last Read Post': [true, 'Scroll back to the last read post when reopening a thread.'], 'Thread Excerpt': [true, 'Show an excerpt of the thread in the tab title.'], 'Thread Stats': [true, 'Display reply and image count.'], 'Thread Watcher': [true, 'Bookmark threads.'], @@ -1451,6 +1451,9 @@ OP = thread.OP; excerpt = ((_ref = OP.info.subject) != null ? _ref.trim() : void 0) || OP.info.comment.replace(/\n+/g, ' // ') || Conf['Anonymize'] && 'Anonymous' || $('.nameBlock', OP.nodes.info).textContent.trim(); + if (excerpt.length > 70) { + excerpt = "" + excerpt.slice(0, 67) + "..."; + } return "/" + thread.board + "/ - " + excerpt; }, postFromRoot: function(root) { @@ -3006,7 +3009,7 @@ if (makeStub == null) { makeStub = Conf['Stubs']; } - if (thread.hidden) { + if (thread.isHidden) { return; } OP = thread.OP; @@ -3972,9 +3975,6 @@ if (g.VIEW === 'catalog' || !Conf['Menu'] || !Conf['Download Link']) { return; } - if (!('download' in $.el('a'))) { - return; - } a = $.el('a', { className: 'download-link', textContent: 'Download file' @@ -4116,10 +4116,10 @@ hide = $.hasClass(this, 'hide-announcement'); text = PSAHiding.trim($.id('globalMessage')); - return $.get('hiddenPSAs', [], function(item) { + return $.get('hiddenPSAs', [], function(_arg) { var hiddenPSAs, i; - hiddenPSAs = item.hiddenPSAs; + hiddenPSAs = _arg.hiddenPSAs; if (hide) { hiddenPSAs.push(text); } else { @@ -4136,7 +4136,7 @@ btn = PSAHiding.btn; psa = $.id('globalMessage'); - return _ref1 = (_ref = PSAHiding.trim(psa), __indexOf.call(hiddenPSAs, _ref) >= 0) ? [true, '[ + ]', 'show-announcement'] : [false, '[ - ]', 'hide-announcement'], psa.hidden = _ref1[0], btn.innerHTML = _ref1[1], btn.className = _ref1[2], _ref1; + return _ref1 = (_ref = PSAHiding.trim(psa), __indexOf.call(hiddenPSAs, _ref) >= 0) ? [true, '[\u00A0+\u00A0]', 'show-announcement'] : [false, '[\u00A0-\u00A0]', 'hide-announcement'], psa.hidden = _ref1[0], btn.firstChild.textContent = _ref1[1], btn.className = _ref1[2], _ref1; }, trim: function(psa) { return psa.textContent.replace(/\W+/g, '').toLowerCase(); @@ -4158,6 +4158,7 @@ }); input = $('input', el); $.on(input, 'change', this.toggle); + $.sync('Header catalog links', CatalogLinks.set); $.event('AddMenuEntry', { type: 'header', el: el, @@ -4172,14 +4173,21 @@ return $.asap((function() { return $.id('boardNavMobile'); }), function() { - return CatalogLinks.toggle.call(input); + return CatalogLinks.set(input.checked); }); }); }, toggle: function() { - var a, board, useCatalog, _i, _len, _ref; + var useCatalog; + $.event('CloseMenu'); $.set('Header catalog links', useCatalog = this.checked); + return CatalogLinks.set(useCatalog); + }, + set: function(useCatalog) { + var a, board, path, _i, _len, _ref; + + path = useCatalog ? 'catalog' : ''; _ref = $$('a', $.id('boardNavDesktop')); for (_i = 0, _len = _ref.length; _i < _len; _i++) { a = _ref[_i]; @@ -4190,7 +4198,7 @@ if (Conf['External Catalog']) { a.href = useCatalog ? CatalogLinks.external(board) : "//boards.4chan.org/" + board + "/"; } else { - a.pathname = "/" + board + "/" + (useCatalog ? 'catalog' : ''); + a.pathname = "/" + board + "/" + path; } a.title = useCatalog ? "" + a.title + " - Catalog" : a.title.replace(/\ -\ Catalog$/, ''); } @@ -4601,20 +4609,20 @@ Header = { init: function() { - var createSubEntry, setting, subEntries, _i, _len, _ref; + var createSubEntry, headerToggler, setting, subEntries, _i, _len, _ref; this.menuButton = $.el('span', { className: 'menu-button', innerHTML: '' }); this.menu = new UI.Menu('header'); - $.on(this.menuButton, 'click', this.menuToggle); - $.on(this.toggle, 'mousedown', this.toggleBarVisibility); - $.on(window, 'load hashchange', Header.hashScroll); - this.positionToggler = $.el('span', { - textContent: 'Header Position', - className: 'header-position-link' + headerToggler = $.el('label', { + innerHTML: ' Auto-hide header' }); + this.headerToggler = headerToggler.firstElementChild; + $.on(this.menuButton, 'click', this.menuToggle); + $.on(window, 'load hashchange', Header.hashScroll); + $.on(this.headerToggler, 'change', this.toggleBarVisibility); createSubEntry = Header.createSubEntry; subEntries = []; _ref = ['sticky top', 'sticky bottom', 'top']; @@ -4622,22 +4630,18 @@ setting = _ref[_i]; subEntries.push(createSubEntry(setting)); } + subEntries.push({ + el: headerToggler + }); this.addShortcut(Header.menuButton); $.event('AddMenuEntry', { type: 'header', - el: this.positionToggler, - order: 108, + el: $.el('span', { + textContent: 'Header' + }), + order: 105, subEntries: subEntries }); - this.headerToggler = $.el('label', { - innerHTML: " Auto-hide header" - }); - $.on(this.headerToggler.firstElementChild, 'change', this.toggleBarVisibility); - $.event('AddMenuEntry', { - type: 'header', - el: this.headerToggler, - order: 109 - }); $.on(d, 'CreateNotification', this.createNotification); $.asap((function() { return d.body; @@ -4665,9 +4669,6 @@ toggle: $.el('div', { id: 'toggle-header-bar' }), - settings: $.el('div', { - id: 'settings-container' - }), createSubEntry: function(setting) { var label; @@ -4697,7 +4698,7 @@ Header.setBarVisibility(Conf['Header auto-hide']); $.sync('Header auto-hide', Header.setBarVisibility); $.add(fullBoardList, __slice.call(nav.childNodes)); - $.add(nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle, Header.settings]); + $.add(nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle]); if (Conf['Custom Board Navigation']) { fullBoardList.hidden = true; customBoardList = $.el('span', { @@ -4807,13 +4808,27 @@ } }, setBarVisibility: function(hide) { - Header.headerToggler.firstElementChild.checked = hide; + Header.headerToggler.checked = hide; + $.event('CloseMenu'); return (hide ? $.addClass : $.rmClass)(Header.nav, 'autohide'); }, + toggleBarVisibility: function(e) { + var hide, message; + + if (e.type === 'mousedown' && e.button !== 0) { + return; + } + hide = this.nodeName === 'INPUT' ? this.checked : !$.hasClass(Header.bar, 'autohide'); + Conf['Header auto-hide'] = hide; + $.set('Header auto-hide', hide); + Header.setBarVisibility(hide); + message = hide ? 'The header bar will automatically hide itself.' : 'The header bar will remain visible.'; + return new Notification('info', message, 2); + }, hashScroll: function() { var post; - if (!(post = this.location.hash.slice(1))) { + if (!(post = $.id(this.location.hash.slice(1)))) { return; } if ((Get.postFromRoot(post)).isHidden) { @@ -4831,18 +4846,6 @@ } return ($.engine === 'webkit' ? d.body : doc).scrollTop += top; }, - toggleBarVisibility: function(e) { - var hide, message; - - if (e.type === 'mousedown' && e.button !== 0) { - return; - } - hide = this.nodeName === 'INPUT' ? this.checked : !$.hasClass(Header.nav, 'autohide'); - Header.setBarVisibility(hide); - message = hide ? 'The header bar will automatically hide itself.' : 'The header bar will remain visible.'; - new Notification('info', message, 2); - return $.set('Header auto-hide', hide); - }, addShortcut: function(el) { var shortcut; @@ -5182,6 +5185,9 @@ return "//nsfw.foolz.us/" + boardID + "/full_image/" + filename; case 'po': return "//archive.thedarkcave.org/" + boardID + "/full_image/" + filename; + case 'hr': + case 'tv': + return "http://archive.4plebs.org/" + boardID + "/full_image/" + filename; case 'ck': case 'fa': case 'lit': @@ -5225,6 +5231,9 @@ case 'out': case 'po': return "//archive.thedarkcave.org/_/api/chan/post/?board=" + boardID + "&num=" + postID; + case 'hr': + case 'x': + return "http://archive.4plebs.org/_/api/chan/post/?board=" + boardID + "&num=" + postID; } }, to: function(data) { @@ -5253,6 +5262,8 @@ case 'out': case 'po': return Redirect.path('//archive.thedarkcave.org', 'foolfuuka', data); + case 'hr': + return Redirect.path('http://archive.4plebs.org', 'foolfuuka', data); case 'ck': case 'fa': case 'lit': @@ -5741,9 +5752,7 @@ }); }, node: function() { - var excerpt; - - return d.title = (excerpt = Get.threadExcerpt(this)).length > 80 ? "" + excerpt.slice(0, 77) + "..." : excerpt; + return d.title = Get.threadExcerpt(this); } }; @@ -6311,7 +6320,7 @@ }); }, node: function() { - var ID, hash, post, posts, root, _ref; + var ID, post, posts, _ref; Unread.thread = this; Unread.title = d.title; @@ -6334,9 +6343,13 @@ if (Conf['Unread Line']) { $.on(d, 'visibilitychange', Unread.setLine); } - if (!Conf['Scroll to Last Read Post']) { - return; + if (Conf['Scroll to Last Read Post']) { + return $.on(window, 'load', Unread.scroll); } + }, + scroll: function() { + var hash, root; + if ((hash = location.hash.match(/\d+/)) && hash[0] in this.posts) { return; } @@ -6498,7 +6511,7 @@ count = Unread.posts.length; if (Conf['Unread Count']) { - d.title = g.DEAD ? "(" + Unread.posts.length + ") /" + g.BOARD + "/ - 404" : "(" + Unread.posts.length + ") " + Unread.title; + d.title = "" + (count || !Conf['Hide Unread Count at (0)'] ? "(" + count + ") " : '') + (g.DEAD ? "/" + g.BOARD + "/ - 404" : "" + Unread.title); } if (!Conf['Unread Tab Icon']) { return; @@ -7551,8 +7564,8 @@ })); } $.on(nodes.filename.parentNode, 'click keyup', QR.openFileInput); - $.on(QR.nodes.el, 'focusin', QR.focusin); - $.on(QR.nodes.el, 'focusout', QR.focusout); + $.on(dialog, 'focusin', QR.focusin); + $.on(dialog, 'focusout', QR.focusout); $.on(nodes.autohide, 'change', QR.toggleHide); $.on(nodes.close, 'click', QR.close); $.on(nodes.dumpButton, 'click', function() { @@ -8506,7 +8519,7 @@ return; } $.event('CloseMenu'); - html = "\n
\n
"; + html = "\n
\n
"; Settings.overlay = overlay = $.el('div', { id: 'overlay' }); @@ -9388,7 +9401,7 @@ } return Main.thisPageIsLegit; }, - css: "/* General */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.captcha-img,\n.field {\nbackground-color: #FFF;\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\noutline: none;\ntransition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field::-moz-placeholder,\n.field:hover::-moz-placeholder {\ncolor: #AAA !important;\nfont-size: 13px !important;\nopacity: 1.0 !important;\n}\n.captch-img:hover,\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.field[disabled] {\nbackground-color: #F2F2F2;\ncolor: #888;\n}\n.move {\ncursor: move;\noverflow: hidden;\n}\nlabel, .favicon {\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;\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#overlay,\n#fourchanx-settings,\n#qp, #ihover,\n#updater, #thread-stats,\n#navlinks, .fixed #header-bar,\n#qr {\nposition: fixed;\n}\n#watcher {\nposition: absolute;\n}\n#fourchanx-settings {\nz-index: 999;\n}\n#overlay {\nz-index: 900;\n}\n#notifications {\nz-index: 70;\n}\n#qp, #ihover {\nz-index: 60;\n}\n#menu {\nz-index: 50;\n}\n#navlinks, #updater, #thread-stats {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#watcher {\nz-index: 20;\n}\n.fixed #header-bar {\nz-index: 10;\n}\n/* Header */\n.fourchan-x body {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\n}\n.fixed.top body {\npadding-top: 2em;\n}\n.fixed.bottom body {\npadding-bottom: 2em;\n}\n.fixed #header-bar {\nright: 0;\nleft: 0;\npadding: 3px 4px 4px;\n}\n.fixed.top #header-bar {\ntop: 0;\n}\n.fixed.bottom #header-bar {\nbottom: 0;\n}\n#header-bar {\nborder-width: 0;\ntransition: all .1s .05s ease-in-out;\n}\n.fixed.top #header-bar {\nborder-bottom-width: 1px;\n}\n.fixed.bottom #header-bar {\nbox-shadow: 0 -1px 2px rgba(0, 0, 0, .15);\nborder-top-width: 1px;\n}\n.fixed.bottom #header-bar .menu-button i {\nborder-top: none;\nborder-bottom: 6px solid;\n}\n#board-list {\ntext-align: center;\n}\n.fixed #header-bar.autohide:not(:hover) {\nbox-shadow: none;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.fixed.top #header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n}\n.fixed.bottom #header-bar.autohide:not(:hover) {\n-webkit-transform: translateY(100%);\ntransform: translateY(100%);\n}\n#toggle-header-bar {\nleft: 0;\nright: 0;\nheight: 10px;\nposition: absolute;\n}\n#header-bar #toggle-header-bar {\ndisplay: none;\n}\n.fixed #header-bar #toggle-header-bar {\ndisplay: block;\n}\n.fixed #header-bar #toggle-header-bar {\ncursor: n-resize;\n}\n.fixed.top header-bar #toggle-header-bar {\ntop: 100%;\n}\n.fixed.bottom #header-bar #toggle-header-bar {\nbottom: 100%;\n}\n.fixed #header-bar #header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a:not(.entry) {\ntext-decoration: none;\npadding: 1px;\n}\n#shortcuts:empty {\ndisplay: none;\n}\n.brackets-wrap::before {\ncontent: \"\\00a0[\";\n}\n.brackets-wrap::after {\ncontent: \"]\\00a0\";\n}\n.disabled,\n.expand-all-shortcut {\nopacity: .45;\n}\n#shortcuts {\nfloat: right;\n}\n#navbotright,\n#navtopright {\ndisplay: none;\n}\n#toggleMsgBtn {\ndisplay: none !important;\n}\n\n/* Notifications */\n#notifications {\nheight: 0;\ntext-align: center;\nposition: fixed;\ntop: 0;\nright: 0;\nleft: 0;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.top:not(.autohide) ~ #notifications {\ntop: 2em;\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;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 38%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 38%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 38%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 38%, .9);\n}\n.notification a {\ncolor: white;\n}\n.notification > .close {\npadding: 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 6px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* Settings */\n#overlay {\nbackground-color: rgba(0, 0, 0, .5);\ntop: 0;\nleft: 0;\nheight: 100%;\nwidth: 100%;\n}\n#fourchanx-settings {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nbox-shadow: 0 0 15px rgba(0, 0, 0, .15);\nheight: 600px;\nmin-height: 0;\nmax-height: 100%;\nwidth: 900px;\nmin-width: 0;\nmax-width: 100%;\npadding: 3px;\ntop: 50%;\nleft: 50%;\n-moz-transform: translate(-50%, -50%);\n-webkit-transform: translate(-50%, -50%);\n-o-transform: translate(-50%, -50%);\ntransform: translate(-50%, -50%);\n}\n#fourchanx-settings > nav {\npadding: 2px 2px 0;\nheight: 15px;\n}\n#fourchanx-settings > nav a {\ntext-decoration: underline;\n}\n#fourchanx-settings > nav a.close {\ntext-decoration: none;\npadding: 2px;\n}\n.section-container {\noverflow: auto;\nposition: absolute;\ntop: 2.1em;\nright: 5px;\nbottom: 5px;\nleft: 5px;\npadding-top: 10px;\n}\n.sections-list {\npadding: 0 3px;\nfloat: left;\n}\n.credits {\nfloat: right;\n}\n.tab-selected {\nfont-weight: 700;\n}\n.section-sauce ul,\n.section-rice ul {\nlist-style: none;\nmargin: 0;\npadding: 8px;\n}\n.section-sauce li,\n.section-rice li {\npadding-left: 4px;\n}\n.section-main label {\ntext-decoration: underline;\n}\n.section-filter ul {\npadding: 0;\n}\n.section-filter li {\nmargin: 10px 40px;\n}\n.section-filter textarea {\nheight: 500px;\n}\n.section-sauce textarea {\nheight: 350px;\n}\n.section-rice .field[name=\"boardnav\"] {\nwidth: 100%;\n}\n.section-rice textarea {\nheight: 150px;\n}\n#fourchanx-settings fieldset {\nborder: 1px solid;\nborder-radius: 3px;\n}\n#fourchanx-settings legend {\nfont-weight: 700;\n}\n#fourchanx-settings textarea {\nfont-family: monospace;\nmin-width: 100%;\nmax-width: 100%;\n}\n#fourchanx-settings code {\ncolor: #000;\nbackground-color: #FFF;\npadding: 0 2px;\n}\n.unscroll {\noverflow: hidden;\n}\n\n/* Announcement Hiding */\n:root.hide-announcement #globalMessage {\ndisplay: none;\n}\na.hide-announcement {\nfloat: left;\n}\n\n/* Unread */\n#unread-line {\nmargin: 0;\n}\n\n/* Thread Updater */\n#updater:not(:hover) {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n#updater > .move {\npadding: 0 3px;\n}\n#updater > div:last-child {\ntext-align: center;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n#updater input[type=\"button\"] {\nwidth: 100%;\n}\n.new {\ncolor: limegreen;\n}\n\n/* Thread Watcher */\n#watcher {\npadding-bottom: 3px;\noverflow: hidden;\nwhite-space: nowrap;\n}\n#watcher:not(:hover) {\nmax-height: 220px;\n}\n#watcher > .move {\npadding-top: 3px;\n}\n#watcher > div {\nmax-width: 200px;\noverflow: hidden;\npadding-left: 3px;\npadding-right: 3px;\ntext-overflow: ellipsis;\n}\n#watcher a {\ntext-decoration: none;\n}\n\n/* Thread Stats */\n#thread-stats {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n\n/* Quote */\n.deadlink {\ntext-decoration: none !important;\n}\n.backlink.deadlink:not(.forwardlink), .quotelink.deadlink:not(.forwardlink) {\ntext-decoration: underline !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 > .opContainer::after {\ncontent: '';\nclear: both;\ndisplay: table;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 2px 2px 5px;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\noutline: 2px solid rgba(216, 94, 49, .7);\n}\n\n/* Quote Threading */\n.threadContainer {\nmargin-left: 20px;\nborder-left: 1px solid rgba(0,0,0,0.5);\n}\n.threadOP {\nclear: both;\n} \n\n/* File */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull,\n.expanded-image > .post > .file > .fileThumb > img[data-md5],\n:not(.expanded-image) > .post > .file > .fileThumb > .full-image {\ndisplay: none;\n}\n.expanding {\nopacity: .5;\n}\n.expanded-image {\nclear: both;\n}\n.expanded-image > .op > .file::after {\ncontent: '';\nclear: both;\ndisplay: table;\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#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Index/Reply Navigation */\n#navlinks {\nfont-size: 16px;\ntop: 25px;\nright: 10px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\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.stub input {\ndisplay: inline-block;\n}\n\n/* QR */\n:root.hide-original-post-form #postForm,\n:root.hide-original-post-form .postingMode,\n:root.hide-original-post-form #togglePostForm,\n#qr.autohide:not(.has-focus):not(:hover) > form,\n.postingMode ~ #qr select,\n#file-n-submit:not(.has-file) #qr-filerm {\ndisplay: none;\n}\n#qr select, #dump-button, .remove, .captcha-img {\ncursor: pointer;\n}\n#qr {\nz-index: 20;\nposition: fixed;\npadding: 1px;\nborder: 1px solid transparent;\nmin-width: 248px;\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nmargin-bottom: 1px;\n}\n#qr .close {\nfloat: right;\npadding: 0 3px;\n}\n#qr .warning {\nmin-height: 1.6em;\nvertical-align: middle;\npadding: 0 1px;\nborder-width: 1px;\nborder-style: solid;\n}\n.persona {\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-button {\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC; \nwidth: 10%;\nmargin: 0;\nfont: 13px sans-serif;\npadding: 1px 0px 2px;\n}\n.persona .field:not(#dump) {\nwidth: 95px;\nmin-width: 30%;\nmax-width: 30%;\n}\n#qr textarea.field {\nheight: 14.8em;\nmin-height: 9em;\n}\n#qr.has-captcha textarea.field {\nheight: 9em;\n}\ninput.field.tripped:not(:hover):not(:focus) {\ncolor: transparent !important; text-shadow: none !important;\n}\n#qr textarea {\nresize: both;\n}\n.captcha-img {\nmargin: 0px;\ntext-align: center;\nbackground-image: #fff;\nfont-size: 0px;\nmin-height: 59px;\nmin-width: 302px;\n}\n.captcha-input {\nwidth: 100%;\nmargin: 1px 0 0;\n}\n.field,\n.selectrice {\n-moz-box-sizing: border-box;\nmargin: 0px;\npadding: 2px 4px 3px;\n}\n#qr textarea {\nmin-width: 100%;\n}\n#qr [type='submit'] {\nwidth: 25%;\nvertical-align: top;\n}\n/* Fake File Input */\n#qr-filename,\n.has-file #qr-no-file {\ndisplay: none;\n}\n#qr-no-file,\n.has-file #qr-filename {\ndisplay: block;\npadding: 0px 4px;\nmargin-bottom: 2px;\n}\n#qr-no-file {\ncolor: #AAA;\n}\n#qr-filename-container {\n-moz-box-sizing: border-box;\ndisplay: inline-block;\nposition: relative;\nwidth: 100px;\nmin-width: 74.6%;\nmax-width: 74.6%;\nmargin-right: 0.4%;\nmargin-top: 1px;\noverflow: hidden;\npadding: 2px 1px 0;\n}\n#qr-filename-container:hover {\ncursor: text;\n}\n#qr-filerm {\nposition: relative;\nright: 14px;\nbottom: 6px;\nmargin-right: -8px;\nz-index: 2;\n}\n#file-n-submit {\nheight: 23px;\n}\n#qr input[type=file] {\ndisplay: none;\n}\n/* Thread Select / Spoiler Label */\n#qr select {\nfloat: right;\n}\n/* Dumping UI */\n.dump #dump-list-container {\ndisplay: block;\n}\n#dump-list-container {\ndisplay: none;\nposition: relative;\noverflow-y: hidden;\nmargin-top: 1px;\n}\n#dump-list {\noverflow-x: auto;\noverflow-y: hidden;\nwhite-space: pre;\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-list:hover {\noverflow-x: auto;\n}\n.qr-preview {\n-moz-box-sizing: border-box;\ncounter-increment: thumbnails;\ncursor: move;\ndisplay: inline-block;\nheight: 90px;\nwidth: 90px;\npadding: 2px;\nopacity: .5;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-moz-transition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qr-preview:hover,\n.qr-preview:focus {\nopacity: .9;\n}\n.qr-preview::before {\ncontent: counter(thumbnails);\ncolor: #fff;\nposition: absolute;\ntop: 3px;\nright: 3px;\ntext-shadow: 0 0 3px #000, 0 0 8px #000;\n}\n.qr-preview#selected {\nopacity: 1;\n}\n.qr-preview.drag {\nbox-shadow: 0 0 10px rgba(0,0,0,.5);\n}\n.qr-preview.over {\nborder-color: #fff;\n}\n.qr-preview > span {\ncolor: #fff;\n}\n.remove {\nbackground: none;\ncolor: #e00;\nfont-weight: 700;\npadding: 3px;\n}\na:only-of-type > .remove {\ndisplay: none;\n}\n.remove:hover::after {\ncontent: \" Remove\";\n}\n.qr-preview > label {\nbackground: rgba(0,0,0,.5);\ncolor: #fff;\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qr-preview > label > input {\nmargin: 0;\n}\n#add-post {\ncursor: pointer;\nfont-size: 2em;\nposition: absolute;\ntop: 50%;\nright: 10px;\n-moz-transform: translateY(-50%);\n}\n.textarea {\nposition: relative;\n}\n#char-count {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nposition: absolute;\nbottom: 1px;\nright: 1px;\npointer-events: none;\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\nposition: relative;\n}\n.menu-button i {\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 {\nposition: fixed;\noutline: none;\nz-index: 22;\n}\n.entry {\nborder-bottom: 1px solid rgba(0,0,0,.25);\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry:last-child {\nborder-bottom: 0;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: .5em solid;\nborder-top: .3em solid transparent;\nborder-bottom: .3em solid transparent;\ndisplay: inline-block;\nmargin: .3em;\nposition: absolute;\nright: 3px;\n}\n.submenu {\ndisplay: none;\nposition: absolute;\nleft: 100%;\ntop: -1px;\n}\n.focused .submenu {\ndisplay: block;\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/* Settings */\n:root.yotsuba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.yotsuba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba #menu {\ncolor: #800000;\n}\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 10pt;\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/* Settings */\n:root.yotsuba-b #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.yotsuba-b .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba-b #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b #menu {\ncolor: #000;\n}\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 10pt;\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/* Settings */\n:root.futaba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.futaba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.futaba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba #menu {\ncolor: #800000;\n}\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 12pt;\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/* Settings */\n:root.burichan #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.burichan .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.burichan #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan #menu {\ncolor: #000000;\n}\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 12pt;\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/* Settings */\n:root.tomorrow #fourchanx-settings fieldset {\nborder-color: #111;\n}\n\n/* Quote */\n:root.tomorrow .backlink.deadlink {\ncolor: #81A2BE !important;\n}\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n.tomorrow #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow #qr select {\ncolor: #C5C8C6;\n}\n:root.tomorrow #qr option {\ncolor: #000;\n}\n:root.tomorrow .qr-preview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow #menu {\ncolor: #C5C8C6;\n}\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\nfont-size: 10pt;\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/* Settings */\n:root.photon #fourchanx-settings fieldset {\nborder-color: #CCC;\n}\n\n/* Quote */\n:root.photon .backlink.deadlink {\ncolor: #F60 !important;\n}\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.photon #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon #menu {\ncolor: #333;\n}\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\nfont-size: 10pt;\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.captcha-img,\n.field {\nbackground-color: #FFF;\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\noutline: none;\ntransition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field::-moz-placeholder,\n.field:hover::-moz-placeholder {\ncolor: #AAA !important;\nfont-size: 13px !important;\nopacity: 1.0 !important;\n}\n.captch-img:hover,\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.field[disabled] {\nbackground-color: #F2F2F2;\ncolor: #888;\n}\n.move {\ncursor: move;\noverflow: hidden;\n}\nlabel, .favicon {\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;\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#overlay,\n#fourchanx-settings,\n#qp, #ihover,\n#updater, #thread-stats,\n#navlinks, .fixed #header-bar,\n#qr {\nposition: fixed;\n}\n#watcher {\nposition: absolute;\n}\n#fourchanx-settings {\nz-index: 999;\n}\n#overlay {\nz-index: 900;\n}\n#notifications {\nz-index: 70;\n}\n#qp, #ihover {\nz-index: 60;\n}\n#menu {\nz-index: 50;\n}\n#navlinks, #updater, #thread-stats {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#watcher {\nz-index: 20;\n}\n.fixed #header-bar {\nz-index: 10;\n}\n/* Header */\n.fixed.top body {\npadding-top: 2em;\n}\n.fixed.bottom body {\npadding-bottom: 2em;\n}\n.fixed #header-bar {\nright: 0;\nleft: 0;\npadding: 3px 4px 4px;\n}\n.fixed.top #header-bar {\ntop: 0;\n}\n.fixed.bottom #header-bar {\nbottom: 0;\n}\n#header-bar {\nborder-width: 0;\ntransition: all .1s .05s ease-in-out;\n}\n.fixed.top #header-bar {\nborder-bottom-width: 1px;\n}\n.fixed.bottom #header-bar {\nbox-shadow: 0 -1px 2px rgba(0, 0, 0, .15);\nborder-top-width: 1px;\n}\n.fixed.bottom #header-bar .menu-button i {\nborder-top: none;\nborder-bottom: 6px solid;\n}\n#board-list {\ntext-align: center;\n}\n.fixed #header-bar.autohide:not(:hover) {\nbox-shadow: none;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.fixed.top #header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n}\n.fixed.bottom #header-bar.autohide:not(:hover) {\n-webkit-transform: translateY(100%);\ntransform: translateY(100%);\n}\n#toggle-header-bar {\nleft: 0;\nright: 0;\nheight: 10px;\nposition: absolute;\n}\n#boardNavDesktop #toggle-header-bar {\ndisplay: none;\n}\n.fixed #boardNavDesktop #toggle-header-bar {\ndisplay: block;\n}\n.fixed.top boardNavDesktop #toggle-header-bar {\ntop: 100%;\n}\n.fixed.bottom #boardNavDesktop #toggle-header-bar {\nbottom: 100%;\n}\n#header-bar a:not(.entry) {\ntext-decoration: none;\npadding: 1px;\n}\n#shortcuts:empty {\ndisplay: none;\n}\n.brackets-wrap::before {\ncontent: \"\\00a0[\";\n}\n.brackets-wrap::after {\ncontent: \"]\\00a0\";\n}\n.disabled,\n.expand-all-shortcut {\nopacity: .45;\n}\n#shortcuts {\nfloat: right;\n}\n#navbotright,\n#navtopright {\ndisplay: none;\n}\n#toggleMsgBtn {\ndisplay: none !important;\n}\n\n/* Notifications */\n#notifications {\nposition: fixed;\ntop: 0;\nheight: 0;\ntext-align: center;\nright: 0;\nleft: 0;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.fixed.top #header-bar #notifications {\nposition: absolute;\ntop: 100%;\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;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 38%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 38%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 38%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 38%, .9);\n}\n.notification a {\ncolor: white;\n}\n.notification > .close {\npadding: 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 6px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* Settings */\n:root.fourchan-x body {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\n}\n#overlay {\nbackground-color: rgba(0, 0, 0, .5);\ntop: 0;\nleft: 0;\nheight: 100%;\nwidth: 100%;\n}\n#fourchanx-settings {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nbox-shadow: 0 0 15px rgba(0, 0, 0, .15);\nheight: 600px;\nmin-height: 0;\nmax-height: 100%;\nwidth: 900px;\nmin-width: 0;\nmax-width: 100%;\nmargin: auto;\npadding: 3px;\ntop: 50%;\nleft: 50%;\n-moz-transform: translate(-50%, -50%);\n-webkit-transform: translate(-50%, -50%);\n-o-transform: translate(-50%, -50%);\ntransform: translate(-50%, -50%);\n}\n#fourchanx-settings > nav {\npadding: 2px 2px 0;\nheight: 15px;\n}\n#fourchanx-settings > nav a {\ntext-decoration: underline;\n}\n#fourchanx-settings > nav a.close {\ntext-decoration: none;\npadding: 2px;\n}\n.section-container {\noverflow: auto;\nposition: absolute;\ntop: 2.1em;\nright: 5px;\nbottom: 5px;\nleft: 5px;\npadding-top: 10px;\n}\n.sections-list {\npadding: 0 3px;\nfloat: left;\n}\n.credits {\nfloat: right;\n}\n.tab-selected {\nfont-weight: 700;\n}\n.section-sauce ul,\n.section-rice ul {\nlist-style: none;\nmargin: 0;\npadding: 8px;\n}\n.section-sauce li,\n.section-rice li {\npadding-left: 4px;\n}\n.section-main label {\ntext-decoration: underline;\n}\n.section-filter ul {\npadding: 0;\n}\n.section-filter li {\nmargin: 10px 40px;\n}\n.section-filter textarea {\nheight: 500px;\n}\n.section-sauce textarea {\nheight: 350px;\n}\n.section-rice .field[name=\"boardnav\"] {\nwidth: 100%;\n}\n.section-rice textarea {\nheight: 150px;\n}\n#fourchanx-settings fieldset {\nborder: 1px solid;\nborder-radius: 3px;\n}\n#fourchanx-settings legend {\nfont-weight: 700;\n}\n#fourchanx-settings textarea {\nfont-family: monospace;\nmin-width: 100%;\nmax-width: 100%;\n}\n#fourchanx-settings code {\ncolor: #000;\nbackground-color: #FFF;\npadding: 0 2px;\n}\n.unscroll {\noverflow: hidden;\n}\n\n/* Announcement Hiding */\n:root.hide-announcement #globalMessage {\ndisplay: none;\n}\na.hide-announcement {\nfloat: left;\n}\n\n/* Unread */\n#unread-line {\nmargin: 0;\n}\n\n/* Thread Updater */\n#updater:not(:hover) {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n#updater > .move {\npadding: 0 3px;\n}\n#updater > div:last-child {\ntext-align: center;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n#updater input[type=\"button\"] {\nwidth: 100%;\n}\n.new {\ncolor: limegreen;\n}\n\n/* Thread Watcher */\n#watcher {\npadding-bottom: 3px;\noverflow: hidden;\nwhite-space: nowrap;\n}\n#watcher:not(:hover) {\nmax-height: 220px;\n}\n#watcher > .move {\npadding-top: 3px;\n}\n#watcher > div {\nmax-width: 200px;\noverflow: hidden;\npadding-left: 3px;\npadding-right: 3px;\ntext-overflow: ellipsis;\n}\n#watcher a {\ntext-decoration: none;\n}\n\n/* Thread Stats */\n#thread-stats {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n\n/* Quote */\n.deadlink {\ntext-decoration: none !important;\n}\n.backlink.deadlink:not(.forwardlink), .quotelink.deadlink:not(.forwardlink) {\ntext-decoration: underline !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 > .opContainer::after {\ncontent: '';\nclear: both;\ndisplay: table;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 2px 2px 5px;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\nmax-height: 80vh;\nmax-width: 50vw;\n}\n.qphl {\noutline: 2px solid rgba(216, 94, 49, .7);\n}\n\n/* Quote Threading */\n.threadContainer {\nmargin-left: 20px;\nborder-left: 1px solid rgba(0,0,0,0.5);\n}\n.threadOP {\nclear: both;\n} \n\n/* File */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull,\n.expanded-image > .post > .file > .fileThumb > img[data-md5],\n:not(.expanded-image) > .post > .file > .fileThumb > .full-image {\ndisplay: none;\n}\n.expanding {\nopacity: .5;\n}\n.expanded-image {\nclear: both;\n}\n.expanded-image > .op > .file::after {\ncontent: '';\nclear: both;\ndisplay: table;\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#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Index/Reply Navigation */\n#navlinks {\nfont-size: 16px;\ntop: 25px;\nright: 10px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\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.stub input {\ndisplay: inline-block;\n}\n\n/* QR */\n:root.hide-original-post-form #postForm,\n:root.hide-original-post-form .postingMode,\n:root.hide-original-post-form #togglePostForm,\n#qr.autohide:not(.has-focus):not(:hover) > form,\n.postingMode ~ #qr select,\n#file-n-submit:not(.has-file) #qr-filerm {\ndisplay: none;\n}\n#qr select, #dump-button, .remove, .captcha-img {\ncursor: pointer;\n}\n#qr {\nz-index: 20;\nposition: fixed;\npadding: 1px;\nborder: 1px solid transparent;\nmin-width: 248px;\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nmargin-bottom: 1px;\n}\n#qr .close {\nfloat: right;\npadding: 0 3px;\n}\n#qr .warning {\nmin-height: 1.6em;\nvertical-align: middle;\npadding: 0 1px;\nborder-width: 1px;\nborder-style: solid;\n}\n.persona {\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-button {\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC; \nwidth: 10%;\nmargin: 0;\nfont: 13px sans-serif;\npadding: 1px 0px 2px;\n}\n.persona .field:not(#dump) {\nwidth: 95px;\nmin-width: 30%;\nmax-width: 30%;\n}\n#qr textarea.field {\nheight: 14.8em;\nmin-height: 9em;\n}\n#qr.has-captcha textarea.field {\nheight: 9em;\n}\ninput.field.tripped:not(:hover):not(:focus) {\ncolor: transparent !important; text-shadow: none !important;\n}\n#qr textarea {\nresize: both;\n}\n.captcha-img {\nmargin: 0px;\ntext-align: center;\nbackground-image: #fff;\nfont-size: 0px;\nmin-height: 59px;\nmin-width: 302px;\n}\n.captcha-input {\nwidth: 100%;\nmargin: 1px 0 0;\n}\n.field,\n.selectrice {\n-moz-box-sizing: border-box;\nmargin: 0px;\npadding: 2px 4px 3px;\n}\n#qr textarea {\nmin-width: 100%;\n}\n#qr [type='submit'] {\nwidth: 25%;\nvertical-align: top;\n}\n/* Fake File Input */\n#qr-filename,\n.has-file #qr-no-file {\ndisplay: none;\n}\n#qr-no-file,\n.has-file #qr-filename {\ndisplay: block;\npadding: 0px 4px;\nmargin-bottom: 2px;\n}\n#qr-no-file {\ncolor: #AAA;\n}\n#qr-filename-container {\n-moz-box-sizing: border-box;\ndisplay: inline-block;\nposition: relative;\nwidth: 100px;\nmin-width: 74.6%;\nmax-width: 74.6%;\nmargin-right: 0.4%;\nmargin-top: 1px;\noverflow: hidden;\npadding: 2px 1px 0;\n}\n#qr-filename-container:hover {\ncursor: text;\n}\n#qr-filerm {\nposition: relative;\nright: 14px;\nbottom: 6px;\nmargin-right: -8px;\nz-index: 2;\n}\n#file-n-submit {\nheight: 23px;\n}\n#qr input[type=file] {\ndisplay: none;\n}\n/* Thread Select / Spoiler Label */\n#qr select {\nfloat: right;\n}\n/* Dumping UI */\n.dump #dump-list-container {\ndisplay: block;\n}\n#dump-list-container {\ndisplay: none;\nposition: relative;\noverflow-y: hidden;\nmargin-top: 1px;\n}\n#dump-list {\noverflow-x: auto;\noverflow-y: hidden;\nwhite-space: pre;\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-list:hover {\noverflow-x: auto;\n}\n.qr-preview {\n-moz-box-sizing: border-box;\ncounter-increment: thumbnails;\ncursor: move;\ndisplay: inline-block;\nheight: 90px;\nwidth: 90px;\npadding: 2px;\nopacity: .5;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-moz-transition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qr-preview:hover,\n.qr-preview:focus {\nopacity: .9;\n}\n.qr-preview::before {\ncontent: counter(thumbnails);\ncolor: #fff;\nposition: absolute;\ntop: 3px;\nright: 3px;\ntext-shadow: 0 0 3px #000, 0 0 8px #000;\n}\n.qr-preview#selected {\nopacity: 1;\n}\n.qr-preview.drag {\nbox-shadow: 0 0 10px rgba(0,0,0,.5);\n}\n.qr-preview.over {\nborder-color: #fff;\n}\n.qr-preview > span {\ncolor: #fff;\n}\n.remove {\nbackground: none;\ncolor: #e00;\nfont-weight: 700;\npadding: 3px;\n}\na:only-of-type > .remove {\ndisplay: none;\n}\n.remove:hover::after {\ncontent: \" Remove\";\n}\n.qr-preview > label {\nbackground: rgba(0,0,0,.5);\ncolor: #fff;\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qr-preview > label > input {\nmargin: 0;\n}\n#add-post {\ncursor: pointer;\nfont-size: 2em;\nposition: absolute;\ntop: 50%;\nright: 10px;\n-moz-transform: translateY(-50%);\n}\n.textarea {\nposition: relative;\n}\n#char-count {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nposition: absolute;\nbottom: 1px;\nright: 1px;\npointer-events: none;\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\nposition: relative;\n}\n.menu-button i {\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 {\nposition: fixed;\noutline: none;\nz-index: 22;\n}\n.entry {\nborder-bottom: 1px solid rgba(0,0,0,.25);\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry:last-child {\nborder-bottom: 0;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: .5em solid;\nborder-top: .3em solid transparent;\nborder-bottom: .3em solid transparent;\ndisplay: inline-block;\nmargin: .3em;\nposition: absolute;\nright: 3px;\n}\n.submenu {\ndisplay: none;\nposition: absolute;\nleft: 100%;\ntop: -1px;\n}\n.focused .submenu {\ndisplay: block;\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/* Settings */\n:root.yotsuba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.yotsuba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba #menu {\ncolor: #800000;\n}\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 10pt;\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/* Settings */\n:root.yotsuba-b #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.yotsuba-b .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba-b #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b #menu {\ncolor: #000;\n}\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 10pt;\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/* Settings */\n:root.futaba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.futaba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.futaba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba #menu {\ncolor: #800000;\n}\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 12pt;\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/* Settings */\n:root.burichan #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.burichan .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.burichan #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan #menu {\ncolor: #000000;\n}\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 12pt;\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/* Settings */\n:root.tomorrow #fourchanx-settings fieldset {\nborder-color: #111;\n}\n\n/* Quote */\n:root.tomorrow .backlink.deadlink {\ncolor: #81A2BE !important;\n}\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n.tomorrow #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow #qr select {\ncolor: #C5C8C6;\n}\n:root.tomorrow #qr option {\ncolor: #000;\n}\n:root.tomorrow .qr-preview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow #menu {\ncolor: #C5C8C6;\n}\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\nfont-size: 10pt;\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/* Settings */\n:root.photon #fourchanx-settings fieldset {\nborder-color: #CCC;\n}\n\n/* Quote */\n:root.photon .backlink.deadlink {\ncolor: #F60 !important;\n}\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.photon #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon #menu {\ncolor: #333;\n}\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\nfont-size: 10pt;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" }; Main.init(); diff --git a/builds/4chan-X.user.js b/builds/4chan-X.user.js index a73f1dd95..f899fcf46 100644 --- a/builds/4chan-X.user.js +++ b/builds/4chan-X.user.js @@ -20,7 +20,7 @@ // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwAgMAAAAqbBEUAAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAHFJREFUKFOt0LENACEIBdBv4Qju4wgWanEj3D6OcIVMKaitYHEU/jwTCQj8W75kiVCSBvdQ5/AvfVHBin11BgdRq3ysBgfwBDRrj3MCIA+oAQaku/Q1cNctrAmyDl577tOThYt/Y1RBM4DgOHzM0HFTAyLukH/cmRnqAAAAAElFTkSuQmCC // ==/UserScript== -/* 4chan X - Version 3.1.4 - 2013-04-21 +/* 4chan X - Version 3.1.4 - 2013-04-22 * https://4chan-x.just-believe.in/ * * Copyright (c) 2009-2011 James Campos @@ -79,7 +79,6 @@ 'Recursive Hiding': [true, 'Hide replies of hidden posts, recursively.'], 'Thread Hiding': [true, 'Add buttons to hide entire threads.'], 'Reply Hiding': [true, 'Add buttons to hide single replies.'], - 'Hiding Buttons': [true, 'Add buttons to hide threads / replies, in addition to menu links.'], 'Stubs': [true, 'Show stubs of hidden threads / replies.'] }, 'Images': { @@ -93,18 +92,19 @@ }, 'Menu': { 'Menu': [true, 'Add a drop-down menu to posts.'], + 'Report Link': [true, 'Add a report link to the menu.'], 'Thread Hiding Link': [true, 'Add a link to hide entire threads.'], 'Reply Hiding Link': [true, 'Add a link to hide single replies.'], - 'Report Link': [true, 'Add a report link to the menu.'], 'Delete Link': [true, 'Add post and image deletion links to the menu.'], - 'Download Link': [true, 'Add a download with original filename link to the menu. Chrome-only currently.'], 'Archive Link': [true, 'Add an archive link to the menu.'] }, 'Monitoring': { 'Thread Updater': [true, 'Fetch and insert new replies. Has more options in its own dialog.'], 'Unread Count': [true, 'Show the unread posts count in the tab title.'], + 'Hide Unread Count at (0)': [false, 'Hide the unread posts count when it reaches 0.'], 'Unread Tab Icon': [true, 'Show a different favicon when there are unread posts.'], 'Unread Line': [true, 'Show a line to distinguish read posts from unread ones.'], + 'Scroll to Last Read Post': [true, 'Scroll back to the last read post when reopening a thread.'], 'Thread Excerpt': [true, 'Show an excerpt of the thread in the tab title.'], 'Thread Stats': [true, 'Display reply and image count.'], 'Thread Watcher': [true, 'Bookmark threads.'], @@ -1442,6 +1442,9 @@ OP = thread.OP; excerpt = ((_ref = OP.info.subject) != null ? _ref.trim() : void 0) || OP.info.comment.replace(/\n+/g, ' // ') || Conf['Anonymize'] && 'Anonymous' || $('.nameBlock', OP.nodes.info).textContent.trim(); + if (excerpt.length > 70) { + excerpt = "" + excerpt.slice(0, 67) + "..."; + } return "/" + thread.board + "/ - " + excerpt; }, postFromRoot: function(root) { @@ -2997,7 +3000,7 @@ if (makeStub == null) { makeStub = Conf['Stubs']; } - if (thread.hidden) { + if (thread.isHidden) { return; } OP = thread.OP; @@ -3960,13 +3963,9 @@ init: function() { var a; - return; if (g.VIEW === 'catalog' || !Conf['Menu'] || !Conf['Download Link']) { return; } - if (!('download' in $.el('a'))) { - return; - } a = $.el('a', { className: 'download-link', textContent: 'Download file' @@ -4108,10 +4107,10 @@ hide = $.hasClass(this, 'hide-announcement'); text = PSAHiding.trim($.id('globalMessage')); - return $.get('hiddenPSAs', [], function(item) { + return $.get('hiddenPSAs', [], function(_arg) { var hiddenPSAs, i; - hiddenPSAs = item.hiddenPSAs; + hiddenPSAs = _arg.hiddenPSAs; if (hide) { hiddenPSAs.push(text); } else { @@ -4128,7 +4127,7 @@ btn = PSAHiding.btn; psa = $.id('globalMessage'); - return _ref1 = (_ref = PSAHiding.trim(psa), __indexOf.call(hiddenPSAs, _ref) >= 0) ? [true, '[ + ]', 'show-announcement'] : [false, '[ - ]', 'hide-announcement'], psa.hidden = _ref1[0], btn.innerHTML = _ref1[1], btn.className = _ref1[2], _ref1; + return _ref1 = (_ref = PSAHiding.trim(psa), __indexOf.call(hiddenPSAs, _ref) >= 0) ? [true, '[\u00A0+\u00A0]', 'show-announcement'] : [false, '[\u00A0-\u00A0]', 'hide-announcement'], psa.hidden = _ref1[0], btn.firstChild.textContent = _ref1[1], btn.className = _ref1[2], _ref1; }, trim: function(psa) { return psa.textContent.replace(/\W+/g, '').toLowerCase(); @@ -4150,6 +4149,7 @@ }); input = $('input', el); $.on(input, 'change', this.toggle); + $.sync('Header catalog links', CatalogLinks.set); $.event('AddMenuEntry', { type: 'header', el: el, @@ -4164,14 +4164,21 @@ return $.asap((function() { return $.id('boardNavMobile'); }), function() { - return CatalogLinks.toggle.call(input); + return CatalogLinks.set(input.checked); }); }); }, toggle: function() { - var a, board, useCatalog, _i, _len, _ref; + var useCatalog; + $.event('CloseMenu'); $.set('Header catalog links', useCatalog = this.checked); + return CatalogLinks.set(useCatalog); + }, + set: function(useCatalog) { + var a, board, path, _i, _len, _ref; + + path = useCatalog ? 'catalog' : ''; _ref = $$('a', $.id('boardNavDesktop')); for (_i = 0, _len = _ref.length; _i < _len; _i++) { a = _ref[_i]; @@ -4182,7 +4189,7 @@ if (Conf['External Catalog']) { a.href = useCatalog ? CatalogLinks.external(board) : "//boards.4chan.org/" + board + "/"; } else { - a.pathname = "/" + board + "/" + (useCatalog ? 'catalog' : ''); + a.pathname = "/" + board + "/" + path; } a.title = useCatalog ? "" + a.title + " - Catalog" : a.title.replace(/\ -\ Catalog$/, ''); } @@ -4593,20 +4600,20 @@ Header = { init: function() { - var createSubEntry, setting, subEntries, _i, _len, _ref; + var createSubEntry, headerToggler, setting, subEntries, _i, _len, _ref; this.menuButton = $.el('span', { className: 'menu-button', innerHTML: '' }); this.menu = new UI.Menu('header'); - $.on(this.menuButton, 'click', this.menuToggle); - $.on(this.toggle, 'mousedown', this.toggleBarVisibility); - $.on(window, 'load hashchange', Header.hashScroll); - this.positionToggler = $.el('span', { - textContent: 'Header Position', - className: 'header-position-link' + headerToggler = $.el('label', { + innerHTML: ' Auto-hide header' }); + this.headerToggler = headerToggler.firstElementChild; + $.on(this.menuButton, 'click', this.menuToggle); + $.on(window, 'load hashchange', Header.hashScroll); + $.on(this.headerToggler, 'change', this.toggleBarVisibility); createSubEntry = Header.createSubEntry; subEntries = []; _ref = ['sticky top', 'sticky bottom', 'top']; @@ -4614,22 +4621,18 @@ setting = _ref[_i]; subEntries.push(createSubEntry(setting)); } + subEntries.push({ + el: headerToggler + }); this.addShortcut(Header.menuButton); $.event('AddMenuEntry', { type: 'header', - el: this.positionToggler, - order: 108, + el: $.el('span', { + textContent: 'Header' + }), + order: 105, subEntries: subEntries }); - this.headerToggler = $.el('label', { - innerHTML: " Auto-hide header" - }); - $.on(this.headerToggler.firstElementChild, 'change', this.toggleBarVisibility); - $.event('AddMenuEntry', { - type: 'header', - el: this.headerToggler, - order: 109 - }); $.on(d, 'CreateNotification', this.createNotification); $.asap((function() { return d.body; @@ -4657,9 +4660,6 @@ toggle: $.el('div', { id: 'toggle-header-bar' }), - settings: $.el('div', { - id: 'settings-container' - }), createSubEntry: function(setting) { var label; @@ -4689,7 +4689,7 @@ Header.setBarVisibility(Conf['Header auto-hide']); $.sync('Header auto-hide', Header.setBarVisibility); $.add(fullBoardList, __slice.call(nav.childNodes)); - $.add(nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle, Header.settings]); + $.add(nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle]); if (Conf['Custom Board Navigation']) { fullBoardList.hidden = true; customBoardList = $.el('span', { @@ -4799,13 +4799,27 @@ } }, setBarVisibility: function(hide) { - Header.headerToggler.firstElementChild.checked = hide; + Header.headerToggler.checked = hide; + $.event('CloseMenu'); return (hide ? $.addClass : $.rmClass)(Header.nav, 'autohide'); }, + toggleBarVisibility: function(e) { + var hide, message; + + if (e.type === 'mousedown' && e.button !== 0) { + return; + } + hide = this.nodeName === 'INPUT' ? this.checked : !$.hasClass(Header.bar, 'autohide'); + Conf['Header auto-hide'] = hide; + $.set('Header auto-hide', hide); + Header.setBarVisibility(hide); + message = hide ? 'The header bar will automatically hide itself.' : 'The header bar will remain visible.'; + return new Notification('info', message, 2); + }, hashScroll: function() { var post; - if (!(post = this.location.hash.slice(1))) { + if (!(post = $.id(this.location.hash.slice(1)))) { return; } if ((Get.postFromRoot(post)).isHidden) { @@ -4823,18 +4837,6 @@ } return ($.engine === 'webkit' ? d.body : doc).scrollTop += top; }, - toggleBarVisibility: function(e) { - var hide, message; - - if (e.type === 'mousedown' && e.button !== 0) { - return; - } - hide = this.nodeName === 'INPUT' ? this.checked : !$.hasClass(Header.nav, 'autohide'); - Header.setBarVisibility(hide); - message = hide ? 'The header bar will automatically hide itself.' : 'The header bar will remain visible.'; - new Notification('info', message, 2); - return $.set('Header auto-hide', hide); - }, addShortcut: function(el) { var shortcut; @@ -5174,6 +5176,9 @@ return "//nsfw.foolz.us/" + boardID + "/full_image/" + filename; case 'po': return "//archive.thedarkcave.org/" + boardID + "/full_image/" + filename; + case 'hr': + case 'tv': + return "http://archive.4plebs.org/" + boardID + "/full_image/" + filename; case 'ck': case 'fa': case 'lit': @@ -5217,6 +5222,9 @@ case 'out': case 'po': return "//archive.thedarkcave.org/_/api/chan/post/?board=" + boardID + "&num=" + postID; + case 'hr': + case 'x': + return "http://archive.4plebs.org/_/api/chan/post/?board=" + boardID + "&num=" + postID; } }, to: function(data) { @@ -5245,6 +5253,8 @@ case 'out': case 'po': return Redirect.path('//archive.thedarkcave.org', 'foolfuuka', data); + case 'hr': + return Redirect.path('http://archive.4plebs.org', 'foolfuuka', data); case 'ck': case 'fa': case 'lit': @@ -5733,9 +5743,7 @@ }); }, node: function() { - var excerpt; - - return d.title = (excerpt = Get.threadExcerpt(this)).length > 80 ? "" + excerpt.slice(0, 77) + "..." : excerpt; + return d.title = Get.threadExcerpt(this); } }; @@ -6303,7 +6311,7 @@ }); }, node: function() { - var ID, hash, post, posts, root, _ref; + var ID, post, posts, _ref; Unread.thread = this; Unread.title = d.title; @@ -6326,9 +6334,13 @@ if (Conf['Unread Line']) { $.on(d, 'visibilitychange', Unread.setLine); } - if (!Conf['Scroll to Last Read Post']) { - return; + if (Conf['Scroll to Last Read Post']) { + return $.on(window, 'load', Unread.scroll); } + }, + scroll: function() { + var hash, root; + if ((hash = location.hash.match(/\d+/)) && hash[0] in this.posts) { return; } @@ -6490,7 +6502,7 @@ count = Unread.posts.length; if (Conf['Unread Count']) { - d.title = g.DEAD ? "(" + Unread.posts.length + ") /" + g.BOARD + "/ - 404" : "(" + Unread.posts.length + ") " + Unread.title; + d.title = "" + (count || !Conf['Hide Unread Count at (0)'] ? "(" + count + ") " : '') + (g.DEAD ? "/" + g.BOARD + "/ - 404" : "" + Unread.title); } if (!Conf['Unread Tab Icon']) { return; @@ -7558,8 +7570,8 @@ $.on(elm, 'blur', QR.focusout); $.on(elm, 'focus', QR.focusin); } - $.on(QR.nodes.el, 'focusin', QR.focusin); - $.on(QR.nodes.el, 'focusout', QR.focusout); + $.on(dialog, 'focusin', QR.focusin); + $.on(dialog, 'focusout', QR.focusout); $.on(nodes.autohide, 'change', QR.toggleHide); $.on(nodes.close, 'click', QR.close); $.on(nodes.dumpButton, 'click', function() { @@ -8523,7 +8535,7 @@ return; } $.event('CloseMenu'); - html = "\n
\n
"; + html = "\n
\n
"; Settings.overlay = overlay = $.el('div', { id: 'overlay' }); @@ -9407,7 +9419,7 @@ } return Main.thisPageIsLegit; }, - css: "/* General */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.captcha-img,\n.field {\nbackground-color: #FFF;\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\noutline: none;\ntransition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field::-moz-placeholder,\n.field:hover::-moz-placeholder {\ncolor: #AAA !important;\nfont-size: 13px !important;\nopacity: 1.0 !important;\n}\n.captch-img:hover,\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.field[disabled] {\nbackground-color: #F2F2F2;\ncolor: #888;\n}\n.move {\ncursor: move;\noverflow: hidden;\n}\nlabel, .favicon {\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;\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#overlay,\n#fourchanx-settings,\n#qp, #ihover,\n#updater, #thread-stats,\n#navlinks, .fixed #header-bar,\n#qr {\nposition: fixed;\n}\n#watcher {\nposition: absolute;\n}\n#fourchanx-settings {\nz-index: 999;\n}\n#overlay {\nz-index: 900;\n}\n#notifications {\nz-index: 70;\n}\n#qp, #ihover {\nz-index: 60;\n}\n#menu {\nz-index: 50;\n}\n#navlinks, #updater, #thread-stats {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#watcher {\nz-index: 20;\n}\n.fixed #header-bar {\nz-index: 10;\n}\n/* Header */\n.fourchan-x body {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\n}\n.fixed.top body {\npadding-top: 2em;\n}\n.fixed.bottom body {\npadding-bottom: 2em;\n}\n.fixed #header-bar {\nright: 0;\nleft: 0;\npadding: 3px 4px 4px;\n}\n.fixed.top #header-bar {\ntop: 0;\n}\n.fixed.bottom #header-bar {\nbottom: 0;\n}\n#header-bar {\nborder-width: 0;\ntransition: all .1s .05s ease-in-out;\n}\n.fixed.top #header-bar {\nborder-bottom-width: 1px;\n}\n.fixed.bottom #header-bar {\nbox-shadow: 0 -1px 2px rgba(0, 0, 0, .15);\nborder-top-width: 1px;\n}\n.fixed.bottom #header-bar .menu-button i {\nborder-top: none;\nborder-bottom: 6px solid;\n}\n#board-list {\ntext-align: center;\n}\n.fixed #header-bar.autohide:not(:hover) {\nbox-shadow: none;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.fixed.top #header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n}\n.fixed.bottom #header-bar.autohide:not(:hover) {\n-webkit-transform: translateY(100%);\ntransform: translateY(100%);\n}\n#toggle-header-bar {\nleft: 0;\nright: 0;\nheight: 10px;\nposition: absolute;\n}\n#header-bar #toggle-header-bar {\ndisplay: none;\n}\n.fixed #header-bar #toggle-header-bar {\ndisplay: block;\n}\n.fixed #header-bar #toggle-header-bar {\ncursor: n-resize;\n}\n.fixed.top header-bar #toggle-header-bar {\ntop: 100%;\n}\n.fixed.bottom #header-bar #toggle-header-bar {\nbottom: 100%;\n}\n.fixed #header-bar #header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a:not(.entry) {\ntext-decoration: none;\npadding: 1px;\n}\n#shortcuts:empty {\ndisplay: none;\n}\n.brackets-wrap::before {\ncontent: \"\\00a0[\";\n}\n.brackets-wrap::after {\ncontent: \"]\\00a0\";\n}\n.disabled,\n.expand-all-shortcut {\nopacity: .45;\n}\n#shortcuts {\nfloat: right;\n}\n#navbotright,\n#navtopright {\ndisplay: none;\n}\n#toggleMsgBtn {\ndisplay: none !important;\n}\n\n/* Notifications */\n#notifications {\nheight: 0;\ntext-align: center;\nposition: fixed;\ntop: 0;\nright: 0;\nleft: 0;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.top:not(.autohide) ~ #notifications {\ntop: 2em;\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;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 38%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 38%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 38%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 38%, .9);\n}\n.notification a {\ncolor: white;\n}\n.notification > .close {\npadding: 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 6px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* Settings */\n#overlay {\nbackground-color: rgba(0, 0, 0, .5);\ntop: 0;\nleft: 0;\nheight: 100%;\nwidth: 100%;\n}\n#fourchanx-settings {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nbox-shadow: 0 0 15px rgba(0, 0, 0, .15);\nheight: 600px;\nmin-height: 0;\nmax-height: 100%;\nwidth: 900px;\nmin-width: 0;\nmax-width: 100%;\npadding: 3px;\ntop: 50%;\nleft: 50%;\n-moz-transform: translate(-50%, -50%);\n-webkit-transform: translate(-50%, -50%);\n-o-transform: translate(-50%, -50%);\ntransform: translate(-50%, -50%);\n}\n#fourchanx-settings > nav {\npadding: 2px 2px 0;\nheight: 15px;\n}\n#fourchanx-settings > nav a {\ntext-decoration: underline;\n}\n#fourchanx-settings > nav a.close {\ntext-decoration: none;\npadding: 2px;\n}\n.section-container {\noverflow: auto;\nposition: absolute;\ntop: 2.1em;\nright: 5px;\nbottom: 5px;\nleft: 5px;\npadding-top: 10px;\n}\n.sections-list {\npadding: 0 3px;\nfloat: left;\n}\n.credits {\nfloat: right;\n}\n.tab-selected {\nfont-weight: 700;\n}\n.section-sauce ul,\n.section-rice ul {\nlist-style: none;\nmargin: 0;\npadding: 8px;\n}\n.section-sauce li,\n.section-rice li {\npadding-left: 4px;\n}\n.section-main label {\ntext-decoration: underline;\n}\n.section-filter ul {\npadding: 0;\n}\n.section-filter li {\nmargin: 10px 40px;\n}\n.section-filter textarea {\nheight: 500px;\n}\n.section-sauce textarea {\nheight: 350px;\n}\n.section-rice .field[name=\"boardnav\"] {\nwidth: 100%;\n}\n.section-rice textarea {\nheight: 150px;\n}\n#fourchanx-settings fieldset {\nborder: 1px solid;\nborder-radius: 3px;\n}\n#fourchanx-settings legend {\nfont-weight: 700;\n}\n#fourchanx-settings textarea {\nfont-family: monospace;\nmin-width: 100%;\nmax-width: 100%;\n}\n#fourchanx-settings code {\ncolor: #000;\nbackground-color: #FFF;\npadding: 0 2px;\n}\n.unscroll {\noverflow: hidden;\n}\n\n/* Announcement Hiding */\n:root.hide-announcement #globalMessage {\ndisplay: none;\n}\na.hide-announcement {\nfloat: left;\n}\n\n/* Unread */\n#unread-line {\nmargin: 0;\n}\n\n/* Thread Updater */\n#updater:not(:hover) {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n#updater > .move {\npadding: 0 3px;\n}\n#updater > div:last-child {\ntext-align: center;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n#updater input[type=\"button\"] {\nwidth: 100%;\n}\n.new {\ncolor: limegreen;\n}\n\n/* Thread Watcher */\n#watcher {\npadding-bottom: 3px;\noverflow: hidden;\nwhite-space: nowrap;\n}\n#watcher:not(:hover) {\nmax-height: 220px;\n}\n#watcher > .move {\npadding-top: 3px;\n}\n#watcher > div {\nmax-width: 200px;\noverflow: hidden;\npadding-left: 3px;\npadding-right: 3px;\ntext-overflow: ellipsis;\n}\n#watcher a {\ntext-decoration: none;\n}\n\n/* Thread Stats */\n#thread-stats {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n\n/* Quote */\n.deadlink {\ntext-decoration: none !important;\n}\n.backlink.deadlink:not(.forwardlink), .quotelink.deadlink:not(.forwardlink) {\ntext-decoration: underline !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 > .opContainer::after {\ncontent: '';\nclear: both;\ndisplay: table;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 2px 2px 5px;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\noutline: 2px solid rgba(216, 94, 49, .7);\n}\n\n/* Quote Threading */\n.threadContainer {\nmargin-left: 20px;\nborder-left: 1px solid rgba(0,0,0,0.5);\n}\n.threadOP {\nclear: both;\n} \n\n/* File */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull,\n.expanded-image > .post > .file > .fileThumb > img[data-md5],\n:not(.expanded-image) > .post > .file > .fileThumb > .full-image {\ndisplay: none;\n}\n.expanding {\nopacity: .5;\n}\n.expanded-image {\nclear: both;\n}\n.expanded-image > .op > .file::after {\ncontent: '';\nclear: both;\ndisplay: table;\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#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Index/Reply Navigation */\n#navlinks {\nfont-size: 16px;\ntop: 25px;\nright: 10px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\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.stub input {\ndisplay: inline-block;\n}\n\n/* QR */\n:root.hide-original-post-form #postForm,\n:root.hide-original-post-form .postingMode,\n:root.hide-original-post-form #togglePostForm,\n#qr.autohide:not(.has-focus):not(:hover) > form,\n.postingMode ~ #qr select,\n#file-n-submit:not(.has-file) #qr-filerm {\ndisplay: none;\n}\n#qr select, #dump-button, .remove, .captcha-img {\ncursor: pointer;\n}\n#qr {\nz-index: 20;\nposition: fixed;\npadding: 1px;\nborder: 1px solid transparent;\nmin-width: 248px;\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nmargin-bottom: 1px;\n}\n#qr .close {\nfloat: right;\npadding: 0 3px;\n}\n#qr .warning {\nmin-height: 1.6em;\nvertical-align: middle;\npadding: 0 1px;\nborder-width: 1px;\nborder-style: solid;\n}\n.persona {\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-button {\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC; \nwidth: 10%;\nmargin: 0;\nfont: 13px sans-serif;\npadding: 1px 0px 2px;\n}\n.persona .field:not(#dump) {\nwidth: 95px;\nmin-width: 30%;\nmax-width: 30%;\n}\n#qr textarea.field {\nheight: 14.8em;\nmin-height: 9em;\n}\n#qr.has-captcha textarea.field {\nheight: 9em;\n}\ninput.field.tripped:not(:hover):not(:focus) {\ncolor: transparent !important; text-shadow: none !important;\n}\n#qr textarea {\nresize: both;\n}\n.captcha-img {\nmargin: 0px;\ntext-align: center;\nbackground-image: #fff;\nfont-size: 0px;\nmin-height: 59px;\nmin-width: 302px;\n}\n.captcha-input {\nwidth: 100%;\nmargin: 1px 0 0;\n}\n.field,\n.selectrice {\n-moz-box-sizing: border-box;\nmargin: 0px;\npadding: 2px 4px 3px;\n}\n#qr textarea {\nmin-width: 100%;\n}\n#qr [type='submit'] {\nwidth: 25%;\nvertical-align: top;\n}\n/* Fake File Input */\n#qr-filename,\n.has-file #qr-no-file {\ndisplay: none;\n}\n#qr-no-file,\n.has-file #qr-filename {\ndisplay: block;\npadding: 0px 4px;\nmargin-bottom: 2px;\n}\n#qr-no-file {\ncolor: #AAA;\n}\n#qr-filename-container {\n-moz-box-sizing: border-box;\ndisplay: inline-block;\nposition: relative;\nwidth: 100px;\nmin-width: 74.6%;\nmax-width: 74.6%;\nmargin-right: 0.4%;\nmargin-top: 1px;\noverflow: hidden;\npadding: 2px 1px 0;\n}\n#qr-filename-container:hover {\ncursor: text;\n}\n#qr-filerm {\nposition: relative;\nright: 14px;\nbottom: 6px;\nmargin-right: -8px;\nz-index: 2;\n}\n#file-n-submit {\nheight: 23px;\n}\n#qr input[type=file] {\ndisplay: none;\n}\n/* Thread Select / Spoiler Label */\n#qr select {\nfloat: right;\n}\n/* Dumping UI */\n.dump #dump-list-container {\ndisplay: block;\n}\n#dump-list-container {\ndisplay: none;\nposition: relative;\noverflow-y: hidden;\nmargin-top: 1px;\n}\n#dump-list {\noverflow-x: auto;\noverflow-y: hidden;\nwhite-space: pre;\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-list:hover {\noverflow-x: auto;\n}\n.qr-preview {\n-moz-box-sizing: border-box;\ncounter-increment: thumbnails;\ncursor: move;\ndisplay: inline-block;\nheight: 90px;\nwidth: 90px;\npadding: 2px;\nopacity: .5;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-moz-transition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qr-preview:hover,\n.qr-preview:focus {\nopacity: .9;\n}\n.qr-preview::before {\ncontent: counter(thumbnails);\ncolor: #fff;\nposition: absolute;\ntop: 3px;\nright: 3px;\ntext-shadow: 0 0 3px #000, 0 0 8px #000;\n}\n.qr-preview#selected {\nopacity: 1;\n}\n.qr-preview.drag {\nbox-shadow: 0 0 10px rgba(0,0,0,.5);\n}\n.qr-preview.over {\nborder-color: #fff;\n}\n.qr-preview > span {\ncolor: #fff;\n}\n.remove {\nbackground: none;\ncolor: #e00;\nfont-weight: 700;\npadding: 3px;\n}\na:only-of-type > .remove {\ndisplay: none;\n}\n.remove:hover::after {\ncontent: \" Remove\";\n}\n.qr-preview > label {\nbackground: rgba(0,0,0,.5);\ncolor: #fff;\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qr-preview > label > input {\nmargin: 0;\n}\n#add-post {\ncursor: pointer;\nfont-size: 2em;\nposition: absolute;\ntop: 50%;\nright: 10px;\n-moz-transform: translateY(-50%);\n}\n.textarea {\nposition: relative;\n}\n#char-count {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nposition: absolute;\nbottom: 1px;\nright: 1px;\npointer-events: none;\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\nposition: relative;\n}\n.menu-button i {\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 {\nposition: fixed;\noutline: none;\nz-index: 22;\n}\n.entry {\nborder-bottom: 1px solid rgba(0,0,0,.25);\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry:last-child {\nborder-bottom: 0;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: .5em solid;\nborder-top: .3em solid transparent;\nborder-bottom: .3em solid transparent;\ndisplay: inline-block;\nmargin: .3em;\nposition: absolute;\nright: 3px;\n}\n.submenu {\ndisplay: none;\nposition: absolute;\nleft: 100%;\ntop: -1px;\n}\n.focused .submenu {\ndisplay: block;\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/* Settings */\n:root.yotsuba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.yotsuba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba #menu {\ncolor: #800000;\n}\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 10pt;\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/* Settings */\n:root.yotsuba-b #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.yotsuba-b .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba-b #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b #menu {\ncolor: #000;\n}\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 10pt;\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/* Settings */\n:root.futaba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.futaba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.futaba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba #menu {\ncolor: #800000;\n}\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 12pt;\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/* Settings */\n:root.burichan #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.burichan .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.burichan #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan #menu {\ncolor: #000000;\n}\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 12pt;\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/* Settings */\n:root.tomorrow #fourchanx-settings fieldset {\nborder-color: #111;\n}\n\n/* Quote */\n:root.tomorrow .backlink.deadlink {\ncolor: #81A2BE !important;\n}\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n.tomorrow #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow #qr select {\ncolor: #C5C8C6;\n}\n:root.tomorrow #qr option {\ncolor: #000;\n}\n:root.tomorrow .qr-preview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow #menu {\ncolor: #C5C8C6;\n}\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\nfont-size: 10pt;\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/* Settings */\n:root.photon #fourchanx-settings fieldset {\nborder-color: #CCC;\n}\n\n/* Quote */\n:root.photon .backlink.deadlink {\ncolor: #F60 !important;\n}\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.photon #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon #menu {\ncolor: #333;\n}\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\nfont-size: 10pt;\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.captcha-img,\n.field {\nbackground-color: #FFF;\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\noutline: none;\ntransition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field::-moz-placeholder,\n.field:hover::-moz-placeholder {\ncolor: #AAA !important;\nfont-size: 13px !important;\nopacity: 1.0 !important;\n}\n.captch-img:hover,\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.field[disabled] {\nbackground-color: #F2F2F2;\ncolor: #888;\n}\n.move {\ncursor: move;\noverflow: hidden;\n}\nlabel, .favicon {\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;\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#overlay,\n#fourchanx-settings,\n#qp, #ihover,\n#updater, #thread-stats,\n#navlinks, .fixed #header-bar,\n#qr {\nposition: fixed;\n}\n#watcher {\nposition: absolute;\n}\n#fourchanx-settings {\nz-index: 999;\n}\n#overlay {\nz-index: 900;\n}\n#notifications {\nz-index: 70;\n}\n#qp, #ihover {\nz-index: 60;\n}\n#menu {\nz-index: 50;\n}\n#navlinks, #updater, #thread-stats {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#watcher {\nz-index: 20;\n}\n.fixed #header-bar {\nz-index: 10;\n}\n/* Header */\n.fixed.top body {\npadding-top: 2em;\n}\n.fixed.bottom body {\npadding-bottom: 2em;\n}\n.fixed #header-bar {\nright: 0;\nleft: 0;\npadding: 3px 4px 4px;\n}\n.fixed.top #header-bar {\ntop: 0;\n}\n.fixed.bottom #header-bar {\nbottom: 0;\n}\n#header-bar {\nborder-width: 0;\ntransition: all .1s .05s ease-in-out;\n}\n.fixed.top #header-bar {\nborder-bottom-width: 1px;\n}\n.fixed.bottom #header-bar {\nbox-shadow: 0 -1px 2px rgba(0, 0, 0, .15);\nborder-top-width: 1px;\n}\n.fixed.bottom #header-bar .menu-button i {\nborder-top: none;\nborder-bottom: 6px solid;\n}\n#board-list {\ntext-align: center;\n}\n.fixed #header-bar.autohide:not(:hover) {\nbox-shadow: none;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.fixed.top #header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n}\n.fixed.bottom #header-bar.autohide:not(:hover) {\n-webkit-transform: translateY(100%);\ntransform: translateY(100%);\n}\n#toggle-header-bar {\nleft: 0;\nright: 0;\nheight: 10px;\nposition: absolute;\n}\n#boardNavDesktop #toggle-header-bar {\ndisplay: none;\n}\n.fixed #boardNavDesktop #toggle-header-bar {\ndisplay: block;\n}\n.fixed.top boardNavDesktop #toggle-header-bar {\ntop: 100%;\n}\n.fixed.bottom #boardNavDesktop #toggle-header-bar {\nbottom: 100%;\n}\n#header-bar a:not(.entry) {\ntext-decoration: none;\npadding: 1px;\n}\n#shortcuts:empty {\ndisplay: none;\n}\n.brackets-wrap::before {\ncontent: \"\\00a0[\";\n}\n.brackets-wrap::after {\ncontent: \"]\\00a0\";\n}\n.disabled,\n.expand-all-shortcut {\nopacity: .45;\n}\n#shortcuts {\nfloat: right;\n}\n#navbotright,\n#navtopright {\ndisplay: none;\n}\n#toggleMsgBtn {\ndisplay: none !important;\n}\n\n/* Notifications */\n#notifications {\nposition: fixed;\ntop: 0;\nheight: 0;\ntext-align: center;\nright: 0;\nleft: 0;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.fixed.top #header-bar #notifications {\nposition: absolute;\ntop: 100%;\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;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 38%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 38%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 38%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 38%, .9);\n}\n.notification a {\ncolor: white;\n}\n.notification > .close {\npadding: 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 6px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* Settings */\n:root.fourchan-x body {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\n}\n#overlay {\nbackground-color: rgba(0, 0, 0, .5);\ntop: 0;\nleft: 0;\nheight: 100%;\nwidth: 100%;\n}\n#fourchanx-settings {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nbox-shadow: 0 0 15px rgba(0, 0, 0, .15);\nheight: 600px;\nmin-height: 0;\nmax-height: 100%;\nwidth: 900px;\nmin-width: 0;\nmax-width: 100%;\nmargin: auto;\npadding: 3px;\ntop: 50%;\nleft: 50%;\n-moz-transform: translate(-50%, -50%);\n-webkit-transform: translate(-50%, -50%);\n-o-transform: translate(-50%, -50%);\ntransform: translate(-50%, -50%);\n}\n#fourchanx-settings > nav {\npadding: 2px 2px 0;\nheight: 15px;\n}\n#fourchanx-settings > nav a {\ntext-decoration: underline;\n}\n#fourchanx-settings > nav a.close {\ntext-decoration: none;\npadding: 2px;\n}\n.section-container {\noverflow: auto;\nposition: absolute;\ntop: 2.1em;\nright: 5px;\nbottom: 5px;\nleft: 5px;\npadding-top: 10px;\n}\n.sections-list {\npadding: 0 3px;\nfloat: left;\n}\n.credits {\nfloat: right;\n}\n.tab-selected {\nfont-weight: 700;\n}\n.section-sauce ul,\n.section-rice ul {\nlist-style: none;\nmargin: 0;\npadding: 8px;\n}\n.section-sauce li,\n.section-rice li {\npadding-left: 4px;\n}\n.section-main label {\ntext-decoration: underline;\n}\n.section-filter ul {\npadding: 0;\n}\n.section-filter li {\nmargin: 10px 40px;\n}\n.section-filter textarea {\nheight: 500px;\n}\n.section-sauce textarea {\nheight: 350px;\n}\n.section-rice .field[name=\"boardnav\"] {\nwidth: 100%;\n}\n.section-rice textarea {\nheight: 150px;\n}\n#fourchanx-settings fieldset {\nborder: 1px solid;\nborder-radius: 3px;\n}\n#fourchanx-settings legend {\nfont-weight: 700;\n}\n#fourchanx-settings textarea {\nfont-family: monospace;\nmin-width: 100%;\nmax-width: 100%;\n}\n#fourchanx-settings code {\ncolor: #000;\nbackground-color: #FFF;\npadding: 0 2px;\n}\n.unscroll {\noverflow: hidden;\n}\n\n/* Announcement Hiding */\n:root.hide-announcement #globalMessage {\ndisplay: none;\n}\na.hide-announcement {\nfloat: left;\n}\n\n/* Unread */\n#unread-line {\nmargin: 0;\n}\n\n/* Thread Updater */\n#updater:not(:hover) {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n#updater > .move {\npadding: 0 3px;\n}\n#updater > div:last-child {\ntext-align: center;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n#updater input[type=\"button\"] {\nwidth: 100%;\n}\n.new {\ncolor: limegreen;\n}\n\n/* Thread Watcher */\n#watcher {\npadding-bottom: 3px;\noverflow: hidden;\nwhite-space: nowrap;\n}\n#watcher:not(:hover) {\nmax-height: 220px;\n}\n#watcher > .move {\npadding-top: 3px;\n}\n#watcher > div {\nmax-width: 200px;\noverflow: hidden;\npadding-left: 3px;\npadding-right: 3px;\ntext-overflow: ellipsis;\n}\n#watcher a {\ntext-decoration: none;\n}\n\n/* Thread Stats */\n#thread-stats {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n\n/* Quote */\n.deadlink {\ntext-decoration: none !important;\n}\n.backlink.deadlink:not(.forwardlink), .quotelink.deadlink:not(.forwardlink) {\ntext-decoration: underline !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 > .opContainer::after {\ncontent: '';\nclear: both;\ndisplay: table;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 2px 2px 5px;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\nmax-height: 80vh;\nmax-width: 50vw;\n}\n.qphl {\noutline: 2px solid rgba(216, 94, 49, .7);\n}\n\n/* Quote Threading */\n.threadContainer {\nmargin-left: 20px;\nborder-left: 1px solid rgba(0,0,0,0.5);\n}\n.threadOP {\nclear: both;\n} \n\n/* File */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull,\n.expanded-image > .post > .file > .fileThumb > img[data-md5],\n:not(.expanded-image) > .post > .file > .fileThumb > .full-image {\ndisplay: none;\n}\n.expanding {\nopacity: .5;\n}\n.expanded-image {\nclear: both;\n}\n.expanded-image > .op > .file::after {\ncontent: '';\nclear: both;\ndisplay: table;\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#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Index/Reply Navigation */\n#navlinks {\nfont-size: 16px;\ntop: 25px;\nright: 10px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\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.stub input {\ndisplay: inline-block;\n}\n\n/* QR */\n:root.hide-original-post-form #postForm,\n:root.hide-original-post-form .postingMode,\n:root.hide-original-post-form #togglePostForm,\n#qr.autohide:not(.has-focus):not(:hover) > form,\n.postingMode ~ #qr select,\n#file-n-submit:not(.has-file) #qr-filerm {\ndisplay: none;\n}\n#qr select, #dump-button, .remove, .captcha-img {\ncursor: pointer;\n}\n#qr {\nz-index: 20;\nposition: fixed;\npadding: 1px;\nborder: 1px solid transparent;\nmin-width: 248px;\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nmargin-bottom: 1px;\n}\n#qr .close {\nfloat: right;\npadding: 0 3px;\n}\n#qr .warning {\nmin-height: 1.6em;\nvertical-align: middle;\npadding: 0 1px;\nborder-width: 1px;\nborder-style: solid;\n}\n.persona {\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-button {\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC; \nwidth: 10%;\nmargin: 0;\nfont: 13px sans-serif;\npadding: 1px 0px 2px;\n}\n.persona .field:not(#dump) {\nwidth: 95px;\nmin-width: 30%;\nmax-width: 30%;\n}\n#qr textarea.field {\nheight: 14.8em;\nmin-height: 9em;\n}\n#qr.has-captcha textarea.field {\nheight: 9em;\n}\ninput.field.tripped:not(:hover):not(:focus) {\ncolor: transparent !important; text-shadow: none !important;\n}\n#qr textarea {\nresize: both;\n}\n.captcha-img {\nmargin: 0px;\ntext-align: center;\nbackground-image: #fff;\nfont-size: 0px;\nmin-height: 59px;\nmin-width: 302px;\n}\n.captcha-input {\nwidth: 100%;\nmargin: 1px 0 0;\n}\n.field,\n.selectrice {\n-moz-box-sizing: border-box;\nmargin: 0px;\npadding: 2px 4px 3px;\n}\n#qr textarea {\nmin-width: 100%;\n}\n#qr [type='submit'] {\nwidth: 25%;\nvertical-align: top;\n}\n/* Fake File Input */\n#qr-filename,\n.has-file #qr-no-file {\ndisplay: none;\n}\n#qr-no-file,\n.has-file #qr-filename {\ndisplay: block;\npadding: 0px 4px;\nmargin-bottom: 2px;\n}\n#qr-no-file {\ncolor: #AAA;\n}\n#qr-filename-container {\n-moz-box-sizing: border-box;\ndisplay: inline-block;\nposition: relative;\nwidth: 100px;\nmin-width: 74.6%;\nmax-width: 74.6%;\nmargin-right: 0.4%;\nmargin-top: 1px;\noverflow: hidden;\npadding: 2px 1px 0;\n}\n#qr-filename-container:hover {\ncursor: text;\n}\n#qr-filerm {\nposition: relative;\nright: 14px;\nbottom: 6px;\nmargin-right: -8px;\nz-index: 2;\n}\n#file-n-submit {\nheight: 23px;\n}\n#qr input[type=file] {\ndisplay: none;\n}\n/* Thread Select / Spoiler Label */\n#qr select {\nfloat: right;\n}\n/* Dumping UI */\n.dump #dump-list-container {\ndisplay: block;\n}\n#dump-list-container {\ndisplay: none;\nposition: relative;\noverflow-y: hidden;\nmargin-top: 1px;\n}\n#dump-list {\noverflow-x: auto;\noverflow-y: hidden;\nwhite-space: pre;\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-list:hover {\noverflow-x: auto;\n}\n.qr-preview {\n-moz-box-sizing: border-box;\ncounter-increment: thumbnails;\ncursor: move;\ndisplay: inline-block;\nheight: 90px;\nwidth: 90px;\npadding: 2px;\nopacity: .5;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-moz-transition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qr-preview:hover,\n.qr-preview:focus {\nopacity: .9;\n}\n.qr-preview::before {\ncontent: counter(thumbnails);\ncolor: #fff;\nposition: absolute;\ntop: 3px;\nright: 3px;\ntext-shadow: 0 0 3px #000, 0 0 8px #000;\n}\n.qr-preview#selected {\nopacity: 1;\n}\n.qr-preview.drag {\nbox-shadow: 0 0 10px rgba(0,0,0,.5);\n}\n.qr-preview.over {\nborder-color: #fff;\n}\n.qr-preview > span {\ncolor: #fff;\n}\n.remove {\nbackground: none;\ncolor: #e00;\nfont-weight: 700;\npadding: 3px;\n}\na:only-of-type > .remove {\ndisplay: none;\n}\n.remove:hover::after {\ncontent: \" Remove\";\n}\n.qr-preview > label {\nbackground: rgba(0,0,0,.5);\ncolor: #fff;\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qr-preview > label > input {\nmargin: 0;\n}\n#add-post {\ncursor: pointer;\nfont-size: 2em;\nposition: absolute;\ntop: 50%;\nright: 10px;\n-moz-transform: translateY(-50%);\n}\n.textarea {\nposition: relative;\n}\n#char-count {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nposition: absolute;\nbottom: 1px;\nright: 1px;\npointer-events: none;\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\nposition: relative;\n}\n.menu-button i {\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 {\nposition: fixed;\noutline: none;\nz-index: 22;\n}\n.entry {\nborder-bottom: 1px solid rgba(0,0,0,.25);\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry:last-child {\nborder-bottom: 0;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: .5em solid;\nborder-top: .3em solid transparent;\nborder-bottom: .3em solid transparent;\ndisplay: inline-block;\nmargin: .3em;\nposition: absolute;\nright: 3px;\n}\n.submenu {\ndisplay: none;\nposition: absolute;\nleft: 100%;\ntop: -1px;\n}\n.focused .submenu {\ndisplay: block;\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/* Settings */\n:root.yotsuba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.yotsuba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba #menu {\ncolor: #800000;\n}\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 10pt;\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/* Settings */\n:root.yotsuba-b #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.yotsuba-b .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba-b #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b #menu {\ncolor: #000;\n}\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 10pt;\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/* Settings */\n:root.futaba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.futaba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.futaba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba #menu {\ncolor: #800000;\n}\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 12pt;\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/* Settings */\n:root.burichan #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.burichan .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.burichan #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan #menu {\ncolor: #000000;\n}\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 12pt;\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/* Settings */\n:root.tomorrow #fourchanx-settings fieldset {\nborder-color: #111;\n}\n\n/* Quote */\n:root.tomorrow .backlink.deadlink {\ncolor: #81A2BE !important;\n}\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n.tomorrow #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow #qr select {\ncolor: #C5C8C6;\n}\n:root.tomorrow #qr option {\ncolor: #000;\n}\n:root.tomorrow .qr-preview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow #menu {\ncolor: #C5C8C6;\n}\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\nfont-size: 10pt;\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/* Settings */\n:root.photon #fourchanx-settings fieldset {\nborder-color: #CCC;\n}\n\n/* Quote */\n:root.photon .backlink.deadlink {\ncolor: #F60 !important;\n}\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.photon #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon #menu {\ncolor: #333;\n}\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\nfont-size: 10pt;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" }; Main.init(); diff --git a/builds/crx/script.js b/builds/crx/script.js index c1c60e1f0..3cd08f526 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -35,7 +35,6 @@ 'Recursive Hiding': [true, 'Hide replies of hidden posts, recursively.'], 'Thread Hiding': [true, 'Add buttons to hide entire threads.'], 'Reply Hiding': [true, 'Add buttons to hide single replies.'], - 'Hiding Buttons': [true, 'Add buttons to hide threads / replies, in addition to menu links.'], 'Stubs': [true, 'Show stubs of hidden threads / replies.'] }, 'Images': { @@ -49,9 +48,9 @@ }, 'Menu': { 'Menu': [true, 'Add a drop-down menu to posts.'], + 'Report Link': [true, 'Add a report link to the menu.'], 'Thread Hiding Link': [true, 'Add a link to hide entire threads.'], 'Reply Hiding Link': [true, 'Add a link to hide single replies.'], - 'Report Link': [true, 'Add a report link to the menu.'], 'Delete Link': [true, 'Add post and image deletion links to the menu.'], 'Download Link': [true, 'Add a download with original filename link to the menu. Chrome-only currently.'], 'Archive Link': [true, 'Add an archive link to the menu.'] @@ -59,8 +58,10 @@ 'Monitoring': { 'Thread Updater': [true, 'Fetch and insert new replies. Has more options in its own dialog.'], 'Unread Count': [true, 'Show the unread posts count in the tab title.'], + 'Hide Unread Count at (0)': [false, 'Hide the unread posts count when it reaches 0.'], 'Unread Tab Icon': [true, 'Show a different favicon when there are unread posts.'], 'Unread Line': [true, 'Show a line to distinguish read posts from unread ones.'], + 'Scroll to Last Read Post': [true, 'Scroll back to the last read post when reopening a thread.'], 'Thread Excerpt': [true, 'Show an excerpt of the thread in the tab title.'], 'Thread Stats': [true, 'Display reply and image count.'], 'Thread Watcher': [true, 'Bookmark threads.'], @@ -1364,6 +1365,9 @@ OP = thread.OP; excerpt = ((_ref = OP.info.subject) != null ? _ref.trim() : void 0) || OP.info.comment.replace(/\n+/g, ' // ') || Conf['Anonymize'] && 'Anonymous' || $('.nameBlock', OP.nodes.info).textContent.trim(); + if (excerpt.length > 70) { + excerpt = "" + excerpt.slice(0, 67) + "..."; + } return "/" + thread.board + "/ - " + excerpt; }, postFromRoot: function(root) { @@ -2919,7 +2923,7 @@ if (makeStub == null) { makeStub = Conf['Stubs']; } - if (thread.hidden) { + if (thread.isHidden) { return; } OP = thread.OP; @@ -3885,9 +3889,6 @@ if (g.VIEW === 'catalog' || !Conf['Menu'] || !Conf['Download Link']) { return; } - if (!('download' in $.el('a'))) { - return; - } a = $.el('a', { className: 'download-link', textContent: 'Download file' @@ -4029,10 +4030,10 @@ hide = $.hasClass(this, 'hide-announcement'); text = PSAHiding.trim($.id('globalMessage')); - return $.get('hiddenPSAs', [], function(item) { + return $.get('hiddenPSAs', [], function(_arg) { var hiddenPSAs, i; - hiddenPSAs = item.hiddenPSAs; + hiddenPSAs = _arg.hiddenPSAs; if (hide) { hiddenPSAs.push(text); } else { @@ -4049,7 +4050,7 @@ btn = PSAHiding.btn; psa = $.id('globalMessage'); - return _ref1 = (_ref = PSAHiding.trim(psa), __indexOf.call(hiddenPSAs, _ref) >= 0) ? [true, '[ + ]', 'show-announcement'] : [false, '[ - ]', 'hide-announcement'], psa.hidden = _ref1[0], btn.innerHTML = _ref1[1], btn.className = _ref1[2], _ref1; + return _ref1 = (_ref = PSAHiding.trim(psa), __indexOf.call(hiddenPSAs, _ref) >= 0) ? [true, '[\u00A0+\u00A0]', 'show-announcement'] : [false, '[\u00A0-\u00A0]', 'hide-announcement'], psa.hidden = _ref1[0], btn.firstChild.textContent = _ref1[1], btn.className = _ref1[2], _ref1; }, trim: function(psa) { return psa.textContent.replace(/\W+/g, '').toLowerCase(); @@ -4071,6 +4072,7 @@ }); input = $('input', el); $.on(input, 'change', this.toggle); + $.sync('Header catalog links', CatalogLinks.set); $.event('AddMenuEntry', { type: 'header', el: el, @@ -4085,14 +4087,21 @@ return $.asap((function() { return $.id('boardNavMobile'); }), function() { - return CatalogLinks.toggle.call(input); + return CatalogLinks.set(input.checked); }); }); }, toggle: function() { - var a, board, useCatalog, _i, _len, _ref; + var useCatalog; + $.event('CloseMenu'); $.set('Header catalog links', useCatalog = this.checked); + return CatalogLinks.set(useCatalog); + }, + set: function(useCatalog) { + var a, board, path, _i, _len, _ref; + + path = useCatalog ? 'catalog' : ''; _ref = $$('a', $.id('boardNavDesktop')); for (_i = 0, _len = _ref.length; _i < _len; _i++) { a = _ref[_i]; @@ -4103,7 +4112,7 @@ if (Conf['External Catalog']) { a.href = useCatalog ? CatalogLinks.external(board) : "//boards.4chan.org/" + board + "/"; } else { - a.pathname = "/" + board + "/" + (useCatalog ? 'catalog' : ''); + a.pathname = "/" + board + "/" + path; } a.title = useCatalog ? "" + a.title + " - Catalog" : a.title.replace(/\ -\ Catalog$/, ''); } @@ -4514,20 +4523,20 @@ Header = { init: function() { - var createSubEntry, setting, subEntries, _i, _len, _ref; + var createSubEntry, headerToggler, setting, subEntries, _i, _len, _ref; this.menuButton = $.el('span', { className: 'menu-button', innerHTML: '' }); this.menu = new UI.Menu('header'); - $.on(this.menuButton, 'click', this.menuToggle); - $.on(this.toggle, 'mousedown', this.toggleBarVisibility); - $.on(window, 'load hashchange', Header.hashScroll); - this.positionToggler = $.el('span', { - textContent: 'Header Position', - className: 'header-position-link' + headerToggler = $.el('label', { + innerHTML: ' Auto-hide header' }); + this.headerToggler = headerToggler.firstElementChild; + $.on(this.menuButton, 'click', this.menuToggle); + $.on(window, 'load hashchange', Header.hashScroll); + $.on(this.headerToggler, 'change', this.toggleBarVisibility); createSubEntry = Header.createSubEntry; subEntries = []; _ref = ['sticky top', 'sticky bottom', 'top']; @@ -4535,22 +4544,18 @@ setting = _ref[_i]; subEntries.push(createSubEntry(setting)); } + subEntries.push({ + el: headerToggler + }); this.addShortcut(Header.menuButton); $.event('AddMenuEntry', { type: 'header', - el: this.positionToggler, - order: 108, + el: $.el('span', { + textContent: 'Header' + }), + order: 105, subEntries: subEntries }); - this.headerToggler = $.el('label', { - innerHTML: " Auto-hide header" - }); - $.on(this.headerToggler.firstElementChild, 'change', this.toggleBarVisibility); - $.event('AddMenuEntry', { - type: 'header', - el: this.headerToggler, - order: 109 - }); $.on(d, 'CreateNotification', this.createNotification); $.asap((function() { return d.body; @@ -4578,9 +4583,6 @@ toggle: $.el('div', { id: 'toggle-header-bar' }), - settings: $.el('div', { - id: 'settings-container' - }), createSubEntry: function(setting) { var label; @@ -4610,7 +4612,7 @@ Header.setBarVisibility(Conf['Header auto-hide']); $.sync('Header auto-hide', Header.setBarVisibility); $.add(fullBoardList, __slice.call(nav.childNodes)); - $.add(nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle, Header.settings]); + $.add(nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle]); if (Conf['Custom Board Navigation']) { fullBoardList.hidden = true; customBoardList = $.el('span', { @@ -4720,13 +4722,27 @@ } }, setBarVisibility: function(hide) { - Header.headerToggler.firstElementChild.checked = hide; + Header.headerToggler.checked = hide; + $.event('CloseMenu'); return (hide ? $.addClass : $.rmClass)(Header.nav, 'autohide'); }, + toggleBarVisibility: function(e) { + var hide, message; + + if (e.type === 'mousedown' && e.button !== 0) { + return; + } + hide = this.nodeName === 'INPUT' ? this.checked : !$.hasClass(Header.bar, 'autohide'); + Conf['Header auto-hide'] = hide; + $.set('Header auto-hide', hide); + Header.setBarVisibility(hide); + message = hide ? 'The header bar will automatically hide itself.' : 'The header bar will remain visible.'; + return new Notification('info', message, 2); + }, hashScroll: function() { var post; - if (!(post = this.location.hash.slice(1))) { + if (!(post = $.id(this.location.hash.slice(1)))) { return; } if ((Get.postFromRoot(post)).isHidden) { @@ -4744,18 +4760,6 @@ } return ($.engine === 'webkit' ? d.body : doc).scrollTop += top; }, - toggleBarVisibility: function(e) { - var hide, message; - - if (e.type === 'mousedown' && e.button !== 0) { - return; - } - hide = this.nodeName === 'INPUT' ? this.checked : !$.hasClass(Header.nav, 'autohide'); - Header.setBarVisibility(hide); - message = hide ? 'The header bar will automatically hide itself.' : 'The header bar will remain visible.'; - new Notification('info', message, 2); - return $.set('Header auto-hide', hide); - }, addShortcut: function(el) { var shortcut; @@ -5095,6 +5099,9 @@ return "//nsfw.foolz.us/" + boardID + "/full_image/" + filename; case 'po': return "//archive.thedarkcave.org/" + boardID + "/full_image/" + filename; + case 'hr': + case 'tv': + return "http://archive.4plebs.org/" + boardID + "/full_image/" + filename; case 'ck': case 'fa': case 'lit': @@ -5138,6 +5145,9 @@ case 'out': case 'po': return "//archive.thedarkcave.org/_/api/chan/post/?board=" + boardID + "&num=" + postID; + case 'hr': + case 'x': + return "http://archive.4plebs.org/_/api/chan/post/?board=" + boardID + "&num=" + postID; } }, to: function(data) { @@ -5166,6 +5176,8 @@ case 'out': case 'po': return Redirect.path('//archive.thedarkcave.org', 'foolfuuka', data); + case 'hr': + return Redirect.path('http://archive.4plebs.org', 'foolfuuka', data); case 'ck': case 'fa': case 'lit': @@ -5654,9 +5666,7 @@ }); }, node: function() { - var excerpt; - - return d.title = (excerpt = Get.threadExcerpt(this)).length > 80 ? "" + excerpt.slice(0, 77) + "..." : excerpt; + return d.title = Get.threadExcerpt(this); } }; @@ -6224,7 +6234,7 @@ }); }, node: function() { - var ID, hash, post, posts, root, _ref; + var ID, post, posts, _ref; Unread.thread = this; Unread.title = d.title; @@ -6247,9 +6257,13 @@ if (Conf['Unread Line']) { $.on(d, 'visibilitychange', Unread.setLine); } - if (!Conf['Scroll to Last Read Post']) { - return; + if (Conf['Scroll to Last Read Post']) { + return $.on(window, 'load', Unread.scroll); } + }, + scroll: function() { + var hash, root; + if ((hash = location.hash.match(/\d+/)) && hash[0] in this.posts) { return; } @@ -6411,7 +6425,7 @@ count = Unread.posts.length; if (Conf['Unread Count']) { - d.title = g.DEAD ? "(" + Unread.posts.length + ") /" + g.BOARD + "/ - 404" : "(" + Unread.posts.length + ") " + Unread.title; + d.title = "" + (count || !Conf['Hide Unread Count at (0)'] ? "(" + count + ") " : '') + (g.DEAD ? "/" + g.BOARD + "/ - 404" : "" + Unread.title); if (!dontrepeat) { setTimeout(function() { d.title = ''; @@ -7471,8 +7485,8 @@ })); } $.on(nodes.filename.parentNode, 'click keyup', QR.openFileInput); - $.on(QR.nodes.el, 'focusin', QR.focusin); - $.on(QR.nodes.el, 'focusout', QR.focusout); + $.on(dialog, 'focusin', QR.focusin); + $.on(dialog, 'focusout', QR.focusout); $.on(nodes.autohide, 'change', QR.toggleHide); $.on(nodes.close, 'click', QR.close); $.on(nodes.dumpButton, 'click', function() { @@ -8425,7 +8439,7 @@ return; } $.event('CloseMenu'); - html = "\n
\n
"; + html = "\n
\n
"; Settings.overlay = overlay = $.el('div', { id: 'overlay' }); @@ -9307,7 +9321,7 @@ } return Main.thisPageIsLegit; }, - css: "/* General */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.captcha-img,\n.field {\nbackground-color: #FFF;\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\noutline: none;\ntransition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field::-moz-placeholder,\n.field:hover::-moz-placeholder {\ncolor: #AAA !important;\nfont-size: 13px !important;\nopacity: 1.0 !important;\n}\n.captch-img:hover,\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.field[disabled] {\nbackground-color: #F2F2F2;\ncolor: #888;\n}\n.move {\ncursor: move;\noverflow: hidden;\n}\nlabel, .favicon {\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;\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#overlay,\n#fourchanx-settings,\n#qp, #ihover,\n#updater, #thread-stats,\n#navlinks, .fixed #header-bar,\n#qr {\nposition: fixed;\n}\n#watcher {\nposition: absolute;\n}\n#fourchanx-settings {\nz-index: 999;\n}\n#overlay {\nz-index: 900;\n}\n#notifications {\nz-index: 70;\n}\n#qp, #ihover {\nz-index: 60;\n}\n#menu {\nz-index: 50;\n}\n#navlinks, #updater, #thread-stats {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#watcher {\nz-index: 20;\n}\n.fixed #header-bar {\nz-index: 10;\n}\n/* Header */\n.fourchan-x body {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\n}\n.fixed.top body {\npadding-top: 2em;\n}\n.fixed.bottom body {\npadding-bottom: 2em;\n}\n.fixed #header-bar {\nright: 0;\nleft: 0;\npadding: 3px 4px 4px;\n}\n.fixed.top #header-bar {\ntop: 0;\n}\n.fixed.bottom #header-bar {\nbottom: 0;\n}\n#header-bar {\nborder-width: 0;\ntransition: all .1s .05s ease-in-out;\n}\n.fixed.top #header-bar {\nborder-bottom-width: 1px;\n}\n.fixed.bottom #header-bar {\nbox-shadow: 0 -1px 2px rgba(0, 0, 0, .15);\nborder-top-width: 1px;\n}\n.fixed.bottom #header-bar .menu-button i {\nborder-top: none;\nborder-bottom: 6px solid;\n}\n#board-list {\ntext-align: center;\n}\n.fixed #header-bar.autohide:not(:hover) {\nbox-shadow: none;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.fixed.top #header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n}\n.fixed.bottom #header-bar.autohide:not(:hover) {\n-webkit-transform: translateY(100%);\ntransform: translateY(100%);\n}\n#toggle-header-bar {\nleft: 0;\nright: 0;\nheight: 10px;\nposition: absolute;\n}\n#header-bar #toggle-header-bar {\ndisplay: none;\n}\n.fixed #header-bar #toggle-header-bar {\ndisplay: block;\n}\n.fixed #header-bar #toggle-header-bar {\ncursor: n-resize;\n}\n.fixed.top header-bar #toggle-header-bar {\ntop: 100%;\n}\n.fixed.bottom #header-bar #toggle-header-bar {\nbottom: 100%;\n}\n.fixed #header-bar #header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a:not(.entry) {\ntext-decoration: none;\npadding: 1px;\n}\n#shortcuts:empty {\ndisplay: none;\n}\n.brackets-wrap::before {\ncontent: \"\\00a0[\";\n}\n.brackets-wrap::after {\ncontent: \"]\\00a0\";\n}\n.disabled,\n.expand-all-shortcut {\nopacity: .45;\n}\n#shortcuts {\nfloat: right;\n}\n#navbotright,\n#navtopright {\ndisplay: none;\n}\n#toggleMsgBtn {\ndisplay: none !important;\n}\n\n/* Notifications */\n#notifications {\nheight: 0;\ntext-align: center;\nposition: fixed;\ntop: 0;\nright: 0;\nleft: 0;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.top:not(.autohide) ~ #notifications {\ntop: 2em;\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;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 38%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 38%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 38%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 38%, .9);\n}\n.notification a {\ncolor: white;\n}\n.notification > .close {\npadding: 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 6px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* Settings */\n#overlay {\nbackground-color: rgba(0, 0, 0, .5);\ntop: 0;\nleft: 0;\nheight: 100%;\nwidth: 100%;\n}\n#fourchanx-settings {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nbox-shadow: 0 0 15px rgba(0, 0, 0, .15);\nheight: 600px;\nmin-height: 0;\nmax-height: 100%;\nwidth: 900px;\nmin-width: 0;\nmax-width: 100%;\npadding: 3px;\ntop: 50%;\nleft: 50%;\n-moz-transform: translate(-50%, -50%);\n-webkit-transform: translate(-50%, -50%);\n-o-transform: translate(-50%, -50%);\ntransform: translate(-50%, -50%);\n}\n#fourchanx-settings > nav {\npadding: 2px 2px 0;\nheight: 15px;\n}\n#fourchanx-settings > nav a {\ntext-decoration: underline;\n}\n#fourchanx-settings > nav a.close {\ntext-decoration: none;\npadding: 2px;\n}\n.section-container {\noverflow: auto;\nposition: absolute;\ntop: 2.1em;\nright: 5px;\nbottom: 5px;\nleft: 5px;\npadding-top: 10px;\n}\n.sections-list {\npadding: 0 3px;\nfloat: left;\n}\n.credits {\nfloat: right;\n}\n.tab-selected {\nfont-weight: 700;\n}\n.section-sauce ul,\n.section-rice ul {\nlist-style: none;\nmargin: 0;\npadding: 8px;\n}\n.section-sauce li,\n.section-rice li {\npadding-left: 4px;\n}\n.section-main label {\ntext-decoration: underline;\n}\n.section-filter ul {\npadding: 0;\n}\n.section-filter li {\nmargin: 10px 40px;\n}\n.section-filter textarea {\nheight: 500px;\n}\n.section-sauce textarea {\nheight: 350px;\n}\n.section-rice .field[name=\"boardnav\"] {\nwidth: 100%;\n}\n.section-rice textarea {\nheight: 150px;\n}\n#fourchanx-settings fieldset {\nborder: 1px solid;\nborder-radius: 3px;\n}\n#fourchanx-settings legend {\nfont-weight: 700;\n}\n#fourchanx-settings textarea {\nfont-family: monospace;\nmin-width: 100%;\nmax-width: 100%;\n}\n#fourchanx-settings code {\ncolor: #000;\nbackground-color: #FFF;\npadding: 0 2px;\n}\n.unscroll {\noverflow: hidden;\n}\n\n/* Announcement Hiding */\n:root.hide-announcement #globalMessage {\ndisplay: none;\n}\na.hide-announcement {\nfloat: left;\n}\n\n/* Unread */\n#unread-line {\nmargin: 0;\n}\n\n/* Thread Updater */\n#updater:not(:hover) {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n#updater > .move {\npadding: 0 3px;\n}\n#updater > div:last-child {\ntext-align: center;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n#updater input[type=\"button\"] {\nwidth: 100%;\n}\n.new {\ncolor: limegreen;\n}\n\n/* Thread Watcher */\n#watcher {\npadding-bottom: 3px;\noverflow: hidden;\nwhite-space: nowrap;\n}\n#watcher:not(:hover) {\nmax-height: 220px;\n}\n#watcher > .move {\npadding-top: 3px;\n}\n#watcher > div {\nmax-width: 200px;\noverflow: hidden;\npadding-left: 3px;\npadding-right: 3px;\ntext-overflow: ellipsis;\n}\n#watcher a {\ntext-decoration: none;\n}\n\n/* Thread Stats */\n#thread-stats {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n\n/* Quote */\n.deadlink {\ntext-decoration: none !important;\n}\n.backlink.deadlink:not(.forwardlink), .quotelink.deadlink:not(.forwardlink) {\ntext-decoration: underline !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 > .opContainer::after {\ncontent: '';\nclear: both;\ndisplay: table;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 2px 2px 5px;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\noutline: 2px solid rgba(216, 94, 49, .7);\n}\n\n/* Quote Threading */\n.threadContainer {\nmargin-left: 20px;\nborder-left: 1px solid rgba(0,0,0,0.5);\n}\n.threadOP {\nclear: both;\n} \n\n/* File */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull,\n.expanded-image > .post > .file > .fileThumb > img[data-md5],\n:not(.expanded-image) > .post > .file > .fileThumb > .full-image {\ndisplay: none;\n}\n.expanding {\nopacity: .5;\n}\n.expanded-image {\nclear: both;\n}\n.expanded-image > .op > .file::after {\ncontent: '';\nclear: both;\ndisplay: table;\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#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Index/Reply Navigation */\n#navlinks {\nfont-size: 16px;\ntop: 25px;\nright: 10px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\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.stub input {\ndisplay: inline-block;\n}\n\n/* QR */\n:root.hide-original-post-form #postForm,\n:root.hide-original-post-form .postingMode,\n:root.hide-original-post-form #togglePostForm,\n#qr.autohide:not(.has-focus):not(:hover) > form,\n.postingMode ~ #qr select,\n#file-n-submit:not(.has-file) #qr-filerm {\ndisplay: none;\n}\n#qr select, #dump-button, .remove, .captcha-img {\ncursor: pointer;\n}\n#qr {\nz-index: 20;\nposition: fixed;\npadding: 1px;\nborder: 1px solid transparent;\nmin-width: 248px;\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nmargin-bottom: 1px;\n}\n#qr .close {\nfloat: right;\npadding: 0 3px;\n}\n#qr .warning {\nmin-height: 1.6em;\nvertical-align: middle;\npadding: 0 1px;\nborder-width: 1px;\nborder-style: solid;\n}\n.persona {\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-button {\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC; \nwidth: 10%;\nmargin: 0;\nfont: 13px sans-serif;\npadding: 1px 0px 2px;\n}\n.persona .field:not(#dump) {\nwidth: 95px;\nmin-width: 30%;\nmax-width: 30%;\n}\n#qr textarea.field {\nheight: 14.8em;\nmin-height: 9em;\n}\n#qr.has-captcha textarea.field {\nheight: 9em;\n}\ninput.field.tripped:not(:hover):not(:focus) {\ncolor: transparent !important; text-shadow: none !important;\n}\n#qr textarea {\nresize: both;\n}\n.captcha-img {\nmargin: 0px;\ntext-align: center;\nbackground-image: #fff;\nfont-size: 0px;\nmin-height: 59px;\nmin-width: 302px;\n}\n.captcha-input {\nwidth: 100%;\nmargin: 1px 0 0;\n}\n.field,\n.selectrice {\n-moz-box-sizing: border-box;\nmargin: 0px;\npadding: 2px 4px 3px;\n}\n#qr textarea {\nmin-width: 100%;\n}\n#qr [type='submit'] {\nwidth: 25%;\nvertical-align: top;\n}\n/* Fake File Input */\n#qr-filename,\n.has-file #qr-no-file {\ndisplay: none;\n}\n#qr-no-file,\n.has-file #qr-filename {\ndisplay: block;\npadding: 0px 4px;\nmargin-bottom: 2px;\n}\n#qr-no-file {\ncolor: #AAA;\n}\n#qr-filename-container {\n-moz-box-sizing: border-box;\ndisplay: inline-block;\nposition: relative;\nwidth: 100px;\nmin-width: 74.6%;\nmax-width: 74.6%;\nmargin-right: 0.4%;\nmargin-top: 1px;\noverflow: hidden;\npadding: 2px 1px 0;\n}\n#qr-filename-container:hover {\ncursor: text;\n}\n#qr-filerm {\nposition: relative;\nright: 14px;\nbottom: 6px;\nmargin-right: -8px;\nz-index: 2;\n}\n#file-n-submit {\nheight: 23px;\n}\n#qr input[type=file] {\ndisplay: none;\n}\n/* Thread Select / Spoiler Label */\n#qr select {\nfloat: right;\n}\n/* Dumping UI */\n.dump #dump-list-container {\ndisplay: block;\n}\n#dump-list-container {\ndisplay: none;\nposition: relative;\noverflow-y: hidden;\nmargin-top: 1px;\n}\n#dump-list {\noverflow-x: auto;\noverflow-y: hidden;\nwhite-space: pre;\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-list:hover {\noverflow-x: auto;\n}\n.qr-preview {\n-moz-box-sizing: border-box;\ncounter-increment: thumbnails;\ncursor: move;\ndisplay: inline-block;\nheight: 90px;\nwidth: 90px;\npadding: 2px;\nopacity: .5;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-moz-transition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qr-preview:hover,\n.qr-preview:focus {\nopacity: .9;\n}\n.qr-preview::before {\ncontent: counter(thumbnails);\ncolor: #fff;\nposition: absolute;\ntop: 3px;\nright: 3px;\ntext-shadow: 0 0 3px #000, 0 0 8px #000;\n}\n.qr-preview#selected {\nopacity: 1;\n}\n.qr-preview.drag {\nbox-shadow: 0 0 10px rgba(0,0,0,.5);\n}\n.qr-preview.over {\nborder-color: #fff;\n}\n.qr-preview > span {\ncolor: #fff;\n}\n.remove {\nbackground: none;\ncolor: #e00;\nfont-weight: 700;\npadding: 3px;\n}\na:only-of-type > .remove {\ndisplay: none;\n}\n.remove:hover::after {\ncontent: \" Remove\";\n}\n.qr-preview > label {\nbackground: rgba(0,0,0,.5);\ncolor: #fff;\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qr-preview > label > input {\nmargin: 0;\n}\n#add-post {\ncursor: pointer;\nfont-size: 2em;\nposition: absolute;\ntop: 50%;\nright: 10px;\n-moz-transform: translateY(-50%);\n}\n.textarea {\nposition: relative;\n}\n#char-count {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nposition: absolute;\nbottom: 1px;\nright: 1px;\npointer-events: none;\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\nposition: relative;\n}\n.menu-button i {\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 {\nposition: fixed;\noutline: none;\nz-index: 22;\n}\n.entry {\nborder-bottom: 1px solid rgba(0,0,0,.25);\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry:last-child {\nborder-bottom: 0;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: .5em solid;\nborder-top: .3em solid transparent;\nborder-bottom: .3em solid transparent;\ndisplay: inline-block;\nmargin: .3em;\nposition: absolute;\nright: 3px;\n}\n.submenu {\ndisplay: none;\nposition: absolute;\nleft: 100%;\ntop: -1px;\n}\n.focused .submenu {\ndisplay: block;\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/* Settings */\n:root.yotsuba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.yotsuba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba #menu {\ncolor: #800000;\n}\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 10pt;\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/* Settings */\n:root.yotsuba-b #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.yotsuba-b .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba-b #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b #menu {\ncolor: #000;\n}\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 10pt;\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/* Settings */\n:root.futaba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.futaba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.futaba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba #menu {\ncolor: #800000;\n}\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 12pt;\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/* Settings */\n:root.burichan #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.burichan .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.burichan #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan #menu {\ncolor: #000000;\n}\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 12pt;\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/* Settings */\n:root.tomorrow #fourchanx-settings fieldset {\nborder-color: #111;\n}\n\n/* Quote */\n:root.tomorrow .backlink.deadlink {\ncolor: #81A2BE !important;\n}\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n.tomorrow #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow #qr select {\ncolor: #C5C8C6;\n}\n:root.tomorrow #qr option {\ncolor: #000;\n}\n:root.tomorrow .qr-preview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow #menu {\ncolor: #C5C8C6;\n}\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\nfont-size: 10pt;\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/* Settings */\n:root.photon #fourchanx-settings fieldset {\nborder-color: #CCC;\n}\n\n/* Quote */\n:root.photon .backlink.deadlink {\ncolor: #F60 !important;\n}\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.photon #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon #menu {\ncolor: #333;\n}\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\nfont-size: 10pt;\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.captcha-img,\n.field {\nbackground-color: #FFF;\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\noutline: none;\ntransition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field::-moz-placeholder,\n.field:hover::-moz-placeholder {\ncolor: #AAA !important;\nfont-size: 13px !important;\nopacity: 1.0 !important;\n}\n.captch-img:hover,\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.field[disabled] {\nbackground-color: #F2F2F2;\ncolor: #888;\n}\n.move {\ncursor: move;\noverflow: hidden;\n}\nlabel, .favicon {\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;\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#overlay,\n#fourchanx-settings,\n#qp, #ihover,\n#updater, #thread-stats,\n#navlinks, .fixed #header-bar,\n#qr {\nposition: fixed;\n}\n#watcher {\nposition: absolute;\n}\n#fourchanx-settings {\nz-index: 999;\n}\n#overlay {\nz-index: 900;\n}\n#notifications {\nz-index: 70;\n}\n#qp, #ihover {\nz-index: 60;\n}\n#menu {\nz-index: 50;\n}\n#navlinks, #updater, #thread-stats {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#watcher {\nz-index: 20;\n}\n.fixed #header-bar {\nz-index: 10;\n}\n/* Header */\n.fixed.top body {\npadding-top: 2em;\n}\n.fixed.bottom body {\npadding-bottom: 2em;\n}\n.fixed #header-bar {\nright: 0;\nleft: 0;\npadding: 3px 4px 4px;\n}\n.fixed.top #header-bar {\ntop: 0;\n}\n.fixed.bottom #header-bar {\nbottom: 0;\n}\n#header-bar {\nborder-width: 0;\ntransition: all .1s .05s ease-in-out;\n}\n.fixed.top #header-bar {\nborder-bottom-width: 1px;\n}\n.fixed.bottom #header-bar {\nbox-shadow: 0 -1px 2px rgba(0, 0, 0, .15);\nborder-top-width: 1px;\n}\n.fixed.bottom #header-bar .menu-button i {\nborder-top: none;\nborder-bottom: 6px solid;\n}\n#board-list {\ntext-align: center;\n}\n.fixed #header-bar.autohide:not(:hover) {\nbox-shadow: none;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.fixed.top #header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n}\n.fixed.bottom #header-bar.autohide:not(:hover) {\n-webkit-transform: translateY(100%);\ntransform: translateY(100%);\n}\n#toggle-header-bar {\nleft: 0;\nright: 0;\nheight: 10px;\nposition: absolute;\n}\n#boardNavDesktop #toggle-header-bar {\ndisplay: none;\n}\n.fixed #boardNavDesktop #toggle-header-bar {\ndisplay: block;\n}\n.fixed.top boardNavDesktop #toggle-header-bar {\ntop: 100%;\n}\n.fixed.bottom #boardNavDesktop #toggle-header-bar {\nbottom: 100%;\n}\n#header-bar a:not(.entry) {\ntext-decoration: none;\npadding: 1px;\n}\n#shortcuts:empty {\ndisplay: none;\n}\n.brackets-wrap::before {\ncontent: \"\\00a0[\";\n}\n.brackets-wrap::after {\ncontent: \"]\\00a0\";\n}\n.disabled,\n.expand-all-shortcut {\nopacity: .45;\n}\n#shortcuts {\nfloat: right;\n}\n#navbotright,\n#navtopright {\ndisplay: none;\n}\n#toggleMsgBtn {\ndisplay: none !important;\n}\n\n/* Notifications */\n#notifications {\nposition: fixed;\ntop: 0;\nheight: 0;\ntext-align: center;\nright: 0;\nleft: 0;\ntransition: all .8s .6s cubic-bezier(.55, .055, .675, .19);\n}\n.fixed.top #header-bar #notifications {\nposition: absolute;\ntop: 100%;\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;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 38%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 38%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 38%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 38%, .9);\n}\n.notification a {\ncolor: white;\n}\n.notification > .close {\npadding: 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 6px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* Settings */\n:root.fourchan-x body {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\n}\n#overlay {\nbackground-color: rgba(0, 0, 0, .5);\ntop: 0;\nleft: 0;\nheight: 100%;\nwidth: 100%;\n}\n#fourchanx-settings {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nbox-shadow: 0 0 15px rgba(0, 0, 0, .15);\nheight: 600px;\nmin-height: 0;\nmax-height: 100%;\nwidth: 900px;\nmin-width: 0;\nmax-width: 100%;\nmargin: auto;\npadding: 3px;\ntop: 50%;\nleft: 50%;\n-moz-transform: translate(-50%, -50%);\n-webkit-transform: translate(-50%, -50%);\n-o-transform: translate(-50%, -50%);\ntransform: translate(-50%, -50%);\n}\n#fourchanx-settings > nav {\npadding: 2px 2px 0;\nheight: 15px;\n}\n#fourchanx-settings > nav a {\ntext-decoration: underline;\n}\n#fourchanx-settings > nav a.close {\ntext-decoration: none;\npadding: 2px;\n}\n.section-container {\noverflow: auto;\nposition: absolute;\ntop: 2.1em;\nright: 5px;\nbottom: 5px;\nleft: 5px;\npadding-top: 10px;\n}\n.sections-list {\npadding: 0 3px;\nfloat: left;\n}\n.credits {\nfloat: right;\n}\n.tab-selected {\nfont-weight: 700;\n}\n.section-sauce ul,\n.section-rice ul {\nlist-style: none;\nmargin: 0;\npadding: 8px;\n}\n.section-sauce li,\n.section-rice li {\npadding-left: 4px;\n}\n.section-main label {\ntext-decoration: underline;\n}\n.section-filter ul {\npadding: 0;\n}\n.section-filter li {\nmargin: 10px 40px;\n}\n.section-filter textarea {\nheight: 500px;\n}\n.section-sauce textarea {\nheight: 350px;\n}\n.section-rice .field[name=\"boardnav\"] {\nwidth: 100%;\n}\n.section-rice textarea {\nheight: 150px;\n}\n#fourchanx-settings fieldset {\nborder: 1px solid;\nborder-radius: 3px;\n}\n#fourchanx-settings legend {\nfont-weight: 700;\n}\n#fourchanx-settings textarea {\nfont-family: monospace;\nmin-width: 100%;\nmax-width: 100%;\n}\n#fourchanx-settings code {\ncolor: #000;\nbackground-color: #FFF;\npadding: 0 2px;\n}\n.unscroll {\noverflow: hidden;\n}\n\n/* Announcement Hiding */\n:root.hide-announcement #globalMessage {\ndisplay: none;\n}\na.hide-announcement {\nfloat: left;\n}\n\n/* Unread */\n#unread-line {\nmargin: 0;\n}\n\n/* Thread Updater */\n#updater:not(:hover) {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n#updater > .move {\npadding: 0 3px;\n}\n#updater > div:last-child {\ntext-align: center;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n#updater input[type=\"button\"] {\nwidth: 100%;\n}\n.new {\ncolor: limegreen;\n}\n\n/* Thread Watcher */\n#watcher {\npadding-bottom: 3px;\noverflow: hidden;\nwhite-space: nowrap;\n}\n#watcher:not(:hover) {\nmax-height: 220px;\n}\n#watcher > .move {\npadding-top: 3px;\n}\n#watcher > div {\nmax-width: 200px;\noverflow: hidden;\npadding-left: 3px;\npadding-right: 3px;\ntext-overflow: ellipsis;\n}\n#watcher a {\ntext-decoration: none;\n}\n\n/* Thread Stats */\n#thread-stats {\nbackground: none;\nborder: none;\nbox-shadow: none;\n}\n\n/* Quote */\n.deadlink {\ntext-decoration: none !important;\n}\n.backlink.deadlink:not(.forwardlink), .quotelink.deadlink:not(.forwardlink) {\ntext-decoration: underline !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 > .opContainer::after {\ncontent: '';\nclear: both;\ndisplay: table;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 2px 2px 5px;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\nmax-height: 80vh;\nmax-width: 50vw;\n}\n.qphl {\noutline: 2px solid rgba(216, 94, 49, .7);\n}\n\n/* Quote Threading */\n.threadContainer {\nmargin-left: 20px;\nborder-left: 1px solid rgba(0,0,0,0.5);\n}\n.threadOP {\nclear: both;\n} \n\n/* File */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull,\n.expanded-image > .post > .file > .fileThumb > img[data-md5],\n:not(.expanded-image) > .post > .file > .fileThumb > .full-image {\ndisplay: none;\n}\n.expanding {\nopacity: .5;\n}\n.expanded-image {\nclear: both;\n}\n.expanded-image > .op > .file::after {\ncontent: '';\nclear: both;\ndisplay: table;\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#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Index/Reply Navigation */\n#navlinks {\nfont-size: 16px;\ntop: 25px;\nright: 10px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\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.stub input {\ndisplay: inline-block;\n}\n\n/* QR */\n:root.hide-original-post-form #postForm,\n:root.hide-original-post-form .postingMode,\n:root.hide-original-post-form #togglePostForm,\n#qr.autohide:not(.has-focus):not(:hover) > form,\n.postingMode ~ #qr select,\n#file-n-submit:not(.has-file) #qr-filerm {\ndisplay: none;\n}\n#qr select, #dump-button, .remove, .captcha-img {\ncursor: pointer;\n}\n#qr {\nz-index: 20;\nposition: fixed;\npadding: 1px;\nborder: 1px solid transparent;\nmin-width: 248px;\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nborder-radius: 3px 3px 0 0;\n}\n#qrtab {\nmargin-bottom: 1px;\n}\n#qr .close {\nfloat: right;\npadding: 0 3px;\n}\n#qr .warning {\nmin-height: 1.6em;\nvertical-align: middle;\npadding: 0 1px;\nborder-width: 1px;\nborder-style: solid;\n}\n.persona {\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-button {\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC; \nwidth: 10%;\nmargin: 0;\nfont: 13px sans-serif;\npadding: 1px 0px 2px;\n}\n.persona .field:not(#dump) {\nwidth: 95px;\nmin-width: 30%;\nmax-width: 30%;\n}\n#qr textarea.field {\nheight: 14.8em;\nmin-height: 9em;\n}\n#qr.has-captcha textarea.field {\nheight: 9em;\n}\ninput.field.tripped:not(:hover):not(:focus) {\ncolor: transparent !important; text-shadow: none !important;\n}\n#qr textarea {\nresize: both;\n}\n.captcha-img {\nmargin: 0px;\ntext-align: center;\nbackground-image: #fff;\nfont-size: 0px;\nmin-height: 59px;\nmin-width: 302px;\n}\n.captcha-input {\nwidth: 100%;\nmargin: 1px 0 0;\n}\n.field,\n.selectrice {\n-moz-box-sizing: border-box;\nmargin: 0px;\npadding: 2px 4px 3px;\n}\n#qr textarea {\nmin-width: 100%;\n}\n#qr [type='submit'] {\nwidth: 25%;\nvertical-align: top;\n}\n/* Fake File Input */\n#qr-filename,\n.has-file #qr-no-file {\ndisplay: none;\n}\n#qr-no-file,\n.has-file #qr-filename {\ndisplay: block;\npadding: 0px 4px;\nmargin-bottom: 2px;\n}\n#qr-no-file {\ncolor: #AAA;\n}\n#qr-filename-container {\n-moz-box-sizing: border-box;\ndisplay: inline-block;\nposition: relative;\nwidth: 100px;\nmin-width: 74.6%;\nmax-width: 74.6%;\nmargin-right: 0.4%;\nmargin-top: 1px;\noverflow: hidden;\npadding: 2px 1px 0;\n}\n#qr-filename-container:hover {\ncursor: text;\n}\n#qr-filerm {\nposition: relative;\nright: 14px;\nbottom: 6px;\nmargin-right: -8px;\nz-index: 2;\n}\n#file-n-submit {\nheight: 23px;\n}\n#qr input[type=file] {\ndisplay: none;\n}\n/* Thread Select / Spoiler Label */\n#qr select {\nfloat: right;\n}\n/* Dumping UI */\n.dump #dump-list-container {\ndisplay: block;\n}\n#dump-list-container {\ndisplay: none;\nposition: relative;\noverflow-y: hidden;\nmargin-top: 1px;\n}\n#dump-list {\noverflow-x: auto;\noverflow-y: hidden;\nwhite-space: pre;\nwidth: 248px;\nmax-width: 100%;\nmin-width: 100%;\n}\n#dump-list:hover {\noverflow-x: auto;\n}\n.qr-preview {\n-moz-box-sizing: border-box;\ncounter-increment: thumbnails;\ncursor: move;\ndisplay: inline-block;\nheight: 90px;\nwidth: 90px;\npadding: 2px;\nopacity: .5;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-moz-transition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qr-preview:hover,\n.qr-preview:focus {\nopacity: .9;\n}\n.qr-preview::before {\ncontent: counter(thumbnails);\ncolor: #fff;\nposition: absolute;\ntop: 3px;\nright: 3px;\ntext-shadow: 0 0 3px #000, 0 0 8px #000;\n}\n.qr-preview#selected {\nopacity: 1;\n}\n.qr-preview.drag {\nbox-shadow: 0 0 10px rgba(0,0,0,.5);\n}\n.qr-preview.over {\nborder-color: #fff;\n}\n.qr-preview > span {\ncolor: #fff;\n}\n.remove {\nbackground: none;\ncolor: #e00;\nfont-weight: 700;\npadding: 3px;\n}\na:only-of-type > .remove {\ndisplay: none;\n}\n.remove:hover::after {\ncontent: \" Remove\";\n}\n.qr-preview > label {\nbackground: rgba(0,0,0,.5);\ncolor: #fff;\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qr-preview > label > input {\nmargin: 0;\n}\n#add-post {\ncursor: pointer;\nfont-size: 2em;\nposition: absolute;\ntop: 50%;\nright: 10px;\n-moz-transform: translateY(-50%);\n}\n.textarea {\nposition: relative;\n}\n#char-count {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nposition: absolute;\nbottom: 1px;\nright: 1px;\npointer-events: none;\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\nposition: relative;\n}\n.menu-button i {\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 {\nposition: fixed;\noutline: none;\nz-index: 22;\n}\n.entry {\nborder-bottom: 1px solid rgba(0,0,0,.25);\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry:last-child {\nborder-bottom: 0;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: .5em solid;\nborder-top: .3em solid transparent;\nborder-bottom: .3em solid transparent;\ndisplay: inline-block;\nmargin: .3em;\nposition: absolute;\nright: 3px;\n}\n.submenu {\ndisplay: none;\nposition: absolute;\nleft: 100%;\ntop: -1px;\n}\n.focused .submenu {\ndisplay: block;\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/* Settings */\n:root.yotsuba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.yotsuba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba #menu {\ncolor: #800000;\n}\n:root.yotsuba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 10pt;\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/* Settings */\n:root.yotsuba-b #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.yotsuba-b .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.yotsuba-b #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b #menu {\ncolor: #000;\n}\n:root.yotsuba-b .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 10pt;\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/* Settings */\n:root.futaba #fourchanx-settings fieldset {\nborder-color: #D9BFB7;\n}\n\n/* Quote */\n:root.futaba .backlink.deadlink {\ncolor: #00E !important;\n}\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.futaba #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba #menu {\ncolor: #800000;\n}\n:root.futaba .entry {\nborder-bottom: 1px solid #D9BFB7;\nfont-size: 12pt;\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/* Settings */\n:root.burichan #fourchanx-settings fieldset {\nborder-color: #B7C5D9;\n}\n\n/* Quote */\n:root.burichan .backlink.deadlink {\ncolor: #34345C !important;\n}\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.burichan #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan #menu {\ncolor: #000000;\n}\n:root.burichan .entry {\nborder-bottom: 1px solid #B7C5D9;\nfont-size: 12pt;\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/* Settings */\n:root.tomorrow #fourchanx-settings fieldset {\nborder-color: #111;\n}\n\n/* Quote */\n:root.tomorrow .backlink.deadlink {\ncolor: #81A2BE !important;\n}\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n.tomorrow #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow #qr select {\ncolor: #C5C8C6;\n}\n:root.tomorrow #qr option {\ncolor: #000;\n}\n:root.tomorrow .qr-preview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow #menu {\ncolor: #C5C8C6;\n}\n:root.tomorrow .entry {\nborder-bottom: 1px solid #111;\nfont-size: 10pt;\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/* Settings */\n:root.photon #fourchanx-settings fieldset {\nborder-color: #CCC;\n}\n\n/* Quote */\n:root.photon .backlink.deadlink {\ncolor: #F60 !important;\n}\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n.photon #dump-list::-webkit-scrollbar-thumb {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .qr-preview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon #menu {\ncolor: #333;\n}\n:root.photon .entry {\nborder-bottom: 1px solid #CCC;\nfont-size: 10pt;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" }; Main.init(); diff --git a/package.json b/package.json index a3869bc44..246d3913a 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,15 @@ }, "devDependencies": { "grunt": "~0.4.1", - "grunt-bump": "~0.0.0", - "grunt-concurrent": "~0.1.1", - "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-coffee": "~0.6.6", + "grunt-bump": "~0.0.2", + "grunt-concurrent": "~0.2.0", + "grunt-contrib-clean": "~0.4.1", + "grunt-contrib-coffee": "~0.6.7", "grunt-contrib-compress": "~0.4.10", "grunt-contrib-concat": "~0.2.0", "grunt-contrib-copy": "~0.4.1", "grunt-contrib-watch": "~0.3.1", - "grunt-exec": "~0.4.0" + "grunt-shell": "~0.2.2" }, "repository": { "type": "git", diff --git a/src/config.coffee b/src/config.coffee index 8e8bb32f7..81c8941ce 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -97,10 +97,6 @@ Config = true 'Add buttons to hide single replies.' ] - 'Hiding Buttons': [ - true - 'Add buttons to hide threads / replies, in addition to menu links.' - ] 'Stubs': [ true 'Show stubs of hidden threads / replies.' @@ -141,6 +137,10 @@ Config = true 'Add a drop-down menu to posts.' ] + 'Report Link': [ + true + 'Add a report link to the menu.' + ] 'Thread Hiding Link': [ true 'Add a link to hide entire threads.' @@ -148,19 +148,17 @@ Config = 'Reply Hiding Link': [ true 'Add a link to hide single replies.' - ] - 'Report Link': [ - true - 'Add a report link to the menu.' ] 'Delete Link': [ true 'Add post and image deletion links to the menu.' ] + <% if (type === 'crx') { %> 'Download Link': [ true 'Add a download with original filename link to the menu. Chrome-only currently.' ] + <% } %> 'Archive Link': [ true 'Add an archive link to the menu.' @@ -175,6 +173,10 @@ Config = true 'Show the unread posts count in the tab title.' ] + 'Hide Unread Count at (0)': [ + false + 'Hide the unread posts count when it reaches 0.' + ] 'Unread Tab Icon': [ true 'Show a different favicon when there are unread posts.' @@ -183,6 +185,10 @@ Config = true 'Show a line to distinguish read posts from unread ones.' ] + 'Scroll to Last Read Post': [ + true + 'Scroll back to the last read post when reopening a thread.' + ] 'Thread Excerpt': [ true 'Show an excerpt of the thread in the tab title.' diff --git a/src/css/style.css b/src/css/style.css index 1c8bf1b48..0c19108fd 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -97,10 +97,6 @@ a[href="javascript:;"] { z-index: 10; } /* Header */ -.fourchan-x body { - -moz-box-sizing: border-box; - box-sizing: border-box; -} .fixed.top body { padding-top: 2em; } @@ -155,24 +151,18 @@ a[href="javascript:;"] { height: 10px; position: absolute; } -#header-bar #toggle-header-bar { +#boardNavDesktop #toggle-header-bar { display: none; } -.fixed #header-bar #toggle-header-bar { +.fixed #boardNavDesktop #toggle-header-bar { display: block; } -.fixed #header-bar #toggle-header-bar { - cursor: n-resize; -} -.fixed.top header-bar #toggle-header-bar { +.fixed.top boardNavDesktop #toggle-header-bar { top: 100%; } -.fixed.bottom #header-bar #toggle-header-bar { +.fixed.bottom #boardNavDesktop #toggle-header-bar { bottom: 100%; } -.fixed #header-bar #header-bar.autohide #toggle-header-bar { - cursor: s-resize; -} #header-bar a:not(.entry) { text-decoration: none; padding: 1px; @@ -203,16 +193,17 @@ a[href="javascript:;"] { /* Notifications */ #notifications { - height: 0; - text-align: center; position: fixed; top: 0; + height: 0; + text-align: center; right: 0; left: 0; transition: all .8s .6s cubic-bezier(.55, .055, .675, .19); } -.top:not(.autohide) ~ #notifications { - top: 2em; +.fixed.top #header-bar #notifications { + position: absolute; + top: 100%; } .notification { color: #FFF; @@ -257,6 +248,10 @@ a[href="javascript:;"] { } /* Settings */ +:root.fourchan-x body { + -moz-box-sizing: border-box; + box-sizing: border-box; +} #overlay { background-color: rgba(0, 0, 0, .5); top: 0; @@ -274,6 +269,7 @@ a[href="javascript:;"] { width: 900px; min-width: 0; max-width: 100%; + margin: auto; padding: 3px; top: 50%; left: 50%; @@ -478,6 +474,8 @@ a.hide-announcement { #qp img { max-height: 300px; max-width: 500px; + max-height: 80vh; + max-width: 50vw; } .qphl { outline: 2px solid rgba(216, 94, 49, .7); diff --git a/src/features.coffee b/src/features.coffee new file mode 100644 index 000000000..822325e8a --- /dev/null +++ b/src/features.coffee @@ -0,0 +1,4382 @@ +Header = + init: -> + headerEl = $.el 'div', + id: 'header' + innerHTML: """ +
+ + + + + + +
+
+
+ """.replace />\s+<' # get rid of spaces between elements + + @bar = $ '#header-bar', headerEl + @toggle = $ '#toggle-header-bar', @bar + + @menu = new UI.Menu 'header' + $.on $('.menu-button', @bar), 'click', @menuToggle + $.on @toggle, 'mousedown', @toggleBarVisibility + $.on window, 'load hashchange', Header.hashScroll + $.on d, 'CreateNotification', @createNotification + + catalogToggler = $.el 'label', + innerHTML: ' Use catalog board links' + headerToggler = $.el 'label', + innerHTML: ' Auto-hide header' + barPositionToggler = $.el 'label', + innerHTML: ' Bottom header' + + @catalogToggler = catalogToggler.firstElementChild + @headerToggler = headerToggler.firstElementChild + @barPositionToggler = barPositionToggler.firstElementChild + + $.on @catalogToggler, 'change', @toggleCatalogLinks + $.on @headerToggler, 'change', @toggleBarVisibility + $.on @barPositionToggler, 'change', @toggleBarPosition + + @setBarVisibility Conf['Header auto-hide'] + @setBarPosition Conf['Bottom header'] + + $.sync 'Header auto-hide', @setBarVisibility + $.sync 'Bottom header', @setBarPosition + + $.event 'AddMenuEntry', + type: 'header' + el: $.el 'span', textContent: 'Header' + order: 105 + subEntries: [ + {el: catalogToggler} + {el: headerToggler} + {el: barPositionToggler} + ] + + $.asap (-> d.body), -> + return unless Main.isThisPageLegit() + # Wait for #boardNavMobile instead of #boardNavDesktop, + # it might be incomplete otherwise. + $.asap (-> $.id 'boardNavMobile'), Header.setBoardList + $.prepend d.body, headerEl + + setBoardList: -> + nav = $.id 'boardNavDesktop' + if a = $ "a[href*='/#{g.BOARD}/']", nav + a.className = 'current' + fullBoardList = $ '#full-board-list', Header.bar + $.add fullBoardList, [nav.childNodes...] + + if Conf['Custom Board Navigation'] + Header.generateBoardList Conf['boardnav'] + $.sync 'boardnav', Header.generateBoardList + btn = $.el 'span', + className: 'hide-board-list-button brackets-wrap' + innerHTML: ' - ' + $.on btn, 'click', Header.toggleBoardList + $.add fullBoardList, btn + else + $.rm $ '#custom-board-list', Header.bar + fullBoardList.hidden = false + + Header.setCatalogLinks Conf['Header catalog links'] + $.sync 'Header catalog links', Header.setCatalogLinks + + generateBoardList: (text) -> + unless list = $ '#custom-board-list', Header.bar + # init'd with the custom board list disabled. + return + $.rmAll list + return unless text + as = $$('#full-board-list a', Header.bar)[0...-2] # ignore the Settings and Home links + nodes = text.match(/[\w@]+(-(all|title|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map (t) -> + if /^[^\w@]/.test t + return $.tn t + if /^toggle-all/.test t + a = $.el 'a', + className: 'show-board-list-button' + textContent: (t.match(/-text:"(.+)"/) || [null, '+'])[1] + href: 'javascript:;' + $.on a, 'click', Header.toggleBoardList + return a + board = if /^current/.test t + g.BOARD.ID + else + t.match(/^[^-]+/)[0] + for a in as + if a.textContent is board + a = a.cloneNode true + if /-title/.test t + a.textContent = a.title + else if /-full/.test t + a.textContent = "/#{board}/ - #{a.title}" + else if /-(index|catalog|text)/.test t + if m = t.match /-(index|catalog)/ + a.setAttribute 'data-only', m[1] + a.href = "//boards.4chan.org/#{board}/" + a.href += 'catalog' if m[1] is 'catalog' + if m = t.match /-text:"(.+)"/ + a.textContent = m[1] + else if board is '@' + $.addClass a, 'navSmall' + return a + $.tn t + $.add list, nodes + + toggleBoardList: -> + {bar} = Header + custom = $ '#custom-board-list', bar + full = $ '#full-board-list', bar + showBoardList = !full.hidden + custom.hidden = !showBoardList + full.hidden = showBoardList + + setCatalogLinks: (useCatalog) -> + Header.catalogToggler.checked = useCatalog + as = $$ '#board-list a[href*="boards.4chan.org"]', Header.bar + path = if useCatalog then 'catalog' else '' + for a in as + continue if a.dataset.only + a.pathname = "/#{a.pathname.split('/')[1]}/#{path}" + return + toggleCatalogLinks: -> + $.cb.checked.call @ + Header.setCatalogLinks @checked + + setBarVisibility: (hide) -> + Header.headerToggler.checked = hide + $.event 'CloseMenu' + (if hide then $.addClass else $.rmClass) Header.bar, 'autohide' + toggleBarVisibility: (e) -> + return if e.type is 'mousedown' and e.button isnt 0 # not LMB + hide = if @nodeName is 'INPUT' + @checked + else + !$.hasClass Header.bar, 'autohide' + Conf['Header auto-hide'] = hide + $.set 'Header auto-hide', hide + Header.setBarVisibility hide + message = if hide + 'The header bar will automatically hide itself.' + else + 'The header bar will remain visible.' + new Notification 'info', message, 2 + + setBarPosition: (bottom) -> + Header.barPositionToggler.checked = bottom + $.event 'CloseMenu' + if bottom + $.addClass doc, 'bottom-header' + $.rmClass doc, 'top-header' + Header.bar.parentNode.className = 'bottom' + else + $.addClass doc, 'top-header' + $.rmClass doc, 'bottom-header' + Header.bar.parentNode.className = 'top' + toggleBarPosition: -> + $.cb.checked.call @ + Header.setBarPosition @checked + + hashScroll: -> + return unless post = $.id @location.hash[1..] + return if (Get.postFromRoot post).isHidden + Header.scrollToPost post + scrollToPost: (post) -> + {top} = post.getBoundingClientRect() + unless Conf['Bottom header'] + headRect = Header.toggle.getBoundingClientRect() + top += - headRect.top - headRect.height + <% if (type === 'crx') { %>d.body<% } else { %>doc<% } %>.scrollTop += top + + addShortcut: (el) -> + shortcut = $.el 'span', + className: 'shortcut' + $.add shortcut, el + $.prepend $('#shortcuts', Header.bar), shortcut + + menuToggle: (e) -> + Header.menu.toggle e, @, g + + createNotification: (e) -> + {type, content, lifetime, cb} = e.detail + notif = new Notification type, content, lifetime + cb notif if cb + +class Notification + constructor: (type, content, @timeout) -> + @add = add.bind @ + @close = close.bind @ + + @el = $.el 'div', + innerHTML: '×
' + @el.style.opacity = 0 + @setType type + $.on @el.firstElementChild, 'click', @close + if typeof content is 'string' + content = $.tn content + $.add @el.lastElementChild, content + + $.ready @add + + setType: (type) -> + @el.className = "notification #{type}" + + add = -> + if d.hidden + $.on d, 'visibilitychange', @add + return + $.off d, 'visibilitychange', @add + $.add $.id('notifications'), @el + @el.clientHeight # force reflow + @el.style.opacity = 1 + setTimeout @close, @timeout * $.SECOND if @timeout + + close = -> + $.rm @el + +Settings = + init: -> + # 4chan X settings link + link = $.el 'a', + className: 'settings-link' + textContent: '<%= meta.name %> Settings' + href: 'javascript:;' + $.on link, 'click', Settings.open + $.event 'AddMenuEntry', + type: 'header' + el: link + order: 111 + + # 4chan settings link + link = $.el 'a', + className: 'fourchan-settings-link' + textContent: '4chan Settings' + href: 'javascript:;' + $.on link, 'click', -> $.id('settingsWindowLink').click() + $.event 'AddMenuEntry', + type: 'header' + el: link + order: 110 + open: -> Conf['Enable 4chan\'s Extension'] + + $.get 'previousversion', null, (item) -> + if previous = item['previousversion'] + return if previous is g.VERSION + # Avoid conflicts between sync'd newer versions + # and out of date extension on this device. + prev = previous.match(/\d+/g).map Number + curr = g.VERSION.match(/\d+/g).map Number + + changelog = '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' + el = $.el 'span', + innerHTML: "<%= meta.name %> has been updated to version #{g.VERSION}." + new Notification 'info', el, 30 + else + $.on d, '4chanXInitFinished', Settings.open + $.set + lastupdate: Date.now() + previousversion: g.VERSION + + Settings.addSection 'Main', Settings.main + Settings.addSection 'Filter', Settings.filter + Settings.addSection 'Sauce', Settings.sauce + Settings.addSection 'Rice', Settings.rice + Settings.addSection 'Keybinds', Settings.keybinds + $.on d, 'AddSettingsSection', Settings.addSection + $.on d, 'OpenSettings', (e) -> Settings.open e.detail + + return if Conf['Enable 4chan\'s Extension'] + settings = JSON.parse(localStorage.getItem '4chan-settings') or {} + return if settings.disableAll + settings.disableAll = true + localStorage.setItem '4chan-settings', JSON.stringify settings + + open: (openSection) -> + $.off d, '4chanXInitFinished', Settings.open + return if Settings.dialog + $.event 'CloseMenu' + + html = """ +
+ +
+
+
+ """ + + Settings.dialog = overlay = $.el 'div', + id: 'overlay' + innerHTML: html + + links = [] + for section in Settings.sections + link = $.el 'a', + className: "tab-#{section.hyphenatedTitle}" + textContent: section.title + href: 'javascript:;' + $.on link, 'click', Settings.openSection.bind section + links.push link, $.tn ' | ' + sectionToOpen = link if section.title is openSection + links.pop() + $.add $('.sections-list', overlay), links + (if sectionToOpen then sectionToOpen else links[0]).click() + + $.on $('.close', overlay), 'click', Settings.close + $.on overlay, 'click', Settings.close + $.on overlay.firstElementChild, 'click', (e) -> e.stopPropagation() + + d.body.style.width = "#{d.body.clientWidth}px" + $.addClass d.body, 'unscroll' + $.add d.body, overlay + close: -> + return unless Settings.dialog + d.body.style.removeProperty 'width' + $.rmClass d.body, 'unscroll' + $.rm Settings.dialog + delete Settings.dialog + + sections: [] + addSection: (title, open) -> + if typeof title isnt 'string' + {title, open} = title.detail + hyphenatedTitle = title.toLowerCase().replace /\s+/g, '-' + Settings.sections.push {title, hyphenatedTitle, open} + openSection: -> + if selected = $ '.tab-selected', Settings.dialog + $.rmClass selected, 'tab-selected' + $.addClass $(".tab-#{@hyphenatedTitle}", Settings.dialog), 'tab-selected' + section = $ 'section', Settings.dialog + $.rmAll section + section.className = "section-#{@hyphenatedTitle}" + @open section, g + section.scrollTop = 0 + + main: (section) -> + section.innerHTML = """ +
+ + + +
+

+ """ + $.on $('.export', section), 'click', Settings.export + $.on $('.import', section), 'click', Settings.import + $.on $('input', section), 'change', Settings.onImport + + items = {} + inputs = {} + for key, obj of Config.main + fs = $.el 'fieldset', + innerHTML: "#{key}" + for key, arr of obj + description = arr[1] + div = $.el 'div', + innerHTML: ": #{description}" + input = $ 'input', div + $.on input, 'change', $.cb.checked + items[key] = Conf[key] + inputs[key] = input + $.add fs, div + $.add section, fs + + $.get items, (items) -> + for key, val of items + inputs[key].checked = val + return + + div = $.el 'div', + innerHTML: ": Clear manually-hidden threads and posts on all boards. Refresh the page to apply." + button = $ 'button', div + hiddenNum = 0 + $.get 'hiddenThreads', boards: {}, (item) -> + for ID, board of item.hiddenThreads.boards + for ID, thread of board + hiddenNum++ + button.textContent = "Hidden: #{hiddenNum}" + $.get 'hiddenPosts', boards: {}, (item) -> + for ID, board of item.hiddenPosts.boards + for ID, thread of board + for ID, post of thread + hiddenNum++ + button.textContent = "Hidden: #{hiddenNum}" + $.on button, 'click', -> + @textContent = 'Hidden: 0' + $.get 'hiddenThreads', boards: {}, (item) -> + for boardID of item.hiddenThreads.boards + localStorage.removeItem "4chan-hide-t-#{boardID}" + $.delete ['hiddenThreads', 'hiddenPosts'] + $.after $('input[name="Stubs"]', section).parentNode.parentNode, div + export: (now, data) -> + unless typeof now is 'number' + now = Date.now() + data = + version: g.VERSION + date: now + Conf['WatchedThreads'] = {} + for db in DataBoards + Conf[db] = boards: {} + # Make sure to export the most recent data. + $.get Conf, (Conf) -> + data.Conf = Conf + Settings.export now, data + return + a = $.el 'a', + className: 'warning' + textContent: 'Save me!' + download: "<%= meta.name %> v#{g.VERSION}-#{now}.json" + href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}" + target: '_blank' + <% if (type === 'userscript') { %> + # XXX Firefox won't let us download automatically. + p = $ '.imp-exp-result', Settings.dialog + $.rmAll p + $.add p, a + <% } else { %> + a.click() + <% } %> + import: -> + @nextElementSibling.click() + onImport: -> + return unless file = @files[0] + output = @parentNode.nextElementSibling + unless confirm 'Your current settings will be entirely overwritten, are you sure?' + output.textContent = 'Import aborted.' + return + reader = new FileReader() + reader.onload = (e) -> + try + data = JSON.parse e.target.result + Settings.loadSettings data + if confirm 'Import successful. Refresh now?' + window.location.reload() + catch err + output.textContent = 'Import failed due to an error.' + c.error err.stack + reader.readAsText file + loadSettings: (data) -> + version = data.version.split '.' + if version[0] is '2' + data = Settings.convertSettings data, + # General confs + 'Disable 4chan\'s extension': '' + 'Catalog Links': '' + 'Reply Navigation': '' + 'Show Stubs': 'Stubs' + 'Image Auto-Gif': 'Auto-GIF' + 'Expand From Current': '' + 'Unread Favicon': 'Unread Tab Icon' + 'Post in Title': 'Thread Excerpt' + 'Auto Hide QR': '' + 'Open Reply in New Tab': '' + 'Remember QR size': '' + 'Quote Inline': 'Quote Inlining' + 'Quote Preview': 'Quote Previewing' + 'Indicate OP quote': 'Mark OP Quotes' + 'Indicate Cross-thread Quotes': 'Mark Cross-thread Quotes' + # filter + 'uniqueid': 'uniqueID' + 'mod': 'capcode' + 'country': 'flag' + 'md5': 'MD5' + # keybinds + 'openEmptyQR': 'Open empty QR' + 'openQR': 'Open QR' + 'openOptions': 'Open settings' + 'close': 'Close' + 'spoiler': 'Spoiler tags' + 'code': 'Code tags' + 'submit': 'Submit QR' + 'watch': 'Watch' + 'update': 'Update' + 'unreadCountTo0': '' + 'expandAllImages': 'Expand images' + 'expandImage': 'Expand image' + 'zero': 'Front page' + 'nextPage': 'Next page' + 'previousPage': 'Previous page' + 'nextThread': 'Next thread' + 'previousThread': 'Previous thread' + 'expandThread': 'Expand thread' + 'openThreadTab': 'Open thread' + 'openThread': 'Open thread tab' + 'nextReply': 'Next reply' + 'previousReply': 'Previous reply' + 'hide': 'Hide' + # updater + 'Scrolling': 'Auto Scroll' + 'Verbose': '' + data.Conf.sauces = data.Conf.sauces.replace /\$\d/g, (c) -> + switch c + when '$1' + '%TURL' + when '$2' + '%URL' + when '$3' + '%MD5' + when '$4' + '%board' + else + c + for key, val of Config.hotkeys + continue unless key of data.Conf + data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) -> + "Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}" + data.Conf.WatchedThreads = data.WatchedThreads + $.set data.Conf + convertSettings: (data, map) -> + for prevKey, newKey of map + data.Conf[newKey] = data.Conf[prevKey] if newKey + delete data.Conf[prevKey] + data + + filter: (section) -> + section.innerHTML = """ + +
+ """ + select = $ 'select', section + $.on select, 'change', Settings.selectFilter + Settings.selectFilter.call select + selectFilter: -> + div = @nextElementSibling + if (name = @value) isnt 'guide' + $.rmAll div + ta = $.el 'textarea', + name: name + className: 'field' + spellcheck: false + $.get name, Conf[name], (item) -> + ta.value = item[name] + $.on ta, 'change', $.cb.value + $.add div, ta + return + div.innerHTML = """ +
Filter is disabled.
+

+ Use regular expressions, one per line.
+ Lines starting with a # will be ignored.
+ For example, /weeaboo/i will filter posts containing the string `weeaboo`, case-insensitive.
+ MD5 filtering uses exact string matching, not regular expressions. +

+
    You can use these settings with each regular expression, separate them with semicolons: +
  • + Per boards, separate them with commas. It is global if not specified.
    + For example: boards:a,jp;. +
  • +
  • + Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).
    + For example: op:only;, op:no; or op:yes;. +
  • +
  • + Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).
    + For example: stub:yes; or stub:no;. +
  • +
  • + Highlight instead of hiding. You can specify a class name to use with a userstyle.
    + For example: highlight; or highlight:wallpaper;. +
  • +
  • + Highlighted OPs will have their threads put on top of board pages by default.
    + For example: top:yes; or top:no;. +
  • +
+ """ + + sauce: (section) -> + section.innerHTML = """ +
Sauce is disabled.
+
Lines starting with a # will be ignored.
+
You can specify a display text by appending ;text:[text] to the URL.
+
    These parameters will be replaced by their corresponding values: +
  • %TURL: Thumbnail URL.
  • +
  • %URL: Full image URL.
  • +
  • %MD5: MD5 hash.
  • +
  • %board: Current board.
  • +
+ + """ + sauce = $ 'textarea', section + $.get 'sauces', Conf['sauces'], (item) -> + sauce.value = item['sauces'] + $.on sauce, 'change', $.cb.value + + rice: (section) -> + section.innerHTML = """ +
+ Custom Board Navigation is disabled. +
+
In the following, board can translate to a board ID (a, b, etc...), the current board (current), or the Status/Twitter link (status, @).
+
Board link: board
+
Title link: board-title
+
Full text link: board-full
+
Custom text link: board-text:"VIP Board"
+
Index-only link: board-index
+
Catalog-only link: board-catalog
+
Combinations are possible: board-index-text:"VIP Index"
+
Full board list toggle: toggle-all
+
+ +
+ Time Formatting is disabled. +
:
+
Supported format specifiers:
+
Day: %a, %A, %d, %e
+
Month: %m, %b, %B
+
Year: %y
+
Hour: %k, %H, %l, %I, %p, %P
+
Minute: %M
+
Second: %S
+
+ +
+ Quote Backlinks formatting is disabled. +
:
+
+ +
+ File Info Formatting is disabled. +
:
+
Link: %l (truncated), %L (untruncated), %T (Unix timestamp)
+
Original file name: %n (truncated), %N (untruncated), %t (Unix timestamp)
+
Spoiler indicator: %p
+
Size: %B (Bytes), %K (KB), %M (MB), %s (4chan default)
+
Resolution: %r (Displays 'PDF' for PDF files)
+
+ +
+ Unread Tab Icon is disabled. + + +
+ +
+ + + + + +
+ """ + items = {} + inputs = {} + for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss'] + input = $ "[name=#{name}]", section + items[name] = Conf[name] + inputs[name] = input + event = if name in ['favicon', 'usercss'] + 'change' + else + 'input' + $.on input, event, $.cb.value + $.get items, (items) -> + for key, val of items + input = inputs[key] + input.value = val + unless key in ['usercss'] + $.on input, event, Settings[key] + Settings[key].call input + return + $.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss + $.on $.id('apply-css'), 'click', Settings.usercss + boardnav: -> + Header.generateBoardList @value + time: -> + funk = Time.createFunc @value + @nextElementSibling.textContent = funk Time, new Date() + backlink: -> + @nextElementSibling.textContent = Conf['backlink'].replace /%id/, '123456789' + fileInfo: -> + data = + isReply: true + file: + URL: '//images.4chan.org/g/src/1334437723720.jpg' + name: 'd9bb2efc98dd0df141a94399ff5880b7.jpg' + size: '276 KB' + sizeInBytes: 276 * 1024 + dimensions: '1280x720' + isImage: true + isSpoiler: true + funk = FileInfo.createFunc @value + @nextElementSibling.innerHTML = funk FileInfo, data + favicon: -> + Favicon.switch() + Unread.update() if g.VIEW is 'thread' and Conf['Unread Tab Icon'] + @nextElementSibling.innerHTML = """ + + + + + """ + togglecss: -> + if $('textarea[name=usercss]', $.x 'ancestor::fieldset[1]', @).disabled = !@checked + CustomCSS.rmStyle() + else + CustomCSS.addStyle() + $.cb.checked.call @ + usercss: -> + CustomCSS.update() + + keybinds: (section) -> + section.innerHTML = """ +
Keybinds are disabled.
+
Allowed keys: a-z, 0-9, Ctrl, Shift, Alt, Meta, Enter, Esc, Up, Down, Right, Left.
+
Press Backspace to disable a keybind.
+ + +
ActionsKeybinds
+ """ + tbody = $ 'tbody', section + items = {} + inputs = {} + for key, arr of Config.hotkeys + tr = $.el 'tr', + innerHTML: "#{arr[1]}" + input = $ 'input', tr + input.name = key + input.spellcheck = false + items[key] = Conf[key] + inputs[key] = input + $.on input, 'keydown', Settings.keybind + $.add tbody, tr + $.get items, (items) -> + for key, val of items + inputs[key].value = val + return + keybind: (e) -> + return if e.keyCode is 9 # tab + e.preventDefault() + e.stopPropagation() + return unless (key = Keybinds.keyCode e)? + @value = key + $.cb.value.call @ + +PSAHiding = + init: -> + return if !Conf['Announcement Hiding'] + + $.addClass doc, 'hide-announcement' + + $.on d, '4chanXInitFinished', @setup + setup: -> + $.off d, '4chanXInitFinished', PSAHiding.setup + + unless psa = $.id 'globalMessage' + $.rmClass doc, 'hide-announcement' + return + + PSAHiding.btn = btn = $.el 'a', + title: 'Toggle announcement.' + innerHTML: '' + href: 'javascript:;' + $.on btn, 'click', PSAHiding.toggle + + text = PSAHiding.trim psa + $.get 'hiddenPSAs', [], (item) -> + PSAHiding.sync item['hiddenPSAs'] + $.before psa, btn + $.rmClass doc, 'hide-announcement' + + $.sync 'hiddenPSAs', PSAHiding.sync + toggle: (e) -> + hide = $.hasClass @, 'hide-announcement' + text = PSAHiding.trim $.id 'globalMessage' + $.get 'hiddenPSAs', [], ({hiddenPSAs}) -> + if hide + hiddenPSAs.push text + else + i = hiddenPSAs.indexOf text + hiddenPSAs.splice i, 1 + hiddenPSAs = hiddenPSAs[-5..] + PSAHiding.sync hiddenPSAs + $.set 'hiddenPSAs', hiddenPSAs + sync: (hiddenPSAs) -> + {btn} = PSAHiding + psa = $.id 'globalMessage' + [psa.hidden, btn.firstChild.textContent, btn.className] = if PSAHiding.trim(psa) in hiddenPSAs + [true, '[\u00A0+\u00A0]', 'show-announcement'] + else + [false, '[\u00A0-\u00A0]', 'hide-announcement'] + trim: (psa) -> + psa.textContent.replace(/\W+/g, '').toLowerCase() + +Fourchan = + init: -> + return if g.VIEW is 'catalog' + + board = g.BOARD.ID + if board is 'g' + $.globalEval """ + window.addEventListener('prettyprint', function(e) { + var pre = e.detail; + pre.innerHTML = prettyPrintOne(pre.innerHTML); + }, false); + """ + Post::callbacks.push + name: 'Parse /g/ code' + cb: @code + if board is 'sci' + # https://github.com/MayhemYDG/4chan-x/issues/645#issuecomment-13704562 + $.globalEval """ + window.addEventListener('jsmath', function(e) { + if (jsMath.loaded) { + // process one post + jsMath.ProcessBeforeShowing(e.detail); + } else { + // load jsMath and process whole document + jsMath.Autoload.Script.Push('ProcessBeforeShowing', [null]); + jsMath.Autoload.LoadJsMath(); + } + }, false); + """ + Post::callbacks.push + name: 'Parse /sci/ math' + cb: @math + code: -> + return if @isClone + for pre in $$ '.prettyprint', @nodes.comment + $.event 'prettyprint', pre, window + return + math: -> + return if @isClone or !$ '.math', @nodes.comment + $.event 'jsmath', @nodes.post, window + parseThread: (threadID, offset, limit) -> + # Fix /sci/ + # Fix /g/ + $.event '4chanParsingDone', + threadId: threadID + offset: offset + limit: limit + +CustomCSS = + init: -> + return if !Conf['Custom CSS'] + @addStyle() + addStyle: -> + @style = $.addStyle Conf['usercss'] + rmStyle: -> + if @style + $.rm @style + delete @style + update: -> + unless @style + @addStyle() + @style.textContent = Conf['usercss'] + +Filter = + filters: {} + init: -> + return if g.VIEW is 'catalog' or !Conf['Filter'] + + for key of Config.filter + @filters[key] = [] + for filter in Conf[key].split '\n' + continue if filter[0] is '#' + + unless regexp = filter.match /\/(.+)\/(\w*)/ + continue + + # Don't mix up filter flags with the regular expression. + filter = filter.replace regexp[0], '' + + # Do not add this filter to the list if it's not a global one + # and it's not specifically applicable to the current board. + # Defaults to global. + boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global' + if boards isnt 'global' and not (g.BOARD.ID in boards.split ',') + continue + + if key in ['uniqueID', 'MD5'] + # MD5 filter will use strings instead of regular expressions. + regexp = regexp[1] + else + try + # Please, don't write silly regular expressions. + regexp = RegExp regexp[1], regexp[2] + catch err + # I warned you, bro. + new Notification 'warning', err.message, 60 + continue + + # Filter OPs along with their threads, replies only, or both. + # Defaults to both. + op = filter.match(/[^t]op:(yes|no|only)/)?[1] or 'yes' + + # Overrule the `Show Stubs` setting. + # Defaults to stub showing. + stub = switch filter.match(/stub:(yes|no)/)?[1] + when 'yes' + true + when 'no' + false + else + Conf['Stubs'] + + # Highlight the post, or hide it. + # If not specified, the highlight class will be filter-highlight. + # Defaults to post hiding. + if hl = /highlight/.test filter + hl = filter.match(/highlight:(\w+)/)?[1] or 'filter-highlight' + # Put highlighted OP's thread on top of the board page or not. + # Defaults to on top. + top = filter.match(/top:(yes|no)/)?[1] or 'yes' + top = top is 'yes' # Turn it into a boolean + + @filters[key].push @createFilter regexp, op, stub, hl, top + + # Only execute filter types that contain valid filters. + unless @filters[key].length + delete @filters[key] + + return unless Object.keys(@filters).length + Post::callbacks.push + name: 'Filter' + cb: @node + + createFilter: (regexp, op, stub, hl, top) -> + test = + if typeof regexp is 'string' + # MD5 checking + (value) -> regexp is value + else + (value) -> regexp.test value + settings = + hide: !hl + stub: stub + class: hl + top: top + (value, isReply) -> + if isReply and op is 'only' or !isReply and op is 'no' + return false + unless test value + return false + settings + + node: -> + return if @isClone + for key of Filter.filters + value = Filter[key] @ + # Continue if there's nothing to filter (no tripcode for example). + continue if value is false + + for filter in Filter.filters[key] + unless result = filter value, @isReply + continue + + # Hide + if result.hide + if @isReply + PostHiding.hide @, result.stub + else if g.VIEW is 'index' + ThreadHiding.hide @thread, result.stub + else + continue + return + + # Highlight + $.addClass @nodes.root, result.class + if !@isReply and result.top and g.VIEW is 'index' + # Put the highlighted OPs' thread on top of the board page... + thisThread = @nodes.root.parentNode + # ...before the first non highlighted thread. + if firstThread = $ 'div[class="postContainer opContainer"]' + unless firstThread is @nodes.root + $.before firstThread.parentNode, [thisThread, thisThread.nextElementSibling] + + name: (post) -> + if 'name' of post.info + return post.info.name + false + uniqueID: (post) -> + if 'uniqueID' of post.info + return post.info.uniqueID + false + tripcode: (post) -> + if 'tripcode' of post.info + return post.info.tripcode + false + capcode: (post) -> + if 'capcode' of post.info + return post.info.capcode + false + email: (post) -> + if 'email' of post.info + return post.info.email + false + subject: (post) -> + if 'subject' of post.info + return post.info.subject or false + false + comment: (post) -> + if 'comment' of post.info + return post.info.comment + false + flag: (post) -> + if 'flag' of post.info + return post.info.flag + false + filename: (post) -> + if post.file + return post.file.name + false + dimensions: (post) -> + if post.file and post.file.isImage + return post.file.dimensions + false + filesize: (post) -> + if post.file + return post.file.size + false + MD5: (post) -> + if post.file + return post.file.MD5 + false + + menu: + init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Filter'] + + div = $.el 'div', + textContent: 'Filter' + + entry = + type: 'post' + el: div + order: 50 + open: (post) -> + Filter.menu.post = post + true + subEntries: [] + + for type in [ + ['Name', 'name'] + ['Unique ID', 'uniqueID'] + ['Tripcode', 'tripcode'] + ['Capcode', 'capcode'] + ['E-mail', 'email'] + ['Subject', 'subject'] + ['Comment', 'comment'] + ['Flag', 'flag'] + ['Filename', 'filename'] + ['Image dimensions', 'dimensions'] + ['Filesize', 'filesize'] + ['Image MD5', 'MD5'] + ] + # Add a sub entry for each filter type. + entry.subEntries.push Filter.menu.createSubEntry type[0], type[1] + + $.event 'AddMenuEntry', entry + + createSubEntry: (text, type) -> + el = $.el 'a', + href: 'javascript:;' + textContent: text + el.setAttribute 'data-type', type + $.on el, 'click', Filter.menu.makeFilter + + return { + el: el + open: (post) -> + value = Filter[type] post + value isnt false + } + + makeFilter: -> + {type} = @dataset + # Convert value -> regexp, unless type is MD5 + value = Filter[type] Filter.menu.post + re = if type in ['uniqueID', 'MD5'] then value else value.replace /// + / + | \\ + | \^ + | \$ + | \n + | \. + | \( + | \) + | \{ + | \} + | \[ + | \] + | \? + | \* + | \+ + | \| + ///g, (c) -> + if c is '\n' + '\\n' + else if c is '\\' + '\\\\' + else + "\\#{c}" + + re = if type in ['uniqueID', 'MD5'] + "/#{re}/" + else + "/^#{re}$/" + + # Add a new line before the regexp unless the text is empty. + $.get type, Conf[type], (item) -> + save = item[type] + save = + if save + "#{save}\n#{re}" + else + re + $.set type, save + + # Open the settings and display & focus the relevant filter textarea. + Settings.open 'Filter' + section = $ '.section-container' + select = $ 'select[name=filter]', section + select.value = type + Settings.selectFilter.call select + ta = $ 'textarea', section + tl = ta.textLength + ta.setSelectionRange tl, tl + ta.focus() + +ThreadHiding = + init: -> + return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] and !Conf['Thread Hiding Link'] + + @db = new DataBoard 'hiddenThreads' + @syncCatalog() + Thread::callbacks.push + name: 'Thread Hiding' + cb: @node + + node: -> + if data = ThreadHiding.db.get {boardID: @board.ID, threadID: @ID} + ThreadHiding.hide @, data.makeStub + return unless Conf['Thread Hiding'] + $.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide' + + syncCatalog: -> + # Sync hidden threads from the catalog into the index. + hiddenThreads = ThreadHiding.db.get + boardID: g.BOARD.ID + defaultValue: {} + # XXX tmp fix + try + hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} + catch e + localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify {} + return ThreadHiding.syncCatalog() + + # Add threads that were hidden in the catalog. + for threadID of hiddenThreadsOnCatalog + unless threadID of hiddenThreads + hiddenThreads[threadID] = {} + + # Remove threads that were un-hidden in the catalog. + for threadID of hiddenThreads + unless threadID of hiddenThreadsOnCatalog + delete hiddenThreads[threadID] + + if (ThreadHiding.db.data.lastChecked or 0) > Date.now() - $.MINUTE + # Was cleaned just now. + ThreadHiding.cleanCatalog hiddenThreadsOnCatalog + + ThreadHiding.db.set + boardID: g.BOARD.ID + val: hiddenThreads + + cleanCatalog: (hiddenThreadsOnCatalog) -> + # We need to clean hidden threads on the catalog ourselves, + # otherwise if we don't visit the catalog regularly + # it will pollute the localStorage and our data. + $.cache "//api.4chan.org/#{g.BOARD}/threads.json", -> + return unless @status is 200 + threads = {} + for page in JSON.parse @response + for thread in page.threads + if thread.no of hiddenThreadsOnCatalog + threads[thread.no] = hiddenThreadsOnCatalog[thread.no] + if Object.keys(threads).length + localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify threads + else + localStorage.removeItem "4chan-hide-t-#{g.BOARD}" + + menu: + init: -> + return if g.VIEW isnt 'index' or !Conf['Menu'] or !Conf['Thread Hiding Link'] + + div = $.el 'div', + className: 'hide-thread-link' + textContent: 'Hide thread' + + apply = $.el 'a', + textContent: 'Apply' + href: 'javascript:;' + $.on apply, 'click', ThreadHiding.menu.hide + + makeStub = $.el 'label', + innerHTML: " Make stub" + + $.event 'AddMenuEntry', + type: 'post' + el: div + order: 20 + open: ({thread, isReply}) -> + if isReply or thread.isHidden + return false + ThreadHiding.menu.thread = thread + true + subEntries: [el: apply; el: makeStub] + hide: -> + makeStub = $('input', @parentNode).checked + {thread} = ThreadHiding.menu + ThreadHiding.hide thread, makeStub + ThreadHiding.saveHiddenState thread, makeStub + $.event 'CloseMenu' + + makeButton: (thread, type) -> + a = $.el 'a', + className: "#{type}-thread-button" + innerHTML: "[ #{if type is 'hide' then '-' else '+'} ]" + href: 'javascript:;' + a.setAttribute 'data-fullid', thread.fullID + $.on a, 'click', ThreadHiding.toggle + a + + saveHiddenState: (thread, makeStub) -> + hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} + if thread.isHidden + ThreadHiding.db.set + boardID: thread.board.ID + threadID: thread.ID + val: {makeStub} + hiddenThreadsOnCatalog[thread] = true + else + ThreadHiding.db.delete + boardID: thread.board.ID + threadID: thread.ID + delete hiddenThreadsOnCatalog[thread] + localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsOnCatalog + + toggle: (thread) -> + unless thread instanceof Thread + thread = g.threads[@dataset.fullid] + if thread.isHidden + ThreadHiding.show thread + else + ThreadHiding.hide thread + ThreadHiding.saveHiddenState thread + + hide: (thread, makeStub=Conf['Stubs']) -> + return if thread.isHidden + {OP} = thread + threadRoot = OP.nodes.root.parentNode + threadRoot.hidden = thread.isHidden = true + + unless makeStub + threadRoot.nextElementSibling.hidden = true #
+ return + + numReplies = 0 + if span = $ '.summary', threadRoot + numReplies = +span.textContent.match /\d+/ + numReplies += $$('.opContainer ~ .replyContainer', threadRoot).length + numReplies = if numReplies is 1 then '1 reply' else "#{numReplies} replies" + opInfo = + if Conf['Anonymize'] + 'Anonymous' + else + $('.nameBlock', OP.nodes.info).textContent + + a = ThreadHiding.makeButton thread, 'show' + $.add a, $.tn " #{opInfo} (#{numReplies})" + thread.stub = $.el 'div', + className: 'stub' + $.add thread.stub, a + if Conf['Menu'] + $.add thread.stub, [$.tn(' '), Menu.makeButton OP] + $.before threadRoot, thread.stub + + show: (thread) -> + if thread.stub + $.rm thread.stub + delete thread.stub + threadRoot = thread.OP.nodes.root.parentNode + threadRoot.nextElementSibling.hidden = + threadRoot.hidden = thread.isHidden = false + +PostHiding = + init: -> + return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] + + @db = new DataBoard 'hiddenPosts' + Post::callbacks.push + name: 'Reply Hiding' + cb: @node + + node: -> + return if !@isReply or @isClone + if data = PostHiding.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID} + if data.thisPost + PostHiding.hide @, data.makeStub, data.hideRecursively + else + Recursive.apply PostHiding.hide, @, data.makeStub, true + Recursive.add PostHiding.hide, @, data.makeStub, true + return unless Conf['Reply Hiding'] + $.replace $('.sideArrows', @nodes.root), PostHiding.makeButton @, 'hide' + + menu: + init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Reply Hiding Link'] + + # Hide + div = $.el 'div', + className: 'hide-reply-link' + textContent: 'Hide reply' + + apply = $.el 'a', + textContent: 'Apply' + href: 'javascript:;' + $.on apply, 'click', PostHiding.menu.hide + + thisPost = $.el 'label', + innerHTML: ' This post' + replies = $.el 'label', + innerHTML: " Hide replies" + makeStub = $.el 'label', + innerHTML: " Make stub" + + $.event 'AddMenuEntry', + type: 'post' + el: div + order: 20 + open: (post) -> + if !post.isReply or post.isClone or post.isHidden + return false + PostHiding.menu.post = post + true + subEntries: [{el: apply}, {el: thisPost}, {el: replies}, {el: makeStub}] + + # Show + div = $.el 'div', + className: 'show-reply-link' + textContent: 'Show reply' + + apply = $.el 'a', + textContent: 'Apply' + href: 'javascript:;' + $.on apply, 'click', PostHiding.menu.show + + thisPost = $.el 'label', + innerHTML: ' This post' + replies = $.el 'label', + innerHTML: " Show replies" + + $.event 'AddMenuEntry', + type: 'post' + el: div + order: 20 + open: (post) -> + if !post.isReply or post.isClone or !post.isHidden + return false + unless data = PostHiding.db.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID} + return false + PostHiding.menu.post = post + thisPost.firstChild.checked = post.isHidden + replies.firstChild.checked = if data?.hideRecursively? then data.hideRecursively else Conf['Recursive Hiding'] + true + subEntries: [{el: apply}, {el: thisPost}, {el: replies}] + hide: -> + parent = @parentNode + thisPost = $('input[name=thisPost]', parent).checked + replies = $('input[name=replies]', parent).checked + makeStub = $('input[name=makeStub]', parent).checked + {post} = PostHiding.menu + if thisPost + PostHiding.hide post, makeStub, replies + else if replies + Recursive.apply PostHiding.hide, post, makeStub, true + Recursive.add PostHiding.hide, post, makeStub, true + else + return + PostHiding.saveHiddenState post, true, thisPost, makeStub, replies + $.event 'CloseMenu' + show: -> + parent = @parentNode + thisPost = $('input[name=thisPost]', parent).checked + replies = $('input[name=replies]', parent).checked + {post} = PostHiding.menu + if thisPost + PostHiding.show post, replies + else if replies + Recursive.apply PostHiding.show, post, true + Recursive.rm PostHiding.hide, post, true + else + return + if data = PostHiding.db.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID} + PostHiding.saveHiddenState post, !(thisPost and replies), !thisPost, data.makeStub, !replies + $.event 'CloseMenu' + + makeButton: (post, type) -> + a = $.el 'a', + className: "#{type}-reply-button" + innerHTML: "[ #{if type is 'hide' then '-' else '+'} ]" + href: 'javascript:;' + $.on a, 'click', PostHiding.toggle + a + + saveHiddenState: (post, isHiding, thisPost, makeStub, hideRecursively) -> + data = + boardID: post.board.ID + threadID: post.thread.ID + postID: post.ID + if isHiding + data.val = + thisPost: thisPost isnt false # undefined -> true + makeStub: makeStub + hideRecursively: hideRecursively + PostHiding.db.set data + else + PostHiding.db.delete data + + toggle: -> + post = Get.postFromNode @ + if post.isHidden + PostHiding.show post + else + PostHiding.hide post + PostHiding.saveHiddenState post, post.isHidden + + hide: (post, makeStub=Conf['Stubs'], hideRecursively=Conf['Recursive Hiding']) -> + return if post.isHidden + post.isHidden = true + + if hideRecursively + Recursive.apply PostHiding.hide, post, makeStub, true + Recursive.add PostHiding.hide, post, makeStub, true + + for quotelink in Get.allQuotelinksLinkingTo post + $.addClass quotelink, 'filtered' + + unless makeStub + post.nodes.root.hidden = true + return + + a = PostHiding.makeButton post, 'show' + postInfo = + if Conf['Anonymize'] + 'Anonymous' + else + $('.nameBlock', post.nodes.info).textContent + $.add a, $.tn " #{postInfo}" + post.nodes.stub = $.el 'div', + className: 'stub' + $.add post.nodes.stub, a + if Conf['Menu'] + $.add post.nodes.stub, [$.tn(' '), Menu.makeButton post] + $.prepend post.nodes.root, post.nodes.stub + + show: (post, showRecursively=Conf['Recursive Hiding']) -> + if post.nodes.stub + $.rm post.nodes.stub + delete post.nodes.stub + else + post.nodes.root.hidden = false + post.isHidden = false + if showRecursively + Recursive.apply PostHiding.show, post, true + Recursive.rm PostHiding.hide, post + for quotelink in Get.allQuotelinksLinkingTo post + $.rmClass quotelink, 'filtered' + return + +Recursive = + recursives: {} + init: -> + return if g.VIEW is 'catalog' + + Post::callbacks.push + name: 'Recursive' + cb: @node + + node: -> + return if @isClone + for quote in @quotes + if obj = Recursive.recursives[quote] + for recursive, i in obj.recursives + recursive @, obj.args[i]... + return + + add: (recursive, post, args...) -> + obj = Recursive.recursives[post.fullID] or= + recursives: [] + args: [] + obj.recursives.push recursive + obj.args.push args + + rm: (recursive, post) -> + return unless obj = Recursive.recursives[post.fullID] + for rec, i in obj.recursives + if rec is recursive + obj.recursives.splice i, 1 + obj.args.splice i, 1 + return + + apply: (recursive, post, args...) -> + {fullID} = post + for ID, post of g.posts + if fullID in post.quotes + recursive post, args... + return + +QuoteStrikeThrough = + init: -> + return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] and !Conf['Filter'] + + Post::callbacks.push + name: 'Strike-through Quotes' + cb: @node + + node: -> + return if @isClone + for quotelink in @nodes.quotelinks + {boardID, postID} = Get.postDataFromLink quotelink + if g.posts["#{boardID}.#{postID}"]?.isHidden + $.addClass quotelink, 'filtered' + return + +Menu = + init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] + + @menu = new UI.Menu 'post' + Post::callbacks.push + name: 'Menu' + cb: @node + + node: -> + button = Menu.makeButton @ + if @isClone + $.replace $('.menu-button', @nodes.info), button + return + $.add @nodes.info, [$.tn('\u00A0'), button] + + makeButton: do -> + a = null + (post) -> + a or= $.el 'a', + className: 'menu-button' + innerHTML: '[]' + href: 'javascript:;' + clone = a.cloneNode true + clone.setAttribute 'data-postid', post.fullID + clone.setAttribute 'data-clone', true if post.isClone + $.on clone, 'click', Menu.toggle + clone + + toggle: (e) -> + post = + if @dataset.clone + Get.postFromNode @ + else + g.posts[@dataset.postid] + Menu.menu.toggle e, @, post + +ReportLink = + init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Report Link'] + + a = $.el 'a', + className: 'report-link' + href: 'javascript:;' + textContent: 'Report this post' + $.on a, 'click', ReportLink.report + $.event 'AddMenuEntry', + type: 'post' + el: a + order: 10 + open: (post) -> + ReportLink.post = post + !post.isDead + report: -> + {post} = ReportLink + url = "//sys.4chan.org/#{post.board}/imgboard.php?mode=report&no=#{post}" + id = Date.now() + set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200" + window.open url, id, set + +DeleteLink = + init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Delete Link'] + + div = $.el 'div', + className: 'delete-link' + textContent: 'Delete' + postEl = $.el 'a', + className: 'delete-post' + href: 'javascript:;' + fileEl = $.el 'a', + className: 'delete-file' + href: 'javascript:;' + + postEntry = + el: postEl + open: -> + postEl.textContent = 'Post' + $.on postEl, 'click', DeleteLink.delete + true + fileEntry = + el: fileEl + open: ({file}) -> + return false if !file or file.isDead + fileEl.textContent = 'File' + $.on fileEl, 'click', DeleteLink.delete + true + + $.event 'AddMenuEntry', + type: 'post' + el: div + order: 40 + open: (post) -> + return false if post.isDead + DeleteLink.post = post + node = div.firstChild + node.textContent = 'Delete' + DeleteLink.cooldown.start post, node + true + subEntries: [postEntry, fileEntry] + + delete: -> + {post} = DeleteLink + return if DeleteLink.cooldown.counting is post + + $.off @, 'click', DeleteLink.delete + @textContent = "Deleting #{@textContent}..." + + pwd = + if m = d.cookie.match /4chan_pass=([^;]+)/ + decodeURIComponent m[1] + else + $.id('delPassword').value + + fileOnly = $.hasClass @, 'delete-file' + + form = + mode: 'usrdel' + onlyimgdel: fileOnly + pwd: pwd + form[post.ID] = 'delete' + + link = @ + $.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"), + onload: -> DeleteLink.load link, post, fileOnly, @response + onerror: -> DeleteLink.error link + , + cred: true + form: $.formData form + load: (link, post, fileOnly, html) -> + tmpDoc = d.implementation.createHTMLDocument '' + tmpDoc.documentElement.innerHTML = html + if tmpDoc.title is '4chan - Banned' # Ban/warn check + s = 'Banned!' + else if msg = tmpDoc.getElementById 'errmsg' # error! + s = msg.textContent + $.on link, 'click', DeleteLink.delete + else + if tmpDoc.title is 'Updating index...' + # We're 100% sure. + (post.origin or post).kill fileOnly + s = 'Deleted' + link.textContent = s + error: (link) -> + link.textContent = 'Connection error, please retry.' + $.on link, 'click', DeleteLink.delete + + cooldown: + start: (post, node) -> + unless QR.db?.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID} + # Only start counting on our posts. + delete DeleteLink.cooldown.counting + return + DeleteLink.cooldown.counting = post + length = if post.board.ID is 'q' + 600 + else + 30 + seconds = Math.ceil (length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND + DeleteLink.cooldown.count post, seconds, length, node + count: (post, seconds, length, node) -> + return if DeleteLink.cooldown.counting isnt post + unless 0 <= seconds <= length + if DeleteLink.cooldown.counting is post + node.textContent = 'Delete' + delete DeleteLink.cooldown.counting + return + setTimeout DeleteLink.cooldown.count, 1000, post, seconds - 1, length, node + node.textContent = "Delete (#{seconds})" + +DownloadLink = + init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Download Link'] + + a = $.el 'a', + className: 'download-link' + textContent: 'Download file' + $.event 'AddMenuEntry', + type: 'post' + el: a + order: 70 + open: ({file}) -> + return false unless file + a.href = file.URL + a.download = file.name + true + +ArchiveLink = + init: -> + return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Archive Link'] + + div = $.el 'div', + textContent: 'Archive' + + entry = + type: 'post' + el: div + order: 90 + open: ({ID, thread, board}) -> + redirect = Redirect.to {postID: ID, threadID: thread.ID, boardID: board.ID} + redirect isnt "//boards.4chan.org/#{board}/" + subEntries: [] + + for type in [ + ['Post', 'post'] + ['Name', 'name'] + ['Tripcode', 'tripcode'] + ['E-mail', 'email'] + ['Subject', 'subject'] + ['Filename', 'filename'] + ['Image MD5', 'MD5'] + ] + # Add a sub entry for each type. + entry.subEntries.push @createSubEntry type[0], type[1] + + $.event 'AddMenuEntry', entry + + createSubEntry: (text, type) -> + el = $.el 'a', + textContent: text + target: '_blank' + + open = if type is 'post' + ({ID, thread, board}) -> + el.href = Redirect.to {postID: ID, threadID: thread.ID, boardID: board.ID} + true + else + (post) -> + value = Filter[type] post + # We want to parse the exact same stuff as the filter does already. + return false unless value + el.href = Redirect.to + boardID: post.board.ID + type: type + value: value + isSearch: true + true + + return { + el: el + open: open + } + +Keybinds = + init: -> + return if g.VIEW is 'catalog' or !Conf['Keybinds'] + + init = -> + $.off d, '4chanXInitFinished', init + $.on d, 'keydown', Keybinds.keydown + for node in $$ '[accesskey]' + node.removeAttribute 'accesskey' + return + $.on d, '4chanXInitFinished', init + + keydown: (e) -> + return unless key = Keybinds.keyCode e + {target} = e + if target.nodeName in ['INPUT', 'TEXTAREA'] + return unless /(Esc|Alt|Ctrl|Meta)/.test key + + threadRoot = Nav.getThread() + if op = $ '.op', threadRoot + thread = Get.postFromNode(op).thread + switch key + # QR & Options + when Conf['Toggle board list'] + if Conf['Custom Board Navigation'] + Header.toggleBoardList() + when Conf['Open empty QR'] + Keybinds.qr threadRoot + when Conf['Open QR'] + Keybinds.qr threadRoot, true + when Conf['Open settings'] + Settings.open() + when Conf['Close'] + if Settings.dialog + Settings.close() + else if (notifications = $$ '.notification').length + for notification in notifications + $('.close', notification).click() + else if QR.nodes + QR.close() + when Conf['Spoiler tags'] + return if target.nodeName isnt 'TEXTAREA' + Keybinds.tags 'spoiler', target + when Conf['Code tags'] + return if target.nodeName isnt 'TEXTAREA' + Keybinds.tags 'code', target + when Conf['Eqn tags'] + return if target.nodeName isnt 'TEXTAREA' + Keybinds.tags 'eqn', target + when Conf['Math tags'] + return if target.nodeName isnt 'TEXTAREA' + Keybinds.tags 'math', target + when Conf['Submit QR'] + QR.submit() if QR.nodes and !QR.status() + # Thread related + when Conf['Watch'] + ThreadWatcher.toggle thread + when Conf['Update'] + ThreadUpdater.update() + # Images + when Conf['Expand image'] + Keybinds.img threadRoot + when Conf['Expand images'] + Keybinds.img threadRoot, true + # Board Navigation + when Conf['Front page'] + window.location = "/#{g.BOARD}/0#delform" + when Conf['Open front page'] + $.open "/#{g.BOARD}/#delform" + when Conf['Next page'] + if form = $ '.next form' + window.location = form.action + when Conf['Previous page'] + if form = $ '.prev form' + window.location = form.action + # Thread Navigation + when Conf['Next thread'] + return if g.VIEW is 'thread' + Nav.scroll +1 + when Conf['Previous thread'] + return if g.VIEW is 'thread' + Nav.scroll -1 + when Conf['Expand thread'] + ExpandThread.toggle thread + when Conf['Open thread'] + Keybinds.open thread + when Conf['Open thread tab'] + Keybinds.open thread, true + # Reply Navigation + when Conf['Next reply'] + Keybinds.hl +1, threadRoot + when Conf['Previous reply'] + Keybinds.hl -1, threadRoot + when Conf['Hide'] + ThreadHiding.toggle thread if g.VIEW is 'index' + else + return + e.preventDefault() + e.stopPropagation() + + keyCode: (e) -> + key = switch kc = e.keyCode + when 8 # return + '' + when 13 + 'Enter' + when 27 + 'Esc' + when 37 + 'Left' + when 38 + 'Up' + when 39 + 'Right' + when 40 + 'Down' + else + if 48 <= kc <= 57 or 65 <= kc <= 90 # 0-9, A-Z + String.fromCharCode(kc).toLowerCase() + else + null + if key + if e.altKey then key = 'Alt+' + key + if e.ctrlKey then key = 'Ctrl+' + key + if e.metaKey then key = 'Meta+' + key + if e.shiftKey then key = 'Shift+' + key + key + + qr: (thread, quote) -> + return unless Conf['Quick Reply'] and QR.postingIsEnabled + QR.open() + if quote + QR.quote.call $ 'input', $('.post.highlight', thread) or thread + QR.nodes.com.focus() + + tags: (tag, ta) -> + value = ta.value + selStart = ta.selectionStart + selEnd = ta.selectionEnd + + ta.value = + value[...selStart] + + "[#{tag}]" + value[selStart...selEnd] + "[/#{tag}]" + + value[selEnd..] + + # Move the caret to the end of the selection. + range = "[#{tag}]".length + selEnd + ta.setSelectionRange range, range + + # Fire the 'input' event + $.event 'input', null, ta + + img: (thread, all) -> + if all + ImageExpand.cb.toggleAll() + else + post = Get.postFromNode $('.post.highlight', thread) or $ '.op', thread + ImageExpand.toggle post + + open: (thread, tab) -> + return if g.VIEW isnt 'index' + url = "/#{thread.board}/res/#{thread}" + if tab + $.open url + else + location.href = url + + hl: (delta, thread) -> + if Conf['Bottom header'] + topMargin = 0 + else + headRect = Header.toggle.getBoundingClientRect() + topMargin = headRect.top + headRect.height + if postEl = $ '.reply.highlight', thread + $.rmClass postEl, 'highlight' + rect = postEl.getBoundingClientRect() + if rect.bottom >= topMargin and rect.top <= doc.clientHeight # We're at least partially visible + root = postEl.parentNode + next = $.x 'child::div[contains(@class,"post reply")]', + if delta is +1 then root.nextElementSibling else root.previousElementSibling + unless next + @focus postEl + return + return unless g.VIEW is 'thread' or $.x('ancestor::div[parent::div[@class="board"]]', next) is thread + rect = next.getBoundingClientRect() + if rect.top < 0 or rect.bottom > doc.clientHeight + if delta is -1 + window.scrollBy 0, rect.top - topMargin + else + next.scrollIntoView false + @focus next + return + + replies = $$ '.reply', thread + replies.reverse() if delta is -1 + for reply in replies + rect = reply.getBoundingClientRect() + if delta is +1 and rect.top >= topMargin or delta is -1 and rect.bottom <= doc.clientHeight + @focus reply + return + + focus: (post) -> + $.addClass post, 'highlight' + +Nav = + init: -> + switch g.VIEW + when 'index' + return unless Conf['Index Navigation'] + when 'thread' + return unless Conf['Reply Navigation'] + else # catalog + return + + span = $.el 'span', + id: 'navlinks' + prev = $.el 'a', + textContent: '▲' + href: 'javascript:;' + next = $.el 'a', + textContent: '▼' + href: 'javascript:;' + + $.on prev, 'click', @prev + $.on next, 'click', @next + + $.add span, [prev, $.tn(' '), next] + append = -> + $.off d, '4chanXInitFinished', append + $.add d.body, span + $.on d, '4chanXInitFinished', append + + prev: -> + if g.VIEW is 'thread' + window.scrollTo 0, 0 + else + Nav.scroll -1 + + next: -> + if g.VIEW is 'thread' + window.scrollTo 0, d.body.scrollHeight + else + Nav.scroll +1 + + getThread: (full) -> + if Conf['Bottom header'] + topMargin = 0 + else + headRect = Header.toggle.getBoundingClientRect() + topMargin = headRect.top + headRect.height + threads = $$ '.thread:not([hidden])' + for thread, i in threads + rect = thread.getBoundingClientRect() + if rect.bottom > topMargin # not scrolled past + return if full then [threads, thread, i, rect, topMargin] else thread + return $ '.board' + + scroll: (delta) -> + [threads, thread, i, rect, topMargin] = Nav.getThread true + top = rect.top - topMargin + + # unless we're not at the beginning of the current thread + # (and thus wanting to move to beginning) + # or we're above the first thread and don't want to skip it + unless (delta is -1 and Math.ceil(top) < 0) or (delta is +1 and top > 1) + i += delta + + top = threads[i]?.getBoundingClientRect().top - topMargin + window.scrollBy 0, top + +Redirect = + image: (boardID, filename) -> + # Do not use g.BOARD, the image url can originate from a cross-quote. + switch boardID + when 'a', 'gd', 'jp', 'm', 'q', 'tg', 'vg', 'vp', 'vr', 'wsg' + "//archive.foolz.us/#{boardID}/full_image/#{filename}" + when 'u' + "//nsfw.foolz.us/#{boardID}/full_image/#{filename}" + when 'po' + "//archive.thedarkcave.org/#{boardID}/full_image/#{filename}" + when 'hr', 'tv' + "http://archive.4plebs.org/#{boardID}/full_image/#{filename}" + when 'ck', 'fa', 'lit', 's4s' + "//fuuka.warosu.org/#{boardID}/full_image/#{filename}" + when 'cgl', 'g', 'mu', 'w' + "//rbt.asia/#{boardID}/full_image/#{filename}" + when 'an', 'k', 'toy', 'x' + "http://archive.heinessen.com/#{boardID}/full_image/#{filename}" + when 'c' + "//archive.nyafuu.org/#{boardID}/full_image/#{filename}" + post: (boardID, postID) -> + # XXX foolz had HSTS set for 120 days, which broke XHR+CORS+Redirection when on HTTP. + # Remove necessary HTTPS procotol in September 2013. + switch boardID + when 'a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'vp', 'vr', 'wsg' + "https://archive.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}" + when 'u' + "https://nsfw.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}" + when 'c', 'int', 'out', 'po' + "//archive.thedarkcave.org/_/api/chan/post/?board=#{boardID}&num=#{postID}" + when 'hr', 'x' + "http://archive.4plebs.org/_/api/chan/post/?board=#{boardID}&num=#{postID}" + # for fuuka-based archives: + # https://github.com/eksopl/fuuka/issues/27 + to: (data) -> + {boardID} = data + switch boardID + when 'a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'vp', 'vr', 'wsg' + Redirect.path '//archive.foolz.us', 'foolfuuka', data + when 'u' + Redirect.path '//nsfw.foolz.us', 'foolfuuka', data + when 'int', 'out', 'po' + Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data + when 'hr' + Redirect.path 'http://archive.4plebs.org', 'foolfuuka', data + when 'ck', 'fa', 'lit', 's4s' + Redirect.path '//fuuka.warosu.org', 'fuuka', data + when 'diy', 'g', 'sci' + Redirect.path '//archive.installgentoo.net', 'fuuka', data + when 'cgl', 'mu', 'w' + Redirect.path '//rbt.asia', 'fuuka', data + when 'an', 'fit', 'k', 'mlp', 'r9k', 'toy', 'x' + Redirect.path 'http://archive.heinessen.com', 'fuuka', data + when 'c' + Redirect.path '//archive.nyafuu.org', 'fuuka', data + else + if data.threadID then "//boards.4chan.org/#{boardID}/" else '' + path: (base, archiver, data) -> + if data.isSearch + {boardID, type, value} = data + type = if type is 'name' + 'username' + else if type is 'MD5' + 'image' + else + type + value = encodeURIComponent value + return if archiver is 'foolfuuka' + "#{base}/#{boardID}/search/#{type}/#{value}" + else if type is 'image' + "#{base}/#{boardID}/?task=search2&search_media_hash=#{value}" + else + "#{base}/#{boardID}/?task=search2&search_#{type}=#{value}" + + {boardID, threadID, postID} = data + # keep the number only if the location.hash was sent f.e. + path = if threadID + "#{boardID}/thread/#{threadID}" + else + "#{boardID}/post/#{postID}" + if archiver is 'foolfuuka' + path += '/' + if threadID and postID + path += if archiver is 'foolfuuka' + "##{postID}" + else + "#p#{postID}" + "#{base}/#{path}" + +Build = + spoilerRange: {} + shortFilename: (filename, isReply) -> + # FILENAME SHORTENING SCIENCE: + # OPs have a +10 characters threshold. + # The file extension is not taken into account. + threshold = if isReply then 30 else 40 + if filename.length - 4 > threshold + "#{filename[...threshold - 5]}(...).#{filename[-3..]}" + else + filename + postFromObject: (data, boardID) -> + o = + # id + postID: data.no + threadID: data.resto or data.no + boardID: boardID + # info + name: data.name + capcode: data.capcode + tripcode: data.trip + uniqueID: data.id + email: if data.email then encodeURI data.email.replace /"/g, '"' else '' + subject: data.sub + flagCode: data.country + flagName: data.country_name + date: data.now + dateUTC: data.time + comment: data.com + # thread status + isSticky: !!data.sticky + isClosed: !!data.closed + # file + if data.ext or data.filedeleted + o.file = + name: data.filename + data.ext + timestamp: "#{data.tim}#{data.ext}" + url: "//images.4chan.org/#{boardID}/src/#{data.tim}#{data.ext}" + height: data.h + width: data.w + MD5: data.md5 + size: data.fsize + turl: "//thumbs.4chan.org/#{boardID}/thumb/#{data.tim}s.jpg" + theight: data.tn_h + twidth: data.tn_w + isSpoiler: !!data.spoiler + isDeleted: !!data.filedeleted + Build.post o + post: (o, isArchived) -> + ### + This function contains code from 4chan-JS (https://github.com/4chan/4chan-JS). + @license: https://github.com/4chan/4chan-JS/blob/master/LICENSE + ### + { + postID, threadID, boardID + name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC + isSticky, isClosed + comment + file + } = o + isOP = postID is threadID + + staticPath = '//static.4chan.org' + + if email + emailStart = '' + emailEnd = '' + else + emailStart = '' + emailEnd = '' + + subject = "#{subject or ''}" + + userID = + if !capcode and uniqueID + " (ID: " + + "#{uniqueID}) " + else + '' + + switch capcode + when 'admin', 'admin_highlight' + capcodeClass = " capcodeAdmin" + capcodeStart = " ## Admin" + capcode = " " + when 'mod' + capcodeClass = " capcodeMod" + capcodeStart = " ## Mod" + capcode = " " + when 'developer' + capcodeClass = " capcodeDeveloper" + capcodeStart = " ## Developer" + capcode = " " + else + capcodeClass = '' + capcodeStart = '' + capcode = '' + + flag = + if flagCode + " #{flagCode}" + else + '' + + if file?.isDeleted + fileHTML = + if isOP + "
" + + "File deleted." + + "
" + else + "
" + + "File deleted." + + "
" + else if file + ext = file.name[-3..] + if !file.twidth and !file.theight and ext is 'gif' # wtf ? + file.twidth = file.width + file.theight = file.height + + fileSize = $.bytesToString file.size + + fileThumb = file.turl + if file.isSpoiler + fileSize = "Spoiler Image, #{fileSize}" + unless isArchived + fileThumb = '//static.4chan.org/image/spoiler' + if spoilerRange = Build.spoilerRange[boardID] + # Randomize the spoiler image. + fileThumb += "-#{boardID}" + Math.floor 1 + spoilerRange * Math.random() + fileThumb += '.png' + file.twidth = file.theight = 100 + + if boardID.ID isnt 'f' + imgSrc = "" + + "#{fileSize}" + + # Ha ha, filenames! + # html -> text, translate WebKit's %22s into "s + a = $.el 'a', innerHTML: file.name + filename = a.textContent.replace /%22/g, '"' + + # shorten filename, get html + a.textContent = Build.shortFilename filename + shortFilename = a.innerHTML + + # get html + a.textContent = filename + filename = a.innerHTML.replace /'/g, ''' + + fileDims = if ext is 'pdf' then 'PDF' else "#{file.width}x#{file.height}" + fileInfo = "File: #{file.timestamp}" + + "-(#{fileSize}, #{fileDims}#{ + if file.isSpoiler + '' + else + ", #{shortFilename}" + }" + ")" + + fileHTML = "
#{fileInfo}
#{imgSrc}
" + else + fileHTML = '' + + tripcode = + if tripcode + " #{tripcode}" + else + '' + + sticky = + if isSticky + ' Sticky' + else + '' + closed = + if isClosed + ' Closed' + else + '' + + container = $.el 'div', + id: "pc#{postID}" + className: "postContainer #{if isOP then 'op' else 'reply'}Container" + innerHTML: \ + (if isOP then '' else "
>>
") + + "
" + + + "' + + + (if isOP then fileHTML else '') + + + "' + + + (if isOP then '' else fileHTML) + + + "
#{comment or ''}
" + + + '
' + + for quote in $$ '.quotelink', container + href = quote.getAttribute 'href' + continue if href[0] is '/' # Cross-board quote, or board link + quote.href = "/#{boardID}/res/#{href}" # Fix pathnames + + container + +Get = + threadExcerpt: (thread) -> + {OP} = thread + excerpt = OP.info.subject?.trim() or + OP.info.comment.replace(/\n+/g, ' // ') or + Conf['Anonymize'] and 'Anonymous' or + $('.nameBlock', OP.nodes.info).textContent.trim() + if excerpt.length > 70 + excerpt = "#{excerpt[...67]}..." + "/#{thread.board}/ - #{excerpt}" + postFromRoot: (root) -> + link = $ 'a[title="Highlight this post"]', root + boardID = link.pathname.split('/')[1] + postID = link.hash[2..] + index = root.dataset.clone + post = g.posts["#{boardID}.#{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 '/' + boardID = path[1] + threadID = path[3] + postID = link.hash[2..] + else # resurrected quote + boardID = link.dataset.boardid + threadID = link.dataset.threadid or 0 + postID = link.dataset.postid + return { + boardID: boardID + threadID: +threadID + postID: +postID + } + allQuotelinksLinkingTo: (post) -> + # Get quotelinks & backlinks linking to the given post. + quotelinks = [] + # First: + # In every posts, + # if it did quote this post, + # get all their backlinks. + for ID, quoterPost of g.posts + if post.fullID in quoterPost.quotes + for quoterPost in [quoterPost].concat quoterPost.clones + quotelinks.push.apply quotelinks, quoterPost.nodes.quotelinks + # Second: + # If we have quote backlinks: + # in all posts this post quoted + # and their clones, + # get all of their backlinks. + if Conf['Quote Backlinks'] + for quote in post.quotes + continue unless quotedPost = g.posts[quote] + for quotedPost in [quotedPost].concat quotedPost.clones + quotelinks.push.apply quotelinks, [quotedPost.nodes.backlinks...] + # Third: + # Filter out irrelevant quotelinks. + quotelinks.filter (quotelink) -> + {boardID, postID} = Get.postDataFromLink quotelink + boardID is post.board.ID and postID is post.ID + postClone: (boardID, threadID, postID, root, context) -> + if post = g.posts["#{boardID}.#{postID}"] + Get.insert post, root, context + return + + root.textContent = "Loading post No.#{postID}..." + if threadID + $.cache "//api.4chan.org/#{boardID}/res/#{threadID}.json", -> + Get.fetchedPost @, boardID, threadID, postID, root, context + else if url = Redirect.post boardID, postID + $.cache url, -> + Get.archivedPost @, boardID, postID, root, context + insert: (post, root, context) -> + # Stop here if the container has been removed while loading. + return unless root.parentNode + clone = post.addClone context + Main.callbackNodes Post, [clone] + + # Get rid of the side arrows. + {nodes} = clone + $.rmAll nodes.root + $.add nodes.root, nodes.post + + $.rmAll root + $.add root, nodes.root + fetchedPost: (req, boardID, threadID, postID, root, context) -> + # In case of multiple callbacks for the same request, + # don't parse the same original post more than once. + if post = g.posts["#{boardID}.#{postID}"] + Get.insert post, root, context + return + + {status} = req + if status not in [200, 304] + # The thread can die by the time we check a quote. + if url = Redirect.post boardID, postID + $.cache url, -> + Get.archivedPost @, boardID, postID, root, context + else + $.addClass root, 'warning' + root.textContent = + if status is 404 + "Thread No.#{threadID} 404'd." + else + "Error #{req.statusText} (#{req.status})." + return + + posts = JSON.parse(req.response).posts + Build.spoilerRange[boardID] = posts[0].custom_spoiler + for post in posts + break if post.no is postID # we found it! + if post.no > postID + # The post can be deleted by the time we check a quote. + if url = Redirect.post boardID, postID + $.cache url, -> + Get.archivedPost @, boardID, postID, root, context + else + $.addClass root, 'warning' + root.textContent = "Post No.#{postID} was not found." + return + + board = g.boards[boardID] or + new Board boardID + thread = g.threads["#{boardID}.#{threadID}"] or + new Thread threadID, board + post = new Post Build.postFromObject(post, boardID), thread, board + Main.callbackNodes Post, [post] + Get.insert post, root, context + archivedPost: (req, boardID, postID, root, context) -> + # In case of multiple callbacks for the same request, + # don't parse the same original post more than once. + if post = g.posts["#{boardID}.#{postID}"] + Get.insert post, root, context + return + + data = JSON.parse req.response + if data.error + $.addClass root, 'warning' + root.textContent = data.error + return + + # convert comment to html + bq = $.el 'blockquote', textContent: data.comment # set this first to convert text to HTML entities + # https://github.com/eksopl/fuuka/blob/master/Board/Yotsuba.pm#L413-452 + # https://github.com/eksopl/asagi/blob/master/src/main/java/net/easymodo/asagi/Yotsuba.java#L109-138 + bq.innerHTML = bq.innerHTML.replace /// + \n + | \[/?b\] + | \[/?spoiler\] + | \[/?code\] + | \[/?moot\] + | \[/?banned\] + ///g, (text) -> + switch text + when '\n' + '
' + when '[b]' + '' + when '[/b]' + '' + when '[spoiler]' + '' + when '[/spoiler]' + '' + when '[code]' + '
'
+          when '[/code]'
+            '
' + when '[moot]' + '
' + when '[/moot]' + '
' + when '[banned]' + '' + when '[/banned]' + '' + + comment = bq.innerHTML + # greentext + .replace(/(^|>)(>[^<$]*)(<|$)/g, '$1$2$3') + # quotes + .replace /((>){2}(>\/[a-z\d]+\/)?\d+)/g, '$1' + + threadID = data.thread_num + o = + # id + postID: "#{postID}" + threadID: "#{threadID}" + boardID: boardID + # info + name: data.name_processed + capcode: switch data.capcode + when 'M' then 'mod' + when 'A' then 'admin' + when 'D' then 'developer' + tripcode: data.trip + uniqueID: data.poster_hash + email: if data.email then encodeURI data.email else '' + subject: data.title_processed + flagCode: data.poster_country + flagName: data.poster_country_name_processed + date: data.fourchan_date + dateUTC: data.timestamp + comment: comment + # file + if data.media?.media_filename + o.file = + name: data.media.media_filename_processed + timestamp: data.media.media_orig + url: data.media.media_link or data.media.remote_media_link + height: data.media.media_h + width: data.media.media_w + MD5: data.media.media_hash + size: data.media.media_size + turl: data.media.thumb_link or "//thumbs.4chan.org/#{boardID}/thumb/#{data.media.preview_orig}" + theight: data.media.preview_h + twidth: data.media.preview_w + isSpoiler: data.media.spoiler is '1' + + board = g.boards[boardID] or + new Board boardID + thread = g.threads["#{boardID}.#{threadID}"] or + new Thread threadID, board + post = new Post Build.post(o, true), thread, board, + isArchived: true + Main.callbackNodes Post, [post] + Get.insert post, root, context + +Quotify = + init: -> + return if g.VIEW is 'catalog' or !Conf['Resurrect Quotes'] + + Post::callbacks.push + name: 'Resurrect Quotes' + cb: @node + node: -> + for deadlink in $$ '.deadlink', @nodes.comment + if @isClone + if $.hasClass deadlink, 'quotelink' + @nodes.quotelinks.push deadlink + else + Quotify.parseDeadlink.call @, deadlink + return + + parseDeadlink: (deadlink) -> + if deadlink.parentNode.className is 'prettyprint' + # Don't quotify deadlinks inside code tags, + # un-`span` them. + $.replace deadlink, [deadlink.childNodes...] + return + + quote = deadlink.textContent + return unless postID = quote.match(/\d+$/)?[0] + boardID = if m = quote.match /^>>>\/([a-z\d]+)/ + m[1] + else + @board.ID + quoteID = "#{boardID}.#{postID}" + + if post = g.posts[quoteID] + unless post.isDead + # Don't (Dead) when quotifying in an archived post, + # and we know the post still exists. + a = $.el 'a', + href: "/#{boardID}/#{post.thread}/res/#p#{postID}" + className: 'quotelink' + textContent: quote + else + # Replace the .deadlink span if we can redirect. + a = $.el 'a', + href: "/#{boardID}/#{post.thread}/res/#p#{postID}" + className: 'quotelink deadlink' + target: '_blank' + textContent: "#{quote}\u00A0(Dead)" + a.setAttribute 'data-boardid', boardID + a.setAttribute 'data-threadid', post.thread.ID + a.setAttribute 'data-postid', postID + else if redirect = Redirect.to {boardID, threadID: 0, postID} + # Replace the .deadlink span if we can redirect. + a = $.el 'a', + href: redirect + className: 'deadlink' + target: '_blank' + textContent: "#{quote}\u00A0(Dead)" + if Redirect.post boardID, postID + # Make it function as a normal quote if we can fetch the post. + $.addClass a, 'quotelink' + a.setAttribute 'data-boardid', boardID + a.setAttribute 'data-postid', postID + + unless quoteID in @quotes + @quotes.push quoteID + + unless a + deadlink.textContent = "#{quote}\u00A0(Dead)" + return + + $.replace deadlink, a + if $.hasClass a, 'quotelink' + @nodes.quotelinks.push a + +QuoteInline = + init: -> + return if g.VIEW is 'catalog' or !Conf['Quote Inlining'] + + Post::callbacks.push + name: 'Quote Inlining' + cb: @node + node: -> + for link in @nodes.quotelinks.concat [@nodes.backlinks...] + $.on link, 'click', QuoteInline.toggle + return + toggle: (e) -> + return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 + e.preventDefault() + {boardID, threadID, postID} = Get.postDataFromLink @ + context = Get.contextFromLink @ + if $.hasClass @, 'inlined' + QuoteInline.rm @, boardID, threadID, postID, context + else + return if $.x "ancestor::div[@id='p#{postID}']", @ + QuoteInline.add @, boardID, threadID, postID, context + @classList.toggle 'inlined' + + findRoot: (quotelink, isBacklink) -> + if isBacklink + quotelink.parentNode.parentNode + else + $.x 'ancestor-or-self::*[parent::blockquote][1]', quotelink + add: (quotelink, boardID, threadID, postID, context) -> + isBacklink = $.hasClass quotelink, 'backlink' + inline = $.el 'div', + id: "i#{postID}" + className: 'inline' + $.after QuoteInline.findRoot(quotelink, isBacklink), inline + Get.postClone boardID, threadID, postID, inline, context + + return unless (post = g.posts["#{boardID}.#{postID}"]) and + context.thread is post.thread + + # Hide forward post if it's a backlink of a post in this thread. + # Will only unhide if there's no inlined backlinks of it anymore. + if isBacklink and Conf['Forward Hiding'] + $.addClass post.nodes.root, 'forwarded' + post.forwarded++ or post.forwarded = 1 + + # Decrease the unread count if this post + # is in the array of unread posts. + return unless Unread.posts + Unread.readSinglePost post + + rm: (quotelink, boardID, threadID, postID, context) -> + isBacklink = $.hasClass quotelink, 'backlink' + # Select the corresponding inlined quote, and remove it. + root = QuoteInline.findRoot quotelink, isBacklink + root = $.x "following-sibling::div[@id='i#{postID}'][1]", root + $.rm root + + # Stop if it only contains text. + return unless el = root.firstElementChild + + # Dereference clone. + post = g.posts["#{boardID}.#{postID}"] + post.rmClone el.dataset.clone + + # Decrease forward count and unhide. + if Conf['Forward Hiding'] and + isBacklink and + context.thread is g.threads["#{boardID}.#{threadID}"] and + not --post.forwarded + delete post.forwarded + $.rmClass post.nodes.root, 'forwarded' + + # Repeat. + while inlined = $ '.inlined', el + {boardID, threadID, postID} = Get.postDataFromLink inlined + QuoteInline.rm inlined, boardID, threadID, postID, context + $.rmClass inlined, 'inlined' + return + +QuotePreview = + init: -> + return if g.VIEW is 'catalog' or !Conf['Quote Previewing'] + + Post::callbacks.push + name: 'Quote Previewing' + cb: @node + node: -> + for link in @nodes.quotelinks.concat [@nodes.backlinks...] + $.on link, 'mouseover', QuotePreview.mouseover + return + mouseover: (e) -> + return if $.hasClass @, 'inlined' + + {boardID, threadID, postID} = Get.postDataFromLink @ + + qp = $.el 'div', + id: 'qp' + className: 'dialog' + $.add d.body, qp + Get.postClone boardID, threadID, postID, qp, Get.contextFromLink @ + + UI.hover + root: @ + el: qp + latestEvent: e + endEvents: 'mouseout click' + cb: QuotePreview.mouseout + asapTest: -> qp.firstElementChild + + return unless origin = g.posts["#{boardID}.#{postID}"] + + if Conf['Quote Highlighting'] + posts = [origin].concat origin.clones + # Remove the clone that's in the qp from the array. + posts.pop() + for post in posts + $.addClass post.nodes.post, 'qphl' + + quoterID = $.x('ancestor::*[@id][1]', @).id.match(/\d+$/)[0] + clone = Get.postFromRoot qp.firstChild + for quote in clone.nodes.quotelinks.concat [clone.nodes.backlinks...] + if quote.hash[2..] is quoterID + $.addClass quote, 'forwardlink' + return + mouseout: -> + # Stop if it only contains text. + return unless root = @el.firstElementChild + + clone = Get.postFromRoot root + post = clone.origin + post.rmClone root.dataset.clone + + return unless Conf['Quote Highlighting'] + for post in [post].concat post.clones + $.rmClass post.nodes.post, 'qphl' + return + +QuoteBacklink = + # Backlinks appending need to work for: + # - previous, same, and following posts. + # - existing and yet-to-exist posts. + # - newly fetched posts. + # - in copies. + # XXX what about order for fetched posts? + # + # First callback creates backlinks and add them to relevant containers. + # Second callback adds relevant containers into posts. + # This is is so that fetched posts can get their backlinks, + # and that as much backlinks are appended in the background as possible. + init: -> + return if g.VIEW is 'catalog' or !Conf['Quote Backlinks'] + + format = Conf['backlink'].replace /%id/g, "' + id + '" + @funk = Function 'id', "return '#{format}'" + @containers = {} + Post::callbacks.push + name: 'Quote Backlinking Part 1' + cb: @firstNode + Post::callbacks.push + name: 'Quote Backlinking Part 2' + cb: @secondNode + firstNode: -> + return if @isClone or !@quotes.length + a = $.el 'a', + href: "/#{@board}/res/#{@thread}#p#{@}" + className: if @isHidden then 'filtered backlink' else 'backlink' + textContent: QuoteBacklink.funk @ID + for quote in @quotes + containers = [QuoteBacklink.getContainer quote] + if (post = g.posts[quote]) and post.nodes.backlinkContainer + # Don't add OP clones when OP Backlinks is disabled, + # as the clones won't have the backlink containers. + for clone in post.clones + containers.push clone.nodes.backlinkContainer + for container in containers + link = a.cloneNode true + if Conf['Quote Previewing'] + $.on link, 'mouseover', QuotePreview.mouseover + if Conf['Quote Inlining'] + $.on link, 'click', QuoteInline.toggle + $.add container, [$.tn(' '), link] + return + secondNode: -> + if @isClone and (@origin.isReply or Conf['OP Backlinks']) + @nodes.backlinkContainer = $ '.container', @nodes.info + return + # Don't backlink the OP. + return unless @isReply or Conf['OP Backlinks'] + container = QuoteBacklink.getContainer @fullID + @nodes.backlinkContainer = container + $.add @nodes.info, container + getContainer: (id) -> + @containers[id] or= + $.el 'span', className: 'container' + +QuoteYou = + init: -> + return if g.VIEW is 'catalog' or !Conf['Mark Quotes of You'] or !Conf['Quick Reply'] + + # \u00A0 is nbsp + @text = '\u00A0(You)' + Post::callbacks.push + name: 'Mark Quotes of You' + cb: @node + node: -> + # Stop there if it's a clone. + return if @isClone + # Stop there if there's no quotes in that post. + return unless (quotes = @quotes).length + {quotelinks} = @nodes + + for quotelink in quotelinks + if QR.db.get Get.postDataFromLink quotelink + $.add quotelink, $.tn QuoteYou.text + return + +QuoteOP = + init: -> + return if g.VIEW is 'catalog' or !Conf['Mark OP Quotes'] + + # \u00A0 is nbsp + @text = '\u00A0(OP)' + Post::callbacks.push + name: 'Mark OP Quotes' + cb: @node + node: -> + # Stop there if it's a clone of a post in the same thread. + return if @isClone and @thread is @context.thread + # Stop there if there's no quotes in that post. + return unless (quotes = @quotes).length + {quotelinks} = @nodes + + # rm (OP) from cross-thread quotes. + if @isClone and @thread.fullID in quotes + for quotelink in quotelinks + quotelink.textContent = quotelink.textContent.replace QuoteOP.text, '' + + op = (if @isClone then @context else @).thread.fullID + # add (OP) to quotes quoting this context's OP. + return unless op in quotes + for quotelink in quotelinks + {boardID, postID} = Get.postDataFromLink quotelink + if "#{boardID}.#{postID}" is op + $.add quotelink, $.tn QuoteOP.text + return + +QuoteCT = + init: -> + return if g.VIEW is 'catalog' or !Conf['Mark Cross-thread Quotes'] + + # \u00A0 is nbsp + @text = '\u00A0(Cross-thread)' + Post::callbacks.push + name: 'Mark Cross-thread Quotes' + cb: @node + node: -> + # Stop there if it's a clone of a post in the same thread. + return if @isClone and @thread is @context.thread + # Stop there if there's no quotes in that post. + return unless (quotes = @quotes).length + {quotelinks} = @nodes + + {board, thread} = if @isClone then @context else @ + for quotelink in quotelinks + {boardID, threadID} = Get.postDataFromLink quotelink + continue unless threadID # deadlink + if @isClone + quotelink.textContent = quotelink.textContent.replace QuoteCT.text, '' + if boardID is @board.ID and threadID isnt thread.ID + $.add quotelink, $.tn QuoteCT.text + return + +Anonymize = + init: -> + return if g.VIEW is 'catalog' or !Conf['Anonymize'] + + Post::callbacks.push + name: 'Anonymize' + cb: @node + node: -> + return if @info.capcode or @isClone + {name, tripcode, email} = @nodes + if @info.name isnt 'Anonymous' + name.textContent = 'Anonymous' + if tripcode + $.rm tripcode + delete @nodes.tripcode + if @info.email + if /sage/i.test @info.email + email.href = 'mailto:sage' + else + $.replace email, name + delete @nodes.email + +Time = + init: -> + return if g.VIEW is 'catalog' or !Conf['Time Formatting'] + + @funk = @createFunc Conf['time'] + Post::callbacks.push + name: 'Time Formatting' + cb: @node + node: -> + return if @isClone + @nodes.date.textContent = Time.funk Time, @info.date + createFunc: (format) -> + code = format.replace /%([A-Za-z])/g, (s, c) -> + if c of Time.formatters + "' + Time.formatters.#{c}.call(date) + '" + else + s + Function 'Time', 'date', "return '#{code}'" + day: [ + 'Sunday' + 'Monday' + 'Tuesday' + 'Wednesday' + 'Thursday' + 'Friday' + 'Saturday' + ] + month: [ + 'January' + 'February' + 'March' + 'April' + 'May' + 'June' + 'July' + 'August' + 'September' + 'October' + 'November' + 'December' + ] + zeroPad: (n) -> if n < 10 then "0#{n}" else n + formatters: + a: -> Time.day[@getDay()][...3] + A: -> Time.day[@getDay()] + b: -> Time.month[@getMonth()][...3] + B: -> Time.month[@getMonth()] + d: -> Time.zeroPad @getDate() + e: -> @getDate() + H: -> Time.zeroPad @getHours() + I: -> Time.zeroPad @getHours() % 12 or 12 + k: -> @getHours() + l: -> @getHours() % 12 or 12 + m: -> Time.zeroPad @getMonth() + 1 + M: -> Time.zeroPad @getMinutes() + p: -> if @getHours() < 12 then 'AM' else 'PM' + P: -> if @getHours() < 12 then 'am' else 'pm' + S: -> Time.zeroPad @getSeconds() + y: -> @getFullYear() - 2000 + +RelativeDates = + INTERVAL: $.MINUTE / 2 + init: -> + return if g.VIEW is 'catalog' or !Conf['Relative Post Dates'] + + # Flush when page becomes visible again or when the thread updates. + $.on d, 'visibilitychange ThreadUpdate', @flush + + # Start the timeout. + @flush() + + Post::callbacks.push + name: 'Relative Post Dates' + cb: @node + node: -> + return if @isClone + + # Show original absolute time as tooltip so users can still know exact times + # Since "Time Formatting" runs its `node` before us, the title tooltip will + # pick up the user-formatted time instead of 4chan time when enabled. + dateEl = @nodes.date + dateEl.title = dateEl.textContent + + RelativeDates.setUpdate @ + + # diff is milliseconds from now. + relative: (diff, now, date) -> + unit = if (number = (diff / $.DAY)) >= 1 + years = now.getYear() - date.getYear() + months = now.getMonth() - date.getMonth() + days = now.getDate() - date.getDate() + if years > 1 + number = years - (months < 0 or months is 0 and days < 0) + 'year' + else if years is 1 and (months > 0 or months is 0 and days >= 0) + number = years + 'year' + else if (months = (months+12)%12 ) > 1 + number = months - (days < 0) + 'month' + else if months is 1 and days >= 0 + number = months + 'month' + else + 'day' + else if (number = (diff / $.HOUR)) >= 1 + 'hour' + else if (number = (diff / $.MINUTE)) >= 1 + 'minute' + else + # prevent "-1 seconds ago" + number = Math.max(0, diff) / $.SECOND + 'second' + + rounded = Math.round number + unit += 's' if rounded isnt 1 # pluralize + + "#{rounded} #{unit} ago" + + # Changing all relative dates as soon as possible incurs many annoying + # redraws and scroll stuttering. Thus, sacrifice accuracy for UX/CPU economy, + # and perform redraws when the DOM is otherwise being manipulated (and scroll + # stuttering won't be noticed), falling back to INTERVAL while the page + # is visible. + # + # Each individual dateTime element will add its update() function to the stale list + # when it is to be called. + stale: [] + flush: -> + # No point in changing the dates until the user sees them. + return if d.hidden + + now = new Date() + update now for update in RelativeDates.stale + RelativeDates.stale = [] + + # Reset automatic flush. + clearTimeout RelativeDates.timeout + RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL + + # Create function `update()`, closed over post, that, when called + # from `flush()`, updates the elements, and re-calls `setOwnTimeout()` to + # re-add `update()` to the stale list later. + setUpdate: (post) -> + setOwnTimeout = (diff) -> + delay = if diff < $.MINUTE + $.SECOND - (diff + $.SECOND / 2) % $.SECOND + else if diff < $.HOUR + $.MINUTE - (diff + $.MINUTE / 2) % $.MINUTE + else if diff < $.DAY + $.HOUR - (diff + $.HOUR / 2) % $.HOUR + else + $.DAY - (diff + $.DAY / 2) % $.DAY + setTimeout markStale, delay + + update = (now) -> + {date} = post.info + diff = now - date + relative = RelativeDates.relative diff, now, date + for singlePost in [post].concat post.clones + singlePost.nodes.date.firstChild.textContent = relative + setOwnTimeout diff + + markStale = -> RelativeDates.stale.push update + + # Kick off initial timeout. + update new Date() + +FileInfo = + init: -> + return if g.VIEW is 'catalog' or !Conf['File Info Formatting'] + + @funk = @createFunc Conf['fileInfo'] + Post::callbacks.push + name: 'File Info Formatting' + cb: @node + node: -> + return if !@file or @isClone + @file.text.innerHTML = FileInfo.funk FileInfo, @ + createFunc: (format) -> + code = format.replace /%(.)/g, (s, c) -> + if c of FileInfo.formatters + "' + FileInfo.formatters.#{c}.call(post) + '" + else + s + Function 'FileInfo', 'post', "return '#{code}'" + convertUnit: (size, unit) -> + if unit is 'B' + return "#{size.toFixed()} Bytes" + i = 1 + ['KB', 'MB'].indexOf unit + size /= 1024 while i-- + size = + if unit is 'MB' + Math.round(size * 100) / 100 + else + size.toFixed() + "#{size} #{unit}" + escape: (name) -> + name.replace /<|>/g, (c) -> + c is '<' and '<' or '>' + formatters: + t: -> @file.URL.match(/\d+\..+$/)[0] + T: -> "#{FileInfo.formatters.t.call @}" + l: -> "#{FileInfo.formatters.n.call @}" + L: -> "#{FileInfo.formatters.N.call @}" + n: -> + fullname = @file.name + shortname = Build.shortFilename @file.name, @isReply + if fullname is shortname + FileInfo.escape fullname + else + "#{FileInfo.escape shortname}#{FileInfo.escape fullname}" + N: -> FileInfo.escape @file.name + p: -> if @file.isSpoiler then 'Spoiler, ' else '' + s: -> @file.size + B: -> FileInfo.convertUnit @file.sizeInBytes, 'B' + K: -> FileInfo.convertUnit @file.sizeInBytes, 'KB' + M: -> FileInfo.convertUnit @file.sizeInBytes, 'MB' + r: -> if @file.isImage then @file.dimensions else 'PDF' + +Sauce = + init: -> + return if g.VIEW is 'catalog' or !Conf['Sauce'] + + links = [] + for link in Conf['sauces'].split '\n' + continue if link[0] is '#' + links.push @createSauceLink link.trim() + return unless links.length + @links = links + @link = $.el 'a', target: '_blank' + Post::callbacks.push + name: 'Sauce' + cb: @node + createSauceLink: (link) -> + link = link.replace /%(T?URL|MD5|board)/g, (parameter) -> + switch parameter + when '%TURL' + "' + encodeURIComponent(post.file.thumbURL) + '" + when '%URL' + "' + encodeURIComponent(post.file.URL) + '" + when '%MD5' + "' + encodeURIComponent(post.file.MD5) + '" + when '%board' + "' + encodeURIComponent(post.board) + '" + else + parameter + text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1] + link = link.replace /;text:.+$/, '' + Function 'post', 'a', """ + a.href = '#{link}'; + a.textContent = '#{text}'; + return a; + """ + node: -> + return if @isClone or !@file + nodes = [] + for link in Sauce.links + # \u00A0 is nbsp + 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'] + + @EAI = $.el 'a', + className: 'expand-all-shortcut' + textContent: 'EAI' + title: 'Expand All Images' + href: 'javascript:;' + $.on @EAI, 'click', ImageExpand.cb.toggleAll + Header.addShortcut @EAI + + Post::callbacks.push + name: 'Image Expansion' + cb: @node + node: -> + return unless @file?.isImage + {thumb} = @file + $.on thumb.parentNode, 'click', ImageExpand.cb.toggle + if @isClone and $.hasClass thumb, 'expanding' + # If we clone a post where the image is still loading, + # make it loading in the clone too. + ImageExpand.contract @ + ImageExpand.expand @ + return + 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 @ + toggleAll: -> + $.event 'CloseMenu' + if ImageExpand.on = $.hasClass ImageExpand.EAI, 'expand-all-shortcut' + ImageExpand.EAI.className = 'contract-all-shortcut' + ImageExpand.EAI.title = 'Contract All Images' + func = ImageExpand.expand + else + ImageExpand.EAI.className = 'expand-all-shortcut' + ImageExpand.EAI.title = 'Expand All Images' + func = ImageExpand.contract + for ID, post of g.posts + for post in [post].concat post.clones + {file} = post + continue unless file and file.isImage and doc.contains post.nodes.root + if ImageExpand.on and + (!Conf['Expand spoilers'] and file.isSpoiler or + Conf['Expand from here'] and file.thumb.getBoundingClientRect().top < 0) + continue + $.queueTask func, post + return + setFitness: -> + {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 null + ImageExpand.resize() + else + $.off window, 'resize', ImageExpand.resize + + toggle: (post) -> + {thumb} = post.file + unless post.file.isExpanded or $.hasClass thumb, 'expanding' + ImageExpand.expand post + return + ImageExpand.contract post + rect = post.nodes.root.getBoundingClientRect() + return unless rect.top <= 0 or rect.left <= 0 + # Scroll back to the thumbnail when contracting the image + # to avoid being left miles away from the relevant post. + {top} = rect + unless Conf['Bottom header'] + headRect = Header.toggle.getBoundingClientRect() + top += - headRect.top - headRect.height + root = <% if (type === 'crx') { %>d.body<% } else { %>doc<% } %> + root.scrollTop += top if rect.top < 0 + root.scrollLeft = 0 if rect.left < 0 + + contract: (post) -> + $.rmClass post.nodes.root, 'expanded-image' + $.rmClass post.file.thumb, 'expanding' + post.file.isExpanded = false + + expand: (post, src) -> + # Do not expand images of hidden/filtered replies, or already expanded pictures. + {thumb} = post.file + return if post.isHidden or post.file.isExpanded or $.hasClass thumb, 'expanding' + $.addClass thumb, 'expanding' + if post.file.fullImage + # Expand already-loaded/ing picture. + $.asap (-> post.file.fullImage.naturalHeight), -> + ImageExpand.completeExpand post + return + post.file.fullImage = img = $.el 'img', + className: 'full-image' + src: src or post.file.URL + $.on img, 'error', ImageExpand.error + $.asap (-> post.file.fullImage.naturalHeight), -> + ImageExpand.completeExpand post + $.after thumb, img + + completeExpand: (post) -> + {thumb} = post.file + return unless $.hasClass thumb, 'expanding' # contracted before the image loaded + post.file.isExpanded = true + unless post.nodes.root.parentNode + # Image might start/finish loading before the post is inserted. + # Don't scroll when it's expanded in a QP for example. + $.addClass post.nodes.root, 'expanded-image' + $.rmClass post.file.thumb, 'expanding' + return + prev = post.nodes.root.getBoundingClientRect() + $.queueTask -> + $.addClass post.nodes.root, 'expanded-image' + $.rmClass post.file.thumb, 'expanding' + return unless prev.top + prev.height <= 0 + root = <% if (type === 'crx') { %>d.body<% } else { %>doc<% } %> + curr = post.nodes.root.getBoundingClientRect() + root.scrollTop += curr.height - prev.height + curr.top - prev.top + + error: -> + post = Get.postFromNode @ + $.rm @ + delete post.file.fullImage + # Images can error: + # - before the image started loading. + # - after the image started loading. + unless $.hasClass(post.file.thumb, 'expanding') or $.hasClass post.nodes.root, 'expanded-image' + # Don't try to re-expend if it was already contracted. + return + ImageExpand.contract post + + src = @src.split '/' + if src[2] is 'images.4chan.org' + if URL = Redirect.image src[3], src[5] + setTimeout ImageExpand.expand, 10000, post, URL + return + if g.DEAD or post.isDead or post.file.isDead + return + + timeoutID = setTimeout ImageExpand.expand, 10000, post + # XXX CORS for images.4chan.org WHEN? + $.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: -> + return if @status isnt 200 + for postObj in JSON.parse(@response).posts + break if postObj.no is post.ID + if postObj.no isnt post.ID + clearTimeout timeoutID + post.kill() + else if postObj.filedeleted + clearTimeout timeoutID + post.kill true + + menu: + init: -> + return if g.VIEW is 'catalog' or !Conf['Image Expansion'] + + el = $.el 'span', + textContent: 'Image Expansion' + className: 'image-expansion-link' + + {createSubEntry} = ImageExpand.menu + subEntries = [] + for key, conf of Config.imageExpansion + subEntries.push createSubEntry key, conf + + $.event 'AddMenuEntry', + type: 'header' + el: el + order: 80 + subEntries: subEntries + + createSubEntry: (type, config) -> + label = $.el 'label', + innerHTML: " #{type}" + input = label.firstElementChild + if type in ['Fit width', 'Fit height'] + $.on input, 'change', ImageExpand.cb.setFitness + if config + label.title = config[1] + input.checked = Conf[type] + $.event 'change', null, input + $.on input, 'change', $.cb.checked + el: label + + 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'] + + Post::callbacks.push + name: 'Reveal Spoilers' + cb: @node + node: -> + return if @isClone or !@file?.isSpoiler + {thumb} = @file + thumb.removeAttribute 'style' + thumb.src = @file.thumbURL + +AutoGIF = + init: -> + return if g.VIEW is 'catalog' or !Conf['Auto-GIF'] or g.BOARD.ID in ['gif', 'wsg'] + + Post::callbacks.push + name: 'Auto-GIF' + cb: @node + node: -> + return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage + {thumb, URL} = @file + return unless /gif$/.test(URL) and !/spoiler/.test thumb.src + if @file.isSpoiler + # Revealed spoilers do not have height/width set, this fixes auto-gifs dimensions. + {style} = thumb + style.maxHeight = style.maxWidth = if @isReply then '125px' else '250px' + gif = $.el 'img' + $.on gif, 'load', -> + # Replace the thumbnail once the GIF has finished loading. + thumb.src = URL + gif.src = URL + +ImageHover = + init: -> + return if g.VIEW is 'catalog' or !Conf['Image Hover'] + + Post::callbacks.push + name: 'Image Hover' + cb: @node + node: -> + return unless @file?.isImage + $.on @file.thumb, 'mouseover', ImageHover.mouseover + mouseover: (e) -> + post = Get.postFromNode @ + el = $.el 'img', + id: 'ihover' + src: post.file.URL + el.setAttribute 'data-fullid', post.fullID + $.add d.body, el + UI.hover + root: @ + el: el + latestEvent: e + endEvents: 'mouseout click' + asapTest: -> el.naturalHeight + $.on el, 'error', ImageHover.error + error: -> + return unless doc.contains @ + post = g.posts[@dataset.fullid] + + src = @src.split '/' + if src[2] is 'images.4chan.org' + if URL = Redirect.image src[3], src[5].replace /\?.+$/, '' + @src = URL + return + if g.DEAD or post.isDead or post.file.isDead + return + + timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000 + # XXX CORS for images.4chan.org WHEN? + $.ajax "//api.4chan.org/#{post.board}/res/#{post.thread}.json", onload: -> + return if @status isnt 200 + for postObj in JSON.parse(@response).posts + break if postObj.no is post.ID + if postObj.no isnt post.ID + clearTimeout timeoutID + post.kill() + else if postObj.filedeleted + clearTimeout timeoutID + post.kill true + +ExpandComment = + init: -> + return if g.VIEW isnt 'index' or !Conf['Comment Expansion'] + + Post::callbacks.push + name: 'Comment Expansion' + cb: @node + node: -> + if a = $ '.abbr > a', @nodes.comment + $.on a, 'click', ExpandComment.cb + cb: (e) -> + e.preventDefault() + post = Get.postFromNode @ + ExpandComment.expand post + expand: (post) -> + if post.nodes.longComment and !post.nodes.longComment.parentNode + $.replace post.nodes.shortComment, post.nodes.longComment + post.nodes.comment = post.nodes.longComment + 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 + contract: (post) -> + return unless post.nodes.shortComment + a = $ '.abbr > a', post.nodes.shortComment + a.textContent = 'here' + $.replace post.nodes.longComment, post.nodes.shortComment + post.nodes.comment = post.nodes.shortComment + parse: (req, a, post) -> + {status} = req + if status not in [200, 304] + a.textContent = "Error #{req.statusText} (#{status})" + return + + posts = JSON.parse(req.response).posts + if spoilerRange = posts[0].custom_spoiler + Build.spoilerRange[g.BOARD] = spoilerRange + + for postObj in posts + break if postObj.no is post.ID + if postObj.no isnt post.ID + a.textContent = "Post No.#{post} not found." + return + + {comment} = post.nodes + clone = comment.cloneNode false + clone.innerHTML = postObj.com + 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 + post.nodes.shortComment = comment + $.replace comment, clone + post.nodes.comment = post.nodes.longComment = clone + post.parseComment() + post.parseQuotes() + if Conf['Resurrect Quotes'] + Quotify.node.call post + if Conf['Quote Previewing'] + QuotePreview.node.call post + if Conf['Quote Inlining'] + QuoteInline.node.call post + if Conf['Mark OP Quotes'] + QuoteOP.node.call post + if Conf['Mark Cross-thread Quotes'] + QuoteCT.node.call post + if g.BOARD.ID is 'g' + Fourchan.code.call post + if g.BOARD.ID is 'sci' + Fourchan.math.call post + +ExpandThread = + init: -> + return if g.VIEW isnt 'index' or !Conf['Thread Expansion'] + + Thread::callbacks.push + name: 'Thread Expansion' + cb: @node + node: -> + return unless span = $ '.summary', @OP.nodes.root.parentNode + a = $.el 'a', + textContent: "+ #{span.textContent}" + className: 'summary' + href: 'javascript:;' + $.on a, 'click', ExpandThread.cbToggle + $.replace span, a + + cbToggle: -> + op = Get.postFromRoot @previousElementSibling + ExpandThread.toggle op.thread + + toggle: (thread) -> + threadRoot = thread.OP.nodes.root.parentNode + a = $ '.summary', threadRoot + + switch thread.isExpanded + when false, undefined + thread.isExpanded = 'loading' + for post in $$ '.thread > .postContainer', threadRoot + ExpandComment.expand Get.postFromRoot post + unless a + thread.isExpanded = true + return + thread.isExpanded = 'loading' + a.textContent = a.textContent.replace '+', '× Loading...' + $.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", -> + ExpandThread.parse @, thread, a + + when 'loading' + thread.isExpanded = false + return unless a + a.textContent = a.textContent.replace '× Loading...', '+' + + when true + thread.isExpanded = false + if a + a.textContent = a.textContent.replace '-', '+' + #goddamit moot + num = if thread.isSticky + 1 + else switch g.BOARD.ID + # XXX boards config + when 'b', 'vg', 'q' then 3 + when 't' then 1 + else 5 + replies = $$('.thread > .replyContainer', threadRoot)[...-num] + for reply in replies + if Conf['Quote Inlining'] + # rm clones + inlined.click() while inlined = $ '.inlined', reply + $.rm reply + for post in $$ '.thread > .postContainer', threadRoot + ExpandComment.contract Get.postFromRoot post + return + + parse: (req, thread, a) -> + return if a.textContent[0] is '+' + {status} = req + if status not in [200, 304] + a.textContent = "Error #{req.statusText} (#{status})" + $.off a, 'click', ExpandThread.cb.toggle + return + + thread.isExpanded = true + a.textContent = a.textContent.replace '× Loading...', '-' + + posts = JSON.parse(req.response).posts + if spoilerRange = posts[0].custom_spoiler + Build.spoilerRange[g.BOARD] = spoilerRange + + replies = posts[1..] + posts = [] + nodes = [] + for reply in replies + if post = thread.posts[reply.no] + nodes.push post.nodes.root + continue + node = Build.postFromObject reply, thread.board + post = new Post node, thread, thread.board + link = $ 'a[title="Highlight this post"]', node + link.href = "res/#{thread}#p#{post}" + link.nextSibling.href = "res/#{thread}#q#{post}" + posts.push post + nodes.push node + Main.callbackNodes Post, posts + $.after a, nodes + + # Enable 4chan features. + if Conf['Enable 4chan\'s Extension'] + $.globalEval "Parser.parseThread(#{thread.ID}, 1, #{nodes.length})" + else + Fourchan.parseThread thread.ID, 1, nodes.length + +ThreadExcerpt = + init: -> + return if g.VIEW isnt 'thread' or !Conf['Thread Excerpt'] + + Thread::callbacks.push + name: 'Thread Excerpt' + cb: @node + node: -> + d.title = Get.threadExcerpt @ + +Unread = + init: -> + return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Tab Icon'] + + @db = new DataBoard 'lastReadPosts', @sync + @hr = $.el 'hr', + id: 'unread-line' + @posts = [] + @postsQuotingYou = [] + + Thread::callbacks.push + name: 'Unread' + cb: @node + + node: -> + Unread.thread = @ + Unread.title = d.title + posts = [] + for ID, post of @posts + posts.push post if post.isReply + Unread.lastReadPost = Unread.db.get + boardID: @board.ID + threadID: @ID + defaultValue: 0 + Unread.addPosts posts + $.on d, 'ThreadUpdate', Unread.onUpdate + $.on d, 'scroll visibilitychange', Unread.read + $.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line'] + $.on window, 'load', Unread.scroll if Conf['Scroll to Last Read Post'] + + scroll: -> + # Let the header's onload callback handle it. + return if (hash = location.hash.match /\d+/) and hash[0] of @posts + if Unread.posts.length + # Scroll to before the first unread post. + while root = $.x 'preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root + break unless (Get.postFromRoot root).isHidden + root.scrollIntoView false + else if posts.length + # Scroll to the last read post. + Header.scrollToPost posts[posts.length - 1].nodes.root + + sync: -> + lastReadPost = Unread.db.get + boardID: Unread.thread.board.ID + threadID: Unread.thread.ID + defaultValue: 0 + return unless Unread.lastReadPost < lastReadPost + Unread.lastReadPost = lastReadPost + Unread.readArray Unread.posts + Unread.readArray Unread.postsQuotingYou + Unread.setLine() + Unread.update() + + addPosts: (newPosts) -> + for post in newPosts + {ID} = post + if ID <= Unread.lastReadPost or post.isHidden + continue + if QR.db + data = + boardID: post.board.ID + threadID: post.thread.ID + postID: post.ID + continue if QR.db.get data + Unread.posts.push post + Unread.addPostQuotingYou post + if Conf['Unread Line'] + # Force line on visible threads if there were no unread posts previously. + Unread.setLine Unread.posts[0] in newPosts + Unread.read() + Unread.update() + + addPostQuotingYou: (post) -> + return unless QR.db + for quotelink in post.nodes.quotelinks + if QR.db.get Get.postDataFromLink quotelink + Unread.postsQuotingYou.push post + return + + onUpdate: (e) -> + if e.detail[404] + Unread.update() + else + Unread.addPosts e.detail.newPosts + + readSinglePost: (post) -> + return if (i = Unread.posts.indexOf post) is -1 + Unread.posts.splice i, 1 + if i is 0 + Unread.lastReadPost = post.ID + Unread.saveLastReadPost() + if (i = Unread.postsQuotingYou.indexOf post) isnt -1 + Unread.postsQuotingYou.splice i, 1 + Unread.update() + + readArray: (arr) -> + for post, i in arr + break if post.ID > Unread.lastReadPost + arr.splice 0, i + + read: (e) -> + return if d.hidden or !Unread.posts.length + height = doc.clientHeight + for post, i in Unread.posts + {bottom} = post.nodes.root.getBoundingClientRect() + break if bottom > height # post is not completely read + return unless i + + Unread.lastReadPost = Unread.posts[i - 1].ID + Unread.saveLastReadPost() + Unread.posts.splice 0, i + Unread.readArray Unread.postsQuotingYou + Unread.update() if e + + saveLastReadPost: $.debounce 2 * $.SECOND, -> + Unread.db.set + boardID: Unread.thread.board.ID + threadID: Unread.thread.ID + val: Unread.lastReadPost + + setLine: (force) -> + return unless d.hidden or force is true + if post = Unread.posts[0] + {root} = post.nodes + if root isnt $ '.thread > .replyContainer', root.parentNode # not the first reply + $.before root, Unread.hr + else + $.rm Unread.hr + + update: <% if (type === 'crx') { %>(dontrepeat) <% } %>-> + count = Unread.posts.length + + if Conf['Unread Count'] + d.title = "#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then "/#{g.BOARD}/ - 404" else "#{Unread.title}"}" + <% if (type === 'crx') { %> + # XXX Chrome bug where it doesn't always update the tab title. + # crbug.com/124381 + # Call it one second later, + # but don't display outdated unread count. + unless dontrepeat + setTimeout -> + d.title = '' + Unread.update true + , $.SECOND + <% } %> + + return unless Conf['Unread Tab Icon'] + + Favicon.el.href = + if g.DEAD + if Unread.postsQuotingYou.length + Favicon.unreadDeadY + else if count + Favicon.unreadDead + else + Favicon.dead + else + if count + if Unread.postsQuotingYou.length + Favicon.unreadY + else + Favicon.unread + else + Favicon.default + + <% if (type !== 'crx') { %> + # `favicon.href = href` doesn't work on Firefox. + # `favicon.href = href` isn't enough on Opera. + # Opera won't always update the favicon if the href didn't change. + $.add d.head, Favicon.el + <% } %> + +Favicon = + init: -> + $.ready -> + Favicon.el = $ 'link[rel="shortcut icon"]', d.head + Favicon.el.type = 'image/x-icon' + {href} = Favicon.el + Favicon.SFW = /ws\.ico$/.test href + Favicon.default = href + Favicon.switch() + + switch: -> + switch Conf['favicon'] + when 'ferongr' + Favicon.unreadDead = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/ferongr/unreadDead.gif", {encoding: "base64"}) %>' + Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/ferongr/unreadDeadY.png", {encoding: "base64"}) %>' + Favicon.unreadSFW = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/ferongr/unreadSFW.gif", {encoding: "base64"}) %>' + Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/ferongr/unreadSFWY.png", {encoding: "base64"}) %>' + Favicon.unreadNSFW = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/ferongr/unreadNSFW.gif", {encoding: "base64"}) %>' + Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/ferongr/unreadNSFWY.png", {encoding: "base64"}) %>' + when 'xat-' + Favicon.unreadDead = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadDead.png", {encoding: "base64"}) %>' + Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadDeadY.png", {encoding: "base64"}) %>' + Favicon.unreadSFW = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadSFW.png", {encoding: "base64"}) %>' + Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadSFWY.png", {encoding: "base64"}) %>' + Favicon.unreadNSFW = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadNSFW.png", {encoding: "base64"}) %>' + Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/xat-/unreadNSFWY.png", {encoding: "base64"}) %>' + when 'Mayhem' + Favicon.unreadDead = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadDead.png", {encoding: "base64"}) %>' + Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadDeadY.png", {encoding: "base64"}) %>' + Favicon.unreadSFW = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadSFW.png", {encoding: "base64"}) %>' + Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadSFWY.png", {encoding: "base64"}) %>' + Favicon.unreadNSFW = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadNSFW.png", {encoding: "base64"}) %>' + Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Mayhem/unreadNSFWY.png", {encoding: "base64"}) %>' + when 'Original' + Favicon.unreadDead = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/Original/unreadDead.gif", {encoding: "base64"}) %>' + Favicon.unreadDeadY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Original/unreadDeadY.png", {encoding: "base64"}) %>' + Favicon.unreadSFW = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/Original/unreadSFW.gif", {encoding: "base64"}) %>' + Favicon.unreadSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Original/unreadSFWY.png", {encoding: "base64"}) %>' + Favicon.unreadNSFW = 'data:image/gif;base64,<%= grunt.file.read("img/favicons/Original/unreadNSFW.gif", {encoding: "base64"}) %>' + Favicon.unreadNSFWY = 'data:image/png;base64,<%= grunt.file.read("img/favicons/Original/unreadNSFWY.png", {encoding: "base64"}) %>' + if Favicon.SFW + Favicon.unread = Favicon.unreadSFW + Favicon.unreadY = Favicon.unreadSFWY + else + Favicon.unread = Favicon.unreadNSFW + Favicon.unreadY = Favicon.unreadNSFWY + + empty: 'data:image/gif;base64,<%= grunt.file.read("img/favicons/empty.gif", {encoding: "base64"}) %>' + dead: 'data:image/gif;base64,<%= grunt.file.read("img/favicons/dead.gif", {encoding: "base64"}) %>' + + +ThreadStats = + init: -> + return if g.VIEW isnt 'thread' or !Conf['Thread Stats'] + @dialog = UI.dialog 'thread-stats', 'bottom: 0; left: 0;', """ +
0 / 0
+ """ + + @postCountEl = $ '#post-count', @dialog + @fileCountEl = $ '#file-count', @dialog + + Thread::callbacks.push + name: 'Thread Stats' + cb: @node + node: -> + postCount = 0 + fileCount = 0 + for ID, post of @posts + postCount++ + fileCount++ if post.file + ThreadStats.thread = @ + ThreadStats.update postCount, fileCount + $.on d, 'ThreadUpdate', ThreadStats.onUpdate + $.add d.body, ThreadStats.dialog + onUpdate: (e) -> + return if e.detail[404] + {postCount, fileCount} = e.detail + ThreadStats.update postCount, fileCount + update: (postCount, fileCount) -> + {thread, postCountEl, fileCountEl} = ThreadStats + postCountEl.textContent = postCount + fileCountEl.textContent = fileCount + (if thread.postLimit and !thread.isSticky then $.addClass else $.rmClass) postCountEl, 'warning' + (if thread.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning' + +ThreadUpdater = + init: -> + return if g.VIEW isnt 'thread' or !Conf['Thread Updater'] + + html = '' + for name, conf of Config.updater.checkbox + checked = if Conf[name] then 'checked' else '' + html += "
" + + checked = if Conf['Auto Update'] then 'checked' else '' + html = """ +
+ #{html} +
+
+
+ """ + + @dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html + @timer = $ '#update-timer', @dialog + @status = $ '#update-status', @dialog + + Thread::callbacks.push + name: 'Thread Updater' + cb: @node + + node: -> + ThreadUpdater.thread = @ + ThreadUpdater.root = @OP.nodes.root.parentNode + ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0] + ThreadUpdater.outdateCount = 0 + ThreadUpdater.lastModified = '0' + + for input in $$ 'input', ThreadUpdater.dialog + if input.type is 'checkbox' + $.on input, 'change', $.cb.checked + switch input.name + when 'Scroll BG' + $.on input, 'change', ThreadUpdater.cb.scrollBG + ThreadUpdater.cb.scrollBG() + when 'Auto Update This' + $.on input, 'change', ThreadUpdater.cb.autoUpdate + $.event 'change', null, input + when 'Interval' + $.on input, 'change', ThreadUpdater.cb.interval + ThreadUpdater.cb.interval.call input + when 'Update' + $.on input, 'click', ThreadUpdater.update + + $.on window, 'online offline', ThreadUpdater.cb.online + $.on d, 'QRPostSuccessful', ThreadUpdater.cb.post + $.on d, 'visibilitychange', ThreadUpdater.cb.visibility + + ThreadUpdater.cb.online() + $.add d.body, ThreadUpdater.dialog + + ### + http://freesound.org/people/pierrecartoons1979/sounds/90112/ + cc-by-nc-3.0 + ### + beep: 'data:audio/wav;base64,<%= grunt.file.read("audio/beep.wav", {encoding: "base64"}) %>' + + cb: + online: -> + if ThreadUpdater.online = navigator.onLine + ThreadUpdater.outdateCount = 0 + ThreadUpdater.set 'timer', ThreadUpdater.getInterval() + ThreadUpdater.update() if Conf['Auto Update This'] + ThreadUpdater.set 'status', null, null + else + ThreadUpdater.set 'timer', null + ThreadUpdater.set 'status', 'Offline', 'warning' + ThreadUpdater.cb.autoUpdate() + post: (e) -> + return unless Conf['Auto Update This'] and e.detail.threadID is ThreadUpdater.thread.ID + ThreadUpdater.outdateCount = 0 + setTimeout ThreadUpdater.update, 1000 if ThreadUpdater.seconds > 2 + visibility: -> + return if d.hidden + # Reset the counter when we focus this tab. + ThreadUpdater.outdateCount = 0 + if ThreadUpdater.seconds > ThreadUpdater.interval + ThreadUpdater.set 'timer', ThreadUpdater.getInterval() + scrollBG: -> + ThreadUpdater.scrollBG = if Conf['Scroll BG'] + -> true + else + -> not d.hidden + autoUpdate: -> + if Conf['Auto Update This'] and ThreadUpdater.online + ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000 + else + clearTimeout ThreadUpdater.timeoutID + interval: -> + val = Math.max 5, parseInt @value, 10 + ThreadUpdater.interval = @value = val + $.cb.value.call @ + load: -> + {req} = ThreadUpdater + switch req.status + when 200 + g.DEAD = false + ThreadUpdater.parse JSON.parse(req.response).posts + ThreadUpdater.lastModified = req.getResponseHeader 'Last-Modified' + ThreadUpdater.set 'timer', ThreadUpdater.getInterval() + when 404 + g.DEAD = true + ThreadUpdater.set 'timer', null + ThreadUpdater.set 'status', '404', 'warning' + clearTimeout ThreadUpdater.timeoutID + ThreadUpdater.thread.kill() + $.event 'ThreadUpdate', + 404: true + thread: ThreadUpdater.thread + else + ThreadUpdater.outdateCount++ + ThreadUpdater.set 'timer', ThreadUpdater.getInterval() + ### + Status Code 304: Not modified + By sending the `If-Modified-Since` header we get a proper status code, and no response. + This saves bandwidth for both the user and the servers and avoid unnecessary computation. + ### + # XXX 304 -> 0 in Opera + [text, klass] = if req.status in [0, 304] + [null, null] + else + ["#{req.statusText} (#{req.status})", 'warning'] + ThreadUpdater.set 'status', text, klass + delete ThreadUpdater.req + + getInterval: -> + i = ThreadUpdater.interval + j = Math.min ThreadUpdater.outdateCount, 10 + unless d.hidden + # Lower the max refresh rate limit on visible tabs. + j = Math.min j, 7 + ThreadUpdater.seconds = Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] + + set: (name, text, klass) -> + el = ThreadUpdater[name] + if node = el.firstChild + # Prevent the creation of a new DOM Node + # by setting the text node's data. + node.data = text + else + el.textContent = text + el.className = klass if klass isnt undefined + + timeout: -> + ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000 + unless n = --ThreadUpdater.seconds + ThreadUpdater.update() + else if n <= -60 + ThreadUpdater.set 'status', 'Retrying', null + ThreadUpdater.update() + else if n > 0 + ThreadUpdater.set 'timer', n + + update: -> + return unless ThreadUpdater.online + ThreadUpdater.seconds = 0 + ThreadUpdater.set 'timer', '...' + if ThreadUpdater.req + # abort() triggers onloadend, we don't want that. + ThreadUpdater.req.onloadend = null + ThreadUpdater.req.abort() + url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json" + ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load, + headers: 'If-Modified-Since': ThreadUpdater.lastModified + + updateThreadStatus: (title, OP) -> + titleLC = title.toLowerCase() + return if ThreadUpdater.thread["is#{title}"] is !!OP[titleLC] + unless ThreadUpdater.thread["is#{title}"] = !!OP[titleLC] + message = if title is 'Sticky' + 'The thread is not a sticky anymore.' + else + 'The thread is not closed anymore.' + new Notification 'info', message, 30 + $.rm $ ".#{titleLC}Icon", ThreadUpdater.thread.OP.nodes.info + return + message = if title is 'Sticky' + 'The thread is now a sticky.' + else + 'The thread is now closed.' + new Notification 'info', message, 30 + icon = $.el 'img', + src: "//static.4chan.org/image/#{titleLC}.gif" + alt: title + title: title + className: "#{titleLC}Icon" + root = $ '[title="Quote this post"]', ThreadUpdater.thread.OP.nodes.info + if title is 'Closed' + root = $('.stickyIcon', ThreadUpdater.thread.OP.nodes.info) or root + $.after root, [$.tn(' '), icon] + + parse: (postObjects) -> + OP = postObjects[0] + Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler + + ThreadUpdater.updateThreadStatus 'Sticky', OP + ThreadUpdater.updateThreadStatus 'Closed', OP + ThreadUpdater.thread.postLimit = !!OP.bumplimit + ThreadUpdater.thread.fileLimit = !!OP.imagelimit + + nodes = [] # post container elements + posts = [] # post objects + index = [] # existing posts + files = [] # existing files + count = 0 # new posts count + # Build the index, create posts. + for postObject in postObjects + num = postObject.no + index.push num + files.push num if postObject.fsize + continue if num <= ThreadUpdater.lastPost + # Insert new posts, not older ones. + count++ + node = Build.postFromObject postObject, ThreadUpdater.thread.board + nodes.push node + posts.push new Post node, ThreadUpdater.thread, ThreadUpdater.thread.board + + deletedPosts = [] + deletedFiles = [] + # Check for deleted posts/files. + for ID, post of ThreadUpdater.thread.posts + # XXX tmp fix for 4chan's racing condition + # giving us false-positive dead posts. + # continue if post.isDead + ID = +ID + if post.isDead and ID in index + post.resurrect() + else unless ID in index + post.kill() + deletedPosts.push post + else if post.file and !post.file.isDead and ID not in files + post.kill true + deletedFiles.push post + + unless count + ThreadUpdater.set 'status', null, null + ThreadUpdater.outdateCount++ + else + ThreadUpdater.set 'status', "+#{count}", 'new' + ThreadUpdater.outdateCount = 0 + if Conf['Beep'] and d.hidden and Unread.posts and !Unread.posts.length + unless ThreadUpdater.audio + ThreadUpdater.audio = $.el 'audio', src: ThreadUpdater.beep + ThreadUpdater.audio.play() + + ThreadUpdater.lastPost = posts[count - 1].ID + Main.callbackNodes Post, posts + + scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and + ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25 + $.add ThreadUpdater.root, nodes + if scroll + if Conf['Bottom Scroll'] + <% if (type === 'crx') { %>d.body<% } else { %>doc<% } %>.scrollTop = d.body.clientHeight + else + Header.scrollToPost nodes[0] + + $.queueTask -> + # Enable 4chan features. + threadID = ThreadUpdater.thread.ID + {length} = $$ '.thread > .postContainer', ThreadUpdater.root + if Conf['Enable 4chan\'s Extension'] + $.globalEval "Parser.parseThread(#{threadID}, #{-count})" + else + Fourchan.parseThread threadID, length - count, length + + $.event 'ThreadUpdate', + 404: false + thread: ThreadUpdater.thread + newPosts: posts + deletedPosts: deletedPosts + deletedFiles: deletedFiles + postCount: OP.replies + 1 + fileCount: OP.images + (!!ThreadUpdater.thread.OP.file and !ThreadUpdater.thread.OP.file.isDead) + +ThreadWatcher = + init: -> + return if g.VIEW is 'catalog' or !Conf['Thread Watcher'] + @dialog = UI.dialog 'watcher', 'top: 50px; left: 0px;', + '
Thread Watcher
' + + $.on d, 'QRPostSuccessful', @cb.post + $.on d, '4chanXInitFinished', @ready + $.sync 'WatchedThreads', @refresh + + Thread::callbacks.push + name: 'Thread Watcher' + cb: @node + + node: -> + favicon = $.el 'img', + className: 'favicon' + $.on favicon, 'click', ThreadWatcher.cb.toggle + $.before $('input', @OP.nodes.post), favicon + return if g.VIEW isnt 'thread' + $.get 'AutoWatch', 0, (item) => + return if item['AutoWatch'] isnt @ID + ThreadWatcher.watch @ + $.delete 'AutoWatch' + + ready: -> + $.off d, '4chanXInitFinished', ThreadWatcher.ready + return unless Main.isThisPageLegit() + ThreadWatcher.refresh() + $.add d.body, ThreadWatcher.dialog + + refresh: (watched) -> + unless watched + $.get 'WatchedThreads', {}, (item) -> + ThreadWatcher.refresh item['WatchedThreads'] + return + nodes = [$('.move', ThreadWatcher.dialog)] + for board of watched + for id, props of watched[board] + x = $.el 'a', + textContent: '×' + href: 'javascript:;' + $.on x, 'click', ThreadWatcher.cb.x + link = $.el 'a', props + link.title = link.textContent + + div = $.el 'div' + $.add div, [x, $.tn(' '), link] + nodes.push div + + $.rmAll ThreadWatcher.dialog + $.add ThreadWatcher.dialog, nodes + + watched = watched[g.BOARD] or {} + for ID, thread of g.BOARD.threads + favicon = $ '.favicon', thread.OP.nodes.post + favicon.src = if ID of watched + Favicon.default + else + Favicon.empty + return + + cb: + toggle: -> + ThreadWatcher.toggle Get.postFromNode(@).thread + x: -> + thread = @nextElementSibling.pathname.split '/' + ThreadWatcher.unwatch thread[1], thread[3] + post: (e) -> + {board, postID, threadID} = e.detail + if postID is threadID + if Conf['Auto Watch'] + $.set 'AutoWatch', threadID + else if Conf['Auto Watch Reply'] + ThreadWatcher.watch board.threads[threadID] + + toggle: (thread) -> + if $('.favicon', thread.OP.nodes.post).src is Favicon.empty + ThreadWatcher.watch thread + else + ThreadWatcher.unwatch thread.board, thread.ID + + unwatch: (board, threadID) -> + $.get 'WatchedThreads', {}, (item) -> + watched = item['WatchedThreads'] + delete watched[board][threadID] + delete watched[board] unless Object.keys(watched[board]).length + ThreadWatcher.refresh watched + $.set 'WatchedThreads', watched + + watch: (thread) -> + $.get 'WatchedThreads', {}, (item) -> + watched = item['WatchedThreads'] + watched[thread.board] or= {} + watched[thread.board][thread] = + href: "/#{thread.board}/res/#{thread}" + textContent: Get.threadExcerpt thread + ThreadWatcher.refresh watched + $.set 'WatchedThreads', watched diff --git a/src/features/filtering/threadhiding.coffee b/src/features/filtering/threadhiding.coffee index 63f0ad43f..69d2af192 100644 --- a/src/features/filtering/threadhiding.coffee +++ b/src/features/filtering/threadhiding.coffee @@ -127,7 +127,7 @@ ThreadHiding = ThreadHiding.saveHiddenState thread hide: (thread, makeStub=Conf['Stubs']) -> - return if thread.hidden + return if thread.isHidden {OP} = thread threadRoot = OP.nodes.root.parentNode threadRoot.hidden = thread.isHidden = true diff --git a/src/features/menu/downloadlink.coffee b/src/features/menu/downloadlink.coffee index b471e815b..a50d4b677 100644 --- a/src/features/menu/downloadlink.coffee +++ b/src/features/menu/downloadlink.coffee @@ -1,14 +1,7 @@ DownloadLink = init: -> - <% if (type === 'userscript') { %> - # Firefox won't let us download cross-domain content. - return - <% } %> return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Download Link'] - # Test for download feature support. - return unless 'download' of $.el 'a' - a = $.el 'a', className: 'download-link' textContent: 'Download file' diff --git a/src/features/misc/announcementhiding.coffee b/src/features/misc/announcementhiding.coffee index 09ed3c9b0..8208b58f1 100644 --- a/src/features/misc/announcementhiding.coffee +++ b/src/features/misc/announcementhiding.coffee @@ -5,6 +5,7 @@ PSAHiding = $.addClass doc, 'hide-announcement' $.on d, '4chanXInitFinished', @setup + setup: -> $.off d, '4chanXInitFinished', PSAHiding.setup @@ -24,11 +25,11 @@ PSAHiding = $.rmClass doc, 'hide-announcement' $.sync 'hiddenPSAs', PSAHiding.sync + toggle: (e) -> hide = $.hasClass @, 'hide-announcement' text = PSAHiding.trim $.id 'globalMessage' - $.get 'hiddenPSAs', [], (item) -> - {hiddenPSAs} = item + $.get 'hiddenPSAs', [], ({hiddenPSAs}) -> if hide hiddenPSAs.push text else @@ -37,12 +38,13 @@ PSAHiding = hiddenPSAs = hiddenPSAs[-5..] PSAHiding.sync hiddenPSAs $.set 'hiddenPSAs', hiddenPSAs + sync: (hiddenPSAs) -> {btn} = PSAHiding psa = $.id 'globalMessage' - [psa.hidden, btn.innerHTML, btn.className] = if PSAHiding.trim(psa) in hiddenPSAs - [true, '[ + ]', 'show-announcement'] + [psa.hidden, btn.firstChild.textContent, btn.className] = if PSAHiding.trim(psa) in hiddenPSAs + [true, '[\u00A0+\u00A0]', 'show-announcement'] else - [false, '[ - ]', 'hide-announcement'] + [false, '[\u00A0-\u00A0]', 'hide-announcement'] trim: (psa) -> psa.textContent.replace(/\W+/g, '').toLowerCase() \ No newline at end of file diff --git a/src/features/misc/cataloglinks.coffee b/src/features/misc/cataloglinks.coffee index ab2e2976a..90ee4ffce 100644 --- a/src/features/misc/cataloglinks.coffee +++ b/src/features/misc/cataloglinks.coffee @@ -9,6 +9,7 @@ CatalogLinks = input = $ 'input', el $.on input, 'change', @toggle + $.sync 'Header catalog links', CatalogLinks.set $.event 'AddMenuEntry', type: 'header' @@ -21,10 +22,15 @@ CatalogLinks = # it might be incomplete otherwise. $.asap (-> $.id 'boardNavMobile'), -> # Set links on load. - CatalogLinks.toggle.call input + CatalogLinks.set input.checked toggle: -> + $.event 'CloseMenu' $.set 'Header catalog links', useCatalog = @checked + CatalogLinks.set useCatalog + + set: (useCatalog) -> + path = if useCatalog then 'catalog' else '' for a in $$ 'a', $.id('boardNavDesktop') board = a.pathname.split('/')[1] continue if ['f', 'status', '4chan'].contains(board) or !board @@ -34,7 +40,7 @@ CatalogLinks = else "//boards.4chan.org/#{board}/" else - a.pathname = "/#{board}/#{if useCatalog then 'catalog' else ''}" + a.pathname = "/#{board}/#{path}" a.title = if useCatalog then "#{a.title} - Catalog" else a.title.replace(/\ -\ Catalog$/, '') @title = "Turn catalog links #{if useCatalog then 'off' else 'on'}." diff --git a/src/features/misc/header.coffee b/src/features/misc/header.coffee index d35eed0bf..64cba7c88 100644 --- a/src/features/misc/header.coffee +++ b/src/features/misc/header.coffee @@ -5,36 +5,32 @@ Header = innerHTML: '' @menu = new UI.Menu 'header' - $.on @menuButton, 'click', @menuToggle - $.on @toggle, 'mousedown', @toggleBarVisibility - $.on window, 'load hashchange', Header.hashScroll - @positionToggler = $.el 'span', - textContent: 'Header Position' - className: 'header-position-link' + headerToggler = $.el 'label', + innerHTML: ' Auto-hide header' + + @headerToggler = headerToggler.firstElementChild + + $.on @menuButton, 'click', @menuToggle + $.on window, 'load hashchange', Header.hashScroll + $.on @headerToggler, 'change', @toggleBarVisibility {createSubEntry} = Header subEntries = [] for setting in ['sticky top', 'sticky bottom', 'top'] subEntries.push createSubEntry setting - + + subEntries.push {el: headerToggler} + @addShortcut Header.menuButton - + $.event 'AddMenuEntry', - type: 'header' - el: @positionToggler - order: 108 + type: 'header' + el: $.el 'span', + textContent: 'Header' + order: 105 subEntries: subEntries - @headerToggler = $.el 'label', - innerHTML: " Auto-hide header" - $.on @headerToggler.firstElementChild, 'change', @toggleBarVisibility - - $.event 'AddMenuEntry', - type: 'header' - el: @headerToggler - order: 109 - $.on d, 'CreateNotification', @createNotification $.asap (-> d.body), -> @@ -57,11 +53,8 @@ Header = toggle: $.el 'div', id: 'toggle-header-bar' - - settings: $.el 'div', - id: 'settings-container' - createSubEntry: (setting)-> + createSubEntry: (setting) -> label = $.el 'label', textContent: "#{setting}" @@ -85,7 +78,7 @@ Header = $.sync 'Header auto-hide', Header.setBarVisibility $.add fullBoardList, [nav.childNodes...] - $.add nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle, Header.settings] + $.add nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle] if Conf['Custom Board Navigation'] fullBoardList.hidden = true @@ -181,11 +174,27 @@ Header = $.addClass doc, 'top' setBarVisibility: (hide) -> - Header.headerToggler.firstElementChild.checked = hide + Header.headerToggler.checked = hide + $.event 'CloseMenu' (if hide then $.addClass else $.rmClass) Header.nav, 'autohide' + toggleBarVisibility: (e) -> + return if e.type is 'mousedown' and e.button isnt 0 # not LMB + hide = if @nodeName is 'INPUT' + @checked + else + !$.hasClass Header.bar, 'autohide' + Conf['Header auto-hide'] = hide + $.set 'Header auto-hide', hide + Header.setBarVisibility hide + message = if hide + 'The header bar will automatically hide itself.' + else + 'The header bar will remain visible.' + new Notification 'info', message, 2 + hashScroll: -> - return unless post = @location.hash[1..] + return unless post = $.id @location.hash[1..] return if (Get.postFromRoot post).isHidden Header.scrollToPost post @@ -196,20 +205,6 @@ Header = top += - headRect.top - headRect.height (if $.engine is 'webkit' then d.body else doc).scrollTop += top - toggleBarVisibility: (e) -> - return if e.type is 'mousedown' and e.button isnt 0 # not LMB - hide = if @nodeName is 'INPUT' - @checked - else - !$.hasClass Header.nav, 'autohide' - Header.setBarVisibility hide - message = if hide - 'The header bar will automatically hide itself.' - else - 'The header bar will remain visible.' - new Notification 'info', message, 2 - $.set 'Header auto-hide', hide - addShortcut: (el) -> shortcut = $.el 'span', className: 'shortcut' diff --git a/src/features/misc/redirection.coffee b/src/features/misc/redirection.coffee index b02aa0d06..ad27d64ac 100644 --- a/src/features/misc/redirection.coffee +++ b/src/features/misc/redirection.coffee @@ -8,6 +8,8 @@ Redirect = "//nsfw.foolz.us/#{boardID}/full_image/#{filename}" when 'po' "//archive.thedarkcave.org/#{boardID}/full_image/#{filename}" + when 'hr', 'tv' + "http://archive.4plebs.org/#{boardID}/full_image/#{filename}" when 'ck', 'fa', 'lit', 's4s' "//fuuka.warosu.org/#{boardID}/full_image/#{filename}" when 'cgl', 'g', 'mu', 'w' @@ -26,6 +28,8 @@ Redirect = "https://nsfw.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}" when 'c', 'int', 'out', 'po' "//archive.thedarkcave.org/_/api/chan/post/?board=#{boardID}&num=#{postID}" + when 'hr', 'x' + "http://archive.4plebs.org/_/api/chan/post/?board=#{boardID}&num=#{postID}" # for fuuka-based archives: # https://github.com/eksopl/fuuka/issues/27 to: (data) -> @@ -37,6 +41,8 @@ Redirect = Redirect.path '//nsfw.foolz.us', 'foolfuuka', data when 'int', 'out', 'po' Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data + when 'hr' + Redirect.path 'http://archive.4plebs.org', 'foolfuuka', data when 'ck', 'fa', 'lit', 's4s' Redirect.path '//fuuka.warosu.org', 'fuuka', data when 'diy', 'g', 'sci' diff --git a/src/features/monitoring/threadexcerpt.coffee b/src/features/monitoring/threadexcerpt.coffee index 4b15995f3..ed67edfa4 100644 --- a/src/features/monitoring/threadexcerpt.coffee +++ b/src/features/monitoring/threadexcerpt.coffee @@ -6,7 +6,4 @@ ThreadExcerpt = name: 'Thread Excerpt' cb: @node node: -> - d.title = if (excerpt = Get.threadExcerpt @).length > 80 - "#{excerpt[...77]}..." - else - excerpt \ No newline at end of file + d.title = Get.threadExcerpt @ \ No newline at end of file diff --git a/src/features/monitoring/unread.coffee b/src/features/monitoring/unread.coffee index b0943b045..8dd1243bf 100644 --- a/src/features/monitoring/unread.coffee +++ b/src/features/monitoring/unread.coffee @@ -26,8 +26,9 @@ Unread = $.on d, 'ThreadUpdate', Unread.onUpdate $.on d, 'scroll visibilitychange', Unread.read $.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line'] + $.on window, 'load', Unread.scroll if Conf['Scroll to Last Read Post'] - return unless Conf['Scroll to Last Read Post'] + scroll: -> # Let the header's onload callback handle it. return if (hash = location.hash.match /\d+/) and hash[0] of @posts if Unread.posts.length @@ -136,10 +137,7 @@ Unread = count = Unread.posts.length if Conf['Unread Count'] - d.title = if g.DEAD - "(#{Unread.posts.length}) /#{g.BOARD}/ - 404" - else - "(#{Unread.posts.length}) #{Unread.title}" + d.title = "#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then "/#{g.BOARD}/ - 404" else "#{Unread.title}"}" <% if (type === 'crx') { %> # XXX Chrome bug where it doesn't always update the tab title. # crbug.com/124381 diff --git a/src/features/posting/qr.coffee b/src/features/posting/qr.coffee index 2505b2b63..d9bfd3a75 100644 --- a/src/features/posting/qr.coffee +++ b/src/features/posting/qr.coffee @@ -805,7 +805,7 @@ QR = QR.mimeTypes = mimeTypes.split ', ' # Add empty mimeType to avoid errors with URLs selected in Window's file dialog. QR.mimeTypes.push '' - nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value + nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value <% if (type !== 'userjs') { %> # Opera's accept attribute is fucked up nodes.fileInput.accept = "text/*, #{mimeTypes}" @@ -842,8 +842,8 @@ QR = $.on elm, 'blur', QR.focusout $.on elm, 'focus', QR.focusin <% } %> - $.on QR.nodes.el, 'focusin', QR.focusin - $.on QR.nodes.el, 'focusout', QR.focusout + $.on dialog, 'focusin', QR.focusin + $.on dialog, 'focusout', QR.focusout $.on nodes.autohide, 'change', QR.toggleHide $.on nodes.close, 'click', QR.close $.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump' diff --git a/src/lib/get.coffee b/src/lib/get.coffee index 386070da7..a40975201 100644 --- a/src/lib/get.coffee +++ b/src/lib/get.coffee @@ -5,6 +5,8 @@ Get = OP.info.comment.replace(/\n+/g, ' // ') or Conf['Anonymize'] and 'Anonymous' or $('.nameBlock', OP.nodes.info).textContent.trim() + if excerpt.length > 70 + excerpt = "#{excerpt[...67]}..." "/#{thread.board}/ - #{excerpt}" postFromRoot: (root) -> link = $ 'a[title="Highlight this post"]', root diff --git a/src/settings.coffee b/src/settings.coffee index 3f0425bc9..fea4262e6 100644 --- a/src/settings.coffee +++ b/src/settings.coffee @@ -51,7 +51,7 @@ Settings =