From 3f34c5fb75cce520dd28acfe3a5a67b2cb3c8193 Mon Sep 17 00:00:00 2001 From: Tuxedo Takodachi Date: Sun, 16 Jul 2023 10:55:01 +0200 Subject: [PATCH] XT 2.1.1: fix error when date format locale not set --- CHANGELOG.md | 5 + builds/4chan-XT-noupdate.user.js | 46067 +++++++++++---------- builds/4chan-XT-noupdate.user.min.js | 16 +- builds/4chan-XT-noupdate.user.min.js.map | 2 +- builds/crx/manifest.json | 2 +- builds/crx/script.js | 19 +- package.json | 4 +- src/Miscellaneous/Time.ts | 11 +- version.json | 4 +- 9 files changed, 23069 insertions(+), 23061 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3997083bf..1d2ea4fd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ 4chan XT uses a different user script namespace, so to migrate you need to export settings from 4chan X, and import them in XT. +### XT v2.1.1 (2023-07-16) + +- Time formatting now falls back to browser locale instead of giving an error when the locale is not set. +- Update notification link now links to the changelog on the right branch on github. + ### XT v2.1.0 (2023-06-24) - Limited support for audio posts: they work in threads but not yet in the gallery. Might add if there's demand. diff --git a/builds/4chan-XT-noupdate.user.js b/builds/4chan-XT-noupdate.user.js index 82993bf33..7ff3526cb 100644 --- a/builds/4chan-XT-noupdate.user.js +++ b/builds/4chan-XT-noupdate.user.js @@ -1,11 +1,11 @@ // ==UserScript== // @name 4chan XT -// @version XT 2.0.1 +// @version XT 2.1.1 // @minGMVer 1.14 // @minFFVer 74 // @namespace 4chan-XT // @description 4chan XT is a script that adds various features to anonymous imageboards. -// @license MIT; https://github.com/TuxedoTako/4chan-xt/blob/master/LICENSE +// @license MIT; https://github.com/TuxedoTako/4chan-xt/blob/project-XT/LICENSE // @include http://boards.4chan.org/* // @include https://boards.4chan.org/* // @include http://sys.4chan.org/* @@ -109,89 +109,89 @@ // @run-at document-start // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII= // ==/UserScript== -/* -* 4chan X -* -* Licensed under the MIT license. -* https://github.com/ccd0/4chan-x/blob/master/LICENSE -* -* Appchan X Copyright © 2013-2016 Zixaphir -* http://zixaphir.github.io/appchan-x/ -* 4chan x Copyright © 2009-2011 James Campos -* https://github.com/aeosynth/4chan-x -* 4chan x Copyright © 2012-2014 Nicolas Stepien -* https://4chan-x.just-believe.in/ -* 4chan x Copyright © 2013-2014 Jordan Bates -* http://seaweedchan.github.io/4chan-x/ -* 4chan x Copyright © 2012-2013 ihavenoface -* http://ihavenoface.github.io/4chan-x/ -* 4chan SS Copyright © 2011-2013 Ahodesuka -* https://github.com/ahodesuka/4chan-Style-Script/ -* -* Permission is hereby granted, free of charge, to any person -* obtaining a copy of this software and associated documentation -* files (the "Software"), to deal in the Software without -* restriction, including without limitation the rights to use, -* copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the -* Software is furnished to do so, subject to the following -* conditions: -* -* The above copyright notice and this permission notice shall be -* included in all copies or substantial portions of the Software. -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -* OTHER DEALINGS IN THE SOFTWARE. -* -* Contributors: -* aeosynth -* mayhemydg -* noface -* !K.WeEabo0o -* blaise -* that4chanwolf -* desuwa -* seaweed -* e000 -* ahodesuka -* Shou -* ferongr -* xat -* Ongpot -* thisisanon -* Anonymous -* Seiba -* herpaderpderp -* WakiMiko -* btmcsweeney -* AppleBloom -* detharonil -* -* All the people who've taken the time to write bug reports. -* -* Thank you. -*/ - -/* -* Contains data from external sources: -* -* src/Monitoring/ThreadUpdater/beep.wav from http://freesound.org/people/pierrecartoons1979/sounds/90112/ -* cc-by-nc-3.0 -* -* Icons used to identify various websites are property of the respective websites. -*/ +/* +* 4chan X +* +* Licensed under the MIT license. +* https://github.com/ccd0/4chan-x/blob/master/LICENSE +* +* Appchan X Copyright © 2013-2016 Zixaphir +* http://zixaphir.github.io/appchan-x/ +* 4chan x Copyright © 2009-2011 James Campos +* https://github.com/aeosynth/4chan-x +* 4chan x Copyright © 2012-2014 Nicolas Stepien +* https://4chan-x.just-believe.in/ +* 4chan x Copyright © 2013-2014 Jordan Bates +* http://seaweedchan.github.io/4chan-x/ +* 4chan x Copyright © 2012-2013 ihavenoface +* http://ihavenoface.github.io/4chan-x/ +* 4chan SS Copyright © 2011-2013 Ahodesuka +* https://github.com/ahodesuka/4chan-Style-Script/ +* +* Permission is hereby granted, free of charge, to any person +* obtaining a copy of this software and associated documentation +* files (the "Software"), to deal in the Software without +* restriction, including without limitation the rights to use, +* copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following +* conditions: +* +* The above copyright notice and this permission notice shall be +* included in all copies or substantial portions of the Software. +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +* OTHER DEALINGS IN THE SOFTWARE. +* +* Contributors: +* aeosynth +* mayhemydg +* noface +* !K.WeEabo0o +* blaise +* that4chanwolf +* desuwa +* seaweed +* e000 +* ahodesuka +* Shou +* ferongr +* xat +* Ongpot +* thisisanon +* Anonymous +* Seiba +* herpaderpderp +* WakiMiko +* btmcsweeney +* AppleBloom +* detharonil +* +* All the people who've taken the time to write bug reports. +* +* Thank you. +*/ + +/* +* Contains data from external sources: +* +* src/Monitoring/ThreadUpdater/beep.wav from http://freesound.org/people/pierrecartoons1979/sounds/90112/ +* cc-by-nc-3.0 +* +* Icons used to identify various websites are property of the respective websites. +*/ (function () { 'use strict'; - var version = { - "version": "XT 2.0.1", - "date": "2023-06-24T16:01:33.425Z" + var version = { + "version": "XT 2.1.1", + "date": "2023-07-16T08:49:02.722Z" }; var meta = { @@ -204,8 +204,8 @@ "faq": "https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions", "captchaFAQ": "https://github.com/ccd0/4chan-x/wiki/Captcha-FAQ", "cssGuide": "https://github.com/ccd0/4chan-x/wiki/Styling-Guide", - "license": "https://github.com/TuxedoTako/4chan-xt/blob/master/LICENSE", - "changelog": "https://github.com/TuxedoTako/4chan-xt/blob/master/CHANGELOG.md", + "license": "https://github.com/TuxedoTako/4chan-xt/blob/project-XT/LICENSE", + "changelog": "https://github.com/TuxedoTako/4chan-xt/blob/project-XT/CHANGELOG.md", "issues": "https://github.com/TuxedoTako/4chan-xt/issues", "newIssue": "https://github.com/TuxedoTako/4chan-xt/issues", "newIssueMaxLength": 8181, @@ -333,1347 +333,1347 @@ return doc$1; }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS206: Consider reworking classes to avoid initClass - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - class Callbacks { - static initClass() { - this.Post = new Callbacks('Post'); - this.Thread = new Callbacks('Thread'); - this.CatalogThread = new Callbacks('Catalog Thread'); - this.CatalogThreadNative = new Callbacks('Catalog Thread'); - } - - constructor(type) { - this.type = type; - this.keys = []; - } - - push({name, cb}) { - if (!this[name]) { this.keys.push(name); } - return this[name] = cb; - } - - execute(node, keys=this.keys, force=false) { - let errors; - if (node.callbacksExecuted && !force) { return; } - node.callbacksExecuted = true; - for (var name of keys) { - try { - this[name]?.call(node); - } catch (err) { - if (!errors) { errors = []; } - errors.push({ - message: ['"', name, '" crashed on node ', this.type, ' No.', node.ID, ' (', node.board, ').'].join(''), - error: err, - html: node.nodes?.root?.outerHTML - }); - } - } - - if (errors) { return Main$1.handleErrors(errors); } - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + class Callbacks { + static initClass() { + this.Post = new Callbacks('Post'); + this.Thread = new Callbacks('Thread'); + this.CatalogThread = new Callbacks('Catalog Thread'); + this.CatalogThreadNative = new Callbacks('Catalog Thread'); + } + + constructor(type) { + this.type = type; + this.keys = []; + } + + push({name, cb}) { + if (!this[name]) { this.keys.push(name); } + return this[name] = cb; + } + + execute(node, keys=this.keys, force=false) { + let errors; + if (node.callbacksExecuted && !force) { return; } + node.callbacksExecuted = true; + for (var name of keys) { + try { + this[name]?.call(node); + } catch (err) { + if (!errors) { errors = []; } + errors.push({ + message: ['"', name, '" crashed on node ', this.type, ' No.', node.ID, ' (', node.board, ').'].join(''), + error: err, + html: node.nodes?.root?.outerHTML + }); + } + } + + if (errors) { return Main$1.handleErrors(errors); } + } + } Callbacks.initClass(); - var userCss = `/* Board title rice */ -div.boardTitle { - font-weight: 400 !important; -} -:root.yotsuba div.boardTitle { - font-family: sans-serif !important; - text-shadow: 1px 1px 1px rgba(100,0,0,0.6); -} -:root.yotsuba-b div.boardTitle { - font-family: sans-serif !important; - text-shadow: 1px 1px 1px rgba(105,10,15,0.6); -} -:root.photon div.boardTitle { - font-family: sans-serif !important; - text-shadow: 1px 1px 1px rgba(0,74,153,0.6); -} -:root.tomorrow div.boardTitle { - font-family: sans-serif !important; - text-shadow: 1px 1px 1px rgba(167,170,168,0.6); -} + var userCss = `/* Board title rice */ +div.boardTitle { + font-weight: 400 !important; +} +:root.yotsuba div.boardTitle { + font-family: sans-serif !important; + text-shadow: 1px 1px 1px rgba(100,0,0,0.6); +} +:root.yotsuba-b div.boardTitle { + font-family: sans-serif !important; + text-shadow: 1px 1px 1px rgba(105,10,15,0.6); +} +:root.photon div.boardTitle { + font-family: sans-serif !important; + text-shadow: 1px 1px 1px rgba(0,74,153,0.6); +} +:root.tomorrow div.boardTitle { + font-family: sans-serif !important; + text-shadow: 1px 1px 1px rgba(167,170,168,0.6); +} `; var banners = ["0.jpg", "1.jpg", "2.jpg", "4.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg", "11.jpg", "12.jpg", "13.jpg", "14.jpg", "16.jpg", "17.jpg", "18.jpg", "19.jpg", "20.jpg", "21.jpg", "22.jpg", "24.jpg", "25.jpg", "26.jpg", "28.jpg", "29.jpg", "33.jpg", "38.jpg", "39.jpg", "43.jpg", "44.jpg", "45.jpg", "46.jpg", "47.jpg", "52.jpg", "54.jpg", "57.jpg", "59.jpg", "60.jpg", "61.jpg", "64.jpg", "66.jpg", "67.jpg", "69.jpg", "71.jpg", "72.jpg", "76.jpg", "77.jpg", "81.jpg", "82.jpg", "83.jpg", "84.jpg", "88.jpg", "90.jpg", "91.jpg", "96.jpg", "98.jpg", "99.jpg", "100.jpg", "104.jpg", "106.jpg", "116.jpg", "119.jpg", "137.jpg", "140.jpg", "148.jpg", "149.jpg", "150.jpg", "154.jpg", "156.jpg", "157.jpg", "158.jpg", "159.jpg", "161.jpg", "162.jpg", "164.jpg", "165.jpg", "166.jpg", "167.jpg", "168.jpg", "169.jpg", "170.jpg", "171.jpg", "172.jpg", "173.jpg", "174.jpg", "175.jpg", "176.jpg", "178.jpg", "179.jpg", "180.jpg", "181.jpg", "182.jpg", "183.jpg", "186.jpg", "189.jpg", "190.jpg", "192.jpg", "193.jpg", "194.jpg", "197.jpg", "198.jpg", "200.jpg", "201.jpg", "202.jpg", "203.jpg", "205.jpg", "206.jpg", "207.jpg", "208.jpg", "210.jpg", "213.jpg", "214.jpg", "215.jpg", "216.jpg", "218.jpg", "219.jpg", "220.jpg", "221.jpg", "222.jpg", "223.jpg", "224.jpg", "227.jpg", "0.png", "1.png", "2.png", "3.png", "5.png", "6.png", "9.png", "10.png", "11.png", "12.png", "14.png", "16.png", "19.png", "20.png", "21.png", "22.png", "23.png", "24.png", "26.png", "27.png", "28.png", "29.png", "30.png", "31.png", "32.png", "33.png", "34.png", "37.png", "39.png", "40.png", "41.png", "42.png", "43.png", "44.png", "45.png", "48.png", "49.png", "50.png", "51.png", "52.png", "53.png", "57.png", "58.png", "59.png", "64.png", "66.png", "67.png", "68.png", "69.png", "70.png", "71.png", "72.png", "76.png", "78.png", "79.png", "81.png", "82.png", "85.png", "86.png", "87.png", "89.png", "95.png", "98.png", "100.png", "101.png", "102.png", "105.png", "106.png", "107.png", "109.png", "110.png", "111.png", "112.png", "113.png", "114.png", "115.png", "116.png", "118.png", "119.png", "120.png", "121.png", "122.png", "123.png", "126.png", "128.png", "130.png", "134.png", "136.png", "138.png", "139.png", "140.png", "142.png", "145.png", "146.png", "149.png", "150.png", "151.png", "152.png", "153.png", "154.png", "155.png", "156.png", "157.png", "158.png", "159.png", "160.png", "163.png", "164.png", "165.png", "166.png", "167.png", "168.png", "169.png", "170.png", "171.png", "172.png", "173.png", "174.png", "178.png", "179.png", "180.png", "181.png", "182.png", "184.png", "186.png", "188.png", "190.png", "192.png", "193.png", "194.png", "195.png", "196.png", "197.png", "198.png", "200.png", "202.png", "203.png", "205.png", "206.png", "207.png", "209.png", "212.png", "213.png", "214.png", "216.png", "217.png", "218.png", "219.png", "220.png", "221.png", "222.png", "223.png", "224.png", "225.png", "226.png", "229.png", "231.png", "232.png", "233.png", "234.png", "235.png", "237.png", "238.png", "239.png", "240.png", "241.png", "242.png", "244.png", "245.png", "246.png", "247.png", "248.png", "249.png", "250.png", "253.png", "254.png", "255.png", "256.png", "257.png", "258.png", "259.png", "260.png", "262.png", "268.png", "0.gif", "1.gif", "2.gif", "3.gif", "4.gif", "5.gif", "6.gif", "7.gif", "8.gif", "9.gif", "10.gif", "12.gif", "13.gif", "14.gif", "15.gif", "16.gif", "18.gif", "19.gif", "20.gif", "21.gif", "22.gif", "23.gif", "24.gif", "28.gif", "29.gif", "30.gif", "33.gif", "34.gif", "35.gif", "36.gif", "37.gif", "39.gif", "40.gif", "42.gif", "44.gif", "45.gif", "46.gif", "48.gif", "50.gif", "52.gif", "54.gif", "55.gif", "57.gif", "58.gif", "59.gif", "60.gif", "61.gif", "63.gif", "64.gif", "66.gif", "67.gif", "68.gif", "69.gif", "70.gif", "72.gif", "73.gif", "75.gif", "76.gif", "77.gif", "78.gif", "80.gif", "81.gif", "82.gif", "83.gif", "86.gif", "87.gif", "88.gif", "92.gif", "93.gif", "94.gif", "95.gif", "96.gif", "97.gif", "98.gif", "99.gif", "100.gif", "101.gif", "102.gif", "103.gif", "104.gif", "105.gif", "106.gif", "108.gif", "109.gif", "110.gif", "111.gif", "112.gif", "113.gif", "115.gif", "116.gif", "117.gif", "118.gif", "119.gif", "120.gif", "122.gif", "123.gif", "124.gif", "127.gif", "129.gif", "130.gif", "131.gif", "134.gif", "135.gif", "136.gif", "138.gif", "139.gif", "141.gif", "144.gif", "146.gif", "148.gif", "149.gif", "153.gif", "154.gif", "155.gif", "157.gif", "158.gif", "159.gif", "160.gif", "161.gif", "162.gif", "164.gif", "166.gif", "167.gif", "168.gif", "169.gif", "170.gif", "171.gif", "172.gif", "173.gif", "174.gif", "175.gif", "176.gif", "177.gif", "178.gif", "181.gif", "182.gif", "183.gif", "185.gif", "186.gif", "187.gif", "188.gif", "189.gif", "190.gif", "191.gif", "192.gif", "193.gif", "195.gif", "196.gif", "197.gif", "200.gif", "201.gif", "202.gif", "203.gif", "204.gif", "205.gif", "206.gif", "207.gif", "208.gif", "209.gif", "210.gif", "211.gif", "212.gif", "213.gif", "214.gif", "215.gif", "216.gif", "217.gif", "219.gif", "220.gif", "221.gif", "222.gif", "224.gif", "225.gif", "226.gif", "227.gif", "228.gif", "230.gif", "232.gif", "233.gif", "234.gif", "235.gif", "238.gif", "240.gif", "241.gif", "243.gif", "244.gif", "245.gif", "246.gif", "247.gif", "249.gif", "250.gif", "251.gif", "253.gif"]; - const Config = { - main: { - 'Miscellaneous': { - 'Redirect to HTTPS': [ - true, - 'Redirect to the HTTPS version of 4chan.' - ], - 'JSON Index': [ - true, - 'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.' - ], - [`Use ${meta.name} Catalog`]: [ - true, - `Link to ${meta.name}'s catalog instead of the native 4chan one.`, - 1 - ], - 'Index Refresh Notifications': [ - false, - 'Show a notice at the top of the page when the index is refreshed.', - 1 - ], - 'Follow Cursor': [ - true, - 'Image Hover and Quote Preview move with the mouse cursor.' - ], - 'Open Threads in New Tab': [ - false, - `Make links to threads in the index / ${meta.name} catalog open in a new tab.` - ], - 'External Catalog': [ - false, - 'Link to external catalog instead of the internal one.' - ], - 'Catalog Links': [ - false, - 'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.' - ], - 'Announcement Hiding': [ - true, - 'Add button to hide 4chan announcements.' - ], - 'Desktop Notifications': [ - true, - `Enables desktop notifications across various ${meta.name} features.` - ], - '404 Redirect': [ - true, - 'Redirect dead threads and images to the archives.' - ], - 'Archive Report': [ - true, - 'Enable reporting posts to supported archives.' - ], - 'Exempt Archives from Encryption': [ - true, - 'Permit loading content from, and warningless redirects to, HTTP-only archives from HTTPS pages.' - ], - 'Keybinds': [ - true, - 'Bind actions to keyboard shortcuts.' - ], - 'Time Formatting': [ - true, - 'Localize and format timestamps.' - ], - 'Relative Post Dates': [ - true, - 'Display dates like "3 minutes ago". Tooltip shows the timestamp.' - ], - 'Relative Date Title': [ - true, - 'Show Relative Post Date only when hovering over dates.', - 1 - ], - 'Comment Expansion': [ - true, - 'Expand comments that are too long to display on the index. Not applicable with JSON Index.' - ], - 'File Info Formatting': [ - true, - 'Reformat the file information.' - ], - 'Thread Expansion': [ - true, - 'Add buttons to expand threads.' - ], - 'Index Navigation': [ - false, - 'Add buttons to navigate between threads.' - ], - 'Reply Navigation': [ - false, - 'Add buttons to navigate to top / bottom of thread.' - ], - 'Unique ID and Capcode Navigation': [ - false, - 'Add buttons to navigate to posts having the same unique ID or capcode.' - ], - 'Custom Board Titles': [ - true, - 'Allow editing of the board title and subtitle by ctrl/\u2318+clicking them.' - ], - 'Persistent Custom Board Titles': [ - false, - 'Force custom board titles to be persistent, even if the board titles are updated.', - 1 - ], - 'Show Updated Notifications': [ - true, - `Show notifications when ${meta.name} is successfully updated.` - ], - 'Color User IDs': [ - true, - 'Assign unique colors to user IDs on boards that use them' - ], - 'Count Posts by ID': [ - true, - 'Display number of posts in the thread when hovering over an ID.' - ], - 'Remove Spoilers': [ - false, - 'Remove all spoilers in text.' - ], - 'Reveal Spoilers': [ - false, - 'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.' - ], - 'Normalize URL': [ - true, - 'Rewrite the URL of the current page, removing slugs and excess slashes, and changing /res/ to /thread/.' - ], - 'Work around CORB Bug': [ - true, - 'Leave this checked until your garbage browser is fixed.' - ], - 'Disable Autoplaying Sounds': [ - false, - 'Prevent sounds on the page from autoplaying.' - ], - 'Disable Native Extension': [ - true, - `${meta.name} is NOT designed to work with the native extension.` - ], - 'Enable Native Flash Embedding': [ - true, - 'Activate the native extension\'s Flash embedding if the native extension is disabled.' - ] - }, - - 'Linkification': { - 'Linkify': [ - true, - 'Convert text into links where applicable.' - ], - 'Link Title': [ - true, - 'Replace the link of a supported site with its actual title.', - 1 - ], - 'Cover Preview': [ - true, - 'Show preview of supported links on hover.', - 1 - ], - 'Embedding': [ - true, - 'Embed supported services. Note: Some services don\'t work on HTTPS.', - 1 - ], - 'Auto-embed': [ - false, - 'Auto-embed Linkify Embeds.', - 2 - ], - 'Floating Embeds': [ - false, - 'Embed content in a frame that remains in place when the page is scrolled.', - 2 - ] - }, - - 'Filtering': { - 'Anonymize': [ - false, - 'Make everyone Anonymous.' - ], - 'Filter': [ - true, - 'Self-moderation placebo.' - ], - 'Filtered Backlinks': [ - false, - 'When enabled, shows backlinks to filtered posts with a line-through decoration. Otherwise, hides the backlinks.', - 1 - ], - 'Filter in Native Catalog': [ - true, - 'Apply 4chan X filters in native catalog.', - 1 - ], - 'MD5 Quick Filter Notifications': [ - true, - 'Show notification when quick filtering MD5s using the button or keybind.', - 1 - ], - 'Recursive Hiding': [ - true, - 'Hide replies of hidden posts, recursively.' - ], - 'Thread Hiding Buttons': [ - true, - 'Add buttons to hide entire threads.' - ], - 'Reply Hiding Buttons': [ - true, - 'Add buttons to hide single replies.' - ], - 'Stubs': [ - true, - 'Show stubs of hidden threads / replies.' - ] - }, - - 'Images and Videos': { - 'Image Expansion': [ - true, - 'Expand images / videos.' - ], - 'Image Hover': [ - true, - 'Show full image / video on mouseover.' - ], - 'Image Hover in Catalog': [ - true, - `Show full image / video on mouseover in ${meta.name} catalog.` - ], - 'Gallery': [ - true, - 'Adds a simple and cute image gallery. Has more options in the gallery menu.' - ], - 'Fullscreen Gallery': [ - false, - 'Open gallery in fullscreen mode.', - 1 - ], - 'PDF in Gallery': [ - false, - 'Show PDF files in gallery.', - 1 - ], - 'Sauce': [ - true, - 'Add sauce links to images.' - ], - 'WEBM Metadata': [ - true, - 'Add link to fetch title metadata from webm videos.' - ], - 'Reveal Spoiler Thumbnails': [ - false, - 'Replace spoiler thumbnails with the original image.' - ], - 'Replace GIF': [ - false, - 'Replace gif thumbnails with the actual image.' - ], - 'Replace JPG': [ - false, - 'Replace jpg thumbnails with the actual image.' - ], - 'Replace PNG': [ - false, - 'Replace png thumbnails with the actual image.' - ], - 'Replace WEBM': [ - false, - 'Replace webm, mp4, and ogv thumbnails with the actual video. Probably will degrade browser performance ;)' - ], - 'Image Prefetching': [ - true, - 'Add a shortcut icon to the header to turn on image preloading.' - ], - 'Fappe Tyme': [ - true, - 'Hide posts without images when header menu item is checked. *hint* *hint*' - ], - 'Werk Tyme': [ - true, - 'Hide all post images when header menu item is checked.' - ], - 'Autoplay': [ - true, - 'Videos begin playing immediately when opened.' - ], - 'Restart when Opened': [ - false, - 'Restart GIFs and WebMs when you hover over or expand them.' - ], - 'Show Controls': [ - true, - 'Show controls on videos expanded inline.' - ], - 'Click Passthrough': [ - false, - 'Clicks on videos trigger your browser\'s default behavior. Videos can be contracted with button / dragging to the left.', - 1 - ], - 'Allow Sound': [ - true, - 'Open videos with the sound unmuted.' - ], - 'Mouse Wheel Volume': [ - true, - 'Adjust volume of videos with the mouse wheel over the thumbnail/filename/gallery.' - ], - 'Loop in New Tab': [ - true, - 'Loop videos opened in their own tabs.' - ], - 'Volume in New Tab': [ - true, - `Apply ${meta.name} mute and volume settings to videos opened in their own tabs.` - ], - 'Enable sound posts': [ - true, - 'Enable loading audio from [sound=] file names. This audio is fetched from third parties.' - ], - }, - - 'Menu': { - 'Menu': [ - true, - 'Add a drop-down menu to posts.' - ], - 'Report Link': [ - true, - 'Add a report link to the menu.', - 1 - ], - 'Copy Text Link': [ - true, - 'Add a link to copy the post\'s text.', - 1 - ], - 'Thread Hiding Link': [ - true, - 'Add a link to hide entire threads.', - 1 - ], - 'Reply Hiding Link': [ - true, - 'Add a link to hide single replies.', - 1 - ], - 'Delete Link': [ - true, - 'Add post and image deletion links to the menu.', - 1 - ], - 'Archive Link': [ - true, - 'Add an archive link to the menu.', - 1 - ], - 'Edit Link': [ - true, - 'Add a link to edit the image in Tegaki, /i/\'s painting program. Requires Quick Reply.', - 1 - ], - 'Download Link': [ - false, - 'Add a download with original filename link to the menu.', - 1 - ] - }, - - 'Monitoring': { - 'Thread Updater': [ - true, - 'Fetch and insert new replies. Has more options in the header menu and the "Advanced" tab.' - ], - 'Unread Count': [ - true, - 'Show the unread posts count in the tab title.' - ], - 'Quoted Title': [ - false, - 'Change the page title to reflect you\'ve been quoted.', - 1 - ], - 'Hide Unread Count at (0)': [ - false, - 'Hide the unread posts count in the tab title when it reaches 0.', - 1 - ], - 'Unread Favicon': [ - true, - 'Show a different favicon when there are unread posts.' - ], - 'Unread Line': [ - true, - 'Show a line to distinguish read posts from unread ones.' - ], - 'Remember Last Read Post': [ - true, - 'Remember how far you\'ve read after you close the thread.' - ], - 'Scroll to Last Read Post': [ - true, - 'Scroll back to the last read post when reopening a thread.', - 1 - ], - 'Unread Line in Index': [ - false, - 'Show a line between read and unread posts in threads in the index.', - 1 - ], - 'Remove Thread Excerpt': [ - false, - 'Replace the excerpt of the thread in the tab title with the board title.' - ], - 'Thread Stats': [ - true, - 'Display reply and image count.' - ], - 'IP Count in Stats': [ - true, - 'Display the unique IP count in the thread stats.', - 1 - ], - 'Page Count in Stats': [ - true, - 'Display the page count in the thread stats.', - 1 - ], - 'Updater and Stats in Header': [ - true, - 'Places the thread updater and thread stats in the header instead of floating them.' - ], - 'Thread Watcher': [ - true, - 'Bookmark threads. Has more options in the thread watcher menu.' - ], - 'Fixed Thread Watcher': [ - true, - 'Makes the thread watcher scroll with the page.', - 1 - ], - 'Persistent Thread Watcher': [ - false, - 'The thread watcher will be visible when the page is loaded.', - 1 - ], - 'Mark New IPs': [ - false, - 'Label each post from a new IP with the thread\'s current IP count.' - ], - 'Reply Pruning': [ - true, - 'Add option in header menu to hide old replies in long threads. Activated by default in stickies.' - ], - 'Prune All Threads': [ - false, - 'Activate Reply Pruning by default in all threads.', - 1 - ] - }, - - 'Posting and Captchas': { - 'Quick Reply': [ - true, - 'All-in-one form to reply, create threads, automate dumping and more.' - ], - 'Persistent QR': [ - false, - 'The Quick reply won\'t disappear after posting.', - 1 - ], - 'Auto Hide QR': [ - true, - 'Automatically hide the quick reply when posting.', - 2 - ], - 'Open Post in New Tab': [ - true, - 'Open new threads in a new tab, and open replies in a new tab if you\'re not already in the thread.', - 1 - ], - 'Remember QR Size': [ - false, - 'Remember the size of the Quick reply.', - 1 - ], - 'Remember Spoiler': [ - false, - 'Remember the spoiler state, instead of resetting after posting.', - 1 - ], - 'Randomize Filename': [ - false, - 'Set the filename to a random timestamp within the past year. Disabled on /f/.', - 1 - ], - 'Show New Thread Option in Threads': [ - true, - 'Show the option to post a new / different thread from inside a thread.', - 1 - ], - 'Show Upload Progress': [ - true, - 'Track progress of file uploads as percentage in submit button.', - 1 - ], - 'Cooldown': [ - true, - 'Indicate the remaining time before posting again.', - 1 - ], - 'Posting Success Notifications': [ - true, - 'Show notifications on successful post creation or file uploading.', - 1 - ], - 'Auto-load captcha': [ - false, - 'Automatically load the captcha in the QR even if your post is empty.', - 1 - ], - 'Post on Captcha Completion': [ - false, - 'Submit the post immediately when the captcha is completed.', - 1 - ], - 'Force Noscript Captcha': [ - false, - 'Use the non-Javascript fallback captcha even if Javascript is enabled.' - ], - 'Pass Link': [ - false, - 'Add a 4chan Pass login link to the bottom of the page.' - ] - }, - - 'Quote Links': { - 'Quote Backlinks': [ - true, - 'Add quote backlinks.' - ], - 'OP Backlinks': [ - true, - 'Add backlinks to the OP.', - 1 - ], - 'Bottom Backlinks': [ - false, - 'Place backlinks at the bottom of posts.', - 1 - ], - 'Quote Inlining': [ - true, - 'Inline quoted post on click.' - ], - 'Inline Cross-thread Quotes Only': [ - false, - 'Don\'t inline quote links when the posts are visible in the thread.', - 1 - ], - 'Quote Hash Navigation': [ - false, - 'Include an extra link after quotes for autoscrolling to quoted posts.', - 1 - ], - 'Forward Hiding': [ - true, - 'Hide original posts of inlined backlinks.', - 1 - ], - 'Quote Previewing': [ - true, - 'Show quoted post on hover.' - ], - 'Quote Highlighting': [ - true, - 'Highlight the previewed post.', - 1 - ], - 'Resurrect Quotes': [ - true, - 'Link dead quotes to the archives, and support inlining/previewing of archive links like quote links.' - ], - 'Remember Your Posts': [ - true, - 'Remember your posting history.' - ], - 'Mark Quotes of You': [ - true, - 'Add \'(You)\' to quotes linking to your posts.', - 1 - ], - 'Highlight Posts Quoting You': [ - true, - 'Highlights any posts that contain a quote to your post.', - 1 - ], - 'Highlight Own Posts': [ - true, - 'Highlights own posts.', - 1 - ], - 'Mark OP Quotes': [ - true, - 'Add \'(OP)\' to OP quotes.' - ], - 'Mark Cross-thread Quotes': [ - true, - 'Add \'(Cross-thread)\' to cross-threads quotes.' - ], - 'Quote Threading': [ - true, - 'Add option in header menu to thread conversations.' - ] - } - }, - - imageExpansion: { - 'Fit width': [ - true, - '' - ], - 'Fit height': [ - false, - '' - ], - 'Scroll into view': [ - true, - 'Scroll down when expanding images to bring the full image into view.' - ], - 'Expand spoilers': [ - true, - 'Expand all images along with spoilers.' - ], - 'Expand videos': [ - true, - 'Expand all images also expands videos.' - ], - 'Expand from here': [ - false, - 'Expand all images only from current position to thread end.' - ], - 'Expand thread only': [ - false, - 'In index, expand all images only within the current thread.' - ], - 'Advance on contract': [ - false, - 'Advance to next post when contracting an expanded image.' - ] - }, - - gallery: { - 'Hide Thumbnails': [ - false - ], - 'Fit Width': [ // 'Fit width' (lowercase W) belongs to Image Expansion. Engine limitations, heh. - true - ], - 'Fit Height': [ - true - ], - 'Stretch to Fit': [ - false - ], - 'Scroll to Post': [ - true - ], - 'Slide Delay': [ - 6.0 - ] - }, - - 'Default Volume': 1.0, - - threadWatcher: { - 'Current Board': [ - false, - 'Only show watched threads from the current board.' - ], - 'Auto Update Thread Watcher': [ - true, - 'Periodically check status of watched threads.' - ], - 'Auto Watch': [ - true, - 'Automatically watch threads you start.' - ], - 'Auto Watch Reply': [ - true, - 'Automatically watch threads you reply to.' - ], - 'Auto Prune': [ - false, - 'Automatically remove dead threads.' - ], - 'Show Page': [ - true, - 'Show what page watched threads are on.' - ], - 'Show Unread Count': [ - true, - 'Show number of unread posts in watched threads.' - ], - 'Show Site Prefix': [ - true, - 'When multiple sites are shown in the thread watcher, add a prefix to board names to distinguish them.' - ], - 'Require OP Quote Link': [ - false, - 'For purposes of thread watcher highlighting, only consider posts with a quote link to the OP as replies to the OP.' - ] - }, - - filter: { - general: '', - - postID: `\ -# Highlight dubs on [s4s]: -#/(\\d)\\1$/;highlight;top:no;boards:s4s\ -`, - - name: `\ -# Filter any namefags: -#/^(?!Anonymous$)/\ -`, - - uniqueID: `\ -# Filter a specific ID: -#/Txhvk1Tl/\ -`, - - tripcode: `\ -# Filter any tripfag -#/^!/\ -`, - - capcode: `\ -# Set a custom class for mods: -#/Mod$/;highlight:mod;op:yes -# Set a custom class for admins: -#/Admin$/;highlight:admin;op:yes\ -`, - - pass: `\ -# Filter anyone using since4pass: -#/./\ -`, - - email: '', - - subject: `\ -# Filter Generals on /v/: -#/general/i;boards:v;op:only\ -`, - - comment: `\ -# Filter Stallman copypasta on /g/: -#/what you\'re refer+ing to as linux/i;boards:g -# Filter posts with 20 or more quote links: -#/(?:>>\\d(?:(?!>>\\d)[^])*){20}/ -# Filter posts like T H I S / H / I / S: -#/^>?\\s?\\w\\s?(\\w)\\s?(\\w)\\s?(\\w).*$[\\s>]+\\1[\\s>]+\\2[\\s>]+\\3/im\ -`, - - flag: '', - filename: '', - dimensions: `\ -# Highlight potential wallpapers: -#/1920x1080/;op:yes;highlight;top:no;boards:w,wg\ -`, - - filesize: '', - - MD5: '' - }, - - sauces: `\ -# Known filename formats: -https://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/ -javascript:void(open("https://www.deviantart.com/"+%$1.replace(/_/g,"-")+"/art/"+parseInt(%$2,36)));regexp:/^\\w+_by_(\\w+)[_-]d([\\da-z]{6})\\b/ -https://imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/ -https://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/ -https://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/ - -# Reverse image search: -https://www.google.com/searchbyimage?sbisrc=4chanx&image_url=%IMG&safe=off -https://yandex.com/images/search?rpt=imageview&url=%IMG -#//tineye.com/search?url=%IMG -#//www.bing.com/images/search?q=imgurl:%IMG&view=detailv2&iss=sbi#enterInsights -#https://lens.google.com/uploadbyurl?url=%IMG;text:lens - -# Specialized reverse image search: -//iqdb.org/?url=%IMG -https://trace.moe/?auto&url=%IMG;text:wait -#//3d.iqdb.org/?url=%IMG -#//saucenao.com/search.php?url=%IMG - -# "View Same" in archives: -http://eye.swfchan.com/search/?q=%name;types:swf -#https://desuarchive.org/_/search/image/%sMD5/ -#https://archive.4plebs.org/_/search/image/%sMD5/ -#https://boards.fireden.net/_/search/image/%sMD5/ -#https://foolz.fireden.net/_/search/image/%sMD5/ - -# Other tools: -#http://exif.regex.info/exif.cgi?imgurl=%URL -#//imgops.com/start?url=%URL;types:gif,jpg,png -#//www.gif-explode.com/%URL;types:gif\ -`, - - FappeT: { - werk: false - }, - - 'Custom CSS': true, - - Index: { - 'Index Mode': 'paged', - 'Previous Index Mode': 'paged', - 'Index Size': 'small', - 'Show Replies': [true, 'Show replies in the index, and also in the catalog if "Catalog hover expand" is checked.'], - 'Catalog Hover Expand': [false, 'Expand the comment and show more details when you hover over a thread in the catalog.'], - 'Catalog Hover Toggle': [true, 'Turn "Catalog hover expand" on and off by clicking in the catalog.'], - 'Pin Watched Threads': [false, 'Move watched threads to the start of the index.'], - 'Anchor Hidden Threads': [true, 'Move hidden threads to the end of the index.'], - 'Refreshed Navigation': [false, 'Refresh index when navigating through pages.'] - }, - - Header: { - 'Fixed Header': true, - 'Header auto-hide': false, - 'Header auto-hide on scroll': false, - 'Bottom Header': false, - 'Centered links': false, - 'Header catalog links': false, - 'Bottom Board List': true, - 'Shortcut Icons': true, - 'Custom Board Navigation': true - }, - - archives: { - archiveLists: 'https://4chenz.github.io/archives.json/archives.json', - lastarchivecheck: 0, - archiveAutoUpdate: true - }, - - externalCatalogURLs: `\ -//catalog.neet.tv/%board/;boards:4chan.org:3,a,adv,an,asp,biz,c,cgl,ck,cm,co,diy,f,fa,fit,g,gd,his,i,int,jp,k,lgbt,lit,m,mlp,mu,n,news,o,out,p,po,pol,s4s,sci,sp,tg,toy,trv,tv,v,vg,vip,vp,vr,w,wg,wsg,wsr,x\ -`, - - boardnav: `\ -[ toggle-all ] -[current-index-text:"Index" -current-catalog-text:"Catalog" -current-expired-text:"Expired" -current-archive-text:"Archive"] -[external-text:"FAQ","${meta.name}"]\ -`, - - QR: { - 'QR.personas': `\ -#options:"sage";boards:jp;always\ -`, - sjisPreview: false - }, - - jsWhitelist: `\ -http://s.4cdn.org -https://s.4cdn.org -http://www.google.com -https://www.google.com -https://www.gstatic.com -http://cdn.mathjax.org -https://cdn.mathjax.org -https://cdnjs.cloudflare.com -https://hcaptcha.com -https://*.hcaptcha.com -'self' -'unsafe-inline' -'unsafe-eval'\ -`, - - captchaLanguage: '', - - time: '%m/%d/%y(%a)%H:%M:%S', - timeLocale: '', - - backlink: '>>%id', - - pastedname: 'file', - - fileInfo: '%l %d (%p%s, %r%g)', - - favicon: 'ferongr', - - usercss: userCss, - - hotkeys: { - // QR & Options - 'Toggle board list': [ - 'Ctrl+b', - 'Toggle the full board list.' - ], - 'Toggle header': [ - 'Shift+h', - 'Toggle the auto-hide option of the header.' - ], - 'Open empty QR': [ - 'q', - 'Open QR without post number inserted.' - ], - 'Open QR': [ - 'Shift+q', - 'Open QR with post number inserted.' - ], - 'Open settings': [ - 'Alt+o', - 'Open Settings.' - ], - 'Close': [ - 'Esc', - 'Close dialogs or notifications.' - ], - 'Spoiler tags': [ - 'Ctrl+s', - 'Insert spoiler tags.' - ], - 'Code tags': [ - 'Alt+c', - 'Insert code tags.' - ], - 'Eqn tags': [ - 'Alt+e', - 'Insert eqn tags.' - ], - 'Math tags': [ - 'Alt+m', - 'Insert math tags.' - ], - 'SJIS tags': [ - 'Alt+a', - 'Insert SJIS tags.' - ], - 'Toggle sage': [ - 'Alt+s', - 'Toggle sage in options field.' - ], - 'Toggle Cooldown': [ - 'Alt+Comma', - 'Toggle custom cooldown timer.' - ], - 'Post from URL': [ - 'Alt+l', - 'Post from URL.' - ], - 'Add new post': [ - 'Alt+n', - 'Add new post to the QR dump list.' - ], - 'Submit QR': [ - 'Ctrl+Enter', - 'Submit post.' - ], - // Thread related - 'Watch': [ - 'w', - 'Watch thread.' - ], - 'Update': [ - 'r', - 'Update the thread / refresh the index.' - ], - 'Update thread watcher': [ - 'Shift+r', - 'Manually refresh thread watcher.' - ], - 'Toggle thread watcher': [ - 't', - 'Toggle visibility of thread watcher.' - ], - 'Toggle threading': [ - 'Shift+t', - 'Toggle threading.' - ], - 'Mark thread read': [ - 'Ctrl+0', - 'Mark thread read from index (requires "Unread Line in Index").' - ], - // Images - 'Expand image': [ - 'Shift+e', - 'Expand selected image.' - ], - 'Expand images': [ - 'e', - 'Expand all images.' - ], - 'Open Gallery': [ - 'g', - 'Opens the gallery.' - ], - 'Next Gallery Image': [ - 'Right', - 'Go to the next image in gallery mode.' - ], - 'Previous Gallery Image': [ - 'Left', - 'Go to the previous image in gallery mode.' - ], - 'Advance Gallery': [ - 'Enter', - 'Go to next image or, if Autoplay is off, play video.' - ], - 'Pause': [ - 'p', - 'Pause/play videos in the gallery.' - ], - 'Slideshow': [ - 'Ctrl+Right', - 'Toggle the gallery slideshow mode.' - ], - 'Rotate image clockwise': [ - 'Shift+Right', - 'Rotate image clockwise in gallery.' - ], - 'Rotate image anticlockwise': [ - 'Shift+Left', - 'Rotate image anticlockwise in gallery.' - ], - 'Download Gallery Image': [ - 'Shift+j', - 'Download current image in gallery.' - ], - 'fappeTyme': [ - 'f', - 'Toggle Fappe Tyme.' - ], - 'werkTyme': [ - 'Shift+w', - 'Toggle Werk Tyme.' - ], - // Board Navigation - 'Front page': [ - '1', - 'Jump to front page.' - ], - 'Open front page': [ - 'Shift+1', - 'Open front page in a new tab.' - ], - 'Next page': [ - 'Ctrl+Right', - 'Jump to the next page.' - ], - 'Previous page': [ - 'Ctrl+Left', - 'Jump to the previous page.' - ], - 'Paged mode': [ - 'Alt+1', - 'Open the index in paged mode.' - ], - 'Infinite scrolling mode': [ - 'Alt+2', - 'Open the index in infinite scrolling mode.' - ], - 'All pages mode': [ - 'Alt+3', - 'Open the index in all threads mode.' - ], - 'Open catalog': [ - 'Shift+c', - 'Open the catalog of the current board.' - ], - 'Search form': [ - 'Ctrl+Alt+s', - 'Focus the search field on the board index.' - ], - 'Cycle sort type': [ - 'Alt+x', - 'Cycle through index sort types.' - ], - // Thread Navigation - 'Next thread': [ - 'Ctrl+Down', - 'See next thread.' - ], - 'Previous thread': [ - 'Ctrl+Up', - 'See previous thread.' - ], - 'Expand thread': [ - 'Ctrl+e', - 'Expand thread.' - ], - 'Open thread': [ - 'o', - 'Open thread in current tab.' - ], - 'Open thread tab': [ - 'Shift+o', - 'Open thread in new tab.' - ], - // Reply Navigation - 'Next reply': [ - 'j', - 'Select next reply.' - ], - 'Previous reply': [ - 'k', - 'Select previous reply.' - ], - 'Deselect reply': [ - 'Shift+d', - 'Deselect reply.' - ], - 'Hide': [ - 'x', - 'Hide thread.' - ], - 'Quick Filter MD5': [ - '5', - 'Add the MD5 of the selected image to the filter list.' - ], - 'Previous Post Quoting You': [ - 'Alt+Up', - 'Scroll to the previous post that quotes you.' - ], - 'Next Post Quoting You': [ - 'Alt+Down', - 'Scroll to the next post that quotes you.' - ] - }, - - updater: { - checkbox: { - 'Beep': [ - false, - 'Beep on new post to completely read thread.' - ], - 'Beep Quoting You': [ - false, - 'Beep on new post quoting you.' - ], - 'Auto Scroll': [ - false, - 'Scroll updated posts into view. Only enabled at bottom of page.' - ], - 'Bottom Scroll': [ - false, - 'Always scroll to the bottom, not the first new post. Useful for event threads.' - ], - 'Scroll BG': [ - false, - 'Auto-scroll background tabs.' - ], - 'Auto Update': [ - true, - 'Automatically fetch new posts.' - ], - 'Optional Increase': [ - false, - 'Increase the intervals between updates on threads without new posts.' - ] - }, - 'Interval': 5 - }, - - customCooldown: 0, - customCooldownEnabled: true, - - 'Thread Quotes': false, - - 'Max Replies': 1000, - - 'Autohiding Scrollbar': false, - - position: { - 'embedding.position': 'top: 50px; right: 0px;', - 'thread-stats.position': 'bottom: 0px; right: 0px;', - 'updater.position': 'bottom: 0px; left: 0px;', - 'thread-watcher.position': 'top: 50px; left: 0px;', - 'qr.position': 'top: 50px; right: 0px;' - }, - - fourchanImageHost: 'i.4cdn.org', - - hiddenPSAList: [{}], - - knownBanners: banners.join(','), - - passMessageClosed: false, - - 'Prerequest Captcha': false, - - 'PSAseen': [[]] + const Config = { + main: { + 'Miscellaneous': { + 'Redirect to HTTPS': [ + true, + 'Redirect to the HTTPS version of 4chan.' + ], + 'JSON Index': [ + true, + 'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.' + ], + [`Use ${meta.name} Catalog`]: [ + true, + `Link to ${meta.name}'s catalog instead of the native 4chan one.`, + 1 + ], + 'Index Refresh Notifications': [ + false, + 'Show a notice at the top of the page when the index is refreshed.', + 1 + ], + 'Follow Cursor': [ + true, + 'Image Hover and Quote Preview move with the mouse cursor.' + ], + 'Open Threads in New Tab': [ + false, + `Make links to threads in the index / ${meta.name} catalog open in a new tab.` + ], + 'External Catalog': [ + false, + 'Link to external catalog instead of the internal one.' + ], + 'Catalog Links': [ + false, + 'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.' + ], + 'Announcement Hiding': [ + true, + 'Add button to hide 4chan announcements.' + ], + 'Desktop Notifications': [ + true, + `Enables desktop notifications across various ${meta.name} features.` + ], + '404 Redirect': [ + true, + 'Redirect dead threads and images to the archives.' + ], + 'Archive Report': [ + true, + 'Enable reporting posts to supported archives.' + ], + 'Exempt Archives from Encryption': [ + true, + 'Permit loading content from, and warningless redirects to, HTTP-only archives from HTTPS pages.' + ], + 'Keybinds': [ + true, + 'Bind actions to keyboard shortcuts.' + ], + 'Time Formatting': [ + true, + 'Localize and format timestamps.' + ], + 'Relative Post Dates': [ + true, + 'Display dates like "3 minutes ago". Tooltip shows the timestamp.' + ], + 'Relative Date Title': [ + true, + 'Show Relative Post Date only when hovering over dates.', + 1 + ], + 'Comment Expansion': [ + true, + 'Expand comments that are too long to display on the index. Not applicable with JSON Index.' + ], + 'File Info Formatting': [ + true, + 'Reformat the file information.' + ], + 'Thread Expansion': [ + true, + 'Add buttons to expand threads.' + ], + 'Index Navigation': [ + false, + 'Add buttons to navigate between threads.' + ], + 'Reply Navigation': [ + false, + 'Add buttons to navigate to top / bottom of thread.' + ], + 'Unique ID and Capcode Navigation': [ + false, + 'Add buttons to navigate to posts having the same unique ID or capcode.' + ], + 'Custom Board Titles': [ + true, + 'Allow editing of the board title and subtitle by ctrl/\u2318+clicking them.' + ], + 'Persistent Custom Board Titles': [ + false, + 'Force custom board titles to be persistent, even if the board titles are updated.', + 1 + ], + 'Show Updated Notifications': [ + true, + `Show notifications when ${meta.name} is successfully updated.` + ], + 'Color User IDs': [ + true, + 'Assign unique colors to user IDs on boards that use them' + ], + 'Count Posts by ID': [ + true, + 'Display number of posts in the thread when hovering over an ID.' + ], + 'Remove Spoilers': [ + false, + 'Remove all spoilers in text.' + ], + 'Reveal Spoilers': [ + false, + 'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.' + ], + 'Normalize URL': [ + true, + 'Rewrite the URL of the current page, removing slugs and excess slashes, and changing /res/ to /thread/.' + ], + 'Work around CORB Bug': [ + true, + 'Leave this checked until your garbage browser is fixed.' + ], + 'Disable Autoplaying Sounds': [ + false, + 'Prevent sounds on the page from autoplaying.' + ], + 'Disable Native Extension': [ + true, + `${meta.name} is NOT designed to work with the native extension.` + ], + 'Enable Native Flash Embedding': [ + true, + 'Activate the native extension\'s Flash embedding if the native extension is disabled.' + ] + }, + + 'Linkification': { + 'Linkify': [ + true, + 'Convert text into links where applicable.' + ], + 'Link Title': [ + true, + 'Replace the link of a supported site with its actual title.', + 1 + ], + 'Cover Preview': [ + true, + 'Show preview of supported links on hover.', + 1 + ], + 'Embedding': [ + true, + 'Embed supported services. Note: Some services don\'t work on HTTPS.', + 1 + ], + 'Auto-embed': [ + false, + 'Auto-embed Linkify Embeds.', + 2 + ], + 'Floating Embeds': [ + false, + 'Embed content in a frame that remains in place when the page is scrolled.', + 2 + ] + }, + + 'Filtering': { + 'Anonymize': [ + false, + 'Make everyone Anonymous.' + ], + 'Filter': [ + true, + 'Self-moderation placebo.' + ], + 'Filtered Backlinks': [ + false, + 'When enabled, shows backlinks to filtered posts with a line-through decoration. Otherwise, hides the backlinks.', + 1 + ], + 'Filter in Native Catalog': [ + true, + 'Apply 4chan X filters in native catalog.', + 1 + ], + 'MD5 Quick Filter Notifications': [ + true, + 'Show notification when quick filtering MD5s using the button or keybind.', + 1 + ], + 'Recursive Hiding': [ + true, + 'Hide replies of hidden posts, recursively.' + ], + 'Thread Hiding Buttons': [ + true, + 'Add buttons to hide entire threads.' + ], + 'Reply Hiding Buttons': [ + true, + 'Add buttons to hide single replies.' + ], + 'Stubs': [ + true, + 'Show stubs of hidden threads / replies.' + ] + }, + + 'Images and Videos': { + 'Image Expansion': [ + true, + 'Expand images / videos.' + ], + 'Image Hover': [ + true, + 'Show full image / video on mouseover.' + ], + 'Image Hover in Catalog': [ + true, + `Show full image / video on mouseover in ${meta.name} catalog.` + ], + 'Gallery': [ + true, + 'Adds a simple and cute image gallery. Has more options in the gallery menu.' + ], + 'Fullscreen Gallery': [ + false, + 'Open gallery in fullscreen mode.', + 1 + ], + 'PDF in Gallery': [ + false, + 'Show PDF files in gallery.', + 1 + ], + 'Sauce': [ + true, + 'Add sauce links to images.' + ], + 'WEBM Metadata': [ + true, + 'Add link to fetch title metadata from webm videos.' + ], + 'Reveal Spoiler Thumbnails': [ + false, + 'Replace spoiler thumbnails with the original image.' + ], + 'Replace GIF': [ + false, + 'Replace gif thumbnails with the actual image.' + ], + 'Replace JPG': [ + false, + 'Replace jpg thumbnails with the actual image.' + ], + 'Replace PNG': [ + false, + 'Replace png thumbnails with the actual image.' + ], + 'Replace WEBM': [ + false, + 'Replace webm, mp4, and ogv thumbnails with the actual video. Probably will degrade browser performance ;)' + ], + 'Image Prefetching': [ + true, + 'Add a shortcut icon to the header to turn on image preloading.' + ], + 'Fappe Tyme': [ + true, + 'Hide posts without images when header menu item is checked. *hint* *hint*' + ], + 'Werk Tyme': [ + true, + 'Hide all post images when header menu item is checked.' + ], + 'Autoplay': [ + true, + 'Videos begin playing immediately when opened.' + ], + 'Restart when Opened': [ + false, + 'Restart GIFs and WebMs when you hover over or expand them.' + ], + 'Show Controls': [ + true, + 'Show controls on videos expanded inline.' + ], + 'Click Passthrough': [ + false, + 'Clicks on videos trigger your browser\'s default behavior. Videos can be contracted with button / dragging to the left.', + 1 + ], + 'Allow Sound': [ + true, + 'Open videos with the sound unmuted.' + ], + 'Mouse Wheel Volume': [ + true, + 'Adjust volume of videos with the mouse wheel over the thumbnail/filename/gallery.' + ], + 'Loop in New Tab': [ + true, + 'Loop videos opened in their own tabs.' + ], + 'Volume in New Tab': [ + true, + `Apply ${meta.name} mute and volume settings to videos opened in their own tabs.` + ], + 'Enable sound posts': [ + true, + 'Enable loading audio from [sound=] file names. This audio is fetched from third parties.' + ], + }, + + 'Menu': { + 'Menu': [ + true, + 'Add a drop-down menu to posts.' + ], + 'Report Link': [ + true, + 'Add a report link to the menu.', + 1 + ], + 'Copy Text Link': [ + true, + 'Add a link to copy the post\'s text.', + 1 + ], + 'Thread Hiding Link': [ + true, + 'Add a link to hide entire threads.', + 1 + ], + 'Reply Hiding Link': [ + true, + 'Add a link to hide single replies.', + 1 + ], + 'Delete Link': [ + true, + 'Add post and image deletion links to the menu.', + 1 + ], + 'Archive Link': [ + true, + 'Add an archive link to the menu.', + 1 + ], + 'Edit Link': [ + true, + 'Add a link to edit the image in Tegaki, /i/\'s painting program. Requires Quick Reply.', + 1 + ], + 'Download Link': [ + false, + 'Add a download with original filename link to the menu.', + 1 + ] + }, + + 'Monitoring': { + 'Thread Updater': [ + true, + 'Fetch and insert new replies. Has more options in the header menu and the "Advanced" tab.' + ], + 'Unread Count': [ + true, + 'Show the unread posts count in the tab title.' + ], + 'Quoted Title': [ + false, + 'Change the page title to reflect you\'ve been quoted.', + 1 + ], + 'Hide Unread Count at (0)': [ + false, + 'Hide the unread posts count in the tab title when it reaches 0.', + 1 + ], + 'Unread Favicon': [ + true, + 'Show a different favicon when there are unread posts.' + ], + 'Unread Line': [ + true, + 'Show a line to distinguish read posts from unread ones.' + ], + 'Remember Last Read Post': [ + true, + 'Remember how far you\'ve read after you close the thread.' + ], + 'Scroll to Last Read Post': [ + true, + 'Scroll back to the last read post when reopening a thread.', + 1 + ], + 'Unread Line in Index': [ + false, + 'Show a line between read and unread posts in threads in the index.', + 1 + ], + 'Remove Thread Excerpt': [ + false, + 'Replace the excerpt of the thread in the tab title with the board title.' + ], + 'Thread Stats': [ + true, + 'Display reply and image count.' + ], + 'IP Count in Stats': [ + true, + 'Display the unique IP count in the thread stats.', + 1 + ], + 'Page Count in Stats': [ + true, + 'Display the page count in the thread stats.', + 1 + ], + 'Updater and Stats in Header': [ + true, + 'Places the thread updater and thread stats in the header instead of floating them.' + ], + 'Thread Watcher': [ + true, + 'Bookmark threads. Has more options in the thread watcher menu.' + ], + 'Fixed Thread Watcher': [ + true, + 'Makes the thread watcher scroll with the page.', + 1 + ], + 'Persistent Thread Watcher': [ + false, + 'The thread watcher will be visible when the page is loaded.', + 1 + ], + 'Mark New IPs': [ + false, + 'Label each post from a new IP with the thread\'s current IP count.' + ], + 'Reply Pruning': [ + true, + 'Add option in header menu to hide old replies in long threads. Activated by default in stickies.' + ], + 'Prune All Threads': [ + false, + 'Activate Reply Pruning by default in all threads.', + 1 + ] + }, + + 'Posting and Captchas': { + 'Quick Reply': [ + true, + 'All-in-one form to reply, create threads, automate dumping and more.' + ], + 'Persistent QR': [ + false, + 'The Quick reply won\'t disappear after posting.', + 1 + ], + 'Auto Hide QR': [ + true, + 'Automatically hide the quick reply when posting.', + 2 + ], + 'Open Post in New Tab': [ + true, + 'Open new threads in a new tab, and open replies in a new tab if you\'re not already in the thread.', + 1 + ], + 'Remember QR Size': [ + false, + 'Remember the size of the Quick reply.', + 1 + ], + 'Remember Spoiler': [ + false, + 'Remember the spoiler state, instead of resetting after posting.', + 1 + ], + 'Randomize Filename': [ + false, + 'Set the filename to a random timestamp within the past year. Disabled on /f/.', + 1 + ], + 'Show New Thread Option in Threads': [ + true, + 'Show the option to post a new / different thread from inside a thread.', + 1 + ], + 'Show Upload Progress': [ + true, + 'Track progress of file uploads as percentage in submit button.', + 1 + ], + 'Cooldown': [ + true, + 'Indicate the remaining time before posting again.', + 1 + ], + 'Posting Success Notifications': [ + true, + 'Show notifications on successful post creation or file uploading.', + 1 + ], + 'Auto-load captcha': [ + false, + 'Automatically load the captcha in the QR even if your post is empty.', + 1 + ], + 'Post on Captcha Completion': [ + false, + 'Submit the post immediately when the captcha is completed.', + 1 + ], + 'Force Noscript Captcha': [ + false, + 'Use the non-Javascript fallback captcha even if Javascript is enabled.' + ], + 'Pass Link': [ + false, + 'Add a 4chan Pass login link to the bottom of the page.' + ] + }, + + 'Quote Links': { + 'Quote Backlinks': [ + true, + 'Add quote backlinks.' + ], + 'OP Backlinks': [ + true, + 'Add backlinks to the OP.', + 1 + ], + 'Bottom Backlinks': [ + false, + 'Place backlinks at the bottom of posts.', + 1 + ], + 'Quote Inlining': [ + true, + 'Inline quoted post on click.' + ], + 'Inline Cross-thread Quotes Only': [ + false, + 'Don\'t inline quote links when the posts are visible in the thread.', + 1 + ], + 'Quote Hash Navigation': [ + false, + 'Include an extra link after quotes for autoscrolling to quoted posts.', + 1 + ], + 'Forward Hiding': [ + true, + 'Hide original posts of inlined backlinks.', + 1 + ], + 'Quote Previewing': [ + true, + 'Show quoted post on hover.' + ], + 'Quote Highlighting': [ + true, + 'Highlight the previewed post.', + 1 + ], + 'Resurrect Quotes': [ + true, + 'Link dead quotes to the archives, and support inlining/previewing of archive links like quote links.' + ], + 'Remember Your Posts': [ + true, + 'Remember your posting history.' + ], + 'Mark Quotes of You': [ + true, + 'Add \'(You)\' to quotes linking to your posts.', + 1 + ], + 'Highlight Posts Quoting You': [ + true, + 'Highlights any posts that contain a quote to your post.', + 1 + ], + 'Highlight Own Posts': [ + true, + 'Highlights own posts.', + 1 + ], + 'Mark OP Quotes': [ + true, + 'Add \'(OP)\' to OP quotes.' + ], + 'Mark Cross-thread Quotes': [ + true, + 'Add \'(Cross-thread)\' to cross-threads quotes.' + ], + 'Quote Threading': [ + true, + 'Add option in header menu to thread conversations.' + ] + } + }, + + imageExpansion: { + 'Fit width': [ + true, + '' + ], + 'Fit height': [ + false, + '' + ], + 'Scroll into view': [ + true, + 'Scroll down when expanding images to bring the full image into view.' + ], + 'Expand spoilers': [ + true, + 'Expand all images along with spoilers.' + ], + 'Expand videos': [ + true, + 'Expand all images also expands videos.' + ], + 'Expand from here': [ + false, + 'Expand all images only from current position to thread end.' + ], + 'Expand thread only': [ + false, + 'In index, expand all images only within the current thread.' + ], + 'Advance on contract': [ + false, + 'Advance to next post when contracting an expanded image.' + ] + }, + + gallery: { + 'Hide Thumbnails': [ + false + ], + 'Fit Width': [ // 'Fit width' (lowercase W) belongs to Image Expansion. Engine limitations, heh. + true + ], + 'Fit Height': [ + true + ], + 'Stretch to Fit': [ + false + ], + 'Scroll to Post': [ + true + ], + 'Slide Delay': [ + 6.0 + ] + }, + + 'Default Volume': 1.0, + + threadWatcher: { + 'Current Board': [ + false, + 'Only show watched threads from the current board.' + ], + 'Auto Update Thread Watcher': [ + true, + 'Periodically check status of watched threads.' + ], + 'Auto Watch': [ + true, + 'Automatically watch threads you start.' + ], + 'Auto Watch Reply': [ + true, + 'Automatically watch threads you reply to.' + ], + 'Auto Prune': [ + false, + 'Automatically remove dead threads.' + ], + 'Show Page': [ + true, + 'Show what page watched threads are on.' + ], + 'Show Unread Count': [ + true, + 'Show number of unread posts in watched threads.' + ], + 'Show Site Prefix': [ + true, + 'When multiple sites are shown in the thread watcher, add a prefix to board names to distinguish them.' + ], + 'Require OP Quote Link': [ + false, + 'For purposes of thread watcher highlighting, only consider posts with a quote link to the OP as replies to the OP.' + ] + }, + + filter: { + general: '', + + postID: `\ +# Highlight dubs on [s4s]: +#/(\\d)\\1$/;highlight;top:no;boards:s4s\ +`, + + name: `\ +# Filter any namefags: +#/^(?!Anonymous$)/\ +`, + + uniqueID: `\ +# Filter a specific ID: +#/Txhvk1Tl/\ +`, + + tripcode: `\ +# Filter any tripfag +#/^!/\ +`, + + capcode: `\ +# Set a custom class for mods: +#/Mod$/;highlight:mod;op:yes +# Set a custom class for admins: +#/Admin$/;highlight:admin;op:yes\ +`, + + pass: `\ +# Filter anyone using since4pass: +#/./\ +`, + + email: '', + + subject: `\ +# Filter Generals on /v/: +#/general/i;boards:v;op:only\ +`, + + comment: `\ +# Filter Stallman copypasta on /g/: +#/what you\'re refer+ing to as linux/i;boards:g +# Filter posts with 20 or more quote links: +#/(?:>>\\d(?:(?!>>\\d)[^])*){20}/ +# Filter posts like T H I S / H / I / S: +#/^>?\\s?\\w\\s?(\\w)\\s?(\\w)\\s?(\\w).*$[\\s>]+\\1[\\s>]+\\2[\\s>]+\\3/im\ +`, + + flag: '', + filename: '', + dimensions: `\ +# Highlight potential wallpapers: +#/1920x1080/;op:yes;highlight;top:no;boards:w,wg\ +`, + + filesize: '', + + MD5: '' + }, + + sauces: `\ +# Known filename formats: +https://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\d+)_p\\d+/ +javascript:void(open("https://www.deviantart.com/"+%$1.replace(/_/g,"-")+"/art/"+parseInt(%$2,36)));regexp:/^\\w+_by_(\\w+)[_-]d([\\da-z]{6})\\b/ +https://imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\d{7})([\\da-zA-Z]{7})(?: \\(\\d+\\))?\\.\\w+$/ +https://flickr.com/photo.gne?id=%$1;regexp:/^(\\d+)_[\\da-f]{10}(?:_\\w)*\\b/ +https://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\d+_(\\d+)_\\d+_[no]\\b/ + +# Reverse image search: +https://www.google.com/searchbyimage?sbisrc=4chanx&image_url=%IMG&safe=off +https://yandex.com/images/search?rpt=imageview&url=%IMG +#//tineye.com/search?url=%IMG +#//www.bing.com/images/search?q=imgurl:%IMG&view=detailv2&iss=sbi#enterInsights +#https://lens.google.com/uploadbyurl?url=%IMG;text:lens + +# Specialized reverse image search: +//iqdb.org/?url=%IMG +https://trace.moe/?auto&url=%IMG;text:wait +#//3d.iqdb.org/?url=%IMG +#//saucenao.com/search.php?url=%IMG + +# "View Same" in archives: +http://eye.swfchan.com/search/?q=%name;types:swf +#https://desuarchive.org/_/search/image/%sMD5/ +#https://archive.4plebs.org/_/search/image/%sMD5/ +#https://boards.fireden.net/_/search/image/%sMD5/ +#https://foolz.fireden.net/_/search/image/%sMD5/ + +# Other tools: +#http://exif.regex.info/exif.cgi?imgurl=%URL +#//imgops.com/start?url=%URL;types:gif,jpg,png +#//www.gif-explode.com/%URL;types:gif\ +`, + + FappeT: { + werk: false + }, + + 'Custom CSS': true, + + Index: { + 'Index Mode': 'paged', + 'Previous Index Mode': 'paged', + 'Index Size': 'small', + 'Show Replies': [true, 'Show replies in the index, and also in the catalog if "Catalog hover expand" is checked.'], + 'Catalog Hover Expand': [false, 'Expand the comment and show more details when you hover over a thread in the catalog.'], + 'Catalog Hover Toggle': [true, 'Turn "Catalog hover expand" on and off by clicking in the catalog.'], + 'Pin Watched Threads': [false, 'Move watched threads to the start of the index.'], + 'Anchor Hidden Threads': [true, 'Move hidden threads to the end of the index.'], + 'Refreshed Navigation': [false, 'Refresh index when navigating through pages.'] + }, + + Header: { + 'Fixed Header': true, + 'Header auto-hide': false, + 'Header auto-hide on scroll': false, + 'Bottom Header': false, + 'Centered links': false, + 'Header catalog links': false, + 'Bottom Board List': true, + 'Shortcut Icons': true, + 'Custom Board Navigation': true + }, + + archives: { + archiveLists: 'https://4chenz.github.io/archives.json/archives.json', + lastarchivecheck: 0, + archiveAutoUpdate: true + }, + + externalCatalogURLs: `\ +//catalog.neet.tv/%board/;boards:4chan.org:3,a,adv,an,asp,biz,c,cgl,ck,cm,co,diy,f,fa,fit,g,gd,his,i,int,jp,k,lgbt,lit,m,mlp,mu,n,news,o,out,p,po,pol,s4s,sci,sp,tg,toy,trv,tv,v,vg,vip,vp,vr,w,wg,wsg,wsr,x\ +`, + + boardnav: `\ +[ toggle-all ] +[current-index-text:"Index" +current-catalog-text:"Catalog" +current-expired-text:"Expired" +current-archive-text:"Archive"] +[external-text:"FAQ","${meta.name}"]\ +`, + + QR: { + 'QR.personas': `\ +#options:"sage";boards:jp;always\ +`, + sjisPreview: false + }, + + jsWhitelist: `\ +http://s.4cdn.org +https://s.4cdn.org +http://www.google.com +https://www.google.com +https://www.gstatic.com +http://cdn.mathjax.org +https://cdn.mathjax.org +https://cdnjs.cloudflare.com +https://hcaptcha.com +https://*.hcaptcha.com +'self' +'unsafe-inline' +'unsafe-eval'\ +`, + + captchaLanguage: '', + + time: '%m/%d/%y(%a)%H:%M:%S', + timeLocale: '', + + backlink: '>>%id', + + pastedname: 'file', + + fileInfo: '%l %d (%p%s, %r%g)', + + favicon: 'ferongr', + + usercss: userCss, + + hotkeys: { + // QR & Options + 'Toggle board list': [ + 'Ctrl+b', + 'Toggle the full board list.' + ], + 'Toggle header': [ + 'Shift+h', + 'Toggle the auto-hide option of the header.' + ], + 'Open empty QR': [ + 'q', + 'Open QR without post number inserted.' + ], + 'Open QR': [ + 'Shift+q', + 'Open QR with post number inserted.' + ], + 'Open settings': [ + 'Alt+o', + 'Open Settings.' + ], + 'Close': [ + 'Esc', + 'Close dialogs or notifications.' + ], + 'Spoiler tags': [ + 'Ctrl+s', + 'Insert spoiler tags.' + ], + 'Code tags': [ + 'Alt+c', + 'Insert code tags.' + ], + 'Eqn tags': [ + 'Alt+e', + 'Insert eqn tags.' + ], + 'Math tags': [ + 'Alt+m', + 'Insert math tags.' + ], + 'SJIS tags': [ + 'Alt+a', + 'Insert SJIS tags.' + ], + 'Toggle sage': [ + 'Alt+s', + 'Toggle sage in options field.' + ], + 'Toggle Cooldown': [ + 'Alt+Comma', + 'Toggle custom cooldown timer.' + ], + 'Post from URL': [ + 'Alt+l', + 'Post from URL.' + ], + 'Add new post': [ + 'Alt+n', + 'Add new post to the QR dump list.' + ], + 'Submit QR': [ + 'Ctrl+Enter', + 'Submit post.' + ], + // Thread related + 'Watch': [ + 'w', + 'Watch thread.' + ], + 'Update': [ + 'r', + 'Update the thread / refresh the index.' + ], + 'Update thread watcher': [ + 'Shift+r', + 'Manually refresh thread watcher.' + ], + 'Toggle thread watcher': [ + 't', + 'Toggle visibility of thread watcher.' + ], + 'Toggle threading': [ + 'Shift+t', + 'Toggle threading.' + ], + 'Mark thread read': [ + 'Ctrl+0', + 'Mark thread read from index (requires "Unread Line in Index").' + ], + // Images + 'Expand image': [ + 'Shift+e', + 'Expand selected image.' + ], + 'Expand images': [ + 'e', + 'Expand all images.' + ], + 'Open Gallery': [ + 'g', + 'Opens the gallery.' + ], + 'Next Gallery Image': [ + 'Right', + 'Go to the next image in gallery mode.' + ], + 'Previous Gallery Image': [ + 'Left', + 'Go to the previous image in gallery mode.' + ], + 'Advance Gallery': [ + 'Enter', + 'Go to next image or, if Autoplay is off, play video.' + ], + 'Pause': [ + 'p', + 'Pause/play videos in the gallery.' + ], + 'Slideshow': [ + 'Ctrl+Right', + 'Toggle the gallery slideshow mode.' + ], + 'Rotate image clockwise': [ + 'Shift+Right', + 'Rotate image clockwise in gallery.' + ], + 'Rotate image anticlockwise': [ + 'Shift+Left', + 'Rotate image anticlockwise in gallery.' + ], + 'Download Gallery Image': [ + 'Shift+j', + 'Download current image in gallery.' + ], + 'fappeTyme': [ + 'f', + 'Toggle Fappe Tyme.' + ], + 'werkTyme': [ + 'Shift+w', + 'Toggle Werk Tyme.' + ], + // Board Navigation + 'Front page': [ + '1', + 'Jump to front page.' + ], + 'Open front page': [ + 'Shift+1', + 'Open front page in a new tab.' + ], + 'Next page': [ + 'Ctrl+Right', + 'Jump to the next page.' + ], + 'Previous page': [ + 'Ctrl+Left', + 'Jump to the previous page.' + ], + 'Paged mode': [ + 'Alt+1', + 'Open the index in paged mode.' + ], + 'Infinite scrolling mode': [ + 'Alt+2', + 'Open the index in infinite scrolling mode.' + ], + 'All pages mode': [ + 'Alt+3', + 'Open the index in all threads mode.' + ], + 'Open catalog': [ + 'Shift+c', + 'Open the catalog of the current board.' + ], + 'Search form': [ + 'Ctrl+Alt+s', + 'Focus the search field on the board index.' + ], + 'Cycle sort type': [ + 'Alt+x', + 'Cycle through index sort types.' + ], + // Thread Navigation + 'Next thread': [ + 'Ctrl+Down', + 'See next thread.' + ], + 'Previous thread': [ + 'Ctrl+Up', + 'See previous thread.' + ], + 'Expand thread': [ + 'Ctrl+e', + 'Expand thread.' + ], + 'Open thread': [ + 'o', + 'Open thread in current tab.' + ], + 'Open thread tab': [ + 'Shift+o', + 'Open thread in new tab.' + ], + // Reply Navigation + 'Next reply': [ + 'j', + 'Select next reply.' + ], + 'Previous reply': [ + 'k', + 'Select previous reply.' + ], + 'Deselect reply': [ + 'Shift+d', + 'Deselect reply.' + ], + 'Hide': [ + 'x', + 'Hide thread.' + ], + 'Quick Filter MD5': [ + '5', + 'Add the MD5 of the selected image to the filter list.' + ], + 'Previous Post Quoting You': [ + 'Alt+Up', + 'Scroll to the previous post that quotes you.' + ], + 'Next Post Quoting You': [ + 'Alt+Down', + 'Scroll to the next post that quotes you.' + ] + }, + + updater: { + checkbox: { + 'Beep': [ + false, + 'Beep on new post to completely read thread.' + ], + 'Beep Quoting You': [ + false, + 'Beep on new post quoting you.' + ], + 'Auto Scroll': [ + false, + 'Scroll updated posts into view. Only enabled at bottom of page.' + ], + 'Bottom Scroll': [ + false, + 'Always scroll to the bottom, not the first new post. Useful for event threads.' + ], + 'Scroll BG': [ + false, + 'Auto-scroll background tabs.' + ], + 'Auto Update': [ + true, + 'Automatically fetch new posts.' + ], + 'Optional Increase': [ + false, + 'Increase the intervals between updates on threads without new posts.' + ] + }, + 'Interval': 5 + }, + + customCooldown: 0, + customCooldownEnabled: true, + + 'Thread Quotes': false, + + 'Max Replies': 1000, + + 'Autohiding Scrollbar': false, + + position: { + 'embedding.position': 'top: 50px; right: 0px;', + 'thread-stats.position': 'bottom: 0px; right: 0px;', + 'updater.position': 'bottom: 0px; left: 0px;', + 'thread-watcher.position': 'top: 50px; left: 0px;', + 'qr.position': 'top: 50px; right: 0px;' + }, + + fourchanImageHost: 'i.4cdn.org', + + hiddenPSAList: [{}], + + knownBanners: banners.join(','), + + passMessageClosed: false, + + 'Prerequest Captcha': false, + + 'PSAseen': [[]] }; - var QuickReplyPage = `
- - × - -
-
-
- - - - - -
-
- - -
-
-
-
- + -
- -
- - - No selected file - - - ✎︎ - - 🔗︎ - - 🕒︎ - + - - -
- - -
- - - + var QuickReplyPage = `
+ + × + +
+
+
+ + + + + +
+
+ + +
+
+
+
+ + +
+ +
+ + + No selected file + + + ✎︎ + + 🔗︎ + + 🕒︎ + + + + +
+ + +
+ + + `; var ferongr_unreadDead = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///9zBQC/AADpDAP/gID/q6voCwJJTwpOAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC'; @@ -1752,268 +1752,268 @@ https://*.hcaptcha.com var empty = 'R0lGODlhEAAQAJEAAAAAAP///9vb2////yH5BAEAAAMALAAAAAAQABAAAAIvnI+pq+D9DBAUoFkPFnbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw=='; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - - var Favicon = { - init() { - return $$1.asap((() => d$1.head && (Favicon.el = $$1('link[rel="shortcut icon"]', d$1.head))), Favicon.initAsap); - }, - - set(status) { - Favicon.status = status; - if (Favicon.el) { - Favicon.el.href = Favicon[status]; - // `favicon.href = href` doesn't work on Firefox. - return $$1.add(d$1.head, Favicon.el); - } - }, - - initAsap() { - Favicon.el.type = 'image/x-icon'; - const {href} = Favicon.el; - Favicon.isSFW = /ws\.ico$/.test(href); - Favicon.default = href; - Favicon.switch(); - if (Favicon.status) { - return Favicon.set(Favicon.status); - } - }, - - switch() { - let items = { - ferongr: [ - ferongr_unreadDead, - ferongr_unreadDeadY, - ferongr_unreadSFW, - ferongr_unreadSFWY, - ferongr_unreadNSFW, - ferongr_unreadNSFWY, - ], - 'xat-': [ - xat_unreadDead, - xat_unreadDeadY, - xat_unreadSFW, - xat_unreadSFWY, - xat_unreadNSFW, - xat_unreadNSFWY, - ], - Mayhem: [ - Mayhem_unreadDead, - Mayhem_unreadDeadY, - Mayhem_unreadSFW, - Mayhem_unreadSFWY, - Mayhem_unreadNSFW, - Mayhem_unreadNSFWY, - ], - '4chanJS': [ - fourChanJS_unreadDead, - fourChanJS_unreadDeadY, - fourChanJS_unreadSFW, - fourChanJS_unreadSFWY, - fourChanJS_unreadNSFW, - fourChanJS_unreadNSFWY, - ], - Original: [ - Original_unreadDead, - Original_unreadDeadY, - Original_unreadSFW, - Original_unreadSFWY, - Original_unreadNSFW, - Original_unreadNSFWY, - ], - 'Metro': [ - Metro_unreadDead, - Metro_unreadDeadY, - Metro_unreadSFW, - Metro_unreadSFWY, - Metro_unreadNSFW, - Metro_unreadNSFWY, - ] - }; - items = $$1.getOwn(items, Conf['favicon']); - - const f = Favicon; - const t = 'data:image/png;base64,'; - let i = 0; - while (items[i]) { - items[i] = t + items[i++]; - } - - [f.unreadDead, f.unreadDeadY, f.unreadSFW, f.unreadSFWY, f.unreadNSFW, f.unreadNSFWY] = items; - return f.update(); - }, - - update() { - if (this.isSFW) { - this.unread = this.unreadSFW; - return this.unreadY = this.unreadSFWY; - } else { - this.unread = this.unreadNSFW; - return this.unreadY = this.unreadNSFWY; - } - }, - - SFW: '//s.4cdn.org/image/favicon-ws.ico', - NSFW: '//s.4cdn.org/image/favicon.ico', - dead: `data:image/gif;base64,${dead}`, - logo: `data:image/png;base64,${empty}`, + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + + var Favicon = { + init() { + return $$1.asap((() => d$1.head && (Favicon.el = $$1('link[rel="shortcut icon"]', d$1.head))), Favicon.initAsap); + }, + + set(status) { + Favicon.status = status; + if (Favicon.el) { + Favicon.el.href = Favicon[status]; + // `favicon.href = href` doesn't work on Firefox. + return $$1.add(d$1.head, Favicon.el); + } + }, + + initAsap() { + Favicon.el.type = 'image/x-icon'; + const {href} = Favicon.el; + Favicon.isSFW = /ws\.ico$/.test(href); + Favicon.default = href; + Favicon.switch(); + if (Favicon.status) { + return Favicon.set(Favicon.status); + } + }, + + switch() { + let items = { + ferongr: [ + ferongr_unreadDead, + ferongr_unreadDeadY, + ferongr_unreadSFW, + ferongr_unreadSFWY, + ferongr_unreadNSFW, + ferongr_unreadNSFWY, + ], + 'xat-': [ + xat_unreadDead, + xat_unreadDeadY, + xat_unreadSFW, + xat_unreadSFWY, + xat_unreadNSFW, + xat_unreadNSFWY, + ], + Mayhem: [ + Mayhem_unreadDead, + Mayhem_unreadDeadY, + Mayhem_unreadSFW, + Mayhem_unreadSFWY, + Mayhem_unreadNSFW, + Mayhem_unreadNSFWY, + ], + '4chanJS': [ + fourChanJS_unreadDead, + fourChanJS_unreadDeadY, + fourChanJS_unreadSFW, + fourChanJS_unreadSFWY, + fourChanJS_unreadNSFW, + fourChanJS_unreadNSFWY, + ], + Original: [ + Original_unreadDead, + Original_unreadDeadY, + Original_unreadSFW, + Original_unreadSFWY, + Original_unreadNSFW, + Original_unreadNSFWY, + ], + 'Metro': [ + Metro_unreadDead, + Metro_unreadDeadY, + Metro_unreadSFW, + Metro_unreadSFWY, + Metro_unreadNSFW, + Metro_unreadNSFWY, + ] + }; + items = $$1.getOwn(items, Conf['favicon']); + + const f = Favicon; + const t = 'data:image/png;base64,'; + let i = 0; + while (items[i]) { + items[i] = t + items[i++]; + } + + [f.unreadDead, f.unreadDeadY, f.unreadSFW, f.unreadSFWY, f.unreadNSFW, f.unreadNSFWY] = items; + return f.update(); + }, + + update() { + if (this.isSFW) { + this.unread = this.unreadSFW; + return this.unreadY = this.unreadSFWY; + } else { + this.unread = this.unreadNSFW; + return this.unreadY = this.unreadNSFWY; + } + }, + + SFW: '//s.4cdn.org/image/favicon-ws.ico', + NSFW: '//s.4cdn.org/image/favicon.ico', + dead: `data:image/gif;base64,${dead}`, + logo: `data:image/png;base64,${empty}`, }; const $$ = (selector, root = d$1.body) => Array.from(root.querySelectorAll(selector)); - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - const CaptchaReplace = { - init() { - if ((g.SITE.software !== 'yotsuba') || (d$1.cookie.indexOf('pass_enabled=1') >= 0)) { return; } - - if (Conf['Force Noscript Captcha'] && Main$1.jsEnabled) { - $$1.ready(Captcha.replace.noscript); - return; - } - - if (Conf['captchaLanguage'].trim()) { - if (['boards.4chan.org', 'boards.4channel.org'].includes(location.hostname)) { - return $$1.onExists(doc$1, '#captchaFormPart', node => $$1.onExists(node, 'iframe[src^="https://www.google.com/recaptcha/"]', Captcha.replace.iframe)); - } else { - return $$1.onExists(doc$1, 'iframe[src^="https://www.google.com/recaptcha/"]', Captcha.replace.iframe); - } - } - }, - - noscript() { - let noscript, original, toggle; - if (!((original = $$1('#g-recaptcha')) && (noscript = $$1('noscript', original.parentNode)))) { return; } - const span = $$1.el('span', - {id: 'captcha-forced-noscript'}); - $$1.replace(noscript, span); - $$1.rm(original); - const insert = function() { - span.innerHTML = noscript.textContent; - return Captcha.replace.iframe($$1('iframe[src^="https://www.google.com/recaptcha/"]', span)); - }; - if (toggle = $$1('#togglePostFormLink a, #form-link')) { - return $$1.on(toggle, 'click', insert); - } else { - return insert(); - } - }, - - iframe(iframe) { - let lang; - if (lang = Conf['captchaLanguage'].trim()) { - const src = /[?&]hl=/.test(iframe.src) ? - iframe.src.replace(/([?&]hl=)[^&]*/, '$1' + encodeURIComponent(lang)) - : - iframe.src + `&hl=${encodeURIComponent(lang)}`; - if (iframe.src !== src) { iframe.src = src; } - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + const CaptchaReplace = { + init() { + if ((g.SITE.software !== 'yotsuba') || (d$1.cookie.indexOf('pass_enabled=1') >= 0)) { return; } + + if (Conf['Force Noscript Captcha'] && Main$1.jsEnabled) { + $$1.ready(Captcha.replace.noscript); + return; + } + + if (Conf['captchaLanguage'].trim()) { + if (['boards.4chan.org', 'boards.4channel.org'].includes(location.hostname)) { + return $$1.onExists(doc$1, '#captchaFormPart', node => $$1.onExists(node, 'iframe[src^="https://www.google.com/recaptcha/"]', Captcha.replace.iframe)); + } else { + return $$1.onExists(doc$1, 'iframe[src^="https://www.google.com/recaptcha/"]', Captcha.replace.iframe); + } + } + }, + + noscript() { + let noscript, original, toggle; + if (!((original = $$1('#g-recaptcha')) && (noscript = $$1('noscript', original.parentNode)))) { return; } + const span = $$1.el('span', + {id: 'captcha-forced-noscript'}); + $$1.replace(noscript, span); + $$1.rm(original); + const insert = function() { + span.innerHTML = noscript.textContent; + return Captcha.replace.iframe($$1('iframe[src^="https://www.google.com/recaptcha/"]', span)); + }; + if (toggle = $$1('#togglePostFormLink a, #form-link')) { + return $$1.on(toggle, 'click', insert); + } else { + return insert(); + } + }, + + iframe(iframe) { + let lang; + if (lang = Conf['captchaLanguage'].trim()) { + const src = /[?&]hl=/.test(iframe.src) ? + iframe.src.replace(/([?&]hl=)[^&]*/, '$1' + encodeURIComponent(lang)) + : + iframe.src + `&hl=${encodeURIComponent(lang)}`; + if (iframe.src !== src) { iframe.src = src; } + } + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - const CaptchaT = { - init() { - if (d$1.cookie.indexOf('pass_enabled=1') >= 0) { return; } - if (!(this.isEnabled = !!$$1('#t-root') || !$$1.id('postForm'))) { return; } - - const root = $$1.el('div', {className: 'captcha-root'}); - this.nodes = {root}; - - $$1.addClass(QR.nodes.el, 'has-captcha', 'captcha-t'); - return $$1.after(QR.nodes.com.parentNode, root); - }, - - moreNeeded() { - }, - - getThread() { - let threadID; - const boardID = g.BOARD.ID; - if (QR.posts[0].thread === 'new') { - threadID = '0'; - } else { - threadID = '' + QR.posts[0].thread; - } - return {boardID, threadID}; - }, - - setup(focus) { - if (!this.isEnabled) { return; } - - if (!this.nodes.container) { - this.nodes.container = $$1.el('div', {className: 'captcha-container'}); - $$1.prepend(this.nodes.root, this.nodes.container); - CaptchaT.currentThread = CaptchaT.getThread(); - $$1.global(function() { - const el = document.querySelector('#qr .captcha-container'); - window.TCaptcha.init(el, this.boardID, +this.threadID); - return window.TCaptcha.setErrorCb(err => window.dispatchEvent(new CustomEvent('CreateNotification', {detail: { - type: 'warning', - content: '' + err - }}) - )); - } - , CaptchaT.currentThread); - } - - if (focus) { - return $$1('#t-resp').focus(); - } - }, - - destroy() { - if (!this.isEnabled || !this.nodes.container) { return; } - $$1.global(() => window.TCaptcha.destroy()); - $$1.rm(this.nodes.container); - return delete this.nodes.container; - }, - - updateThread() { - if (!this.isEnabled) { return; } - const {boardID, threadID} = (CaptchaT.currentThread || {}); - const newThread = CaptchaT.getThread(); - if ((newThread.boardID !== boardID) || (newThread.threadID !== threadID)) { - CaptchaT.destroy(); - return CaptchaT.setup(); - } - }, - - getOne() { - let el; - let response = {}; - if (this.nodes.container) { - for (var key of ['t-response', 't-challenge']) { - response[key] = $$1(`[name='${key}']`, this.nodes.container).value; - } - } - if (!response['t-response'] && !((el = $$1('#t-msg')) && /Verification not required/i.test(el.textContent))) { - response = null; - } - return response; - }, - - setUsed() { - if (!this.isEnabled) { return; } - if (this.nodes.container) { - return $$1.global(() => window.TCaptcha.clearChallenge()); - } - }, - - occupied() { - return !!this.nodes.container; - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + const CaptchaT = { + init() { + if (d$1.cookie.indexOf('pass_enabled=1') >= 0) { return; } + if (!(this.isEnabled = !!$$1('#t-root') || !$$1.id('postForm'))) { return; } + + const root = $$1.el('div', {className: 'captcha-root'}); + this.nodes = {root}; + + $$1.addClass(QR.nodes.el, 'has-captcha', 'captcha-t'); + return $$1.after(QR.nodes.com.parentNode, root); + }, + + moreNeeded() { + }, + + getThread() { + let threadID; + const boardID = g.BOARD.ID; + if (QR.posts[0].thread === 'new') { + threadID = '0'; + } else { + threadID = '' + QR.posts[0].thread; + } + return {boardID, threadID}; + }, + + setup(focus) { + if (!this.isEnabled) { return; } + + if (!this.nodes.container) { + this.nodes.container = $$1.el('div', {className: 'captcha-container'}); + $$1.prepend(this.nodes.root, this.nodes.container); + CaptchaT.currentThread = CaptchaT.getThread(); + $$1.global(function() { + const el = document.querySelector('#qr .captcha-container'); + window.TCaptcha.init(el, this.boardID, +this.threadID); + return window.TCaptcha.setErrorCb(err => window.dispatchEvent(new CustomEvent('CreateNotification', {detail: { + type: 'warning', + content: '' + err + }}) + )); + } + , CaptchaT.currentThread); + } + + if (focus) { + return $$1('#t-resp').focus(); + } + }, + + destroy() { + if (!this.isEnabled || !this.nodes.container) { return; } + $$1.global(() => window.TCaptcha.destroy()); + $$1.rm(this.nodes.container); + return delete this.nodes.container; + }, + + updateThread() { + if (!this.isEnabled) { return; } + const {boardID, threadID} = (CaptchaT.currentThread || {}); + const newThread = CaptchaT.getThread(); + if ((newThread.boardID !== boardID) || (newThread.threadID !== threadID)) { + CaptchaT.destroy(); + return CaptchaT.setup(); + } + }, + + getOne() { + let el; + let response = {}; + if (this.nodes.container) { + for (var key of ['t-response', 't-challenge']) { + response[key] = $$1(`[name='${key}']`, this.nodes.container).value; + } + } + if (!response['t-response'] && !((el = $$1('#t-msg')) && /Verification not required/i.test(el.textContent))) { + response = null; + } + return response; + }, + + setUsed() { + if (!this.isEnabled) { return; } + if (this.nodes.container) { + return $$1.global(() => window.TCaptcha.clearChallenge()); + } + }, + + occupied() { + return !!this.nodes.container; + } }; // This file was created because these functions on $ were sometimes not initialized yet because of circular @@ -2067,242 +2067,242 @@ https://*.hcaptcha.com const DAY = HOUR * 24; const platform = window.GM_xmlhttpRequest ? 'userscript' : 'crx'; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS206: Consider reworking classes to avoid initClass - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - class DataBoard { - static initClass() { - this.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles']; - - this.changes = []; - } - - constructor(key, sync, dontClean) { - this.onSync = this.onSync.bind(this); - this.key = key; - this.initData(Conf[this.key]); - $$1.sync(this.key, this.onSync); - if (!dontClean) { this.clean(); } - if (!sync) { return; } - // Chrome also fires the onChanged callback on the current tab, - // so we only start syncing when we're ready. - var init = () => { - $$1.off(d$1, '4chanXInitFinished', init); - return this.sync = sync; - }; - $$1.on(d$1, '4chanXInitFinished', init); - } - - initData(data) { - let boards; - this.data = data; - if (this.data.boards) { - let lastChecked; - ({boards, lastChecked} = this.data); - this.data['4chan.org'] = {boards, lastChecked}; - delete this.data.boards; - delete this.data.lastChecked; - } - return this.data[g.SITE.ID] || (this.data[g.SITE.ID] = { boards: dict() }); - } - - save(change, cb) { - change(); - DataBoard.changes.push(change); - return $$1.get(this.key, { boards: dict() }, items => { - if (!DataBoard.changes.length) { return; } - const needSync = ((items[this.key].version || 0) > (this.data.version || 0)); - if (needSync) { - this.initData(items[this.key]); - for (change of DataBoard.changes) { change(); } - } - DataBoard.changes = []; - this.data.version = (this.data.version || 0) + 1; - return $$1.set(this.key, this.data, () => { - if (needSync) { this.sync?.(); } - return cb?.(); - }); - }); - } - - forceSync(cb) { - return $$1.get(this.key, { boards: dict() }, items => { - if ((items[this.key].version || 0) > (this.data.version || 0)) { - this.initData(items[this.key]); - for (var change of DataBoard.changes) { change(); } - this.sync?.(); - } - return cb?.(); - }); - } - - delete({siteID, boardID, threadID, postID}, cb) { - if (!siteID) { siteID = g.SITE.ID; } - if (!this.data[siteID]) { return; } - return this.save(() => { - if (postID) { - if (!this.data[siteID].boards[boardID]?.[threadID]) { return; } - delete this.data[siteID].boards[boardID][threadID][postID]; - return this.deleteIfEmpty({siteID, boardID, threadID}); - } else if (threadID) { - if (!this.data[siteID].boards[boardID]) { return; } - delete this.data[siteID].boards[boardID][threadID]; - return this.deleteIfEmpty({siteID, boardID}); - } else { - return delete this.data[siteID].boards[boardID]; - } - } - , cb); - } - - deleteIfEmpty({siteID, boardID, threadID}) { - if (!this.data[siteID]) { return; } - if (threadID) { - if (!Object.keys(this.data[siteID].boards[boardID][threadID]).length) { - delete this.data[siteID].boards[boardID][threadID]; - return this.deleteIfEmpty({siteID, boardID}); - } - } else if (!Object.keys(this.data[siteID].boards[boardID]).length) { - return delete this.data[siteID].boards[boardID]; - } - } - - set(data, cb) { - return this.save(() => { - return this.setUnsafe(data); - } - , cb); - } - - setUnsafe({siteID, boardID, threadID, postID, val}) { - if (!siteID) { siteID = g.SITE.ID; } - if (!this.data[siteID]) { this.data[siteID] = { boards: dict() }; } - if (postID !== undefined) { - let base; - return (((base = this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict())))[threadID] || (base[threadID] = dict()))[postID] = val; - } else if (threadID !== undefined) { - return (this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict()))[threadID] = val; - } else { - return this.data[siteID].boards[boardID] = val; - } - } - - extend({siteID, boardID, threadID, postID, val}, cb) { - return this.save(() => { - const oldVal = this.get({ siteID, boardID, threadID, postID, defaultValue: dict() }); - for (var key in val) { - var subVal = val[key]; - if (typeof subVal === 'undefined') { - delete oldVal[key]; - } else { - oldVal[key] = subVal; - } - } - return this.setUnsafe({siteID, boardID, threadID, postID, val: oldVal}); - } - , cb); - } - - setLastChecked(key='lastChecked') { - return this.save(() => { - return this.data[key] = Date.now(); - }); - } - - get({siteID, boardID, threadID, postID, defaultValue}) { - let board, val; - if (!siteID) { siteID = g.SITE.ID; } - if (board = this.data[siteID]?.boards[boardID]) { - let thread; - if (threadID == null) { - if (postID != null) { - for (thread = 0; thread < board.length; thread++) { - board[thread]; - if (postID in thread) { - val = thread[postID]; - break; - } - } - } else { - val = board; - } - } else if (thread = board[threadID]) { - val = (postID != null) ? - thread[postID] - : - thread; - } - } - return val || defaultValue; - } - - clean() { - let boardID, middle; - const siteID = g.SITE.ID; - for (boardID in this.data[siteID].boards) { - this.data[siteID].boards[boardID]; - this.deleteIfEmpty({siteID, boardID}); - } - const now = Date.now(); - if (now - (2 * HOUR) >= ((middle = this.data[siteID].lastChecked || 0)) || middle > now) { - this.data[siteID].lastChecked = now; - for (boardID in this.data[siteID].boards) { - this.ajaxClean(boardID); - } - } - } - - ajaxClean(boardID) { - const that = this; - const siteID = g.SITE.ID; - const threadsList = g.SITE.urls.threadsListJSON?.({siteID, boardID}); - if (!threadsList) { return; } - return $$1.cache(threadsList, function() { - if (this.status !== 200) { return; } - const archiveList = g.SITE.urls.archiveListJSON?.({siteID, boardID}); - if (!archiveList) { return that.ajaxCleanParse(boardID, this.response); } - const response1 = this.response; - return $$1.cache(archiveList, function() { - if ((this.status !== 200) && (!!g.SITE.archivedBoardsKnown || (this.status !== 404))) { return; } - return that.ajaxCleanParse(boardID, response1, this.response); - }); - }); - } - - ajaxCleanParse(boardID, response1, response2) { - let board, ID; - const siteID = g.SITE.ID; - if (!(board = this.data[siteID].boards[boardID])) { return; } - const threads = dict(); - if (response1) { - for (var page of response1) { - for (var thread of page.threads) { - ID = thread.no; - if (ID in board) { threads[ID] = board[ID]; } - } - } - } - if (response2) { - for (ID of response2) { - if (ID in board) { threads[ID] = board[ID]; } - } - } - this.data[siteID].boards[boardID] = threads; - this.deleteIfEmpty({siteID, boardID}); - return $$1.set(this.key, this.data); - } - - onSync(data) { - if ((data.version || 0) <= (this.data.version || 0)) { return; } - this.initData(data); - return this.sync?.(); - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + class DataBoard { + static initClass() { + this.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles']; + + this.changes = []; + } + + constructor(key, sync, dontClean) { + this.onSync = this.onSync.bind(this); + this.key = key; + this.initData(Conf[this.key]); + $$1.sync(this.key, this.onSync); + if (!dontClean) { this.clean(); } + if (!sync) { return; } + // Chrome also fires the onChanged callback on the current tab, + // so we only start syncing when we're ready. + var init = () => { + $$1.off(d$1, '4chanXInitFinished', init); + return this.sync = sync; + }; + $$1.on(d$1, '4chanXInitFinished', init); + } + + initData(data) { + let boards; + this.data = data; + if (this.data.boards) { + let lastChecked; + ({boards, lastChecked} = this.data); + this.data['4chan.org'] = {boards, lastChecked}; + delete this.data.boards; + delete this.data.lastChecked; + } + return this.data[g.SITE.ID] || (this.data[g.SITE.ID] = { boards: dict() }); + } + + save(change, cb) { + change(); + DataBoard.changes.push(change); + return $$1.get(this.key, { boards: dict() }, items => { + if (!DataBoard.changes.length) { return; } + const needSync = ((items[this.key].version || 0) > (this.data.version || 0)); + if (needSync) { + this.initData(items[this.key]); + for (change of DataBoard.changes) { change(); } + } + DataBoard.changes = []; + this.data.version = (this.data.version || 0) + 1; + return $$1.set(this.key, this.data, () => { + if (needSync) { this.sync?.(); } + return cb?.(); + }); + }); + } + + forceSync(cb) { + return $$1.get(this.key, { boards: dict() }, items => { + if ((items[this.key].version || 0) > (this.data.version || 0)) { + this.initData(items[this.key]); + for (var change of DataBoard.changes) { change(); } + this.sync?.(); + } + return cb?.(); + }); + } + + delete({siteID, boardID, threadID, postID}, cb) { + if (!siteID) { siteID = g.SITE.ID; } + if (!this.data[siteID]) { return; } + return this.save(() => { + if (postID) { + if (!this.data[siteID].boards[boardID]?.[threadID]) { return; } + delete this.data[siteID].boards[boardID][threadID][postID]; + return this.deleteIfEmpty({siteID, boardID, threadID}); + } else if (threadID) { + if (!this.data[siteID].boards[boardID]) { return; } + delete this.data[siteID].boards[boardID][threadID]; + return this.deleteIfEmpty({siteID, boardID}); + } else { + return delete this.data[siteID].boards[boardID]; + } + } + , cb); + } + + deleteIfEmpty({siteID, boardID, threadID}) { + if (!this.data[siteID]) { return; } + if (threadID) { + if (!Object.keys(this.data[siteID].boards[boardID][threadID]).length) { + delete this.data[siteID].boards[boardID][threadID]; + return this.deleteIfEmpty({siteID, boardID}); + } + } else if (!Object.keys(this.data[siteID].boards[boardID]).length) { + return delete this.data[siteID].boards[boardID]; + } + } + + set(data, cb) { + return this.save(() => { + return this.setUnsafe(data); + } + , cb); + } + + setUnsafe({siteID, boardID, threadID, postID, val}) { + if (!siteID) { siteID = g.SITE.ID; } + if (!this.data[siteID]) { this.data[siteID] = { boards: dict() }; } + if (postID !== undefined) { + let base; + return (((base = this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict())))[threadID] || (base[threadID] = dict()))[postID] = val; + } else if (threadID !== undefined) { + return (this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict()))[threadID] = val; + } else { + return this.data[siteID].boards[boardID] = val; + } + } + + extend({siteID, boardID, threadID, postID, val}, cb) { + return this.save(() => { + const oldVal = this.get({ siteID, boardID, threadID, postID, defaultValue: dict() }); + for (var key in val) { + var subVal = val[key]; + if (typeof subVal === 'undefined') { + delete oldVal[key]; + } else { + oldVal[key] = subVal; + } + } + return this.setUnsafe({siteID, boardID, threadID, postID, val: oldVal}); + } + , cb); + } + + setLastChecked(key='lastChecked') { + return this.save(() => { + return this.data[key] = Date.now(); + }); + } + + get({siteID, boardID, threadID, postID, defaultValue}) { + let board, val; + if (!siteID) { siteID = g.SITE.ID; } + if (board = this.data[siteID]?.boards[boardID]) { + let thread; + if (threadID == null) { + if (postID != null) { + for (thread = 0; thread < board.length; thread++) { + board[thread]; + if (postID in thread) { + val = thread[postID]; + break; + } + } + } else { + val = board; + } + } else if (thread = board[threadID]) { + val = (postID != null) ? + thread[postID] + : + thread; + } + } + return val || defaultValue; + } + + clean() { + let boardID, middle; + const siteID = g.SITE.ID; + for (boardID in this.data[siteID].boards) { + this.data[siteID].boards[boardID]; + this.deleteIfEmpty({siteID, boardID}); + } + const now = Date.now(); + if (now - (2 * HOUR) >= ((middle = this.data[siteID].lastChecked || 0)) || middle > now) { + this.data[siteID].lastChecked = now; + for (boardID in this.data[siteID].boards) { + this.ajaxClean(boardID); + } + } + } + + ajaxClean(boardID) { + const that = this; + const siteID = g.SITE.ID; + const threadsList = g.SITE.urls.threadsListJSON?.({siteID, boardID}); + if (!threadsList) { return; } + return $$1.cache(threadsList, function() { + if (this.status !== 200) { return; } + const archiveList = g.SITE.urls.archiveListJSON?.({siteID, boardID}); + if (!archiveList) { return that.ajaxCleanParse(boardID, this.response); } + const response1 = this.response; + return $$1.cache(archiveList, function() { + if ((this.status !== 200) && (!!g.SITE.archivedBoardsKnown || (this.status !== 404))) { return; } + return that.ajaxCleanParse(boardID, response1, this.response); + }); + }); + } + + ajaxCleanParse(boardID, response1, response2) { + let board, ID; + const siteID = g.SITE.ID; + if (!(board = this.data[siteID].boards[boardID])) { return; } + const threads = dict(); + if (response1) { + for (var page of response1) { + for (var thread of page.threads) { + ID = thread.no; + if (ID in board) { threads[ID] = board[ID]; } + } + } + } + if (response2) { + for (ID of response2) { + if (ID in board) { threads[ID] = board[ID]; } + } + } + this.data[siteID].boards[boardID] = threads; + this.deleteIfEmpty({siteID, boardID}); + return $$1.set(this.key, this.data); + } + + onSync(data) { + if ((data.version || 0) <= (this.data.version || 0)) { return; } + this.initData(data); + return this.sync?.(); + } + } DataBoard.initClass(); /* @@ -2344,1042 +2344,1042 @@ https://*.hcaptcha.com } } - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - class Thread { - toString() { return this.ID; } - - constructor(ID, board) { - this.board = board; - this.ID = +ID; - this.threadID = this.ID; - this.boardID = this.board.ID; - this.siteID = g.SITE.ID; - this.fullID = `${this.board}.${this.ID}`; - this.posts = new SimpleDict(); - this.isDead = false; - this.isHidden = false; - this.isSticky = false; - this.isClosed = false; - this.isArchived = false; - this.postLimit = false; - this.fileLimit = false; - this.lastPost = 0; - this.ipCount = undefined; - this.json = null; - - this.OP = null; - this.catalogView = null; - - this.nodes = - {root: null}; - - this.board.threads.push(this.ID, this); - g.threads.push(this.fullID, this); - } - - setPage(pageNum) { - let icon; - const {info, reply} = this.OP.nodes; - if (!(icon = $$1('.page-num', info))) { - icon = $$1.el('span', {className: 'page-num'}); - $$1.replace(reply.parentNode.previousSibling, [$$1.tn(' '), icon, $$1.tn(' ')]); - } - icon.title = `This thread is on page ${pageNum} in the original index.`; - icon.textContent = `[${pageNum}]`; - if (this.catalogView) { return this.catalogView.nodes.pageCount.textContent = pageNum; } - } - - setCount(type, count, reachedLimit) { - if (!this.catalogView) { return; } - const el = this.catalogView.nodes[`${type}Count`]; - el.textContent = count; - return (reachedLimit ? $$1.addClass : $$1.rmClass)(el, 'warning'); - } - - setStatus(type, status) { - const name = `is${type}`; - if (this[name] === status) { return; } - this[name] = status; - if (!this.OP) { return; } - this.setIcon('Sticky', this.isSticky); - this.setIcon('Closed', this.isClosed && !this.isArchived); - return this.setIcon('Archived', this.isArchived); - } - - setIcon(type, status) { - const typeLC = type.toLowerCase(); - let icon = $$1(`.${typeLC}Icon`, this.OP.nodes.info); - if (!!icon === status) { return; } - - if (!status) { - $$1.rm(icon.previousSibling); - $$1.rm(icon); - if (this.catalogView) { $$1.rm($$1(`.${typeLC}Icon`, this.catalogView.nodes.icons)); } - return; - } - icon = $$1.el('img', { - src: `${g.SITE.Build.staticPath}${typeLC}${g.SITE.Build.gifIcon}`, - alt: type, - title: type, - className: `${typeLC}Icon retina` - } - ); - if (g.BOARD.ID === 'f') { - icon.style.cssText = 'height: 18px; width: 18px;'; - } - - const root = (type !== 'Sticky') && this.isSticky ? - $$1('.stickyIcon', this.OP.nodes.info) - : - $$1('.page-num', this.OP.nodes.info) || this.OP.nodes.quote; - $$1.after(root, [$$1.tn(' '), icon]); - - if (!this.catalogView) { return; } - return ((type === 'Sticky') && this.isClosed ? $$1.prepend : $$1.add)(this.catalogView.nodes.icons, icon.cloneNode()); - } - - kill() { - return this.isDead = true; - } - - collect() { - let n = 0; - this.posts.forEach(function(post) { - if (post.clones.length) { - return n++; - } else { - return post.collect(); - } - }); - if (!n) { - g.threads.rm(this.fullID); - return this.board.threads.rm(this); - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + class Thread { + toString() { return this.ID; } + + constructor(ID, board) { + this.board = board; + this.ID = +ID; + this.threadID = this.ID; + this.boardID = this.board.ID; + this.siteID = g.SITE.ID; + this.fullID = `${this.board}.${this.ID}`; + this.posts = new SimpleDict(); + this.isDead = false; + this.isHidden = false; + this.isSticky = false; + this.isClosed = false; + this.isArchived = false; + this.postLimit = false; + this.fileLimit = false; + this.lastPost = 0; + this.ipCount = undefined; + this.json = null; + + this.OP = null; + this.catalogView = null; + + this.nodes = + {root: null}; + + this.board.threads.push(this.ID, this); + g.threads.push(this.fullID, this); + } + + setPage(pageNum) { + let icon; + const {info, reply} = this.OP.nodes; + if (!(icon = $$1('.page-num', info))) { + icon = $$1.el('span', {className: 'page-num'}); + $$1.replace(reply.parentNode.previousSibling, [$$1.tn(' '), icon, $$1.tn(' ')]); + } + icon.title = `This thread is on page ${pageNum} in the original index.`; + icon.textContent = `[${pageNum}]`; + if (this.catalogView) { return this.catalogView.nodes.pageCount.textContent = pageNum; } + } + + setCount(type, count, reachedLimit) { + if (!this.catalogView) { return; } + const el = this.catalogView.nodes[`${type}Count`]; + el.textContent = count; + return (reachedLimit ? $$1.addClass : $$1.rmClass)(el, 'warning'); + } + + setStatus(type, status) { + const name = `is${type}`; + if (this[name] === status) { return; } + this[name] = status; + if (!this.OP) { return; } + this.setIcon('Sticky', this.isSticky); + this.setIcon('Closed', this.isClosed && !this.isArchived); + return this.setIcon('Archived', this.isArchived); + } + + setIcon(type, status) { + const typeLC = type.toLowerCase(); + let icon = $$1(`.${typeLC}Icon`, this.OP.nodes.info); + if (!!icon === status) { return; } + + if (!status) { + $$1.rm(icon.previousSibling); + $$1.rm(icon); + if (this.catalogView) { $$1.rm($$1(`.${typeLC}Icon`, this.catalogView.nodes.icons)); } + return; + } + icon = $$1.el('img', { + src: `${g.SITE.Build.staticPath}${typeLC}${g.SITE.Build.gifIcon}`, + alt: type, + title: type, + className: `${typeLC}Icon retina` + } + ); + if (g.BOARD.ID === 'f') { + icon.style.cssText = 'height: 18px; width: 18px;'; + } + + const root = (type !== 'Sticky') && this.isSticky ? + $$1('.stickyIcon', this.OP.nodes.info) + : + $$1('.page-num', this.OP.nodes.info) || this.OP.nodes.quote; + $$1.after(root, [$$1.tn(' '), icon]); + + if (!this.catalogView) { return; } + return ((type === 'Sticky') && this.isClosed ? $$1.prepend : $$1.add)(this.catalogView.nodes.icons, icon.cloneNode()); + } + + kill() { + return this.isDead = true; + } + + collect() { + let n = 0; + this.posts.forEach(function(post) { + if (post.clones.length) { + return n++; + } else { + return post.collect(); + } + }); + if (!n) { + g.threads.rm(this.fullID); + return this.board.threads.rm(this); + } + } } - class CatalogThread { - toString() { return this.ID; } - - constructor(root, thread) { - this.thread = thread; - this.ID = this.thread.ID; - this.board = this.thread.board; - const {post} = this.thread.OP.nodes; - this.nodes = { - root, - thumb: $$1('.catalog-thumb', post), - icons: $$1('.catalog-icons', post), - postCount: $$1('.post-count', post), - fileCount: $$1('.file-count', post), - pageCount: $$1('.page-count', post), - replies: null - }; - this.thread.catalogView = this; - } + class CatalogThread { + toString() { return this.ID; } + + constructor(root, thread) { + this.thread = thread; + this.ID = this.thread.ID; + this.board = this.thread.board; + const {post} = this.thread.OP.nodes; + this.nodes = { + root, + thumb: $$1('.catalog-thumb', post), + icons: $$1('.catalog-icons', post), + postCount: $$1('.post-count', post), + fileCount: $$1('.file-count', post), + pageCount: $$1('.page-count', post), + replies: null + }; + this.thread.catalogView = this; + } } - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS206: Consider reworking classes to avoid initClass - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - const dialog = function(id, properties) { - const el = $$1.el('div', { - className: 'dialog', - id - } - ); - $$1.extend(el, properties); - el.style.cssText = Conf[`${id}.position`]; - - const move = $$1('.move', el); - $$1.on(move, 'touchstart mousedown', dragstart); - for (var child of move.children) { - if (!child.tagName) { continue; } - $$1.on(child, 'touchstart mousedown', e => e.stopPropagation()); - } - - return el; + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + const dialog = function(id, properties) { + const el = $$1.el('div', { + className: 'dialog', + id + } + ); + $$1.extend(el, properties); + el.style.cssText = Conf[`${id}.position`]; + + const move = $$1('.move', el); + $$1.on(move, 'touchstart mousedown', dragstart); + for (var child of move.children) { + if (!child.tagName) { continue; } + $$1.on(child, 'touchstart mousedown', e => e.stopPropagation()); + } + + return el; + }; + + var Menu$1 = (function() { + let currentMenu = undefined; + let lastToggledButton = undefined; + Menu$1 = class Menu { + static initClass() { + currentMenu = null; + lastToggledButton = null; + } + + constructor(type) { + // XXX AddMenuEntry event is deprecated + this.setPosition = this.setPosition.bind(this); + this.close = this.close.bind(this); + this.keybinds = this.keybinds.bind(this); + this.onFocus = this.onFocus.bind(this); + this.addEntry = this.addEntry.bind(this); + this.type = type; + $$1.on(d$1, 'AddMenuEntry', ({detail}) => { + if (detail.type !== this.type) { return; } + delete detail.open; + return this.addEntry(detail); + }); + this.entries = []; + } + + makeMenu() { + const menu = $$1.el('div', { + className: 'dialog', + id: 'menu', + tabIndex: 0 + } + ); + menu.dataset.type = this.type; + $$1.on(menu, 'click', e => e.stopPropagation()); + $$1.on(menu, 'keydown', this.keybinds); + return menu; + } + + toggle(e, button, data) { + e.preventDefault(); + e.stopPropagation(); + + if (currentMenu) { + // Close if it's already opened. + // Reopen if we clicked on another button. + const previousButton = lastToggledButton; + currentMenu.close(); + if (previousButton === button) { return; } + } + + if (!this.entries.length) { return; } + return this.open(button, data); + } + + open(button, data) { + let entry; + const menu = (this.menu = this.makeMenu()); + currentMenu = this; + lastToggledButton = button; + + this.entries.sort((first, second) => first.order - second.order); + + for (entry of this.entries) { + this.insertEntry(entry, menu, data); + } + + $$1.addClass(lastToggledButton, 'active'); + + $$1.on(d$1, 'click CloseMenu', this.close); + $$1.on(d$1, 'scroll', this.setPosition); + $$1.on(window, 'resize', this.setPosition); + $$1.after(button, menu); + + this.setPosition(); + + entry = $$1('.entry', menu); + // We've removed flexbox, so we don't use order anymore. + // while prevEntry = @findNextEntry entry, -1 + // entry = prevEntry + this.focus(entry); + + return menu.focus(); + } + + setPosition() { + const mRect = this.menu.getBoundingClientRect(); + const bRect = lastToggledButton.getBoundingClientRect(); + window.scrollY + bRect.top; + window.scrollX + bRect.left; + const cHeight = doc$1.clientHeight; + const cWidth = doc$1.clientWidth; + const [top, bottom] = (bRect.top + bRect.height + mRect.height) < cHeight ? + [`${bRect.bottom}px`, ''] + : + ['', `${cHeight - bRect.top}px`]; + const [left, right] = (bRect.left + mRect.width) < cWidth ? + [`${bRect.left}px`, ''] + : + ['', `${cWidth - bRect.right}px`]; + $$1.extend(this.menu.style, {top, right, bottom, left}); + return this.menu.classList.toggle('left', right); + } + + insertEntry(entry, parent, data) { + let submenu; + if (typeof entry.open === 'function') { + try { + if (!entry.open(data)) { return; } + } catch (err) { + Main$1.handleErrors({ + message: `Error in building the ${this.type} menu.`, + error: err + }); + return; + } + } + $$1.add(parent, entry.el); + + if (!entry.subEntries) { return; } + if (submenu = $$1('.submenu', entry.el)) { + // Reset sub menu, remove irrelevant entries. + $$1.rm(submenu); + } + submenu = $$1.el('div', + {className: 'dialog submenu'}); + for (var subEntry of entry.subEntries) { + this.insertEntry(subEntry, submenu, data); + } + $$1.add(entry.el, submenu); + } + + close() { + $$1.rm(this.menu); + delete this.menu; + $$1.rmClass(lastToggledButton, 'active'); + currentMenu = null; + lastToggledButton = null; + $$1.off(d$1, 'click scroll CloseMenu', this.close); + $$1.off(d$1, 'scroll', this.setPosition); + return $$1.off(window, 'resize', this.setPosition); + } + + findNextEntry(entry, direction) { + const entries = [...entry.parentNode.children]; + entries.sort((first, second) => first.style.order - second.style.order); + return entries[entries.indexOf(entry) + direction]; + } + + keybinds(e) { + let subEntry; + let next, submenu; + let entry = $$1('.focused', this.menu); + while ((subEntry = $$1('.focused', entry))) { + entry = subEntry; + } + + switch (e.keyCode) { + case 27: // Esc + lastToggledButton.focus(); + this.close(); + break; + case 13: case 32: // Enter, Space + entry.click(); + break; + case 38: // Up + if (next = this.findNextEntry(entry, -1)) { + this.focus(next); + } + break; + case 40: // Down + if (next = this.findNextEntry(entry, +1)) { + this.focus(next); + } + break; + case 39: // Right + if ((submenu = $$1('.submenu', entry)) && (next = submenu.firstElementChild)) { + let nextPrev; + while ((nextPrev = this.findNextEntry(next, -1))) { + next = nextPrev; + } + this.focus(next); + } + break; + case 37: // Left + if (next = $$1.x('parent::*[contains(@class,"submenu")]/parent::*', entry)) { + this.focus(next); + } + break; + default: + return; + } + + e.preventDefault(); + return e.stopPropagation(); + } + + onFocus(e) { + e.stopPropagation(); + return this.focus(e.target); + } + + focus(entry) { + let focused, submenu; + while ((focused = $$1.x('parent::*/child::*[contains(@class,"focused")]', entry))) { + $$1.rmClass(focused, 'focused'); + } + for (focused of $$('.focused', entry)) { + $$1.rmClass(focused, 'focused'); + } + $$1.addClass(entry, 'focused'); + + // Submenu positioning. + if (!(submenu = $$1('.submenu', entry))) { return; } + const sRect = submenu.getBoundingClientRect(); + const eRect = entry.getBoundingClientRect(); + const cHeight = doc$1.clientHeight; + const cWidth = doc$1.clientWidth; + const [top, bottom] = (eRect.top + sRect.height) < cHeight ? + ['0px', 'auto'] + : + ['auto', '0px']; + const [left, right] = (eRect.right + sRect.width) < (cWidth - 150) ? + ['100%', 'auto'] + : + ['auto', '100%']; + const {style} = submenu; + style.top = top; + style.bottom = bottom; + style.left = left; + return style.right = right; + } + + addEntry(entry) { + this.parseEntry(entry); + return this.entries.push(entry); + } + + parseEntry(entry) { + const {el, subEntries} = entry; + $$1.addClass(el, 'entry'); + $$1.on(el, 'focus mouseover', this.onFocus); + el.style.order = entry.order || 100; + if (!subEntries) { return; } + $$1.addClass(el, 'has-submenu'); + for (var subEntry of subEntries) { + this.parseEntry(subEntry); + } + } + }; + Menu$1.initClass(); + return Menu$1; + })(); + + var dragstart = function (e) { + let isTouching; + if ((e.type === 'mousedown') && (e.button !== 0)) { return; } // not LMB + // prevent text selection + e.preventDefault(); + if (isTouching = e.type === 'touchstart') { + e = e.changedTouches[e.changedTouches.length - 1]; + } + // distance from pointer to el edge is constant; calculate it here. + const el = $$1.x('ancestor::div[contains(@class,"dialog")][1]', this); + const rect = el.getBoundingClientRect(); + const screenHeight = doc$1.clientHeight; + const screenWidth = doc$1.clientWidth; + const o = { + id: el.id, + style: el.style, + dx: e.clientX - rect.left, + dy: e.clientY - rect.top, + height: screenHeight - rect.height, + width: screenWidth - rect.width, + screenHeight, + screenWidth, + isTouching + }; + + [o.topBorder, o.bottomBorder] = Conf['Header auto-hide'] || !Conf['Fixed Header'] ? + [0, 0] + : Conf['Bottom Header'] ? + [0, Header$1.bar.getBoundingClientRect().height] + : + [Header$1.bar.getBoundingClientRect().height, 0]; + + if (isTouching) { + o.identifier = e.identifier; + o.move = touchmove.bind(o); + o.up = touchend.bind(o); + $$1.on(d$1, 'touchmove', o.move); + return $$1.on(d$1, 'touchend touchcancel', o.up); + } else { // mousedown + o.move = drag.bind(o); + o.up = dragend.bind(o); + $$1.on(d$1, 'mousemove', o.move); + return $$1.on(d$1, 'mouseup', o.up); + } + }; + + var touchmove = function (e) { + for (var touch of e.changedTouches) { + if (touch.identifier === this.identifier) { + drag.call(this, touch); + return; + } + } + }; + + var drag = function (e) { + const {clientX, clientY} = e; + + let left = clientX - this.dx; + left = left < 10 ? + 0 + : (this.width - left) < 10 ? + '' + : + ((left / this.screenWidth) * 100) + '%'; + + let top = clientY - this.dy; + top = top < (10 + this.topBorder) ? + this.topBorder + 'px' + : (this.height - top) < (10 + this.bottomBorder) ? + '' + : + ((top / this.screenHeight) * 100) + '%'; + + const right = left === '' ? + 0 + : + ''; + + const bottom = top === '' ? + this.bottomBorder + 'px' + : + ''; + + const {style} = this; + style.left = left; + style.right = right; + style.top = top; + return style.bottom = bottom; + }; + + var touchend = function (e) { + for (var touch of e.changedTouches) { + if (touch.identifier === this.identifier) { + dragend.call(this); + return; + } + } + }; + + var dragend = function () { + if (this.isTouching) { + $$1.off(d$1, 'touchmove', this.move); + $$1.off(d$1, 'touchend touchcancel', this.up); + } else { // mouseup + $$1.off(d$1, 'mousemove', this.move); + $$1.off(d$1, 'mouseup', this.up); + } + return $$1.set(`${this.id}.position`, this.style.cssText); + }; + + const hoverstart = function ({ root, el, latestEvent, endEvents, height, width, cb, noRemove }) { + const rect = root.getBoundingClientRect(); + const o = { + root, + el, + style: el.style, + isImage: ['IMG', 'VIDEO'].includes(el.nodeName), + cb, + endEvents, + latestEvent, + clientHeight: doc$1.clientHeight, + clientWidth: doc$1.clientWidth, + height, + width, + noRemove, + clientX: (rect.left + rect.right) / 2, + clientY: (rect.top + rect.bottom) / 2 + }; + o.hover = hover.bind(o); + o.hoverend = hoverend.bind(o); + + o.hover(o.latestEvent); + new MutationObserver(function() { + if (el.parentNode) { return o.hover(o.latestEvent); } + }).observe(el, {childList: true}); + + $$1.on(root, endEvents, o.hoverend); + if ($$1.x('ancestor::div[contains(@class,"inline")][1]', root)) { + $$1.on(d$1, 'keydown', o.hoverend); + } + $$1.on(root, 'mousemove', o.hover); + + // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955 + o.workaround = function(e) { if (!root.contains(e.target)) { return o.hoverend(e); } }; + return $$1.on(doc$1, 'mousemove', o.workaround); + }; + + hoverstart.padding = 25; + + var hover = function (e) { + this.latestEvent = e; + const height = (this.height || this.el.offsetHeight) + hoverstart.padding; + const width = (this.width || this.el.offsetWidth); + const {clientX, clientY} = Conf['Follow Cursor'] ? e : this; + + const top = this.isImage ? + Math.max(0, (clientY * (this.clientHeight - height)) / this.clientHeight) + : + Math.max(0, Math.min(this.clientHeight - height, clientY - 120)); + + let threshold = this.clientWidth / 2; + if (!this.isImage) { threshold = Math.max(threshold, this.clientWidth - 400); } + let marginX = (clientX <= threshold ? clientX : this.clientWidth - clientX) + 45; + if (this.isImage) { marginX = Math.min(marginX, this.clientWidth - width); } + marginX += 'px'; + const [left, right] = clientX <= threshold ? [marginX, ''] : ['', marginX]; + + const {style} = this; + style.top = top + 'px'; + style.left = left; + return style.right = right; + }; + + var hoverend = function (e) { + if (((e.type === 'keydown') && (e.keyCode !== 13)) || (e.target.nodeName === "TEXTAREA")) { return; } + if (!this.noRemove) { $$1.rm(this.el); } + $$1.off(this.root, this.endEvents, this.hoverend); + $$1.off(d$1, 'keydown', this.hoverend); + $$1.off(this.root, 'mousemove', this.hover); + // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955 + $$1.off(doc$1, 'mousemove', this.workaround); + if (this.cb) { return this.cb.call(this); } + }; + + const checkbox = function (name, text, checked) { + if (checked == null) { checked = Conf[name]; } + const label = $$1.el('label'); + const input = $$1.el('input', {type: 'checkbox', name, checked}); + $$1.add(label, [input, $$1.tn(` ${text}`)]); + return label; + }; + + const UI = { + dialog, + Menu: Menu$1, + hover: hoverstart, + checkbox }; - var Menu$1 = (function() { - let currentMenu = undefined; - let lastToggledButton = undefined; - Menu$1 = class Menu { - static initClass() { - currentMenu = null; - lastToggledButton = null; - } - - constructor(type) { - // XXX AddMenuEntry event is deprecated - this.setPosition = this.setPosition.bind(this); - this.close = this.close.bind(this); - this.keybinds = this.keybinds.bind(this); - this.onFocus = this.onFocus.bind(this); - this.addEntry = this.addEntry.bind(this); - this.type = type; - $$1.on(d$1, 'AddMenuEntry', ({detail}) => { - if (detail.type !== this.type) { return; } - delete detail.open; - return this.addEntry(detail); - }); - this.entries = []; - } - - makeMenu() { - const menu = $$1.el('div', { - className: 'dialog', - id: 'menu', - tabIndex: 0 - } - ); - menu.dataset.type = this.type; - $$1.on(menu, 'click', e => e.stopPropagation()); - $$1.on(menu, 'keydown', this.keybinds); - return menu; - } - - toggle(e, button, data) { - e.preventDefault(); - e.stopPropagation(); - - if (currentMenu) { - // Close if it's already opened. - // Reopen if we clicked on another button. - const previousButton = lastToggledButton; - currentMenu.close(); - if (previousButton === button) { return; } - } - - if (!this.entries.length) { return; } - return this.open(button, data); - } - - open(button, data) { - let entry; - const menu = (this.menu = this.makeMenu()); - currentMenu = this; - lastToggledButton = button; - - this.entries.sort((first, second) => first.order - second.order); - - for (entry of this.entries) { - this.insertEntry(entry, menu, data); - } - - $$1.addClass(lastToggledButton, 'active'); - - $$1.on(d$1, 'click CloseMenu', this.close); - $$1.on(d$1, 'scroll', this.setPosition); - $$1.on(window, 'resize', this.setPosition); - $$1.after(button, menu); - - this.setPosition(); - - entry = $$1('.entry', menu); - // We've removed flexbox, so we don't use order anymore. - // while prevEntry = @findNextEntry entry, -1 - // entry = prevEntry - this.focus(entry); - - return menu.focus(); - } - - setPosition() { - const mRect = this.menu.getBoundingClientRect(); - const bRect = lastToggledButton.getBoundingClientRect(); - window.scrollY + bRect.top; - window.scrollX + bRect.left; - const cHeight = doc$1.clientHeight; - const cWidth = doc$1.clientWidth; - const [top, bottom] = (bRect.top + bRect.height + mRect.height) < cHeight ? - [`${bRect.bottom}px`, ''] - : - ['', `${cHeight - bRect.top}px`]; - const [left, right] = (bRect.left + mRect.width) < cWidth ? - [`${bRect.left}px`, ''] - : - ['', `${cWidth - bRect.right}px`]; - $$1.extend(this.menu.style, {top, right, bottom, left}); - return this.menu.classList.toggle('left', right); - } - - insertEntry(entry, parent, data) { - let submenu; - if (typeof entry.open === 'function') { - try { - if (!entry.open(data)) { return; } - } catch (err) { - Main$1.handleErrors({ - message: `Error in building the ${this.type} menu.`, - error: err - }); - return; - } - } - $$1.add(parent, entry.el); - - if (!entry.subEntries) { return; } - if (submenu = $$1('.submenu', entry.el)) { - // Reset sub menu, remove irrelevant entries. - $$1.rm(submenu); - } - submenu = $$1.el('div', - {className: 'dialog submenu'}); - for (var subEntry of entry.subEntries) { - this.insertEntry(subEntry, submenu, data); - } - $$1.add(entry.el, submenu); - } - - close() { - $$1.rm(this.menu); - delete this.menu; - $$1.rmClass(lastToggledButton, 'active'); - currentMenu = null; - lastToggledButton = null; - $$1.off(d$1, 'click scroll CloseMenu', this.close); - $$1.off(d$1, 'scroll', this.setPosition); - return $$1.off(window, 'resize', this.setPosition); - } - - findNextEntry(entry, direction) { - const entries = [...entry.parentNode.children]; - entries.sort((first, second) => first.style.order - second.style.order); - return entries[entries.indexOf(entry) + direction]; - } - - keybinds(e) { - let subEntry; - let next, submenu; - let entry = $$1('.focused', this.menu); - while ((subEntry = $$1('.focused', entry))) { - entry = subEntry; - } - - switch (e.keyCode) { - case 27: // Esc - lastToggledButton.focus(); - this.close(); - break; - case 13: case 32: // Enter, Space - entry.click(); - break; - case 38: // Up - if (next = this.findNextEntry(entry, -1)) { - this.focus(next); - } - break; - case 40: // Down - if (next = this.findNextEntry(entry, +1)) { - this.focus(next); - } - break; - case 39: // Right - if ((submenu = $$1('.submenu', entry)) && (next = submenu.firstElementChild)) { - let nextPrev; - while ((nextPrev = this.findNextEntry(next, -1))) { - next = nextPrev; - } - this.focus(next); - } - break; - case 37: // Left - if (next = $$1.x('parent::*[contains(@class,"submenu")]/parent::*', entry)) { - this.focus(next); - } - break; - default: - return; - } - - e.preventDefault(); - return e.stopPropagation(); - } - - onFocus(e) { - e.stopPropagation(); - return this.focus(e.target); - } - - focus(entry) { - let focused, submenu; - while ((focused = $$1.x('parent::*/child::*[contains(@class,"focused")]', entry))) { - $$1.rmClass(focused, 'focused'); - } - for (focused of $$('.focused', entry)) { - $$1.rmClass(focused, 'focused'); - } - $$1.addClass(entry, 'focused'); - - // Submenu positioning. - if (!(submenu = $$1('.submenu', entry))) { return; } - const sRect = submenu.getBoundingClientRect(); - const eRect = entry.getBoundingClientRect(); - const cHeight = doc$1.clientHeight; - const cWidth = doc$1.clientWidth; - const [top, bottom] = (eRect.top + sRect.height) < cHeight ? - ['0px', 'auto'] - : - ['auto', '0px']; - const [left, right] = (eRect.right + sRect.width) < (cWidth - 150) ? - ['100%', 'auto'] - : - ['auto', '100%']; - const {style} = submenu; - style.top = top; - style.bottom = bottom; - style.left = left; - return style.right = right; - } - - addEntry(entry) { - this.parseEntry(entry); - return this.entries.push(entry); - } - - parseEntry(entry) { - const {el, subEntries} = entry; - $$1.addClass(el, 'entry'); - $$1.on(el, 'focus mouseover', this.onFocus); - el.style.order = entry.order || 100; - if (!subEntries) { return; } - $$1.addClass(el, 'has-submenu'); - for (var subEntry of subEntries) { - this.parseEntry(subEntry); - } - } - }; - Menu$1.initClass(); - return Menu$1; - })(); - - var dragstart = function (e) { - let isTouching; - if ((e.type === 'mousedown') && (e.button !== 0)) { return; } // not LMB - // prevent text selection - e.preventDefault(); - if (isTouching = e.type === 'touchstart') { - e = e.changedTouches[e.changedTouches.length - 1]; - } - // distance from pointer to el edge is constant; calculate it here. - const el = $$1.x('ancestor::div[contains(@class,"dialog")][1]', this); - const rect = el.getBoundingClientRect(); - const screenHeight = doc$1.clientHeight; - const screenWidth = doc$1.clientWidth; - const o = { - id: el.id, - style: el.style, - dx: e.clientX - rect.left, - dy: e.clientY - rect.top, - height: screenHeight - rect.height, - width: screenWidth - rect.width, - screenHeight, - screenWidth, - isTouching - }; - - [o.topBorder, o.bottomBorder] = Conf['Header auto-hide'] || !Conf['Fixed Header'] ? - [0, 0] - : Conf['Bottom Header'] ? - [0, Header$1.bar.getBoundingClientRect().height] - : - [Header$1.bar.getBoundingClientRect().height, 0]; - - if (isTouching) { - o.identifier = e.identifier; - o.move = touchmove.bind(o); - o.up = touchend.bind(o); - $$1.on(d$1, 'touchmove', o.move); - return $$1.on(d$1, 'touchend touchcancel', o.up); - } else { // mousedown - o.move = drag.bind(o); - o.up = dragend.bind(o); - $$1.on(d$1, 'mousemove', o.move); - return $$1.on(d$1, 'mouseup', o.up); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var Nav = { + init() { + switch (g.VIEW) { + case 'index': + if (!Conf['Index Navigation']) { return; } + break; + case 'thread': + if (!Conf['Reply Navigation']) { return; } + break; + default: + return; + } + + const span = $$1.el('span', + {id: 'navlinks'}); + const prev = $$1.el('a', { + textContent: '▲', + href: 'javascript:;' + } + ); + const next = $$1.el('a', { + textContent: '▼', + href: 'javascript:;' + } + ); + + $$1.on(prev, 'click', this.prev); + $$1.on(next, 'click', this.next); + + $$1.add(span, [prev, $$1.tn(' '), next]); + var append = function() { + $$1.off(d$1, '4chanXInitFinished', append); + return $$1.add(d$1.body, span); + }; + return $$1.on(d$1, '4chanXInitFinished', append); + }, + + prev() { + if (g.VIEW === 'thread') { + return window.scrollTo(0, 0); + } else { + return Nav.scroll(-1); + } + }, + + next() { + if (g.VIEW === 'thread') { + return window.scrollTo(0, d$1.body.scrollHeight); + } else { + return Nav.scroll(+1); + } + }, + + getThread() { + if (g.VIEW === 'thread') { return g.threads.get(`${g.BOARD}.${g.THREADID}`).nodes.root; } + if ($$1.hasClass(doc$1, 'catalog-mode')) { return; } + for (var threadRoot of $$(g.SITE.selectors.thread)) { + var thread = Get$1.threadFromRoot(threadRoot); + if (thread.isHidden && !thread.stub) { continue; } + if (Header$1.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) { // not scrolled past + return threadRoot; + } + } + }, + + scroll(delta) { + let next; + d$1.activeElement?.blur(); + let thread = Nav.getThread(); + if (!thread) { return; } + const axis = delta === +1 ? + 'following' + : + 'preceding'; + if (next = $$1.x(`${axis}-sibling::${g.SITE.xpath.thread}[not(@hidden)][1]`, thread)) { + // 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. + const top = Header$1.getTopOf(thread); + if (((delta === +1) && (top < 5)) || ((delta === -1) && (top > -5))) { thread = next; } + } + // Add extra space to the end of the page if necessary so that all threads can be selected by keybinds. + const extra = (Header$1.getTopOf(thread) + doc$1.clientHeight) - d$1.body.getBoundingClientRect().bottom; + if (extra > 0) { d$1.body.style.marginBottom = `${extra}px`; } + + Header$1.scrollTo(thread); + + if ((extra > 0) && !Nav.haveExtra) { + Nav.haveExtra = true; + return $$1.on(d$1, 'scroll', Nav.removeExtra); + } + }, + + removeExtra() { + const extra = doc$1.clientHeight - d$1.body.getBoundingClientRect().bottom; + if (extra > 0) { + return d$1.body.style.marginBottom = `${extra}px`; + } else { + d$1.body.style.marginBottom = ''; + delete Nav.haveExtra; + return $$1.off(d$1, 'scroll', Nav.removeExtra); + } + } }; - var touchmove = function (e) { - for (var touch of e.changedTouches) { - if (touch.identifier === this.identifier) { - drag.call(this, touch); - return; - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var ImageHost = { + init() { + if ((!(this.useFaster = /\S/.test(Conf['fourchanImageHost']))) || (g.SITE.software !== 'yotsuba') || !['index', 'thread'].includes(g.VIEW)) { return; } + return Callbacks.Post.push({ + name: 'Image Host Rewriting', + cb: this.node + }); + }, + + suggestions: ['i.4cdn.org', 'is2.4chan.org'], + + host() { + return Conf['fourchanImageHost'].trim() || 'i.4cdn.org'; + }, + flashHost() { + return 'i.4cdn.org'; + }, + thumbHost() { + return 'i.4cdn.org'; + }, + test(hostname) { + return (hostname === 'i.4cdn.org') || ImageHost.regex.test(hostname); + }, + + regex: /^is\d*\.4chan(?:nel)?\.org$/, + + node() { + if (this.isClone) { return; } + const host = ImageHost.host(); + if (this.file && ImageHost.test(this.file.url.split('/')[2]) && !/\.swf$/.test(this.file.url)) { + this.file.link.hostname = host; + if (this.file.thumbLink) { this.file.thumbLink.hostname = host; } + this.file.url = this.file.link.href; + } + return ImageHost.fixLinks($$('a', this.nodes.comment)); + }, + + fixLinks(links) { + for (var link of links) { + if (ImageHost.test(link.hostname) && !/\.swf$/.test(link.pathname)) { + var host = ImageHost.host(); + if (link.hostname !== host) { link.hostname = host; } + } + } + } }; - var drag = function (e) { - const {clientX, clientY} = e; - - let left = clientX - this.dx; - left = left < 10 ? - 0 - : (this.width - left) < 10 ? - '' - : - ((left / this.screenWidth) * 100) + '%'; - - let top = clientY - this.dy; - top = top < (10 + this.topBorder) ? - this.topBorder + 'px' - : (this.height - top) < (10 + this.bottomBorder) ? - '' - : - ((top / this.screenHeight) * 100) + '%'; - - const right = left === '' ? - 0 - : - ''; - - const bottom = top === '' ? - this.bottomBorder + 'px' - : - ''; - - const {style} = this; - style.left = left; - style.right = right; - style.top = top; - return style.bottom = bottom; + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var Volume = { + init() { + if (!['index', 'thread'].includes(g.VIEW) || + (!Conf['Image Expansion'] && !Conf['Image Hover'] && !Conf['Image Hover in Catalog'] && !Conf['Gallery'])) { return; } + + $$1.sync('Allow Sound', function(x) { + Conf['Allow Sound'] = x; + if (Volume.inputs) Volume.inputs.unmute.checked = x; + }); + + $$1.sync('Default Volume', function(x) { + Conf['Default Volume'] = x; + if (Volume.inputs) Volume.inputs.volume.value = x; + }); + + if (Conf['Mouse Wheel Volume']) { + Callbacks.Post.push({ + name: 'Mouse Wheel Volume', + cb: this.node + }); + } + + if (g.SITE.noAudio?.(g.BOARD)) { return; } + + if (Conf['Mouse Wheel Volume']) { + Callbacks.CatalogThread.push({ + name: 'Mouse Wheel Volume', + cb: this.catalogNode + }); + } + + const unmuteEntry = UI.checkbox('Allow Sound', 'Allow Sound'); + unmuteEntry.title = Config.main['Images and Videos']['Allow Sound'][1]; + + const volumeEntry = $$1.el('label', + {title: 'Default volume for videos.'}); + $$1.extend(volumeEntry, + {innerHTML: " Volume"}); + + this.inputs = { + unmute: unmuteEntry.firstElementChild, + volume: volumeEntry.firstElementChild + }; + + $$1.on(this.inputs.unmute, 'change', $$1.cb.checked); + $$1.on(this.inputs.volume, 'change', $$1.cb.value); + + Header$1.menu.addEntry({el: unmuteEntry, order: 200}); + return Header$1.menu.addEntry({el: volumeEntry, order: 201}); + }, + + setup(video) { + video.muted = !Conf['Allow Sound']; + video.volume = Conf['Default Volume']; + return $$1.on(video, 'volumechange', Volume.change); + }, + + change() { + const {muted, volume} = this; + const items = { + 'Allow Sound': !muted, + 'Default Volume': volume + }; + for (var key in items) { + var val = items[key]; + if (Conf[key] === val) { + delete items[key]; + } + } + $$1.set(items); + $$1.extend(Conf, items); + if (Volume.inputs) { + Volume.inputs.unmute.checked = !muted; + return Volume.inputs.volume.value = volume; + } + }, + + node() { + if (g.SITE.noAudio?.(this.board)) { return; } + for (var file of this.files) { + if (file.isVideo) { + if (file.thumb) { $$1.on(file.thumb, 'wheel', Volume.wheel.bind(Header$1.hover)); } + $$1.on(($$1('.file-info', file.text) || file.link), 'wheel', Volume.wheel.bind(file.thumbLink)); + } + } + }, + + catalogNode() { + const file = this.thread.OP.files[0]; + if (!file?.isVideo) { return; } + return $$1.on(this.nodes.thumb, 'wheel', Volume.wheel.bind(Header$1.hover)); + }, + + wheel(e) { + let el; + if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) { return; } + if (!(el = $$1('video:not([data-md5])', this))) { return; } + if (el.muted || !$$1.hasAudio(el)) { return; } + let volume = el.volume + 0.1; + if (e.deltaY < 0) { volume *= 1.1; } + if (e.deltaY > 0) { volume /= 1.1; } + el.volume = $$1.minmax(volume - 0.1, 0, 1); + return e.preventDefault(); + } }; - var touchend = function (e) { - for (var touch of e.changedTouches) { - if (touch.identifier === this.identifier) { - dragend.call(this); - return; - } - } - }; - - var dragend = function () { - if (this.isTouching) { - $$1.off(d$1, 'touchmove', this.move); - $$1.off(d$1, 'touchend touchcancel', this.up); - } else { // mouseup - $$1.off(d$1, 'mousemove', this.move); - $$1.off(d$1, 'mouseup', this.up); - } - return $$1.set(`${this.id}.position`, this.style.cssText); - }; - - const hoverstart = function ({ root, el, latestEvent, endEvents, height, width, cb, noRemove }) { - const rect = root.getBoundingClientRect(); - const o = { - root, - el, - style: el.style, - isImage: ['IMG', 'VIDEO'].includes(el.nodeName), - cb, - endEvents, - latestEvent, - clientHeight: doc$1.clientHeight, - clientWidth: doc$1.clientWidth, - height, - width, - noRemove, - clientX: (rect.left + rect.right) / 2, - clientY: (rect.top + rect.bottom) / 2 - }; - o.hover = hover.bind(o); - o.hoverend = hoverend.bind(o); - - o.hover(o.latestEvent); - new MutationObserver(function() { - if (el.parentNode) { return o.hover(o.latestEvent); } - }).observe(el, {childList: true}); - - $$1.on(root, endEvents, o.hoverend); - if ($$1.x('ancestor::div[contains(@class,"inline")][1]', root)) { - $$1.on(d$1, 'keydown', o.hoverend); - } - $$1.on(root, 'mousemove', o.hover); - - // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955 - o.workaround = function(e) { if (!root.contains(e.target)) { return o.hoverend(e); } }; - return $$1.on(doc$1, 'mousemove', o.workaround); - }; - - hoverstart.padding = 25; - - var hover = function (e) { - this.latestEvent = e; - const height = (this.height || this.el.offsetHeight) + hoverstart.padding; - const width = (this.width || this.el.offsetWidth); - const {clientX, clientY} = Conf['Follow Cursor'] ? e : this; - - const top = this.isImage ? - Math.max(0, (clientY * (this.clientHeight - height)) / this.clientHeight) - : - Math.max(0, Math.min(this.clientHeight - height, clientY - 120)); - - let threshold = this.clientWidth / 2; - if (!this.isImage) { threshold = Math.max(threshold, this.clientWidth - 400); } - let marginX = (clientX <= threshold ? clientX : this.clientWidth - clientX) + 45; - if (this.isImage) { marginX = Math.min(marginX, this.clientWidth - width); } - marginX += 'px'; - const [left, right] = clientX <= threshold ? [marginX, ''] : ['', marginX]; - - const {style} = this; - style.top = top + 'px'; - style.left = left; - return style.right = right; - }; - - var hoverend = function (e) { - if (((e.type === 'keydown') && (e.keyCode !== 13)) || (e.target.nodeName === "TEXTAREA")) { return; } - if (!this.noRemove) { $$1.rm(this.el); } - $$1.off(this.root, this.endEvents, this.hoverend); - $$1.off(d$1, 'keydown', this.hoverend); - $$1.off(this.root, 'mousemove', this.hover); - // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955 - $$1.off(doc$1, 'mousemove', this.workaround); - if (this.cb) { return this.cb.call(this); } - }; - - const checkbox = function (name, text, checked) { - if (checked == null) { checked = Conf[name]; } - const label = $$1.el('label'); - const input = $$1.el('input', {type: 'checkbox', name, checked}); - $$1.add(label, [input, $$1.tn(` ${text}`)]); - return label; - }; - - const UI = { - dialog, - Menu: Menu$1, - hover: hoverstart, - checkbox - }; - - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var Nav = { - init() { - switch (g.VIEW) { - case 'index': - if (!Conf['Index Navigation']) { return; } - break; - case 'thread': - if (!Conf['Reply Navigation']) { return; } - break; - default: - return; - } - - const span = $$1.el('span', - {id: 'navlinks'}); - const prev = $$1.el('a', { - textContent: '▲', - href: 'javascript:;' - } - ); - const next = $$1.el('a', { - textContent: '▼', - href: 'javascript:;' - } - ); - - $$1.on(prev, 'click', this.prev); - $$1.on(next, 'click', this.next); - - $$1.add(span, [prev, $$1.tn(' '), next]); - var append = function() { - $$1.off(d$1, '4chanXInitFinished', append); - return $$1.add(d$1.body, span); - }; - return $$1.on(d$1, '4chanXInitFinished', append); - }, - - prev() { - if (g.VIEW === 'thread') { - return window.scrollTo(0, 0); - } else { - return Nav.scroll(-1); - } - }, - - next() { - if (g.VIEW === 'thread') { - return window.scrollTo(0, d$1.body.scrollHeight); - } else { - return Nav.scroll(+1); - } - }, - - getThread() { - if (g.VIEW === 'thread') { return g.threads.get(`${g.BOARD}.${g.THREADID}`).nodes.root; } - if ($$1.hasClass(doc$1, 'catalog-mode')) { return; } - for (var threadRoot of $$(g.SITE.selectors.thread)) { - var thread = Get$1.threadFromRoot(threadRoot); - if (thread.isHidden && !thread.stub) { continue; } - if (Header$1.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) { // not scrolled past - return threadRoot; - } - } - }, - - scroll(delta) { - let next; - d$1.activeElement?.blur(); - let thread = Nav.getThread(); - if (!thread) { return; } - const axis = delta === +1 ? - 'following' - : - 'preceding'; - if (next = $$1.x(`${axis}-sibling::${g.SITE.xpath.thread}[not(@hidden)][1]`, thread)) { - // 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. - const top = Header$1.getTopOf(thread); - if (((delta === +1) && (top < 5)) || ((delta === -1) && (top > -5))) { thread = next; } - } - // Add extra space to the end of the page if necessary so that all threads can be selected by keybinds. - const extra = (Header$1.getTopOf(thread) + doc$1.clientHeight) - d$1.body.getBoundingClientRect().bottom; - if (extra > 0) { d$1.body.style.marginBottom = `${extra}px`; } - - Header$1.scrollTo(thread); - - if ((extra > 0) && !Nav.haveExtra) { - Nav.haveExtra = true; - return $$1.on(d$1, 'scroll', Nav.removeExtra); - } - }, - - removeExtra() { - const extra = doc$1.clientHeight - d$1.body.getBoundingClientRect().bottom; - if (extra > 0) { - return d$1.body.style.marginBottom = `${extra}px`; - } else { - d$1.body.style.marginBottom = ''; - delete Nav.haveExtra; - return $$1.off(d$1, 'scroll', Nav.removeExtra); - } - } - }; - - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var ImageHost = { - init() { - if ((!(this.useFaster = /\S/.test(Conf['fourchanImageHost']))) || (g.SITE.software !== 'yotsuba') || !['index', 'thread'].includes(g.VIEW)) { return; } - return Callbacks.Post.push({ - name: 'Image Host Rewriting', - cb: this.node - }); - }, - - suggestions: ['i.4cdn.org', 'is2.4chan.org'], - - host() { - return Conf['fourchanImageHost'].trim() || 'i.4cdn.org'; - }, - flashHost() { - return 'i.4cdn.org'; - }, - thumbHost() { - return 'i.4cdn.org'; - }, - test(hostname) { - return (hostname === 'i.4cdn.org') || ImageHost.regex.test(hostname); - }, - - regex: /^is\d*\.4chan(?:nel)?\.org$/, - - node() { - if (this.isClone) { return; } - const host = ImageHost.host(); - if (this.file && ImageHost.test(this.file.url.split('/')[2]) && !/\.swf$/.test(this.file.url)) { - this.file.link.hostname = host; - if (this.file.thumbLink) { this.file.thumbLink.hostname = host; } - this.file.url = this.file.link.href; - } - return ImageHost.fixLinks($$('a', this.nodes.comment)); - }, - - fixLinks(links) { - for (var link of links) { - if (ImageHost.test(link.hostname) && !/\.swf$/.test(link.pathname)) { - var host = ImageHost.host(); - if (link.hostname !== host) { link.hostname = host; } - } - } - } - }; - - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var Volume = { - init() { - if (!['index', 'thread'].includes(g.VIEW) || - (!Conf['Image Expansion'] && !Conf['Image Hover'] && !Conf['Image Hover in Catalog'] && !Conf['Gallery'])) { return; } - - $$1.sync('Allow Sound', function(x) { - Conf['Allow Sound'] = x; - if (Volume.inputs) Volume.inputs.unmute.checked = x; - }); - - $$1.sync('Default Volume', function(x) { - Conf['Default Volume'] = x; - if (Volume.inputs) Volume.inputs.volume.value = x; - }); - - if (Conf['Mouse Wheel Volume']) { - Callbacks.Post.push({ - name: 'Mouse Wheel Volume', - cb: this.node - }); - } - - if (g.SITE.noAudio?.(g.BOARD)) { return; } - - if (Conf['Mouse Wheel Volume']) { - Callbacks.CatalogThread.push({ - name: 'Mouse Wheel Volume', - cb: this.catalogNode - }); - } - - const unmuteEntry = UI.checkbox('Allow Sound', 'Allow Sound'); - unmuteEntry.title = Config.main['Images and Videos']['Allow Sound'][1]; - - const volumeEntry = $$1.el('label', - {title: 'Default volume for videos.'}); - $$1.extend(volumeEntry, - {innerHTML: " Volume"}); - - this.inputs = { - unmute: unmuteEntry.firstElementChild, - volume: volumeEntry.firstElementChild - }; - - $$1.on(this.inputs.unmute, 'change', $$1.cb.checked); - $$1.on(this.inputs.volume, 'change', $$1.cb.value); - - Header$1.menu.addEntry({el: unmuteEntry, order: 200}); - return Header$1.menu.addEntry({el: volumeEntry, order: 201}); - }, - - setup(video) { - video.muted = !Conf['Allow Sound']; - video.volume = Conf['Default Volume']; - return $$1.on(video, 'volumechange', Volume.change); - }, - - change() { - const {muted, volume} = this; - const items = { - 'Allow Sound': !muted, - 'Default Volume': volume - }; - for (var key in items) { - var val = items[key]; - if (Conf[key] === val) { - delete items[key]; - } - } - $$1.set(items); - $$1.extend(Conf, items); - if (Volume.inputs) { - Volume.inputs.unmute.checked = !muted; - return Volume.inputs.volume.value = volume; - } - }, - - node() { - if (g.SITE.noAudio?.(this.board)) { return; } - for (var file of this.files) { - if (file.isVideo) { - if (file.thumb) { $$1.on(file.thumb, 'wheel', Volume.wheel.bind(Header$1.hover)); } - $$1.on(($$1('.file-info', file.text) || file.link), 'wheel', Volume.wheel.bind(file.thumbLink)); - } - } - }, - - catalogNode() { - const file = this.thread.OP.files[0]; - if (!file?.isVideo) { return; } - return $$1.on(this.nodes.thumb, 'wheel', Volume.wheel.bind(Header$1.hover)); - }, - - wheel(e) { - let el; - if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) { return; } - if (!(el = $$1('video:not([data-md5])', this))) { return; } - if (el.muted || !$$1.hasAudio(el)) { return; } - let volume = el.volume + 0.1; - if (e.deltaY < 0) { volume *= 1.1; } - if (e.deltaY > 0) { volume /= 1.1; } - el.volume = $$1.minmax(volume - 0.1, 0, 1); - return e.preventDefault(); - } - }; - - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS204: Change includes calls to have a more natural evaluation order - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var ImageCommon = { - // Pause and mute video in preparation for removing the element from the document. - pause(video) { - if (video.nodeName !== 'VIDEO') { return; } - video.pause(); - $$1.off(video, 'volumechange', Volume.change); - return video.muted = true; - }, - - rewind(el) { - if (el.nodeName === 'VIDEO') { - if (el.readyState >= el.HAVE_METADATA) { return el.currentTime = 0; } - } else if (/\.gif$/.test(el.src)) { - return $$1.queueTask(() => el.src = el.src); - } - }, - - pushCache(el) { - ImageCommon.cache = el; - return $$1.on(el, 'error', ImageCommon.cacheError); - }, - - popCache() { - const el = ImageCommon.cache; - $$1.off(el, 'error', ImageCommon.cacheError); - delete ImageCommon.cache; - return el; - }, - - cacheError() { - if (ImageCommon.cache === this) { return delete ImageCommon.cache; } - }, - - decodeError(file, fileObj) { - let message; - if (file.error?.code !== MediaError.MEDIA_ERR_DECODE) { return false; } - if (!(message = $$1('.warning', fileObj.thumb.parentNode))) { - message = $$1.el('div', {className: 'warning'}); - $$1.after(fileObj.thumb, message); - } - message.textContent = 'Error: Corrupt or unplayable video'; - return true; - }, - - isFromArchive(file) { - return (g.SITE.software === 'yotsuba') && !ImageHost.test(file.src.split('/')[2]); - }, - - error(file, post, fileObj, delay, cb) { - let timeoutID; - const src = fileObj.url.split('/'); - let url = null; - if ((g.SITE.software === 'yotsuba') && Conf['404 Redirect']) { - url = Redirect$1.to('file', { - boardID: post.board.ID, - filename: src[src.length - 1] - }); - } - if (!url || !Redirect$1.securityCheck(url)) { url = null; } - - if ((post.isDead || fileObj.isDead) && !ImageCommon.isFromArchive(file)) { return cb(url); } - - if (delay != null) { timeoutID = setTimeout((() => cb(url)), delay); } - if (post.isDead || fileObj.isDead) { return; } - const redirect = function() { - if (!ImageCommon.isFromArchive(file)) { - if (delay != null) { clearTimeout(timeoutID); } - return cb(url); - } - }; - - const threadJSON = g.SITE.urls.threadJSON?.(post); - if (!threadJSON) { return; } - var parseJSON = function(isArchiveURL) { - let needle, postObj; - if (this.status === 404) { - let archivedThreadJSON; - if (!isArchiveURL && (archivedThreadJSON = g.SITE.urls.archivedThreadJSON?.(post))) { - $$1.ajax(archivedThreadJSON, {onloadend() { return parseJSON.call(this, true); }}); - } else { - post.kill(!post.isClone, fileObj.index); - } - } - if (this.status !== 200) { return redirect(); } - for (postObj of this.response.posts) { - if (postObj.no === post.ID) { break; } - } - if (postObj.no !== post.ID) { - post.kill(); - return redirect(); - } else if ((needle = fileObj.docIndex, g.SITE.Build.parseJSON(postObj, post.board).filesDeleted.includes(needle))) { - post.kill(true); - return redirect(); - } else { - return url = fileObj.url; - } - }; - return $$1.ajax(threadJSON, {onloadend() { return parseJSON.call(this); }}); - }, - - // Add controls, but not until the mouse is moved over the video. - addControls(video) { - var handler = function() { - $$1.off(video, 'mouseover', handler); - // Hacky workaround for Firefox forever-loading bug for very short videos - const t = new Date().getTime(); - return $$1.asap((() => ($$1.engine !== 'gecko') || ((video.readyState >= 3) && (video.currentTime <= Math.max(0.1, (video.duration - 0.5)))) || (new Date().getTime() >= (t + 1000))), () => video.controls = true); - }; - return $$1.on(video, 'mouseover', handler); - }, - - // XXX Estimate whether clicks are on the video controls and should be ignored. - onControls(e) { - return (Conf['Show Controls'] && Conf['Click Passthrough'] && (e.target.nodeName === 'VIDEO')) || - (e.target.controls && ((e.target.getBoundingClientRect().bottom - e.clientY) < 35)); - }, - - download(e) { - if (this.protocol === 'blob:') { return true; } - e.preventDefault(); - const {href, download} = this; - return CrossOrigin$1.file(href, function(blob) { - if (blob) { - const a = $$1.el('a', { - href: URL.createObjectURL(blob), - download, - hidden: true - } - ); - $$1.add(d$1.body, a); - a.click(); - return $$1.rm(a); - } else { - return new Notice('warning', `Could not download ${href}`, 20); - } - }); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var ImageCommon = { + // Pause and mute video in preparation for removing the element from the document. + pause(video) { + if (video.nodeName !== 'VIDEO') { return; } + video.pause(); + $$1.off(video, 'volumechange', Volume.change); + return video.muted = true; + }, + + rewind(el) { + if (el.nodeName === 'VIDEO') { + if (el.readyState >= el.HAVE_METADATA) { return el.currentTime = 0; } + } else if (/\.gif$/.test(el.src)) { + return $$1.queueTask(() => el.src = el.src); + } + }, + + pushCache(el) { + ImageCommon.cache = el; + return $$1.on(el, 'error', ImageCommon.cacheError); + }, + + popCache() { + const el = ImageCommon.cache; + $$1.off(el, 'error', ImageCommon.cacheError); + delete ImageCommon.cache; + return el; + }, + + cacheError() { + if (ImageCommon.cache === this) { return delete ImageCommon.cache; } + }, + + decodeError(file, fileObj) { + let message; + if (file.error?.code !== MediaError.MEDIA_ERR_DECODE) { return false; } + if (!(message = $$1('.warning', fileObj.thumb.parentNode))) { + message = $$1.el('div', {className: 'warning'}); + $$1.after(fileObj.thumb, message); + } + message.textContent = 'Error: Corrupt or unplayable video'; + return true; + }, + + isFromArchive(file) { + return (g.SITE.software === 'yotsuba') && !ImageHost.test(file.src.split('/')[2]); + }, + + error(file, post, fileObj, delay, cb) { + let timeoutID; + const src = fileObj.url.split('/'); + let url = null; + if ((g.SITE.software === 'yotsuba') && Conf['404 Redirect']) { + url = Redirect$1.to('file', { + boardID: post.board.ID, + filename: src[src.length - 1] + }); + } + if (!url || !Redirect$1.securityCheck(url)) { url = null; } + + if ((post.isDead || fileObj.isDead) && !ImageCommon.isFromArchive(file)) { return cb(url); } + + if (delay != null) { timeoutID = setTimeout((() => cb(url)), delay); } + if (post.isDead || fileObj.isDead) { return; } + const redirect = function() { + if (!ImageCommon.isFromArchive(file)) { + if (delay != null) { clearTimeout(timeoutID); } + return cb(url); + } + }; + + const threadJSON = g.SITE.urls.threadJSON?.(post); + if (!threadJSON) { return; } + var parseJSON = function(isArchiveURL) { + let needle, postObj; + if (this.status === 404) { + let archivedThreadJSON; + if (!isArchiveURL && (archivedThreadJSON = g.SITE.urls.archivedThreadJSON?.(post))) { + $$1.ajax(archivedThreadJSON, {onloadend() { return parseJSON.call(this, true); }}); + } else { + post.kill(!post.isClone, fileObj.index); + } + } + if (this.status !== 200) { return redirect(); } + for (postObj of this.response.posts) { + if (postObj.no === post.ID) { break; } + } + if (postObj.no !== post.ID) { + post.kill(); + return redirect(); + } else if ((needle = fileObj.docIndex, g.SITE.Build.parseJSON(postObj, post.board).filesDeleted.includes(needle))) { + post.kill(true); + return redirect(); + } else { + return url = fileObj.url; + } + }; + return $$1.ajax(threadJSON, {onloadend() { return parseJSON.call(this); }}); + }, + + // Add controls, but not until the mouse is moved over the video. + addControls(video) { + var handler = function() { + $$1.off(video, 'mouseover', handler); + // Hacky workaround for Firefox forever-loading bug for very short videos + const t = new Date().getTime(); + return $$1.asap((() => ($$1.engine !== 'gecko') || ((video.readyState >= 3) && (video.currentTime <= Math.max(0.1, (video.duration - 0.5)))) || (new Date().getTime() >= (t + 1000))), () => video.controls = true); + }; + return $$1.on(video, 'mouseover', handler); + }, + + // XXX Estimate whether clicks are on the video controls and should be ignored. + onControls(e) { + return (Conf['Show Controls'] && Conf['Click Passthrough'] && (e.target.nodeName === 'VIDEO')) || + (e.target.controls && ((e.target.getBoundingClientRect().bottom - e.clientY) < 35)); + }, + + download(e) { + if (this.protocol === 'blob:') { return true; } + e.preventDefault(); + const {href, download} = this; + return CrossOrigin$1.file(href, function(blob) { + if (blob) { + const a = $$1.el('a', { + href: URL.createObjectURL(blob), + download, + hidden: true + } + ); + $$1.add(d$1.body, a); + a.click(); + return $$1.rm(a); + } else { + return new Notice('warning', `Could not download ${href}`, 20); + } + }); + } }; const Audio = { @@ -4281,4591 +4281,4591 @@ https://*.hcaptcha.com } PostClone.suffix = 0; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var Menu = { - init() { - if (!['index', 'thread'].includes(g.VIEW) || !Conf['Menu']) { return; } - - this.button = $$1.el('a', { - className: 'menu-button', - href: 'javascript:;' - } - ); - - $$1.extend(this.button, {textContent: "🞃"}); - - this.menu = new UI.Menu('post'); - Callbacks.Post.push({ - name: 'Menu', - cb: this.node - }); - - return Callbacks.CatalogThread.push({ - name: 'Menu', - cb: this.catalogNode - }); - }, - - node() { - if (this.isClone) { - const button = $$1('.menu-button', this.nodes.info); - $$1.rmClass(button, 'active'); - $$1.rm($$1('.dialog', this.nodes.info)); - Menu.makeButton(this, button); - return; - } - return $$1.add(this.nodes.info, Menu.makeButton(this)); - }, - - catalogNode() { - return $$1.after(this.nodes.icons, Menu.makeButton(this.thread.OP)); - }, - - makeButton(post, button) { - if (!button) { button = Menu.button.cloneNode(true); } - $$1.on(button, 'click', function(e) { - return Menu.menu.toggle(e, this, post); - }); - return button; - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var Menu = { + init() { + if (!['index', 'thread'].includes(g.VIEW) || !Conf['Menu']) { return; } + + this.button = $$1.el('a', { + className: 'menu-button', + href: 'javascript:;' + } + ); + + $$1.extend(this.button, {textContent: "🞃"}); + + this.menu = new UI.Menu('post'); + Callbacks.Post.push({ + name: 'Menu', + cb: this.node + }); + + return Callbacks.CatalogThread.push({ + name: 'Menu', + cb: this.catalogNode + }); + }, + + node() { + if (this.isClone) { + const button = $$1('.menu-button', this.nodes.info); + $$1.rmClass(button, 'active'); + $$1.rm($$1('.dialog', this.nodes.info)); + Menu.makeButton(this, button); + return; + } + return $$1.add(this.nodes.info, Menu.makeButton(this)); + }, + + catalogNode() { + return $$1.after(this.nodes.icons, Menu.makeButton(this.thread.OP)); + }, + + makeButton(post, button) { + if (!button) { button = Menu.button.cloneNode(true); } + $$1.on(button, 'click', function(e) { + return Menu.menu.toggle(e, this, post); + }); + return button; + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var Recursive = { - recursives: dict(), - init() { - if (!['index', 'thread'].includes(g.VIEW)) { return; } - return Callbacks.Post.push({ - name: 'Recursive', - cb: this.node - }); - }, - - node() { - if (this.isClone || this.isFetchedQuote) { return; } - for (var quote of this.quotes) { - var obj; - if ((obj = Recursive.recursives[quote])) { - for (var i = 0; i < obj.recursives.length; i++) { - var recursive = obj.recursives[i]; - recursive(this, ...obj.args[i]); - } - } - } - }, - - add(recursive, post, ...args) { - const obj = Recursive.recursives[post.fullID] || (Recursive.recursives[post.fullID] = { - recursives: [], - args: [] - }); - obj.recursives.push(recursive); - return obj.args.push(args); - }, - - rm(recursive, post) { - let obj; - if (!(obj = Recursive.recursives[post.fullID])) { return; } - for (let i = 0; i < obj.recursives.length; i++) { - var rec = obj.recursives[i]; - if (rec === recursive) { - obj.recursives.splice(i, 1); - obj.args.splice(i, 1); - } - } - }, - - apply(recursive, post, ...args) { - const {fullID} = post; - return g.posts.forEach(function(post) { - if (post.quotes.includes(fullID)) { - return recursive(post, ...args); - } - }); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var Recursive = { + recursives: dict(), + init() { + if (!['index', 'thread'].includes(g.VIEW)) { return; } + return Callbacks.Post.push({ + name: 'Recursive', + cb: this.node + }); + }, + + node() { + if (this.isClone || this.isFetchedQuote) { return; } + for (var quote of this.quotes) { + var obj; + if ((obj = Recursive.recursives[quote])) { + for (var i = 0; i < obj.recursives.length; i++) { + var recursive = obj.recursives[i]; + recursive(this, ...obj.args[i]); + } + } + } + }, + + add(recursive, post, ...args) { + const obj = Recursive.recursives[post.fullID] || (Recursive.recursives[post.fullID] = { + recursives: [], + args: [] + }); + obj.recursives.push(recursive); + return obj.args.push(args); + }, + + rm(recursive, post) { + let obj; + if (!(obj = Recursive.recursives[post.fullID])) { return; } + for (let i = 0; i < obj.recursives.length; i++) { + var rec = obj.recursives[i]; + if (rec === recursive) { + obj.recursives.splice(i, 1); + obj.args.splice(i, 1); + } + } + }, + + apply(recursive, post, ...args) { + const {fullID} = post; + return g.posts.forEach(function(post) { + if (post.quotes.includes(fullID)) { + return recursive(post, ...args); + } + }); + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var PostHiding = { - init() { - if (!['index', 'thread'].includes(g.VIEW) || (!Conf['Reply Hiding Buttons'] && !(Conf['Menu'] && Conf['Reply Hiding Link']))) { return; } - - if (Conf['Reply Hiding Buttons']) { - $$1.addClass(doc$1, "reply-hide"); - } - - this.db = new DataBoard('hiddenPosts'); - return Callbacks.Post.push({ - name: 'Reply Hiding', - cb: this.node - }); - }, - - isHidden(boardID, threadID, postID) { - return !!(PostHiding.db && PostHiding.db.get({boardID, threadID, postID})); - }, - - node() { - let data, sa; - if (!this.isReply || this.isClone || this.isFetchedQuote) { return; } - - if (data = PostHiding.db.get({boardID: this.board.ID, threadID: this.thread.ID, postID: this.ID})) { - if (data.thisPost) { - PostHiding.hide(this, data.makeStub, data.hideRecursively); - } else { - Recursive.apply(PostHiding.hide, this, data.makeStub, true); - Recursive.add(PostHiding.hide, this, data.makeStub, true); - } - } - - if (!Conf['Reply Hiding Buttons']) { return; } - - const button = PostHiding.makeButton(this, 'hide'); - if (sa = g.SITE.selectors.sideArrows) { - const sideArrows = $$1(sa, this.nodes.root); - $$1.replace(sideArrows.firstChild, button); - return sideArrows.className = 'replacedSideArrows'; - } else { - return $$1.prepend(this.nodes.info, button); - } - }, - - menu: { - init() { - if (!['index', 'thread'].includes(g.VIEW) || !Conf['Menu'] || !Conf['Reply Hiding Link']) { return; } - - // Hide - let div = $$1.el('div', { - className: 'hide-reply-link', - textContent: 'Hide' - } - ); - - let apply = $$1.el('a', { - textContent: 'Apply', - href: 'javascript:;' - } - ); - $$1.on(apply, 'click', PostHiding.menu.hide); - - let thisPost = UI.checkbox('thisPost', 'This post', true); - let replies = UI.checkbox('replies', 'Hide replies', Conf['Recursive Hiding']); - const makeStub = UI.checkbox('makeStub', 'Make stub', Conf['Stubs']); - - Menu.menu.addEntry({ - el: div, - order: 20, - open(post) { - if (!post.isReply || post.isClone || post.isHidden) { - return false; - } - PostHiding.menu.post = post; - return true; - }, - subEntries: [ - {el: apply} - , - {el: thisPost} - , - {el: replies} - , - {el: makeStub} - ]}); - - // Show - div = $$1.el('div', { - className: 'show-reply-link', - textContent: 'Show' - } - ); - - apply = $$1.el('a', { - textContent: 'Apply', - href: 'javascript:;' - } - ); - $$1.on(apply, 'click', PostHiding.menu.show); - - thisPost = UI.checkbox('thisPost', 'This post', false); - replies = UI.checkbox('replies', 'Show replies', false); - const hideStubLink = $$1.el('a', { - textContent: 'Hide stub', - href: 'javascript:;' - } - ); - $$1.on(hideStubLink, 'click', PostHiding.menu.hideStub); - - Menu.menu.addEntry({ - el: div, - order: 20, - open(post) { - let data; - if (!post.isReply || post.isClone || !post.isHidden) { - return false; - } - if (!(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 = (data?.hideRecursively != null) ? data.hideRecursively : Conf['Recursive Hiding']; - return true; - }, - subEntries: [ - {el: apply} - , - {el: thisPost} - , - {el: replies} - ]}); - - return Menu.menu.addEntry({ - el: hideStubLink, - order: 15, - open(post) { - if (!post.isReply || post.isClone || !post.isHidden) { - return false; - } - if (!(PostHiding.db.get({boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}))) { - return false; - } - return PostHiding.menu.post = post; - } - }); - }, - - hide() { - const parent = this.parentNode; - const thisPost = $$1('input[name=thisPost]', parent).checked; - const replies = $$1('input[name=replies]', parent).checked; - const makeStub = $$1('input[name=makeStub]', parent).checked; - const {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); - return $$1.event('CloseMenu'); - }, - - show() { - let data; - const parent = this.parentNode; - const thisPost = $$1('input[name=thisPost]', parent).checked; - const replies = $$1('input[name=replies]', parent).checked; - const {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 && replies), !thisPost, data.makeStub, !replies); - } - return $$1.event('CloseMenu'); - }, - hideStub() { - let data; - const {post} = PostHiding.menu; - if (data = PostHiding.db.get({boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID})) { - PostHiding.show(post, data.hideRecursively); - PostHiding.hide(post, false, data.hideRecursively); - PostHiding.saveHiddenState(post, true, true, false, data.hideRecursively); - } - $$1.event('CloseMenu'); - } - }, - - makeButton(post, type) { - const span = $$1.el('span', { - textContent: type === 'hide' ? '➖︎' : '➕︎', - }); - const a = $$1.el('a', { - className: `${type}-reply-button`, - href: 'javascript:;' - } - ); - $$1.add(a, span); - $$1.on(a, 'click', PostHiding.toggle); - return a; - }, - - saveHiddenState(post, isHiding, thisPost, makeStub, hideRecursively) { - const data = { - boardID: post.board.ID, - threadID: post.thread.ID, - postID: post.ID - }; - if (isHiding) { - data.val = { - thisPost: thisPost !== false, // undefined -> true - makeStub, - hideRecursively - }; - return PostHiding.db.set(data); - } else { - return PostHiding.db.delete(data); - } - }, - - toggle() { - const post = Get$1.postFromNode(this); - PostHiding[(post.isHidden ? 'show' : 'hide')](post); - return PostHiding.saveHiddenState(post, post.isHidden); - }, - - hide(post, makeStub=Conf['Stubs'], hideRecursively=Conf['Recursive Hiding']) { - if (post.isHidden) { return; } - post.isHidden = true; - - if (hideRecursively) { - Recursive.apply(PostHiding.hide, post, makeStub, true); - Recursive.add(PostHiding.hide, post, makeStub, true); - } - - for (var quotelink of Get$1.allQuotelinksLinkingTo(post)) { - $$1.addClass(quotelink, 'filtered'); - } - - if (!makeStub) { - post.nodes.root.hidden = true; - return; - } - - const a = PostHiding.makeButton(post, 'show'); - $$1.add(a, $$1.tn(` ${post.info.nameBlock}`)); - post.nodes.stub = $$1.el('div', - {className: 'stub'}); - $$1.add(post.nodes.stub, a); - if (Conf['Menu']) { - $$1.add(post.nodes.stub, Menu.makeButton(post)); - } - return $$1.prepend(post.nodes.root, post.nodes.stub); - }, - - show(post, showRecursively=Conf['Recursive Hiding']) { - if (post.nodes.stub) { - $$1.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 (var quotelink of Get$1.allQuotelinksLinkingTo(post)) { - $$1.rmClass(quotelink, 'filtered'); - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var PostHiding = { + init() { + if (!['index', 'thread'].includes(g.VIEW) || (!Conf['Reply Hiding Buttons'] && !(Conf['Menu'] && Conf['Reply Hiding Link']))) { return; } + + if (Conf['Reply Hiding Buttons']) { + $$1.addClass(doc$1, "reply-hide"); + } + + this.db = new DataBoard('hiddenPosts'); + return Callbacks.Post.push({ + name: 'Reply Hiding', + cb: this.node + }); + }, + + isHidden(boardID, threadID, postID) { + return !!(PostHiding.db && PostHiding.db.get({boardID, threadID, postID})); + }, + + node() { + let data, sa; + if (!this.isReply || this.isClone || this.isFetchedQuote) { return; } + + if (data = PostHiding.db.get({boardID: this.board.ID, threadID: this.thread.ID, postID: this.ID})) { + if (data.thisPost) { + PostHiding.hide(this, data.makeStub, data.hideRecursively); + } else { + Recursive.apply(PostHiding.hide, this, data.makeStub, true); + Recursive.add(PostHiding.hide, this, data.makeStub, true); + } + } + + if (!Conf['Reply Hiding Buttons']) { return; } + + const button = PostHiding.makeButton(this, 'hide'); + if (sa = g.SITE.selectors.sideArrows) { + const sideArrows = $$1(sa, this.nodes.root); + $$1.replace(sideArrows.firstChild, button); + return sideArrows.className = 'replacedSideArrows'; + } else { + return $$1.prepend(this.nodes.info, button); + } + }, + + menu: { + init() { + if (!['index', 'thread'].includes(g.VIEW) || !Conf['Menu'] || !Conf['Reply Hiding Link']) { return; } + + // Hide + let div = $$1.el('div', { + className: 'hide-reply-link', + textContent: 'Hide' + } + ); + + let apply = $$1.el('a', { + textContent: 'Apply', + href: 'javascript:;' + } + ); + $$1.on(apply, 'click', PostHiding.menu.hide); + + let thisPost = UI.checkbox('thisPost', 'This post', true); + let replies = UI.checkbox('replies', 'Hide replies', Conf['Recursive Hiding']); + const makeStub = UI.checkbox('makeStub', 'Make stub', Conf['Stubs']); + + Menu.menu.addEntry({ + el: div, + order: 20, + open(post) { + if (!post.isReply || post.isClone || post.isHidden) { + return false; + } + PostHiding.menu.post = post; + return true; + }, + subEntries: [ + {el: apply} + , + {el: thisPost} + , + {el: replies} + , + {el: makeStub} + ]}); + + // Show + div = $$1.el('div', { + className: 'show-reply-link', + textContent: 'Show' + } + ); + + apply = $$1.el('a', { + textContent: 'Apply', + href: 'javascript:;' + } + ); + $$1.on(apply, 'click', PostHiding.menu.show); + + thisPost = UI.checkbox('thisPost', 'This post', false); + replies = UI.checkbox('replies', 'Show replies', false); + const hideStubLink = $$1.el('a', { + textContent: 'Hide stub', + href: 'javascript:;' + } + ); + $$1.on(hideStubLink, 'click', PostHiding.menu.hideStub); + + Menu.menu.addEntry({ + el: div, + order: 20, + open(post) { + let data; + if (!post.isReply || post.isClone || !post.isHidden) { + return false; + } + if (!(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 = (data?.hideRecursively != null) ? data.hideRecursively : Conf['Recursive Hiding']; + return true; + }, + subEntries: [ + {el: apply} + , + {el: thisPost} + , + {el: replies} + ]}); + + return Menu.menu.addEntry({ + el: hideStubLink, + order: 15, + open(post) { + if (!post.isReply || post.isClone || !post.isHidden) { + return false; + } + if (!(PostHiding.db.get({boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}))) { + return false; + } + return PostHiding.menu.post = post; + } + }); + }, + + hide() { + const parent = this.parentNode; + const thisPost = $$1('input[name=thisPost]', parent).checked; + const replies = $$1('input[name=replies]', parent).checked; + const makeStub = $$1('input[name=makeStub]', parent).checked; + const {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); + return $$1.event('CloseMenu'); + }, + + show() { + let data; + const parent = this.parentNode; + const thisPost = $$1('input[name=thisPost]', parent).checked; + const replies = $$1('input[name=replies]', parent).checked; + const {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 && replies), !thisPost, data.makeStub, !replies); + } + return $$1.event('CloseMenu'); + }, + hideStub() { + let data; + const {post} = PostHiding.menu; + if (data = PostHiding.db.get({boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID})) { + PostHiding.show(post, data.hideRecursively); + PostHiding.hide(post, false, data.hideRecursively); + PostHiding.saveHiddenState(post, true, true, false, data.hideRecursively); + } + $$1.event('CloseMenu'); + } + }, + + makeButton(post, type) { + const span = $$1.el('span', { + textContent: type === 'hide' ? '➖︎' : '➕︎', + }); + const a = $$1.el('a', { + className: `${type}-reply-button`, + href: 'javascript:;' + } + ); + $$1.add(a, span); + $$1.on(a, 'click', PostHiding.toggle); + return a; + }, + + saveHiddenState(post, isHiding, thisPost, makeStub, hideRecursively) { + const data = { + boardID: post.board.ID, + threadID: post.thread.ID, + postID: post.ID + }; + if (isHiding) { + data.val = { + thisPost: thisPost !== false, // undefined -> true + makeStub, + hideRecursively + }; + return PostHiding.db.set(data); + } else { + return PostHiding.db.delete(data); + } + }, + + toggle() { + const post = Get$1.postFromNode(this); + PostHiding[(post.isHidden ? 'show' : 'hide')](post); + return PostHiding.saveHiddenState(post, post.isHidden); + }, + + hide(post, makeStub=Conf['Stubs'], hideRecursively=Conf['Recursive Hiding']) { + if (post.isHidden) { return; } + post.isHidden = true; + + if (hideRecursively) { + Recursive.apply(PostHiding.hide, post, makeStub, true); + Recursive.add(PostHiding.hide, post, makeStub, true); + } + + for (var quotelink of Get$1.allQuotelinksLinkingTo(post)) { + $$1.addClass(quotelink, 'filtered'); + } + + if (!makeStub) { + post.nodes.root.hidden = true; + return; + } + + const a = PostHiding.makeButton(post, 'show'); + $$1.add(a, $$1.tn(` ${post.info.nameBlock}`)); + post.nodes.stub = $$1.el('div', + {className: 'stub'}); + $$1.add(post.nodes.stub, a); + if (Conf['Menu']) { + $$1.add(post.nodes.stub, Menu.makeButton(post)); + } + return $$1.prepend(post.nodes.root, post.nodes.stub); + }, + + show(post, showRecursively=Conf['Recursive Hiding']) { + if (post.nodes.stub) { + $$1.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 (var quotelink of Get$1.allQuotelinksLinkingTo(post)) { + $$1.rmClass(quotelink, 'filtered'); + } + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var RelativeDates = { - INTERVAL: 30000, - - init() { - if ( - (['index', 'thread', 'archive'].includes(g.VIEW) && Conf['Relative Post Dates'] && !Conf['Relative Date Title']) || - Index$1.enabled - ) { - this.flush(); - $$1.on(d$1, 'visibilitychange PostsInserted', this.flush); - } - - if (Conf['Relative Post Dates']) { - return Callbacks.Post.push({ - name: 'Relative Post Dates', - cb: this.node - }); - } - }, - - node() { - if (!this.info.date) { return; } - const dateEl = this.nodes.date; - if (Conf['Relative Date Title']) { - $$1.on(dateEl, 'mouseover', () => RelativeDates.hover(this)); - return; - } - if (this.isClone) { return; } - - // 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.title = dateEl.textContent; - - return RelativeDates.update(this); - }, - - // diff is milliseconds from now. - relative(diff, now, date, abbrev) { - let number; - let unit = (() => { - if ((number = (diff / DAY)) >= 1) { - const years = now.getFullYear() - date.getFullYear(); - let months = now.getMonth() - date.getMonth(); - const days = now.getDate() - date.getDate(); - if (years > 1) { - number = years - ((months < 0) || ((months === 0) && (days < 0))); - return 'year'; - } else if ((years === 1) && ((months > 0) || ((months === 0) && (days >= 0)))) { - number = years; - return 'year'; - } else if ((months = months + (12*years)) > 1) { - number = months - (days < 0); - return 'month'; - } else if ((months === 1) && (days >= 0)) { - number = months; - return 'month'; - } else { - return 'day'; - } - } else if ((number = (diff / HOUR)) >= 1) { - return 'hour'; - } else if ((number = (diff / MINUTE)) >= 1) { - return 'minute'; - } else { - // prevent "-1 seconds ago" - number = Math.max(0, diff) / SECOND; - return 'second'; - } - })(); - - const rounded = Math.round(number); - - if (abbrev) { - unit = unit === 'month' ? 'mo' : unit[0]; - } else { - if (rounded !== 1) { unit += 's'; } // pluralize - } - - if (abbrev) { return `${rounded}${unit}`; } else { return `${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. - if (d$1.hidden) { return; } - - const now = new Date(); - for (var data of RelativeDates.stale) { RelativeDates.update(data, now); } - RelativeDates.stale = []; - - // Reset automatic flush. - clearTimeout(RelativeDates.timeout); - return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL); - }, - - hover(post) { - const { - date - } = post.info; - const now = new Date(); - const diff = now - date; - return post.nodes.date.title = RelativeDates.relative(diff, now, date); - }, - - // `update()`, when called from `flush()`, updates the elements, - // and re-calls `setOwnTimeout()` to re-add `data` to the stale list later. - update(data, now) { - let abbrev, date; - const isPost = data instanceof Post; - if (isPost) { - ({ - date - } = data.info); - abbrev = false; - } else { - date = new Date(+data.dataset.utc); - abbrev = !!data.dataset.abbrev; - } - if (!now) { now = new Date(); } - const diff = now - date; - const relative = RelativeDates.relative(diff, now, date, abbrev); - if (isPost) { - for (var singlePost of [data].concat(data.clones)) { - singlePost.nodes.date.firstChild.textContent = relative; - } - } else { - data.firstChild.textContent = relative; - } - return RelativeDates.setOwnTimeout(diff, data); - }, - - setOwnTimeout(diff, data) { - const delay = diff < MINUTE ? - SECOND - ((diff + (SECOND / 2)) % SECOND) - : diff < HOUR ? - MINUTE - ((diff + (MINUTE / 2)) % MINUTE) - : diff < DAY ? - HOUR - ((diff + (HOUR / 2)) % HOUR) - : - DAY - ((diff + (DAY / 2)) % DAY); - return setTimeout(RelativeDates.markStale, delay, data); - }, - - markStale(data) { - if (RelativeDates.stale.includes(data)) { return; } // We can call RelativeDates.update() multiple times. - if (data instanceof Post && !g.posts.get(data.fullID)) { return; } // collected post. - if (data instanceof Element && !doc$1.contains(data)) { return; } // removed catalog reply. - return RelativeDates.stale.push(data); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var RelativeDates = { + INTERVAL: 30000, + + init() { + if ( + (['index', 'thread', 'archive'].includes(g.VIEW) && Conf['Relative Post Dates'] && !Conf['Relative Date Title']) || + Index$1.enabled + ) { + this.flush(); + $$1.on(d$1, 'visibilitychange PostsInserted', this.flush); + } + + if (Conf['Relative Post Dates']) { + return Callbacks.Post.push({ + name: 'Relative Post Dates', + cb: this.node + }); + } + }, + + node() { + if (!this.info.date) { return; } + const dateEl = this.nodes.date; + if (Conf['Relative Date Title']) { + $$1.on(dateEl, 'mouseover', () => RelativeDates.hover(this)); + return; + } + if (this.isClone) { return; } + + // 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.title = dateEl.textContent; + + return RelativeDates.update(this); + }, + + // diff is milliseconds from now. + relative(diff, now, date, abbrev) { + let number; + let unit = (() => { + if ((number = (diff / DAY)) >= 1) { + const years = now.getFullYear() - date.getFullYear(); + let months = now.getMonth() - date.getMonth(); + const days = now.getDate() - date.getDate(); + if (years > 1) { + number = years - ((months < 0) || ((months === 0) && (days < 0))); + return 'year'; + } else if ((years === 1) && ((months > 0) || ((months === 0) && (days >= 0)))) { + number = years; + return 'year'; + } else if ((months = months + (12*years)) > 1) { + number = months - (days < 0); + return 'month'; + } else if ((months === 1) && (days >= 0)) { + number = months; + return 'month'; + } else { + return 'day'; + } + } else if ((number = (diff / HOUR)) >= 1) { + return 'hour'; + } else if ((number = (diff / MINUTE)) >= 1) { + return 'minute'; + } else { + // prevent "-1 seconds ago" + number = Math.max(0, diff) / SECOND; + return 'second'; + } + })(); + + const rounded = Math.round(number); + + if (abbrev) { + unit = unit === 'month' ? 'mo' : unit[0]; + } else { + if (rounded !== 1) { unit += 's'; } // pluralize + } + + if (abbrev) { return `${rounded}${unit}`; } else { return `${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. + if (d$1.hidden) { return; } + + const now = new Date(); + for (var data of RelativeDates.stale) { RelativeDates.update(data, now); } + RelativeDates.stale = []; + + // Reset automatic flush. + clearTimeout(RelativeDates.timeout); + return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL); + }, + + hover(post) { + const { + date + } = post.info; + const now = new Date(); + const diff = now - date; + return post.nodes.date.title = RelativeDates.relative(diff, now, date); + }, + + // `update()`, when called from `flush()`, updates the elements, + // and re-calls `setOwnTimeout()` to re-add `data` to the stale list later. + update(data, now) { + let abbrev, date; + const isPost = data instanceof Post; + if (isPost) { + ({ + date + } = data.info); + abbrev = false; + } else { + date = new Date(+data.dataset.utc); + abbrev = !!data.dataset.abbrev; + } + if (!now) { now = new Date(); } + const diff = now - date; + const relative = RelativeDates.relative(diff, now, date, abbrev); + if (isPost) { + for (var singlePost of [data].concat(data.clones)) { + singlePost.nodes.date.firstChild.textContent = relative; + } + } else { + data.firstChild.textContent = relative; + } + return RelativeDates.setOwnTimeout(diff, data); + }, + + setOwnTimeout(diff, data) { + const delay = diff < MINUTE ? + SECOND - ((diff + (SECOND / 2)) % SECOND) + : diff < HOUR ? + MINUTE - ((diff + (MINUTE / 2)) % MINUTE) + : diff < DAY ? + HOUR - ((diff + (HOUR / 2)) % HOUR) + : + DAY - ((diff + (DAY / 2)) % DAY); + return setTimeout(RelativeDates.markStale, delay, data); + }, + + markStale(data) { + if (RelativeDates.stale.includes(data)) { return; } // We can call RelativeDates.update() multiple times. + if (data instanceof Post && !g.posts.get(data.fullID)) { return; } // collected post. + if (data instanceof Element && !doc$1.contains(data)) { return; } // removed catalog reply. + return RelativeDates.stale.push(data); + } }; - var ThreadWatcherPage = `
- Thread Watcher 🗘 - - 🞃 - × -
-
+ var ThreadWatcherPage = `
+ Thread Watcher 🗘 + + 🞃 + × +
+
`; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS205: Consider reworking code to avoid use of IIFEs - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var BoardConfig = { - cbs: [], - - init() { - let middle; - if (g.SITE.software !== 'yotsuba') { return; } - const now = Date.now(); - if (now - (2 * HOUR) >= ((middle = Conf['boardConfig'].lastChecked || 0)) || middle > now) { - return $$1.ajax(`${location.protocol}//a.4cdn.org/boards.json`, - {onloadend: this.load}); - } else { - const {boards} = Conf['boardConfig']; - return this.set(boards); - } - }, - - load() { - let boards; - if ((this.status === 200) && this.response && this.response.boards) { - boards = dict(); - for (var board of this.response.boards) { - boards[board.board] = board; - } - $$1.set('boardConfig', {boards, lastChecked: Date.now()}); - } else { - ({boards} = Conf['boardConfig']); - const err = (() => { switch (this.status) { - case 0: return 'Connection Error'; - case 200: return 'Invalid Data'; - default: return `Error ${this.statusText} (${this.status})`; - } })(); - new Notice('warning', `Failed to load board configuration. ${err}`, 20); - } - return BoardConfig.set(boards); - }, - - set(boards) { - this.boards = boards; - for (var ID in g.boards) { - var board = g.boards[ID]; - board.config = this.boards[ID] || {}; - } - for (var cb of this.cbs) { - $$1.queueTask(cb); - } - }, - - ready(cb) { - if (this.boards) { - return cb(); - } else { - return this.cbs.push(cb); - } - }, - - sfwBoards(sfw) { - return (() => { - const result = []; - const object = this.boards || Conf['boardConfig'].boards; - for (var board in object) { - var data = object[board]; - if (!!data.ws_board === sfw) { - result.push(board); - } - } - return result; - })(); - }, - - isSFW(board) { - return !!(this.boards || Conf['boardConfig'].boards)[board]?.ws_board; - }, - - domain(board) { - return `boards.${BoardConfig.isSFW(board) ? '4channel' : '4chan'}.org`; - }, - - isArchived(board) { - // assume archive exists if no data available to prevent cleaning of archived threads - const data = (this.boards || Conf['boardConfig'].boards)[board]; - return !data || data.is_archived; - }, - - noAudio(boardID) { - if (g.SITE.software !== 'yotsuba') { return false; } - const boards = this.boards || Conf['boardConfig'].boards; - return boards && boards[boardID] && !boards[boardID].webm_audio; - }, - - title(boardID) { - return (this.boards || Conf['boardConfig'].boards)?.[boardID]?.title || ''; - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var BoardConfig = { + cbs: [], + + init() { + let middle; + if (g.SITE.software !== 'yotsuba') { return; } + const now = Date.now(); + if (now - (2 * HOUR) >= ((middle = Conf['boardConfig'].lastChecked || 0)) || middle > now) { + return $$1.ajax(`${location.protocol}//a.4cdn.org/boards.json`, + {onloadend: this.load}); + } else { + const {boards} = Conf['boardConfig']; + return this.set(boards); + } + }, + + load() { + let boards; + if ((this.status === 200) && this.response && this.response.boards) { + boards = dict(); + for (var board of this.response.boards) { + boards[board.board] = board; + } + $$1.set('boardConfig', {boards, lastChecked: Date.now()}); + } else { + ({boards} = Conf['boardConfig']); + const err = (() => { switch (this.status) { + case 0: return 'Connection Error'; + case 200: return 'Invalid Data'; + default: return `Error ${this.statusText} (${this.status})`; + } })(); + new Notice('warning', `Failed to load board configuration. ${err}`, 20); + } + return BoardConfig.set(boards); + }, + + set(boards) { + this.boards = boards; + for (var ID in g.boards) { + var board = g.boards[ID]; + board.config = this.boards[ID] || {}; + } + for (var cb of this.cbs) { + $$1.queueTask(cb); + } + }, + + ready(cb) { + if (this.boards) { + return cb(); + } else { + return this.cbs.push(cb); + } + }, + + sfwBoards(sfw) { + return (() => { + const result = []; + const object = this.boards || Conf['boardConfig'].boards; + for (var board in object) { + var data = object[board]; + if (!!data.ws_board === sfw) { + result.push(board); + } + } + return result; + })(); + }, + + isSFW(board) { + return !!(this.boards || Conf['boardConfig'].boards)[board]?.ws_board; + }, + + domain(board) { + return `boards.${BoardConfig.isSFW(board) ? '4channel' : '4chan'}.org`; + }, + + isArchived(board) { + // assume archive exists if no data available to prevent cleaning of archived threads + const data = (this.boards || Conf['boardConfig'].boards)[board]; + return !data || data.is_archived; + }, + + noAudio(boardID) { + if (g.SITE.software !== 'yotsuba') { return false; } + const boards = this.boards || Conf['boardConfig'].boards; + return boards && boards[boardID] && !boards[boardID].webm_audio; + }, + + title(boardID) { + return (this.boards || Conf['boardConfig'].boards)?.[boardID]?.title || ''; + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - class Board { - toString() { return this.ID; } - - constructor(ID) { - this.ID = ID; - this.boardID = this.ID; - this.siteID = g.SITE.ID; - this.threads = new SimpleDict(); - this.posts = new SimpleDict(); - this.config = BoardConfig.boards?.[this.ID] || {}; - - g.boards[this] = this; - } - - cooldowns() { - const c2 = (this.config || {}).cooldowns || {}; - const c = { - thread: c2.threads || 0, - reply: c2.replies || 0, - image: c2.images || 0, - thread_global: 300 // inter-board thread cooldown - }; - // Pass users have reduced cooldowns. - if (d$1.cookie.indexOf('pass_enabled=1') >= 0) { - for (var key of ['reply', 'image']) { - c[key] = Math.ceil(c[key] / 2); - } - } - return c; - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + class Board { + toString() { return this.ID; } + + constructor(ID) { + this.ID = ID; + this.boardID = this.ID; + this.siteID = g.SITE.ID; + this.threads = new SimpleDict(); + this.posts = new SimpleDict(); + this.config = BoardConfig.boards?.[this.ID] || {}; + + g.boards[this] = this; + } + + cooldowns() { + const c2 = (this.config || {}).cooldowns || {}; + const c = { + thread: c2.threads || 0, + reply: c2.replies || 0, + image: c2.images || 0, + thread_global: 300 // inter-board thread cooldown + }; + // Pass users have reduced cooldowns. + if (d$1.cookie.indexOf('pass_enabled=1') >= 0) { + for (var key of ['reply', 'image']) { + c[key] = Math.ceil(c[key] / 2); + } + } + return c; + } } - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - const PostRedirect = { - init() { - return $$1.on(d$1, 'QRPostSuccessful', e => { - if (!e.detail.redirect) { return; } - this.event = e; - this.delays = 0; - return $$1.queueTask(() => { - if ((e === this.event) && (this.delays === 0)) { - return location.href = e.detail.redirect; - } - }); - }); - }, - - delays: 0, - - delay() { - if (!this.event) { return null; } - const e = this.event; - this.delays++; - return () => { - if (e !== this.event) { return; } - this.delays--; - if (this.delays === 0) { - return location.href = e.detail.redirect; - } - }; - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + const PostRedirect = { + init() { + return $$1.on(d$1, 'QRPostSuccessful', e => { + if (!e.detail.redirect) { return; } + this.event = e; + this.delays = 0; + return $$1.queueTask(() => { + if ((e === this.event) && (this.delays === 0)) { + return location.href = e.detail.redirect; + } + }); + }); + }, + + delays: 0, + + delay() { + if (!this.event) { return null; } + const e = this.event; + this.delays++; + return () => { + if (e !== this.event) { return; } + this.delays--; + if (this.delays === 0) { + return location.href = e.detail.redirect; + } + }; + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var ExpandComment = { - init() { - if ((g.VIEW !== 'index') || !Conf['Comment Expansion'] || Conf['JSON Index']) { return; } - - return Callbacks.Post.push({ - name: 'Comment Expansion', - cb: this.node - }); - }, - - node() { - let a; - if (a = $$1('.abbr > a:not([onclick])', this.nodes.comment)) { - return $$1.on(a, 'click', ExpandComment.cb); - } - }, - - callbacks: [], - - cb(e) { - e.preventDefault(); - return ExpandComment.expand(Get$1.postFromNode(this)); - }, - - expand(post) { - let a; - if (post.nodes.longComment && !post.nodes.longComment.parentNode) { - $$1.replace(post.nodes.shortComment, post.nodes.longComment); - post.nodes.comment = post.nodes.longComment; - return; - } - if (!(a = $$1('.abbr > a', post.nodes.comment))) { return; } - a.textContent = `Post No.${post} Loading...`; - return $$1.cache(g.SITE.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), function() { return ExpandComment.parse(this, a, post); }); - }, - - contract(post) { - if (!post.nodes.shortComment) { return; } - const a = $$1('.abbr > a', post.nodes.shortComment); - a.textContent = 'here'; - $$1.replace(post.nodes.longComment, post.nodes.shortComment); - return post.nodes.comment = post.nodes.shortComment; - }, - - parse(req, a, post) { - let postObj, spoilerRange; - const {status} = req; - if (![200, 304].includes(status)) { - a.textContent = status ? `Error ${req.statusText} (${status})` : 'Connection Error'; - return; - } - - const { - posts - } = req.response; - if (spoilerRange = posts[0].custom_spoiler) { - g.SITE.Build.spoilerRange[g.BOARD] = spoilerRange; - } - - for (postObj of posts) { - if (postObj.no === post.ID) { break; } - } - if (postObj.no !== post.ID) { - a.textContent = `Post No.${post} not found.`; - return; - } - - const {comment} = post.nodes; - const clone = comment.cloneNode(false); - clone.innerHTML = postObj.com; - // Fix pathnames - for (var quote of $$('.quotelink', clone)) { - var href = quote.getAttribute('href'); - if (href[0] === '/') { continue; } // Cross-board quote, or board link - if (href[0] === '#') { - quote.href = `${a.pathname.split(/\/+/).splice(0,4).join('/')}${href}`; - } else { - quote.href = `${a.pathname.split(/\/+/).splice(0,3).join('/')}/${href}`; - } - } - post.nodes.shortComment = comment; - $$1.replace(comment, clone); - post.nodes.comment = (post.nodes.longComment = clone); - post.parseComment(); - post.parseQuotes(); - - for (var callback of ExpandComment.callbacks) { - callback.call(post); - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var ExpandComment = { + init() { + if ((g.VIEW !== 'index') || !Conf['Comment Expansion'] || Conf['JSON Index']) { return; } + + return Callbacks.Post.push({ + name: 'Comment Expansion', + cb: this.node + }); + }, + + node() { + let a; + if (a = $$1('.abbr > a:not([onclick])', this.nodes.comment)) { + return $$1.on(a, 'click', ExpandComment.cb); + } + }, + + callbacks: [], + + cb(e) { + e.preventDefault(); + return ExpandComment.expand(Get$1.postFromNode(this)); + }, + + expand(post) { + let a; + if (post.nodes.longComment && !post.nodes.longComment.parentNode) { + $$1.replace(post.nodes.shortComment, post.nodes.longComment); + post.nodes.comment = post.nodes.longComment; + return; + } + if (!(a = $$1('.abbr > a', post.nodes.comment))) { return; } + a.textContent = `Post No.${post} Loading...`; + return $$1.cache(g.SITE.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), function() { return ExpandComment.parse(this, a, post); }); + }, + + contract(post) { + if (!post.nodes.shortComment) { return; } + const a = $$1('.abbr > a', post.nodes.shortComment); + a.textContent = 'here'; + $$1.replace(post.nodes.longComment, post.nodes.shortComment); + return post.nodes.comment = post.nodes.shortComment; + }, + + parse(req, a, post) { + let postObj, spoilerRange; + const {status} = req; + if (![200, 304].includes(status)) { + a.textContent = status ? `Error ${req.statusText} (${status})` : 'Connection Error'; + return; + } + + const { + posts + } = req.response; + if (spoilerRange = posts[0].custom_spoiler) { + g.SITE.Build.spoilerRange[g.BOARD] = spoilerRange; + } + + for (postObj of posts) { + if (postObj.no === post.ID) { break; } + } + if (postObj.no !== post.ID) { + a.textContent = `Post No.${post} not found.`; + return; + } + + const {comment} = post.nodes; + const clone = comment.cloneNode(false); + clone.innerHTML = postObj.com; + // Fix pathnames + for (var quote of $$('.quotelink', clone)) { + var href = quote.getAttribute('href'); + if (href[0] === '/') { continue; } // Cross-board quote, or board link + if (href[0] === '#') { + quote.href = `${a.pathname.split(/\/+/).splice(0,4).join('/')}${href}`; + } else { + quote.href = `${a.pathname.split(/\/+/).splice(0,3).join('/')}/${href}`; + } + } + post.nodes.shortComment = comment; + $$1.replace(comment, clone); + post.nodes.comment = (post.nodes.longComment = clone); + post.parseComment(); + post.parseQuotes(); + + for (var callback of ExpandComment.callbacks) { + callback.call(post); + } + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var QuoteYou = { - init() { - if (!Conf['Remember Your Posts']) { return; } - - this.db = new DataBoard('yourPosts'); - $$1.sync('Remember Your Posts', enabled => Conf['Remember Your Posts'] = enabled); - $$1.on(d$1, 'QRPostSuccessful', function(e) { - const cb = PostRedirect.delay(); - return $$1.get('Remember Your Posts', Conf['Remember Your Posts'], function(items) { - if (!items['Remember Your Posts']) { return; } - const {boardID, threadID, postID} = e.detail; - return QuoteYou.db.set({boardID, threadID, postID, val: true}, cb); - }); - }); - - if (!['index', 'thread', 'archive'].includes(g.VIEW)) { return; } - - if (Conf['Highlight Own Posts']) { - $$1.addClass(doc$1, 'highlight-own'); - } - - if (Conf['Highlight Posts Quoting You']) { - $$1.addClass(doc$1, 'highlight-you'); - } - - if (Conf['Comment Expansion']) { - ExpandComment.callbacks.push(this.node); - } - - // \u00A0 is nbsp - this.mark = $$1.el('span', { - textContent: '\u00A0(You)', - className: 'qmark-you' - } - ); - Callbacks.Post.push({ - name: 'Mark Quotes of You', - cb: this.node - }); - - return QuoteYou.menu.init(); - }, - - isYou(post) { - return !!QuoteYou.db?.get({ - boardID: post.boardID, - threadID: post.threadID, - postID: post.ID - }); - }, - - node() { - if (this.isClone) { return; } - - if (QuoteYou.isYou(this)) { - $$1.addClass(this.nodes.root, 'yourPost'); - } - - // Stop there if there's no quotes in that post. - if (!this.quotes.length) { return; } - - for (var quotelink of this.nodes.quotelinks) { - if (QuoteYou.db.get(Get$1.postDataFromLink(quotelink))) { - if (Conf['Mark Quotes of You']) { $$1.add(quotelink, QuoteYou.mark.cloneNode(true)); } - $$1.addClass(quotelink, 'you'); - $$1.addClass(this.nodes.root, 'quotesYou'); - } - } - }, - - menu: { - init() { - const label = $$1.el('label', - {className: 'toggle-you'} - , - {innerHTML: ' You'}); - const input = $$1('input', label); - $$1.on(input, 'change', QuoteYou.menu.toggle); - return Menu.menu?.addEntry({ - el: label, - order: 80, - open(post) { - QuoteYou.menu.post = (post.origin || post); - input.checked = QuoteYou.isYou(post); - return true; - } - }); - }, - - toggle() { - const {post} = QuoteYou.menu; - const data = {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID, val: true}; - if (this.checked) { - QuoteYou.db.set(data); - } else { - QuoteYou.db.delete(data); - } - for (var clone of [post].concat(post.clones)) { - clone.nodes.root.classList.toggle('yourPost', this.checked); - } - for (var quotelink of Get$1.allQuotelinksLinkingTo(post)) { - if (this.checked) { - if (Conf['Mark Quotes of You']) { $$1.add(quotelink, QuoteYou.mark.cloneNode(true)); } - } else { - $$1.rm($$1('.qmark-you', quotelink)); - } - quotelink.classList.toggle('you', this.checked); - if ($$1.hasClass(quotelink, 'quotelink')) { - var quoter = Get$1.postFromNode(quotelink).nodes.root; - quoter.classList.toggle('quotesYou', !!$$1('.quotelink.you', quoter)); - } - } - } - }, - - cb: { - seek(type) { - let highlighted, post; - let result; - const {highlight} = g.SITE.classes; - if (highlighted = $$1(`.${highlight}`)) { $$1.rmClass(highlighted, highlight); } - - if (!QuoteYou.lastRead || !doc$1.contains(QuoteYou.lastRead) || !$$1.hasClass(QuoteYou.lastRead, 'quotesYou')) { - if (!(post = (QuoteYou.lastRead = $$1('.quotesYou')))) { - new Notice('warning', 'No posts are currently quoting you, loser.', 20); - return; - } - if (QuoteYou.cb.scroll(post)) { return; } - } else { - post = QuoteYou.lastRead; - } - - const str = `${type}::div[contains(@class,'quotesYou')]`; - - while (post = (result = $$1.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0)) { - if (QuoteYou.cb.scroll(post)) { return; } - } - - const posts = $$('.quotesYou'); - return QuoteYou.cb.scroll(posts[type === 'following' ? 0 : posts.length - 1]); - }, - - scroll(root) { - const post = Get$1.postFromRoot(root); - if (!post.nodes.post.getBoundingClientRect().height) { - return false; - } else { - QuoteYou.lastRead = root; - location.href = Get$1.url('post', post); - Header$1.scrollTo(post.nodes.post); - if (post.isReply) { - const sel = `${g.SITE.selectors.postContainer}${g.SITE.selectors.highlightable.reply}`; - let node = post.nodes.root; - if (!node.matches(sel)) { node = $$1(sel, node); } - $$1.addClass(node, g.SITE.classes.highlight); - } - return true; - } - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var QuoteYou = { + init() { + if (!Conf['Remember Your Posts']) { return; } + + this.db = new DataBoard('yourPosts'); + $$1.sync('Remember Your Posts', enabled => Conf['Remember Your Posts'] = enabled); + $$1.on(d$1, 'QRPostSuccessful', function(e) { + const cb = PostRedirect.delay(); + return $$1.get('Remember Your Posts', Conf['Remember Your Posts'], function(items) { + if (!items['Remember Your Posts']) { return; } + const {boardID, threadID, postID} = e.detail; + return QuoteYou.db.set({boardID, threadID, postID, val: true}, cb); + }); + }); + + if (!['index', 'thread', 'archive'].includes(g.VIEW)) { return; } + + if (Conf['Highlight Own Posts']) { + $$1.addClass(doc$1, 'highlight-own'); + } + + if (Conf['Highlight Posts Quoting You']) { + $$1.addClass(doc$1, 'highlight-you'); + } + + if (Conf['Comment Expansion']) { + ExpandComment.callbacks.push(this.node); + } + + // \u00A0 is nbsp + this.mark = $$1.el('span', { + textContent: '\u00A0(You)', + className: 'qmark-you' + } + ); + Callbacks.Post.push({ + name: 'Mark Quotes of You', + cb: this.node + }); + + return QuoteYou.menu.init(); + }, + + isYou(post) { + return !!QuoteYou.db?.get({ + boardID: post.boardID, + threadID: post.threadID, + postID: post.ID + }); + }, + + node() { + if (this.isClone) { return; } + + if (QuoteYou.isYou(this)) { + $$1.addClass(this.nodes.root, 'yourPost'); + } + + // Stop there if there's no quotes in that post. + if (!this.quotes.length) { return; } + + for (var quotelink of this.nodes.quotelinks) { + if (QuoteYou.db.get(Get$1.postDataFromLink(quotelink))) { + if (Conf['Mark Quotes of You']) { $$1.add(quotelink, QuoteYou.mark.cloneNode(true)); } + $$1.addClass(quotelink, 'you'); + $$1.addClass(this.nodes.root, 'quotesYou'); + } + } + }, + + menu: { + init() { + const label = $$1.el('label', + {className: 'toggle-you'} + , + {innerHTML: ' You'}); + const input = $$1('input', label); + $$1.on(input, 'change', QuoteYou.menu.toggle); + return Menu.menu?.addEntry({ + el: label, + order: 80, + open(post) { + QuoteYou.menu.post = (post.origin || post); + input.checked = QuoteYou.isYou(post); + return true; + } + }); + }, + + toggle() { + const {post} = QuoteYou.menu; + const data = {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID, val: true}; + if (this.checked) { + QuoteYou.db.set(data); + } else { + QuoteYou.db.delete(data); + } + for (var clone of [post].concat(post.clones)) { + clone.nodes.root.classList.toggle('yourPost', this.checked); + } + for (var quotelink of Get$1.allQuotelinksLinkingTo(post)) { + if (this.checked) { + if (Conf['Mark Quotes of You']) { $$1.add(quotelink, QuoteYou.mark.cloneNode(true)); } + } else { + $$1.rm($$1('.qmark-you', quotelink)); + } + quotelink.classList.toggle('you', this.checked); + if ($$1.hasClass(quotelink, 'quotelink')) { + var quoter = Get$1.postFromNode(quotelink).nodes.root; + quoter.classList.toggle('quotesYou', !!$$1('.quotelink.you', quoter)); + } + } + } + }, + + cb: { + seek(type) { + let highlighted, post; + let result; + const {highlight} = g.SITE.classes; + if (highlighted = $$1(`.${highlight}`)) { $$1.rmClass(highlighted, highlight); } + + if (!QuoteYou.lastRead || !doc$1.contains(QuoteYou.lastRead) || !$$1.hasClass(QuoteYou.lastRead, 'quotesYou')) { + if (!(post = (QuoteYou.lastRead = $$1('.quotesYou')))) { + new Notice('warning', 'No posts are currently quoting you, loser.', 20); + return; + } + if (QuoteYou.cb.scroll(post)) { return; } + } else { + post = QuoteYou.lastRead; + } + + const str = `${type}::div[contains(@class,'quotesYou')]`; + + while (post = (result = $$1.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0)) { + if (QuoteYou.cb.scroll(post)) { return; } + } + + const posts = $$('.quotesYou'); + return QuoteYou.cb.scroll(posts[type === 'following' ? 0 : posts.length - 1]); + }, + + scroll(root) { + const post = Get$1.postFromRoot(root); + if (!post.nodes.post.getBoundingClientRect().height) { + return false; + } else { + QuoteYou.lastRead = root; + location.href = Get$1.url('post', post); + Header$1.scrollTo(post.nodes.post); + if (post.isReply) { + const sel = `${g.SITE.selectors.postContainer}${g.SITE.selectors.highlightable.reply}`; + let node = post.nodes.root; + if (!node.matches(sel)) { node = $$1(sel, node); } + $$1.addClass(node, g.SITE.classes.highlight); + } + return true; + } + } + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - class RandomAccessList { - constructor(items) { - this.length = 0; - if (items) { for (var item of items) { this.push(item); } } - } - - push(data) { - let item; - let {ID} = data; - if (!ID) { ID = data.id; } - if (this[ID]) { return; } - const {last} = this; - this[ID] = (item = { - prev: last, - next: null, - data, - ID - }); - item.prev = last; - this.last = last ? - (last.next = item) - : - (this.first = item); - return this.length++; - } - - before(root, item) { - if ((item.next === root) || (item === root)) { return; } - - this.rmi(item); - - const {prev} = root; - root.prev = item; - item.next = root; - item.prev = prev; - if (prev) { - return prev.next = item; - } else { - return this.first = item; - } - } - - after(root, item) { - if ((item.prev === root) || (item === root)) { return; } - - this.rmi(item); - - const {next} = root; - root.next = item; - item.prev = root; - item.next = next; - if (next) { - return next.prev = item; - } else { - return this.last = item; - } - } - - prepend(item) { - const {first} = this; - if ((item === first) || !this[item.ID]) { return; } - this.rmi(item); - item.next = first; - if (first) { - first.prev = item; - } else { - this.last = item; - } - this.first = item; - return delete item.prev; - } - - shift() { - return this.rm(this.first.ID); - } - - order() { - let item; - const order = [(item = this.first)]; - while ((item = item.next)) { order.push(item); } - return order; - } - - rm(ID) { - const item = this[ID]; - if (!item) { return; } - delete this[ID]; - this.length--; - this.rmi(item); - delete item.next; - return delete item.prev; - } - - rmi(item) { - const {prev, next} = item; - if (prev) { - prev.next = next; - } else { - this.first = next; - } - if (next) { - return next.prev = prev; - } else { - return this.last = prev; - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + class RandomAccessList { + constructor(items) { + this.length = 0; + if (items) { for (var item of items) { this.push(item); } } + } + + push(data) { + let item; + let {ID} = data; + if (!ID) { ID = data.id; } + if (this[ID]) { return; } + const {last} = this; + this[ID] = (item = { + prev: last, + next: null, + data, + ID + }); + item.prev = last; + this.last = last ? + (last.next = item) + : + (this.first = item); + return this.length++; + } + + before(root, item) { + if ((item.next === root) || (item === root)) { return; } + + this.rmi(item); + + const {prev} = root; + root.prev = item; + item.next = root; + item.prev = prev; + if (prev) { + return prev.next = item; + } else { + return this.first = item; + } + } + + after(root, item) { + if ((item.prev === root) || (item === root)) { return; } + + this.rmi(item); + + const {next} = root; + root.next = item; + item.prev = root; + item.next = next; + if (next) { + return next.prev = item; + } else { + return this.last = item; + } + } + + prepend(item) { + const {first} = this; + if ((item === first) || !this[item.ID]) { return; } + this.rmi(item); + item.next = first; + if (first) { + first.prev = item; + } else { + this.last = item; + } + this.first = item; + return delete item.prev; + } + + shift() { + return this.rm(this.first.ID); + } + + order() { + let item; + const order = [(item = this.first)]; + while ((item = item.next)) { order.push(item); } + return order; + } + + rm(ID) { + const item = this[ID]; + if (!item) { return; } + delete this[ID]; + this.length--; + this.rmi(item); + delete item.next; + return delete item.prev; + } + + rmi(item) { + const {prev, next} = item; + if (prev) { + prev.next = next; + } else { + this.first = next; + } + if (next) { + return next.prev = prev; + } else { + return this.last = prev; + } + } } - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var Unread = { - init() { - if ((g.VIEW !== 'thread') || ( - !Conf['Unread Count'] && - !Conf['Unread Favicon'] && - !Conf['Unread Line'] && - !Conf['Remember Last Read Post'] && - !Conf['Desktop Notifications'] && - !Conf['Quote Threading'] - )) { return; } - - if (Conf['Remember Last Read Post']) { - $$1.sync('Remember Last Read Post', enabled => Conf['Remember Last Read Post'] = enabled); - this.db = new DataBoard('lastReadPosts', this.sync); - } - - this.hr = $$1.el('hr', { - id: 'unread-line', - className: 'unread-line' - } - ); - this.posts = new Set(); - this.postsQuotingYou = new Set(); - this.order = new RandomAccessList(); - this.position = null; - - Callbacks.Thread.push({ - name: 'Unread', - cb: this.node - }); - - return Callbacks.Post.push({ - name: 'Unread', - cb: this.addPost - }); - }, - - node() { - Unread.thread = this; - Unread.title = d$1.title; - Unread.lastReadPost = Unread.db?.get({ - boardID: this.board.ID, - threadID: this.ID - }) || 0; - Unread.readCount = 0; - for (var ID of this.posts.keys) { if (+ID <= Unread.lastReadPost) { Unread.readCount++; } } - $$1.one(d$1, '4chanXInitFinished', Unread.ready); - $$1.on(d$1, 'PostsInserted', Unread.onUpdate); - $$1.on(d$1, 'ThreadUpdate', function(e) { if (e.detail[404]) { return Unread.update(); } }); - const resetLink = $$1.el('a', { - href: 'javascript:;', - className: 'unread-reset', - textContent: 'Mark all unread' - } - ); - $$1.on(resetLink, 'click', Unread.reset); - return Header$1.menu.addEntry({ - el: resetLink, - order: 70 - }); - }, - - ready() { - if (Conf['Remember Last Read Post'] && Conf['Scroll to Last Read Post']) { Unread.scroll(); } - Unread.setLine(true); - Unread.read(); - Unread.update(); - $$1.on(d$1, 'scroll visibilitychange', Unread.read); - if (Conf['Unread Line']) { return $$1.on(d$1, 'visibilitychange', Unread.setLine); } - }, - - positionPrev() { - if (Unread.position) { return Unread.position.prev; } else { return Unread.order.last; } - }, - - scroll() { - // Let the header's onload callback handle it. - let hash; - if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) { return; } - - let position = Unread.positionPrev(); - while (position) { - var {bottom} = position.data.nodes; - if (!bottom.getBoundingClientRect().height) { - // Don't try to scroll to posts with display: none - position = position.prev; - } else { - Header$1.scrollToIfNeeded(bottom, true); - break; - } - } - }, - - reset() { - if (Unread.lastReadPost == null) { return; } - - Unread.posts = new Set(); - Unread.postsQuotingYou = new Set(); - Unread.order = new RandomAccessList(); - Unread.position = null; - Unread.lastReadPost = 0; - Unread.readCount = 0; - Unread.thread.posts.forEach(post => Unread.addPost.call(post)); - - $$1.forceSync('Remember Last Read Post'); - if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) { - Unread.db.set({ - boardID: Unread.thread.board.ID, - threadID: Unread.thread.ID, - val: 0 - }); - } - - Unread.updatePosition(); - Unread.setLine(); - return Unread.update(); - }, - - sync() { - if (Unread.lastReadPost == null) { return; } - const lastReadPost = Unread.db.get({ - boardID: Unread.thread.board.ID, - threadID: Unread.thread.ID, - defaultValue: 0 - }); - if (Unread.lastReadPost >= lastReadPost) { return; } - Unread.lastReadPost = lastReadPost; - - const postIDs = Unread.thread.posts.keys; - for (let i = Unread.readCount, end = postIDs.length; i < end; i++) { - var ID = +postIDs[i]; - if (!Unread.thread.posts.get(ID).isFetchedQuote) { - if (ID > Unread.lastReadPost) { break; } - Unread.posts.delete(ID); - Unread.postsQuotingYou.delete(ID); - } - Unread.readCount++; - } - - Unread.updatePosition(); - Unread.setLine(); - return Unread.update(); - }, - - addPost() { - if (this.isFetchedQuote || this.isClone) { return; } - Unread.order.push(this); - if ((this.ID <= Unread.lastReadPost) || this.isHidden || QuoteYou.isYou(this)) { return; } - Unread.posts.add((Unread.posts.last = this.ID)); - Unread.addPostQuotingYou(this); - return Unread.position != null ? Unread.position : (Unread.position = Unread.order[this.ID]); - }, - - addPostQuotingYou(post) { - for (var quotelink of post.nodes.quotelinks) { - if (QuoteYou.db?.get(Get$1.postDataFromLink(quotelink))) { - Unread.postsQuotingYou.add((Unread.postsQuotingYou.last = post.ID)); - Unread.openNotification(post); - return; - } - } - }, - - openNotification(post, predicate=' replied to you') { - if (!Header$1.areNotificationsEnabled) { return; } - const notif = new Notification(`${post.info.nameBlock}${predicate}`, { - body: post.commentDisplay(), - icon: Favicon.logo - } - ); - notif.onclick = function() { - Header$1.scrollToIfNeeded(post.nodes.bottom, true); - return window.focus(); - }; - return notif.onshow = () => setTimeout(() => notif.close() - , 7 * SECOND); - }, - - onUpdate() { - return $$1.queueTask(function() { // ThreadUpdater may scroll immediately after inserting posts - Unread.setLine(); - Unread.read(); - return Unread.update(); - }); - }, - - readSinglePost(post) { - const {ID} = post; - if (!Unread.posts.has(ID)) { return; } - Unread.posts.delete(ID); - Unread.postsQuotingYou.delete(ID); - Unread.updatePosition(); - Unread.saveLastReadPost(); - return Unread.update(); - }, - - read: debounce(100, function(e) { - // Update the lastReadPost when hidden posts are added to the thread. - if (!Unread.posts.size && (Unread.readCount !== Unread.thread.posts.keys.length)) { - Unread.saveLastReadPost(); - } - - if (d$1.hidden || !Unread.posts.size) { return; } - - let count = 0; - while (Unread.position) { - var {ID, data} = Unread.position; - var {bottom} = data.nodes; - if (!!bottom.getBoundingClientRect().height && // post has been hidden - (Header$1.getBottomOf(bottom) <= -1)) { break; } // post is completely read - count++; - Unread.posts.delete(ID); - Unread.postsQuotingYou.delete(ID); - Unread.position = Unread.position.next; - } - - if (!count) { return; } - Unread.updatePosition(); - Unread.saveLastReadPost(); - if (e) { return Unread.update(); } - }), - - updatePosition() { - while (Unread.position && !Unread.posts.has(Unread.position.ID)) { - Unread.position = Unread.position.next; - } - }, - - saveLastReadPost: debounce(2 * SECOND, function() { - let ID; - $$1.forceSync('Remember Last Read Post'); - if (!Conf['Remember Last Read Post'] || !Unread.db) { return; } - const postIDs = Unread.thread.posts.keys; - for (let i = Unread.readCount, end = postIDs.length; i < end; i++) { - ID = +postIDs[i]; - if (!Unread.thread.posts.get(ID).isFetchedQuote) { - if (Unread.posts.has(ID)) { break; } - Unread.lastReadPost = ID; - } - Unread.readCount++; - } - if (Unread.thread.isDead && !Unread.thread.isArchived) { return; } - return Unread.db.set({ - boardID: Unread.thread.board.ID, - threadID: Unread.thread.ID, - val: Unread.lastReadPost - }); - }), - - setLine(force) { - if (!Conf['Unread Line']) { return; } - if (Unread.hr.hidden || d$1.hidden || (force === true)) { - const oldPosition = Unread.linePosition; - if (Unread.linePosition = Unread.positionPrev()) { - if (Unread.linePosition !== oldPosition) { - let node = Unread.linePosition.data.nodes.bottom; - if (node.nextSibling?.tagName === 'BR') { node = node.nextSibling; } - $$1.after(node, Unread.hr); - } - } else { - $$1.rm(Unread.hr); - } - } - return Unread.hr.hidden = Unread.linePosition === Unread.order.last; - }, - - update() { - const count = Unread.posts.size; - const countQuotingYou = Unread.postsQuotingYou.size; - - if (Conf['Unread Count']) { - const titleQuotingYou = Conf['Quoted Title'] && countQuotingYou ? '(!) ' : ''; - const titleCount = count || !Conf['Hide Unread Count at (0)'] ? `(${count}) ` : ''; - const titleDead = Unread.thread.isDead ? - Unread.title.replace('-', (Unread.thread.isArchived ? '- Archived -' : '- 404 -')) - : - Unread.title; - d$1.title = `${titleQuotingYou}${titleCount}${titleDead}`; - } - - Unread.saveThreadWatcherCount(); - - if (Conf['Unread Favicon'] && (g.SITE.software === 'yotsuba')) { - const {isDead} = Unread.thread; - return Favicon.set(( - countQuotingYou ? - (isDead ? 'unreadDeadY' : 'unreadY') - : count ? - (isDead ? 'unreadDead' : 'unread') - : - (isDead ? 'dead' : 'default') - ) - ); - } - }, - - saveThreadWatcherCount: debounce(2 * SECOND, function() { - $$1.forceSync('Remember Last Read Post'); - if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) { - let posts; - const quotingYou = !Conf['Require OP Quote Link'] && QuoteYou.isYou(Unread.thread.OP) ? Unread.posts : Unread.postsQuotingYou; - if (!quotingYou.size) { - quotingYou.last = 0; - } else if (!quotingYou.has(quotingYou.last)) { - quotingYou.last = 0; - posts = Unread.thread.posts.keys; - for (let i = posts.length - 1; i >= 0; i--) { - if (quotingYou.has(+posts[i])) { - quotingYou.last = posts[i]; - break; - } - } - } - return ThreadWatcher$1.update(g.SITE.ID, Unread.thread.board.ID, Unread.thread.ID, { - last: Unread.thread.lastPost, - isDead: Unread.thread.isDead, - isArchived: Unread.thread.isArchived, - unread: Unread.posts.size, - quotingYou: (quotingYou.last || 0) - } - ); - } - }) + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var Unread = { + init() { + if ((g.VIEW !== 'thread') || ( + !Conf['Unread Count'] && + !Conf['Unread Favicon'] && + !Conf['Unread Line'] && + !Conf['Remember Last Read Post'] && + !Conf['Desktop Notifications'] && + !Conf['Quote Threading'] + )) { return; } + + if (Conf['Remember Last Read Post']) { + $$1.sync('Remember Last Read Post', enabled => Conf['Remember Last Read Post'] = enabled); + this.db = new DataBoard('lastReadPosts', this.sync); + } + + this.hr = $$1.el('hr', { + id: 'unread-line', + className: 'unread-line' + } + ); + this.posts = new Set(); + this.postsQuotingYou = new Set(); + this.order = new RandomAccessList(); + this.position = null; + + Callbacks.Thread.push({ + name: 'Unread', + cb: this.node + }); + + return Callbacks.Post.push({ + name: 'Unread', + cb: this.addPost + }); + }, + + node() { + Unread.thread = this; + Unread.title = d$1.title; + Unread.lastReadPost = Unread.db?.get({ + boardID: this.board.ID, + threadID: this.ID + }) || 0; + Unread.readCount = 0; + for (var ID of this.posts.keys) { if (+ID <= Unread.lastReadPost) { Unread.readCount++; } } + $$1.one(d$1, '4chanXInitFinished', Unread.ready); + $$1.on(d$1, 'PostsInserted', Unread.onUpdate); + $$1.on(d$1, 'ThreadUpdate', function(e) { if (e.detail[404]) { return Unread.update(); } }); + const resetLink = $$1.el('a', { + href: 'javascript:;', + className: 'unread-reset', + textContent: 'Mark all unread' + } + ); + $$1.on(resetLink, 'click', Unread.reset); + return Header$1.menu.addEntry({ + el: resetLink, + order: 70 + }); + }, + + ready() { + if (Conf['Remember Last Read Post'] && Conf['Scroll to Last Read Post']) { Unread.scroll(); } + Unread.setLine(true); + Unread.read(); + Unread.update(); + $$1.on(d$1, 'scroll visibilitychange', Unread.read); + if (Conf['Unread Line']) { return $$1.on(d$1, 'visibilitychange', Unread.setLine); } + }, + + positionPrev() { + if (Unread.position) { return Unread.position.prev; } else { return Unread.order.last; } + }, + + scroll() { + // Let the header's onload callback handle it. + let hash; + if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) { return; } + + let position = Unread.positionPrev(); + while (position) { + var {bottom} = position.data.nodes; + if (!bottom.getBoundingClientRect().height) { + // Don't try to scroll to posts with display: none + position = position.prev; + } else { + Header$1.scrollToIfNeeded(bottom, true); + break; + } + } + }, + + reset() { + if (Unread.lastReadPost == null) { return; } + + Unread.posts = new Set(); + Unread.postsQuotingYou = new Set(); + Unread.order = new RandomAccessList(); + Unread.position = null; + Unread.lastReadPost = 0; + Unread.readCount = 0; + Unread.thread.posts.forEach(post => Unread.addPost.call(post)); + + $$1.forceSync('Remember Last Read Post'); + if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) { + Unread.db.set({ + boardID: Unread.thread.board.ID, + threadID: Unread.thread.ID, + val: 0 + }); + } + + Unread.updatePosition(); + Unread.setLine(); + return Unread.update(); + }, + + sync() { + if (Unread.lastReadPost == null) { return; } + const lastReadPost = Unread.db.get({ + boardID: Unread.thread.board.ID, + threadID: Unread.thread.ID, + defaultValue: 0 + }); + if (Unread.lastReadPost >= lastReadPost) { return; } + Unread.lastReadPost = lastReadPost; + + const postIDs = Unread.thread.posts.keys; + for (let i = Unread.readCount, end = postIDs.length; i < end; i++) { + var ID = +postIDs[i]; + if (!Unread.thread.posts.get(ID).isFetchedQuote) { + if (ID > Unread.lastReadPost) { break; } + Unread.posts.delete(ID); + Unread.postsQuotingYou.delete(ID); + } + Unread.readCount++; + } + + Unread.updatePosition(); + Unread.setLine(); + return Unread.update(); + }, + + addPost() { + if (this.isFetchedQuote || this.isClone) { return; } + Unread.order.push(this); + if ((this.ID <= Unread.lastReadPost) || this.isHidden || QuoteYou.isYou(this)) { return; } + Unread.posts.add((Unread.posts.last = this.ID)); + Unread.addPostQuotingYou(this); + return Unread.position != null ? Unread.position : (Unread.position = Unread.order[this.ID]); + }, + + addPostQuotingYou(post) { + for (var quotelink of post.nodes.quotelinks) { + if (QuoteYou.db?.get(Get$1.postDataFromLink(quotelink))) { + Unread.postsQuotingYou.add((Unread.postsQuotingYou.last = post.ID)); + Unread.openNotification(post); + return; + } + } + }, + + openNotification(post, predicate=' replied to you') { + if (!Header$1.areNotificationsEnabled) { return; } + const notif = new Notification(`${post.info.nameBlock}${predicate}`, { + body: post.commentDisplay(), + icon: Favicon.logo + } + ); + notif.onclick = function() { + Header$1.scrollToIfNeeded(post.nodes.bottom, true); + return window.focus(); + }; + return notif.onshow = () => setTimeout(() => notif.close() + , 7 * SECOND); + }, + + onUpdate() { + return $$1.queueTask(function() { // ThreadUpdater may scroll immediately after inserting posts + Unread.setLine(); + Unread.read(); + return Unread.update(); + }); + }, + + readSinglePost(post) { + const {ID} = post; + if (!Unread.posts.has(ID)) { return; } + Unread.posts.delete(ID); + Unread.postsQuotingYou.delete(ID); + Unread.updatePosition(); + Unread.saveLastReadPost(); + return Unread.update(); + }, + + read: debounce(100, function(e) { + // Update the lastReadPost when hidden posts are added to the thread. + if (!Unread.posts.size && (Unread.readCount !== Unread.thread.posts.keys.length)) { + Unread.saveLastReadPost(); + } + + if (d$1.hidden || !Unread.posts.size) { return; } + + let count = 0; + while (Unread.position) { + var {ID, data} = Unread.position; + var {bottom} = data.nodes; + if (!!bottom.getBoundingClientRect().height && // post has been hidden + (Header$1.getBottomOf(bottom) <= -1)) { break; } // post is completely read + count++; + Unread.posts.delete(ID); + Unread.postsQuotingYou.delete(ID); + Unread.position = Unread.position.next; + } + + if (!count) { return; } + Unread.updatePosition(); + Unread.saveLastReadPost(); + if (e) { return Unread.update(); } + }), + + updatePosition() { + while (Unread.position && !Unread.posts.has(Unread.position.ID)) { + Unread.position = Unread.position.next; + } + }, + + saveLastReadPost: debounce(2 * SECOND, function() { + let ID; + $$1.forceSync('Remember Last Read Post'); + if (!Conf['Remember Last Read Post'] || !Unread.db) { return; } + const postIDs = Unread.thread.posts.keys; + for (let i = Unread.readCount, end = postIDs.length; i < end; i++) { + ID = +postIDs[i]; + if (!Unread.thread.posts.get(ID).isFetchedQuote) { + if (Unread.posts.has(ID)) { break; } + Unread.lastReadPost = ID; + } + Unread.readCount++; + } + if (Unread.thread.isDead && !Unread.thread.isArchived) { return; } + return Unread.db.set({ + boardID: Unread.thread.board.ID, + threadID: Unread.thread.ID, + val: Unread.lastReadPost + }); + }), + + setLine(force) { + if (!Conf['Unread Line']) { return; } + if (Unread.hr.hidden || d$1.hidden || (force === true)) { + const oldPosition = Unread.linePosition; + if (Unread.linePosition = Unread.positionPrev()) { + if (Unread.linePosition !== oldPosition) { + let node = Unread.linePosition.data.nodes.bottom; + if (node.nextSibling?.tagName === 'BR') { node = node.nextSibling; } + $$1.after(node, Unread.hr); + } + } else { + $$1.rm(Unread.hr); + } + } + return Unread.hr.hidden = Unread.linePosition === Unread.order.last; + }, + + update() { + const count = Unread.posts.size; + const countQuotingYou = Unread.postsQuotingYou.size; + + if (Conf['Unread Count']) { + const titleQuotingYou = Conf['Quoted Title'] && countQuotingYou ? '(!) ' : ''; + const titleCount = count || !Conf['Hide Unread Count at (0)'] ? `(${count}) ` : ''; + const titleDead = Unread.thread.isDead ? + Unread.title.replace('-', (Unread.thread.isArchived ? '- Archived -' : '- 404 -')) + : + Unread.title; + d$1.title = `${titleQuotingYou}${titleCount}${titleDead}`; + } + + Unread.saveThreadWatcherCount(); + + if (Conf['Unread Favicon'] && (g.SITE.software === 'yotsuba')) { + const {isDead} = Unread.thread; + return Favicon.set(( + countQuotingYou ? + (isDead ? 'unreadDeadY' : 'unreadY') + : count ? + (isDead ? 'unreadDead' : 'unread') + : + (isDead ? 'dead' : 'default') + ) + ); + } + }, + + saveThreadWatcherCount: debounce(2 * SECOND, function() { + $$1.forceSync('Remember Last Read Post'); + if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) { + let posts; + const quotingYou = !Conf['Require OP Quote Link'] && QuoteYou.isYou(Unread.thread.OP) ? Unread.posts : Unread.postsQuotingYou; + if (!quotingYou.size) { + quotingYou.last = 0; + } else if (!quotingYou.has(quotingYou.last)) { + quotingYou.last = 0; + posts = Unread.thread.posts.keys; + for (let i = posts.length - 1; i >= 0; i--) { + if (quotingYou.has(+posts[i])) { + quotingYou.last = posts[i]; + break; + } + } + } + return ThreadWatcher$1.update(g.SITE.ID, Unread.thread.board.ID, Unread.thread.ID, { + last: Unread.thread.lastPost, + isDead: Unread.thread.isDead, + isArchived: Unread.thread.isArchived, + unread: Unread.posts.size, + quotingYou: (quotingYou.last || 0) + } + ); + } + }) }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var ExpandThread = { - statuses: dict(), - init() { - if (!((g.VIEW === 'index') && Conf['Thread Expansion'])) { return; } - if (Conf['JSON Index']) { - return $$1.on(d$1, 'IndexRefreshInternal', this.onIndexRefresh); - } else { - return Callbacks.Thread.push({ - name: 'Expand Thread', - cb() { return ExpandThread.setButton(this); } - }); - } - }, - - setButton(thread) { - let a; - if (!(thread.nodes.root && (a = $$1('.summary', thread.nodes.root)))) { return; } - a.textContent = g.SITE.Build.summaryText('+', ...a.textContent.match(/\d+/g)); - a.style.cursor = 'pointer'; - return $$1.on(a, 'click', ExpandThread.cbToggle); - }, - - disconnect(refresh) { - if ((g.VIEW === 'thread') || !Conf['Thread Expansion']) { return; } - for (var threadID in ExpandThread.statuses) { - var oldReq; - var status = ExpandThread.statuses[threadID]; - if (oldReq = status.req) { - delete status.req; - oldReq.abort(); - } - delete ExpandThread.statuses[threadID]; - } - - if (!refresh) { return $$1.off(d$1, 'IndexRefreshInternal', this.onIndexRefresh); } - }, - - onIndexRefresh() { - ExpandThread.disconnect(true); - return g.BOARD.threads.forEach(thread => ExpandThread.setButton(thread)); - }, - - cbToggle(e) { - if ($$1.modifiedClick(e)) { return; } - e.preventDefault(); - return ExpandThread.toggle(Get$1.threadFromNode(this)); - }, - - cbToggleBottom(e) { - if ($$1.modifiedClick(e)) { return; } - e.preventDefault(); - const thread = Get$1.threadFromNode(this); - $$1.rm(this); // remove before fixing bottom of thread position - const {bottom} = thread.nodes.root.getBoundingClientRect(); - ExpandThread.toggle(thread); - return window.scrollBy(0, (thread.nodes.root.getBoundingClientRect().bottom - bottom)); - }, - - toggle(thread) { - let a; - if (!(thread.nodes.root && (a = $$1('.summary', thread.nodes.root)))) { return; } - if (thread.ID in ExpandThread.statuses) { - return ExpandThread.contract(thread, a, thread.nodes.root); - } else { - return ExpandThread.expand(thread, a); - } - }, - - expand(thread, a) { - let status; - ExpandThread.statuses[thread] = (status = {}); - a.textContent = g.SITE.Build.summaryText('...', ...a.textContent.match(/\d+/g)); - status.req = $$1.cache(g.SITE.urls.threadJSON({boardID: thread.board.ID, threadID: thread.ID}), function() { - if (this !== status.req) { return; } // aborted - delete status.req; - return ExpandThread.parse(this, thread, a); - }); - return status.numReplies = $$(g.SITE.selectors.replyOriginal, thread.nodes.root).length; - }, - - contract(thread, a, threadRoot) { - let oldReq; - const status = ExpandThread.statuses[thread]; - delete ExpandThread.statuses[thread]; - if (oldReq = status.req) { - delete status.req; - oldReq.abort(); - if (a) { a.textContent = g.SITE.Build.summaryText('+', ...a.textContent.match(/\d+/g)); } - return; - } - - let replies = $$('.thread > .replyContainer', threadRoot); - if (status.numReplies) { replies = replies.slice(0, (-status.numReplies)); } - let postsCount = 0; - let filesCount = 0; - for (var reply of replies) { - // rm clones - if (Conf['Quote Inlining']) { var inlined; - while ((inlined = $$1('.inlined', reply))) { inlined.click(); } } - postsCount++; - if ('file' in Get$1.postFromRoot(reply)) { filesCount++; } - $$1.rm(reply); - } - if (Index$1.enabled) { // otherwise handled by Main.addPosts - $$1.event('PostsRemoved', null, a.parentNode); - } - a.textContent = g.SITE.Build.summaryText('+', postsCount, filesCount); - return $$1.rm($$1('.summary-bottom', threadRoot)); - }, - - parse(req, thread, a) { - let root; - if (![200, 304].includes(req.status)) { - a.textContent = req.status ? `Error ${req.statusText} (${req.status})` : 'Connection Error'; - return; - } - - g.SITE.Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler; - - const posts = []; - const postsRoot = []; - let filesCount = 0; - for (var postData of req.response.posts) { - var post; - if (postData.no === thread.ID) { continue; } - if ((post = thread.posts.get(postData.no)) && !post.isFetchedQuote) { - if ('file' in post) { filesCount++; } - ({root} = post.nodes); - postsRoot.push(root); - continue; - } - root = g.SITE.Build.postFromObject(postData, thread.board.ID); - post = new Post(root, thread, thread.board); - if ('file' in post) { filesCount++; } - posts.push(post); - postsRoot.push(root); - } - Main$1.callbackNodes('Post', posts); - $$1.after(a, postsRoot); - $$1.event('PostsInserted', null, a.parentNode); - - const postsCount = postsRoot.length; - a.textContent = g.SITE.Build.summaryText('-', postsCount, filesCount); - - if (root) { - const a2 = a.cloneNode(true); - a2.classList.add('summary-bottom'); - $$1.on(a2, 'click', ExpandThread.cbToggleBottom); - return $$1.after(root, a2); - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var ExpandThread = { + statuses: dict(), + init() { + if (!((g.VIEW === 'index') && Conf['Thread Expansion'])) { return; } + if (Conf['JSON Index']) { + return $$1.on(d$1, 'IndexRefreshInternal', this.onIndexRefresh); + } else { + return Callbacks.Thread.push({ + name: 'Expand Thread', + cb() { return ExpandThread.setButton(this); } + }); + } + }, + + setButton(thread) { + let a; + if (!(thread.nodes.root && (a = $$1('.summary', thread.nodes.root)))) { return; } + a.textContent = g.SITE.Build.summaryText('+', ...a.textContent.match(/\d+/g)); + a.style.cursor = 'pointer'; + return $$1.on(a, 'click', ExpandThread.cbToggle); + }, + + disconnect(refresh) { + if ((g.VIEW === 'thread') || !Conf['Thread Expansion']) { return; } + for (var threadID in ExpandThread.statuses) { + var oldReq; + var status = ExpandThread.statuses[threadID]; + if (oldReq = status.req) { + delete status.req; + oldReq.abort(); + } + delete ExpandThread.statuses[threadID]; + } + + if (!refresh) { return $$1.off(d$1, 'IndexRefreshInternal', this.onIndexRefresh); } + }, + + onIndexRefresh() { + ExpandThread.disconnect(true); + return g.BOARD.threads.forEach(thread => ExpandThread.setButton(thread)); + }, + + cbToggle(e) { + if ($$1.modifiedClick(e)) { return; } + e.preventDefault(); + return ExpandThread.toggle(Get$1.threadFromNode(this)); + }, + + cbToggleBottom(e) { + if ($$1.modifiedClick(e)) { return; } + e.preventDefault(); + const thread = Get$1.threadFromNode(this); + $$1.rm(this); // remove before fixing bottom of thread position + const {bottom} = thread.nodes.root.getBoundingClientRect(); + ExpandThread.toggle(thread); + return window.scrollBy(0, (thread.nodes.root.getBoundingClientRect().bottom - bottom)); + }, + + toggle(thread) { + let a; + if (!(thread.nodes.root && (a = $$1('.summary', thread.nodes.root)))) { return; } + if (thread.ID in ExpandThread.statuses) { + return ExpandThread.contract(thread, a, thread.nodes.root); + } else { + return ExpandThread.expand(thread, a); + } + }, + + expand(thread, a) { + let status; + ExpandThread.statuses[thread] = (status = {}); + a.textContent = g.SITE.Build.summaryText('...', ...a.textContent.match(/\d+/g)); + status.req = $$1.cache(g.SITE.urls.threadJSON({boardID: thread.board.ID, threadID: thread.ID}), function() { + if (this !== status.req) { return; } // aborted + delete status.req; + return ExpandThread.parse(this, thread, a); + }); + return status.numReplies = $$(g.SITE.selectors.replyOriginal, thread.nodes.root).length; + }, + + contract(thread, a, threadRoot) { + let oldReq; + const status = ExpandThread.statuses[thread]; + delete ExpandThread.statuses[thread]; + if (oldReq = status.req) { + delete status.req; + oldReq.abort(); + if (a) { a.textContent = g.SITE.Build.summaryText('+', ...a.textContent.match(/\d+/g)); } + return; + } + + let replies = $$('.thread > .replyContainer', threadRoot); + if (status.numReplies) { replies = replies.slice(0, (-status.numReplies)); } + let postsCount = 0; + let filesCount = 0; + for (var reply of replies) { + // rm clones + if (Conf['Quote Inlining']) { var inlined; + while ((inlined = $$1('.inlined', reply))) { inlined.click(); } } + postsCount++; + if ('file' in Get$1.postFromRoot(reply)) { filesCount++; } + $$1.rm(reply); + } + if (Index$1.enabled) { // otherwise handled by Main.addPosts + $$1.event('PostsRemoved', null, a.parentNode); + } + a.textContent = g.SITE.Build.summaryText('+', postsCount, filesCount); + return $$1.rm($$1('.summary-bottom', threadRoot)); + }, + + parse(req, thread, a) { + let root; + if (![200, 304].includes(req.status)) { + a.textContent = req.status ? `Error ${req.statusText} (${req.status})` : 'Connection Error'; + return; + } + + g.SITE.Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler; + + const posts = []; + const postsRoot = []; + let filesCount = 0; + for (var postData of req.response.posts) { + var post; + if (postData.no === thread.ID) { continue; } + if ((post = thread.posts.get(postData.no)) && !post.isFetchedQuote) { + if ('file' in post) { filesCount++; } + ({root} = post.nodes); + postsRoot.push(root); + continue; + } + root = g.SITE.Build.postFromObject(postData, thread.board.ID); + post = new Post(root, thread, thread.board); + if ('file' in post) { filesCount++; } + posts.push(post); + postsRoot.push(root); + } + Main$1.callbackNodes('Post', posts); + $$1.after(a, postsRoot); + $$1.event('PostsInserted', null, a.parentNode); + + const postsCount = postsRoot.length; + a.textContent = g.SITE.Build.summaryText('-', postsCount, filesCount); + + if (root) { + const a2 = a.cloneNode(true); + a2.classList.add('summary-bottom'); + $$1.on(a2, 'click', ExpandThread.cbToggleBottom); + return $$1.after(root, a2); + } + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var UnreadIndex = { - lastReadPost: dict(), - hr: dict(), - markReadLink: dict(), - - init() { - if ((g.VIEW !== 'index') || !Conf['Remember Last Read Post'] || !Conf['Unread Line in Index']) { return; } - - this.enabled = true; - this.db = new DataBoard('lastReadPosts', this.sync); - - Callbacks.Thread.push({ - name: 'Unread Line in Index', - cb: this.node - }); - - $$1.on(d$1, 'IndexRefreshInternal', this.onIndexRefresh); - return $$1.on(d$1, 'PostsInserted PostsRemoved', this.onPostsInserted); - }, - - node() { - UnreadIndex.lastReadPost[this.fullID] = UnreadIndex.db.get({ - boardID: this.board.ID, - threadID: this.ID - }) || 0; - if (!Index$1.enabled) { // let onIndexRefresh handle JSON Index - return UnreadIndex.update(this); - } - }, - - onIndexRefresh(e) { - if (e.detail.isCatalog) { return; } - return (() => { - const result = []; - for (var threadID of e.detail.threadIDs) { - var thread = g.threads.get(threadID); - result.push(UnreadIndex.update(thread)); - } - return result; - })(); - }, - - onPostsInserted(e) { - if (e.target === Index$1.root) { return; } // onIndexRefresh handles this case - const thread = Get$1.threadFromNode(e.target); - if (!thread || (thread.nodes.root !== e.target)) { return; } - const wasVisible = !!UnreadIndex.hr[thread.fullID]?.parentNode; - UnreadIndex.update(thread); - if (Conf['Scroll to Last Read Post'] && (e.type === 'PostsInserted') && !wasVisible && !!UnreadIndex.hr[thread.fullID]?.parentNode) { - return Header$1.scrollToIfNeeded(UnreadIndex.hr[thread.fullID], true); - } - }, - - sync() { - return g.threads.forEach(function(thread) { - const lastReadPost = UnreadIndex.db.get({ - boardID: thread.board.ID, - threadID: thread.ID - }) || 0; - if (lastReadPost !== UnreadIndex.lastReadPost[thread.fullID]) { - UnreadIndex.lastReadPost[thread.fullID] = lastReadPost; - if (thread.nodes.root?.parentNode) { - return UnreadIndex.update(thread); - } - } - }); - }, - - update(thread) { - let divider; - const lastReadPost = UnreadIndex.lastReadPost[thread.fullID]; - let repliesShown = 0; - let repliesRead = 0; - let firstUnread = null; - thread.posts.forEach(function(post) { - if (post.isReply && thread.nodes.root.contains(post.nodes.root)) { - repliesShown++; - if (post.ID <= lastReadPost) { - return repliesRead++; - } else if ((!firstUnread || (post.ID < firstUnread.ID)) && !post.isHidden && !QuoteYou.isYou(post)) { - return firstUnread = post; - } - } - }); - - let hr = UnreadIndex.hr[thread.fullID]; - if (firstUnread && (repliesRead || ((lastReadPost === thread.OP.ID) && (!$$1(g.SITE.selectors.summary, thread.nodes.root) || thread.ID in ExpandThread.statuses)))) { - if (!hr) { - hr = (UnreadIndex.hr[thread.fullID] = $$1.el('hr', - {className: 'unread-line'})); - } - $$1.before(firstUnread.nodes.root, hr); - } else { - $$1.rm(hr); - } - - const hasUnread = repliesShown ? - firstUnread || !repliesRead - : Index$1.enabled ? - thread.lastPost > lastReadPost - : - thread.OP.ID > lastReadPost; - thread.nodes.root.classList.toggle('unread-thread', hasUnread); - - let link = UnreadIndex.markReadLink[thread.fullID]; - if (!link) { - link = (UnreadIndex.markReadLink[thread.fullID] = $$1.el('a', { - className: 'unread-mark-read brackets-wrap', - href: 'javascript:;', - textContent: 'Mark Read' - } - )); - $$1.on(link, 'click', UnreadIndex.markRead); - } - if (divider = $$1(g.SITE.selectors.threadDivider, thread.nodes.root)) { // divider inside thread as in Tinyboard - return $$1.before(divider, link); - } else { - return $$1.add(thread.nodes.root, link); - } - }, - - markRead() { - const thread = Get$1.threadFromNode(this); - UnreadIndex.lastReadPost[thread.fullID] = thread.lastPost; - UnreadIndex.db.set({ - boardID: thread.board.ID, - threadID: thread.ID, - val: thread.lastPost - }); - $$1.rm(UnreadIndex.hr[thread.fullID]); - thread.nodes.root.classList.remove('unread-thread'); - return ThreadWatcher$1.update(g.SITE.ID, thread.board.ID, thread.ID, { - last: thread.lastPost, - unread: 0, - quotingYou: 0 - } - ); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var UnreadIndex = { + lastReadPost: dict(), + hr: dict(), + markReadLink: dict(), + + init() { + if ((g.VIEW !== 'index') || !Conf['Remember Last Read Post'] || !Conf['Unread Line in Index']) { return; } + + this.enabled = true; + this.db = new DataBoard('lastReadPosts', this.sync); + + Callbacks.Thread.push({ + name: 'Unread Line in Index', + cb: this.node + }); + + $$1.on(d$1, 'IndexRefreshInternal', this.onIndexRefresh); + return $$1.on(d$1, 'PostsInserted PostsRemoved', this.onPostsInserted); + }, + + node() { + UnreadIndex.lastReadPost[this.fullID] = UnreadIndex.db.get({ + boardID: this.board.ID, + threadID: this.ID + }) || 0; + if (!Index$1.enabled) { // let onIndexRefresh handle JSON Index + return UnreadIndex.update(this); + } + }, + + onIndexRefresh(e) { + if (e.detail.isCatalog) { return; } + return (() => { + const result = []; + for (var threadID of e.detail.threadIDs) { + var thread = g.threads.get(threadID); + result.push(UnreadIndex.update(thread)); + } + return result; + })(); + }, + + onPostsInserted(e) { + if (e.target === Index$1.root) { return; } // onIndexRefresh handles this case + const thread = Get$1.threadFromNode(e.target); + if (!thread || (thread.nodes.root !== e.target)) { return; } + const wasVisible = !!UnreadIndex.hr[thread.fullID]?.parentNode; + UnreadIndex.update(thread); + if (Conf['Scroll to Last Read Post'] && (e.type === 'PostsInserted') && !wasVisible && !!UnreadIndex.hr[thread.fullID]?.parentNode) { + return Header$1.scrollToIfNeeded(UnreadIndex.hr[thread.fullID], true); + } + }, + + sync() { + return g.threads.forEach(function(thread) { + const lastReadPost = UnreadIndex.db.get({ + boardID: thread.board.ID, + threadID: thread.ID + }) || 0; + if (lastReadPost !== UnreadIndex.lastReadPost[thread.fullID]) { + UnreadIndex.lastReadPost[thread.fullID] = lastReadPost; + if (thread.nodes.root?.parentNode) { + return UnreadIndex.update(thread); + } + } + }); + }, + + update(thread) { + let divider; + const lastReadPost = UnreadIndex.lastReadPost[thread.fullID]; + let repliesShown = 0; + let repliesRead = 0; + let firstUnread = null; + thread.posts.forEach(function(post) { + if (post.isReply && thread.nodes.root.contains(post.nodes.root)) { + repliesShown++; + if (post.ID <= lastReadPost) { + return repliesRead++; + } else if ((!firstUnread || (post.ID < firstUnread.ID)) && !post.isHidden && !QuoteYou.isYou(post)) { + return firstUnread = post; + } + } + }); + + let hr = UnreadIndex.hr[thread.fullID]; + if (firstUnread && (repliesRead || ((lastReadPost === thread.OP.ID) && (!$$1(g.SITE.selectors.summary, thread.nodes.root) || thread.ID in ExpandThread.statuses)))) { + if (!hr) { + hr = (UnreadIndex.hr[thread.fullID] = $$1.el('hr', + {className: 'unread-line'})); + } + $$1.before(firstUnread.nodes.root, hr); + } else { + $$1.rm(hr); + } + + const hasUnread = repliesShown ? + firstUnread || !repliesRead + : Index$1.enabled ? + thread.lastPost > lastReadPost + : + thread.OP.ID > lastReadPost; + thread.nodes.root.classList.toggle('unread-thread', hasUnread); + + let link = UnreadIndex.markReadLink[thread.fullID]; + if (!link) { + link = (UnreadIndex.markReadLink[thread.fullID] = $$1.el('a', { + className: 'unread-mark-read brackets-wrap', + href: 'javascript:;', + textContent: 'Mark Read' + } + )); + $$1.on(link, 'click', UnreadIndex.markRead); + } + if (divider = $$1(g.SITE.selectors.threadDivider, thread.nodes.root)) { // divider inside thread as in Tinyboard + return $$1.before(divider, link); + } else { + return $$1.add(thread.nodes.root, link); + } + }, + + markRead() { + const thread = Get$1.threadFromNode(this); + UnreadIndex.lastReadPost[thread.fullID] = thread.lastPost; + UnreadIndex.db.set({ + boardID: thread.board.ID, + threadID: thread.ID, + val: thread.lastPost + }); + $$1.rm(UnreadIndex.hr[thread.fullID]); + thread.nodes.root.classList.remove('unread-thread'); + return ThreadWatcher$1.update(g.SITE.ID, thread.board.ID, thread.ID, { + last: thread.lastPost, + unread: 0, + quotingYou: 0 + } + ); + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - - var ThreadWatcher = { - init() { - let sc; - if (!(this.enabled = Conf['Thread Watcher'])) { return; } - - this.shortcut = (sc = $$1.el('a', { - id: 'watcher-link', - textContent: '👁︎', - title: 'Thread Watcher', - href: 'javascript:;', - } - )); - - this.db = new DataBoard('watchedThreads', this.refresh, true); - this.dbLM = new DataBoard('watcherLastModified', null, true); - this.dialog = UI.dialog('thread-watcher', { innerHTML: ThreadWatcherPage }); - this.status = $$1('#watcher-status', this.dialog); - this.list = this.dialog.lastElementChild; - this.refreshButton = $$1('.refresh', this.dialog); - this.closeButton = $$1('.move > .close', this.dialog); - this.unreaddb = Unread.db || UnreadIndex.db || new DataBoard('lastReadPosts'); - this.unreadEnabled = Conf['Remember Last Read Post']; - - $$1.on(d$1, 'QRPostSuccessful', this.cb.post); - $$1.on(sc, 'click', this.toggleWatcher); - $$1.on(this.refreshButton, 'click', this.buttonFetchAll); - $$1.on(this.closeButton, 'click', this.toggleWatcher); - - this.menu.addHeaderMenuEntry(); - $$1.onExists(doc$1, 'body', this.addDialog); - - switch (g.VIEW) { - case 'index': - $$1.on(d$1, 'IndexUpdate', this.cb.onIndexUpdate); - break; - case 'thread': - $$1.on(d$1, 'ThreadUpdate', this.cb.onThreadRefresh); - break; - } - - if (Conf['Fixed Thread Watcher']) { - $$1.addClass(doc$1, 'fixed-watcher'); - } - if (!Conf['Persistent Thread Watcher']) { - $$1.addClass(ThreadWatcher.shortcut, 'disabled'); - this.dialog.hidden = true; - } - - Header$1.addShortcut('watcher', sc, 510); - - ThreadWatcher.initLastModified(); - ThreadWatcher.fetchAuto(); - $$1.on(window, 'visibilitychange focus', () => $$1.queueTask(ThreadWatcher.fetchAuto)); - - if (Conf['Menu'] && Index$1.enabled) { - Menu.menu.addEntry({ - el: $$1.el('a', { - href: 'javascript:;', - className: 'has-shortcut-text' - } - , {innerHTML: 'Alt+click'}), - order: 6, - open({thread}) { - if (Conf['Index Mode'] !== 'catalog') { return false; } - this.el.firstElementChild.textContent = ThreadWatcher.isWatched(thread) ? - 'Unwatch' - : - 'Watch'; - if (this.cb) { $$1.off(this.el, 'click', this.cb); } - this.cb = function() { - $$1.event('CloseMenu'); - return ThreadWatcher.toggle(thread); - }; - $$1.on(this.el, 'click', this.cb); - return true; - } - }); - } - - if (!['index', 'thread'].includes(g.VIEW)) { return; } - - Callbacks.Post.push({ - name: 'Thread Watcher', - cb: this.node - }); - return Callbacks.CatalogThread.push({ - name: 'Thread Watcher', - cb: this.catalogNode - }); - }, - - isWatched(thread) { - return !!ThreadWatcher.db?.get({boardID: thread.board.ID, threadID: thread.ID}); - }, - - isWatchedRaw(boardID, threadID) { - return !!ThreadWatcher.db?.get({boardID, threadID}); - }, - - setToggler(toggler, isWatched) { - toggler.classList.toggle('watched', isWatched); - return toggler.title = `${isWatched ? 'Unwatch' : 'Watch'} Thread`; - }, - - node() { - let toggler; - if (this.isReply) { return; } - if (this.isClone) { - toggler = $$1('.watch-thread-link', this.nodes.info); - } else { - toggler = $$1.el('a', { - href: 'javascript:;', - className: 'watch-thread-link' - } - ); - $$1.before($$1('input', this.nodes.info), toggler); - } - const siteID = g.SITE.ID; - const boardID = this.board.ID; - const threadID = this.thread.ID; - const data = ThreadWatcher.db.get({siteID, boardID, threadID}); - ThreadWatcher.setToggler(toggler, !!data); - $$1.on(toggler, 'click', ThreadWatcher.cb.toggle); - // Add missing excerpt for threads added by Auto Watch - if (data && (data.excerpt == null)) { - return $$1.queueTask(() => { - return ThreadWatcher.update(siteID, boardID, threadID, {excerpt: Get$1.threadExcerpt(this.thread)}); - }); - } - }, - - catalogNode() { - if (ThreadWatcher.isWatched(this.thread)) { $$1.addClass(this.nodes.root, 'watched'); } - return $$1.on(this.nodes.root, 'mousedown click', e => { - if ((e.button !== 0) || !e.altKey) { return; } - if (e.type === 'click') { ThreadWatcher.toggle(this.thread); } - return e.preventDefault(); - }); - }, // Also on mousedown to prevent highlighting thumbnail in Firefox. - - addDialog() { - if (!Main$1.isThisPageLegit()) { return; } - ThreadWatcher.build(); - return $$1.prepend(d$1.body, ThreadWatcher.dialog); - }, - - toggleWatcher() { - $$1.toggleClass(ThreadWatcher.shortcut, 'disabled'); - return ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden; - }, - - cb: { - openAll() { - if ($$1.hasClass(this, 'disabled')) { return; } - for (var a of $$('a.watcher-link', ThreadWatcher.list)) { - $$1.open(a.href); - } - return $$1.event('CloseMenu'); - }, - openUnread() { - if ($$1.hasClass(this, 'disabled')) { return; } - for (var a of $$('.replies-unread > a.watcher-link', ThreadWatcher.list)) { - $$1.open(a.href); - } - return $$1.event('CloseMenu'); - }, - openDeads() { - if ($$1.hasClass(this, 'disabled')) { return; } - for (var a of $$('.dead-thread > a.watcher-link', ThreadWatcher.list)) { - $$1.open(a.href); - } - return $$1.event('CloseMenu'); - }, - pruneDeads() { - if ($$1.hasClass(this, 'disabled')) { return; } - for (var {siteID, boardID, threadID, data} of ThreadWatcher.getAll()) { - if (data.isDead) { - ThreadWatcher.db.delete({siteID, boardID, threadID}); - } - } - ThreadWatcher.refresh(); - return $$1.event('CloseMenu'); - }, - dismiss() { - for (var {siteID, boardID, threadID, data} of ThreadWatcher.getAll()) { - if (data.quotingYou) { - ThreadWatcher.update(siteID, boardID, threadID, {dismiss: data.quotingYou || 0}); - } - } - return $$1.event('CloseMenu'); - }, - toggle() { - const {thread} = Get$1.postFromNode(this); - return ThreadWatcher.toggle(thread); - }, - rm() { - const {siteID} = this.parentNode.dataset; - const [boardID, threadID] = this.parentNode.dataset.fullID.split('.'); - return ThreadWatcher.rm(siteID, boardID, +threadID); - }, - post(e) { - const {boardID, threadID, postID} = e.detail; - const cb = PostRedirect.delay(); - if (postID === threadID) { - if (Conf['Auto Watch']) { - return ThreadWatcher.addRaw(boardID, threadID, {}, cb); - } - } else if (Conf['Auto Watch Reply']) { - return ThreadWatcher.add((g.threads.get(boardID + '.' + threadID) || new Thread(threadID, g.boards[boardID] || new Board(boardID))), cb); - } - }, - onIndexUpdate(e) { - const {db} = ThreadWatcher; - const siteID = g.SITE.ID; - const boardID = g.BOARD.ID; - let nKilled = 0; - for (var threadID in db.data[siteID].boards[boardID]) { - // Don't prune threads that have yet to appear in index. - var data = db.data[siteID].boards[boardID][threadID]; - if (!data?.isDead && !e.detail.threads.includes(`${boardID}.${threadID}`)) { - if (!e.detail.threads.some(fullID => +fullID.split('.')[1] > threadID)) { continue; } - if (Conf['Auto Prune'] || !(data && (typeof data === 'object'))) { // corrupt data - db.delete({boardID, threadID}); - nKilled++; - } else { - ThreadWatcher.fetchStatus({siteID, boardID, threadID, data}); - } - } - } - if (nKilled) { return ThreadWatcher.refresh(); } - }, - onThreadRefresh(e) { - const thread = g.threads.get(e.detail.threadID); - if (!e.detail[404] || !ThreadWatcher.isWatched(thread)) { return; } - // Update dead status. - return ThreadWatcher.add(thread); - } - }, - - requests: [], - fetched: 0, - - fetch(url, {siteID, force}, args, cb) { - if (ThreadWatcher.requests.length === 0) { - ThreadWatcher.status.textContent = '...'; - $$1.addClass(ThreadWatcher.refreshButton, 'spin'); - } - const onloadend = function() { - if (this.finished) { return; } - this.finished = true; - ThreadWatcher.fetched++; - if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { - ThreadWatcher.clearRequests(); - } else { - ThreadWatcher.status.textContent = `${Math.round((ThreadWatcher.fetched / ThreadWatcher.requests.length) * 100)}%`; - } - return cb.apply(this, args); - }; - const ajax = siteID === g.SITE.ID ? $$1.ajax : CrossOrigin$1.ajax; - if (force) { - delete $$1.lastModified.ThreadWatcher?.[url]; - } - const req = $$1.whenModified( - url, - 'ThreadWatcher', - onloadend, - { timeout: MINUTE, ajax } - ); - return ThreadWatcher.requests.push(req); - }, - - clearRequests() { - ThreadWatcher.requests = []; - ThreadWatcher.fetched = 0; - ThreadWatcher.status.textContent = ''; - return $$1.rmClass(ThreadWatcher.refreshButton, 'spin'); - }, - - abort() { - delete ThreadWatcher.syncing; - for (var req of ThreadWatcher.requests) { - if (!req.finished) { - req.finished = true; - req.abort(); - } - } - return ThreadWatcher.clearRequests(); - }, - - initLastModified() { - const lm = ($$1.lastModified['ThreadWatcher'] || ($$1.lastModified['ThreadWatcher'] = dict())); - for (var siteID in ThreadWatcher.dbLM.data) { - var boards = ThreadWatcher.dbLM.data[siteID]; - for (var boardID in boards.boards) { - var data = boards.boards[boardID]; - if (ThreadWatcher.db.get({siteID, boardID})) { - for (var url in data) { - var date = data[url]; - lm[url] = date; - } - } else { - ThreadWatcher.dbLM.delete({siteID, boardID}); - } - } - } - }, - - fetchAuto() { - let middle; - clearTimeout(ThreadWatcher.timeout); - if (!Conf['Auto Update Thread Watcher']) { return; } - const {db} = ThreadWatcher; - const interval = Conf['Show Page'] || (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) ? 5 * MINUTE : 2 * HOUR; - const now = Date.now(); - if ((now - interval >= ((middle = db.data.lastChecked || 0)) || middle > now) && !d$1.hidden && !!d$1.hasFocus()) { - ThreadWatcher.fetchAllStatus(interval); - } - return ThreadWatcher.timeout = setTimeout(ThreadWatcher.fetchAuto, interval); - }, - - buttonFetchAll() { - if (ThreadWatcher.syncing || ThreadWatcher.requests.length) { - return ThreadWatcher.abort(); - } else { - return ThreadWatcher.fetchAllStatus(); - } - }, - - fetchAllStatus(interval=0) { - ThreadWatcher.status.textContent = '...'; - $$1.addClass(ThreadWatcher.refreshButton, 'spin'); - ThreadWatcher.syncing = true; - const dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter(x => x); - let n = 0; - return dbs.map((dbi) => - dbi.forceSync(function() { - if ((++n) === dbs.length) { - let middle; - if (!ThreadWatcher.syncing) { return; } // aborted - delete ThreadWatcher.syncing; - if (0 > (middle = Date.now() - (ThreadWatcher.db.data.lastChecked || 0)) || middle >= interval) { // not checked in another tab - // XXX On vichan boards, last_modified field of threads.json does not account for sage posts. - // Occasionally check replies field of catalog.json to find these posts. - let middle1; - const {db} = ThreadWatcher; - const now = Date.now(); - const deep = !(now - (2 * HOUR) < ((middle1 = db.data.lastChecked2 || 0)) && middle1 <= now); - const boards = ThreadWatcher.getAll(true); - for (var board of boards) { - ThreadWatcher.fetchBoard(board, deep); - } - db.setLastChecked(); - if (deep) { db.setLastChecked('lastChecked2'); } - } - if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { - return ThreadWatcher.clearRequests(); - } - } - })); - }, - - fetchBoard(board, deep) { - if (!board.some(thread => !thread.data.isDead)) { return; } - let force = false; - for (var thread of board) { - var {data} = thread; - if (!data.isDead && (data.last !== -1)) { - if (Conf['Show Page'] && (data.page == null)) { force = true; } - if ((data.modified == null)) { force = (thread.force = true); } - } - } - const {siteID, boardID} = board[0]; - const site = g.sites[siteID]; - if (!site) { return; } - const urlF = deep && site.threadModTimeIgnoresSage ? 'catalogJSON' : 'threadsListJSON'; - const url = site.urls[urlF]?.({siteID, boardID}); - if (!url) { return; } - return ThreadWatcher.fetch(url, {siteID, force}, [board, url], ThreadWatcher.parseBoard); - }, - - parseBoard(board, url) { - let page, thread; - if (this.status !== 200) { return; } - const {siteID, boardID} = board[0]; - const lmDate = this.getResponseHeader('Last-Modified'); - ThreadWatcher.dbLM.extend({siteID, boardID, val: $$1.item(url, lmDate)}); - const threads = dict(); - let pageLength = 0; - let nThreads = 0; - let oldest = null; - try { - pageLength = this.response[0]?.threads.length || 0; - for (let i = 0; i < this.response.length; i++) { - page = this.response[i]; - for (var item of page.threads) { - threads[item.no] = { - page: i + 1, - index: nThreads, - modified: item.last_modified, - replies: item.replies - }; - nThreads++; - if ((oldest == null) || (item.no < oldest)) { - oldest = item.no; - } - } - } - } catch (error) { - for (thread of board) { - ThreadWatcher.fetchStatus(thread); - } - } - for (thread of board) { - var {threadID, data} = thread; - if (threads[threadID]) { - var index, modified, replies; - ({page, index, modified, replies} = threads[threadID]); - if (Conf['Show Page']) { - var lastPage = g.sites[siteID].isPrunedByAge?.({siteID, boardID}) ? - threadID === oldest - : - index >= (nThreads - pageLength); - ThreadWatcher.update(siteID, boardID, threadID, {page, lastPage}); - } - if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) { - if ((modified !== data.modified) || ((replies != null) && (replies !== data.replies))) { - (thread.newData || (thread.newData = {})).modified = modified; - ThreadWatcher.fetchStatus(thread); - } - } - } else { - ThreadWatcher.fetchStatus(thread); - } - } - }, - - fetchStatus(thread) { - const {siteID, boardID, threadID, data, force} = thread; - const url = g.sites[siteID]?.urls.threadJSON?.({siteID, boardID, threadID}); - if (!url) { return; } - if (data.isDead && !force) { return; } - if (data.last === -1) { return; } // 404 or no JSON API - return ThreadWatcher.fetch(url, {siteID, force}, [thread], ThreadWatcher.parseStatus); - }, - - parseStatus(thread, isArchiveURL) { - let isDead, last; - let {siteID, boardID, threadID, data, newData, force} = thread; - const site = g.sites[siteID]; - if ((this.status === 200) && this.response) { - let isArchived; - last = this.response.posts[this.response.posts.length-1].no; - const replies = this.response.posts.length-1; - isDead = (isArchived = !!(this.response.posts[0].archived || isArchiveURL)); - if (isDead && Conf['Auto Prune']) { - ThreadWatcher.rm(siteID, boardID, threadID); - return; - } - - if ((last === data.last) && (isDead === data.isDead) && (isArchived === data.isArchived)) { return; } - - const lastReadPost = ThreadWatcher.unreaddb.get({siteID, boardID, threadID, defaultValue: 0}); - let unread = data.unread || 0; - let quotingYou = data.quotingYou || 0; - const youOP = !!QuoteYou.db?.get({siteID, boardID, threadID, postID: threadID}); - - for (var postObj of this.response.posts) { - if ((postObj.no <= (data.last || 0)) || (postObj.no <= lastReadPost)) { continue; } - if (QuoteYou.db?.get({siteID, boardID, threadID, postID: postObj.no})) { continue; } - - var quotesYou = false; - if (!Conf['Require OP Quote Link'] && youOP) { - quotesYou = true; - } else if (QuoteYou.db && postObj.com) { - var match; - var regexp = site.regexp.quotelinkHTML; - regexp.lastIndex = 0; - while (match = regexp.exec(postObj.com)) { - if (QuoteYou.db.get({ - siteID, - boardID: match[1] ? encodeURIComponent(match[1]) : boardID, - threadID: match[2] || threadID, - postID: match[3] || match[2] || threadID - })) { - quotesYou = true; - break; - } - } - } - - if (!unread || (!quotingYou && quotesYou)) { - if (Filter.isHidden(site.Build.parseJSON(postObj, {siteID, boardID}))) { continue; } - } - - unread++; - if (quotesYou) { quotingYou = postObj.no; } - } - - if (!newData) { newData = {}; } - $$1.extend(newData, {last, replies, isDead, isArchived, unread, quotingYou}); - return ThreadWatcher.update(siteID, boardID, threadID, newData); - - } else if (this.status === 404) { - const archiveURL = g.sites[siteID]?.urls.archivedThreadJSON?.({siteID, boardID, threadID}); - if (!isArchiveURL && archiveURL) { - return ThreadWatcher.fetch(archiveURL, {siteID, force}, [thread, true], ThreadWatcher.parseStatus); - } else if (site.mayLackJSON && (data.last == null)) { - return ThreadWatcher.update(siteID, boardID, threadID, {last: -1}); - } else { - return ThreadWatcher.update(siteID, boardID, threadID, {isDead: true}); - } - } - }, - - getAll(groupByBoard) { - const all = []; - for (var siteID in ThreadWatcher.db.data) { - var boards = ThreadWatcher.db.data[siteID]; - for (var boardID in boards.boards) { - var cont; - var threads = boards.boards[boardID]; - if (Conf['Current Board'] && ((siteID !== g.SITE.ID) || (boardID !== g.BOARD.ID))) { - continue; - } - if (groupByBoard) { - all.push((cont = [])); - } - for (var threadID in threads) { - var data = threads[threadID]; - if (data && (typeof data === 'object')) { - (groupByBoard ? cont : all).push({siteID, boardID, threadID, data}); - } - } - } - } - return all; - }, - - makeLine(siteID, boardID, threadID, data) { - let page; - const x = $$1.el('a', { - textContent: '✕', - href: 'javascript:;' - } - ); - $$1.on(x, 'click', ThreadWatcher.cb.rm); - - let {excerpt, isArchived} = data; - if (!excerpt) { excerpt = `/${boardID}/ - No.${threadID}`; } - if (Conf['Show Site Prefix']) { excerpt = ThreadWatcher.prefixes[siteID] + excerpt; } - - const link = $$1.el('a', { - href: g.sites[siteID]?.urls.thread({siteID, boardID, threadID}, isArchived) || '', - title: excerpt, - className: 'watcher-link' - } - ); - - if (Conf['Show Page'] && (data.page != null)) { - page = $$1.el('span', { - textContent: `[${data.page}]`, - className: 'watcher-page' - } - ); - $$1.add(link, page); - } - - if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count'] && (data.unread != null)) { - const count = $$1.el('span', { - textContent: `(${data.unread})`, - className: 'watcher-unread' - } - ); - $$1.add(link, count); - } - - const title = $$1.el('span', { - textContent: excerpt, - className: 'watcher-title' - } - ); - $$1.add(link, title); - - const div = $$1.el('div'); - const fullID = `${boardID}.${threadID}`; - div.dataset.fullID = fullID; - div.dataset.siteID = siteID; - if ((g.VIEW === 'thread') && (fullID === `${g.BOARD}.${g.THREADID}`)) { $$1.addClass(div, 'current'); } - if (data.isDead) { $$1.addClass(div, 'dead-thread'); } - if (Conf['Show Page']) { - if (data.lastPage) { $$1.addClass(div, 'last-page'); } - if (data.page != null) { div.dataset.page = data.page; } - } - if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) { - if (data.unread === 0) { $$1.addClass(div, 'replies-read'); } - if (data.unread) { $$1.addClass(div, 'replies-unread'); } - if ((data.quotingYou || 0) > (data.dismiss || 0)) { $$1.addClass(div, 'replies-quoting-you'); } - } - $$1.add(div, [x, $$1.tn(' '), link]); - return div; - }, - - setPrefixes(threads) { - const prefixes = dict(); - for (var {siteID} of threads) { - if (siteID in prefixes) { continue; } - var len = 0; - var prefix = ''; - var conflicts = Object.keys(prefixes); - while (conflicts.length > 0) { - len++; - prefix = siteID.slice(0, len); - var conflicts2 = []; - for (var siteID2 of conflicts) { - if (siteID2.slice(0, len) === prefix) { - conflicts2.push(siteID2); - } else if (prefixes[siteID2].length < len) { - prefixes[siteID2] = siteID2.slice(0, len); - } - } - conflicts = conflicts2; - } - prefixes[siteID] = prefix; - } - return ThreadWatcher.prefixes = prefixes; - }, - - build() { - const nodes = []; - const threads = ThreadWatcher.getAll(); - ThreadWatcher.setPrefixes(threads); - for (var {siteID, boardID, threadID, data} of threads) { - // Add missing excerpt for threads added by Auto Watch - var thread; - if ((data.excerpt == null) && (siteID === g.SITE.ID) && (thread = g.threads.get(`${boardID}.${threadID}`)) && thread.OP) { - ThreadWatcher.db.extend({boardID, threadID, val: {excerpt: Get$1.threadExcerpt(thread)}}); - } - nodes.push(ThreadWatcher.makeLine(siteID, boardID, threadID, data)); - } - const {list} = ThreadWatcher; - $$1.rmAll(list); - $$1.add(list, nodes); - - return ThreadWatcher.refreshIcon(); - }, - - refresh() { - ThreadWatcher.build(); - - g.threads.forEach(function(thread) { - const isWatched = ThreadWatcher.isWatched(thread); - if (thread.OP) { - for (var post of [thread.OP, ...thread.OP.clones]) { - var toggler; - if (toggler = $$1('.watch-thread-link', post.nodes.info)) { - ThreadWatcher.setToggler(toggler, isWatched); - } - } - } - if (thread.catalogView) { return thread.catalogView.nodes.root.classList.toggle('watched', isWatched); } - }); - - if (Conf['Pin Watched Threads']) { - return $$1.event('SortIndex', {deferred: Conf['Index Mode'] !== 'catalog'}); - } - }, - - refreshIcon() { - for (var className of ['replies-unread', 'replies-quoting-you']) { - ThreadWatcher.shortcut.classList.toggle(className, !!$$1(`.${className}`, ThreadWatcher.dialog)); - } - }, - - update(siteID, boardID, threadID, newData) { - let data, key, line, val; - if (!(data = ThreadWatcher.db?.get({siteID, boardID, threadID}))) { return; } - if (newData.isDead && Conf['Auto Prune']) { - ThreadWatcher.rm(siteID, boardID, threadID); - return; - } - if (newData.isDead || (newData.last === -1)) { - for (key of ['isArchived', 'page', 'lastPage', 'unread', 'quotingyou']) { - if (!(key in newData)) { - newData[key] = undefined; - } - } - } - if ((newData.last != null) && (newData.last < data.last)) { - newData.modified = undefined; - } - let n = 0; - for (key in newData) { val = newData[key]; if (data[key] !== val) { n++; } } - if (!n) { return; } - ThreadWatcher.db.extend({siteID, boardID, threadID, val: newData}); - if (line = $$1(`#watched-threads > [data-site-i-d='${siteID}'][data-full-i-d='${boardID}.${threadID}']`, ThreadWatcher.dialog)) { - const newLine = ThreadWatcher.makeLine(siteID, boardID, threadID, data); - $$1.replace(line, newLine); - return ThreadWatcher.refreshIcon(); - } else { - return ThreadWatcher.refresh(); - } - }, - - set404(boardID, threadID, cb) { - let data; - if (!(data = ThreadWatcher.db?.get({boardID, threadID}))) { return cb(); } - if (Conf['Auto Prune']) { - ThreadWatcher.db.delete({boardID, threadID}); - return cb(); - } - if (data.isDead && !((data.isArchived != null) || (data.page != null) || (data.lastPage != null) || (data.unread != null) || (data.quotingYou != null))) { return cb(); } - return ThreadWatcher.db.extend({boardID, threadID, val: {isDead: true, isArchived: undefined, page: undefined, lastPage: undefined, unread: undefined, quotingYou: undefined}}, cb); - }, - - toggle(thread) { - const siteID = g.SITE.ID; - const boardID = thread.board.ID; - const threadID = thread.ID; - if (ThreadWatcher.db.get({boardID, threadID})) { - return ThreadWatcher.rm(siteID, boardID, threadID); - } else { - return ThreadWatcher.add(thread); - } - }, - - add(thread, cb) { - const data = {}; - const siteID = g.SITE.ID; - const boardID = thread.board.ID; - const threadID = thread.ID; - if (thread.isDead) { - if (Conf['Auto Prune'] && ThreadWatcher.db.get({boardID, threadID})) { - ThreadWatcher.rm(siteID, boardID, threadID, cb); - return; - } - data.isDead = true; - } - if (thread.OP) { data.excerpt = Get$1.threadExcerpt(thread); } - return ThreadWatcher.addRaw(boardID, threadID, data, cb); - }, - - addRaw(boardID, threadID, data, cb) { - const oldData = ThreadWatcher.db.get({ boardID, threadID, defaultValue: dict() }); - delete oldData.last; - delete oldData.modified; - $$1.extend(oldData, data); - ThreadWatcher.db.set({boardID, threadID, val: oldData}, cb); - ThreadWatcher.refresh(); - const thread = {siteID: g.SITE.ID, boardID, threadID, data, force: true}; - if (Conf['Show Page'] && !data.isDead) { - return ThreadWatcher.fetchBoard([thread]); - } else if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) { - return ThreadWatcher.fetchStatus(thread); - } - }, - - rm(siteID, boardID, threadID, cb) { - ThreadWatcher.db.delete({siteID, boardID, threadID}, cb); - return ThreadWatcher.refresh(); - }, - - menu: { - init() { - if (!Conf['Thread Watcher']) { return; } - const menu = (this.menu = new UI.Menu('thread watcher')); - $$1.on($$1('.menu-button', ThreadWatcher.dialog), 'click', function(e) { - return menu.toggle(e, this, ThreadWatcher); - }); - return this.addMenuEntries(); - }, - - addHeaderMenuEntry() { - if (g.VIEW !== 'thread') { return; } - const entryEl = $$1.el('a', - {href: 'javascript:;'}); - Header$1.menu.addEntry({ - el: entryEl, - order: 60, - open() { - const [addClass, rmClass, text] = !!ThreadWatcher.db.get({boardID: g.BOARD.ID, threadID: g.THREADID}) ? - ['unwatch-thread', 'watch-thread', 'Unwatch thread'] - : - ['watch-thread', 'unwatch-thread', 'Watch thread']; - $$1.addClass(entryEl, addClass); - $$1.rmClass(entryEl, rmClass); - entryEl.textContent = text; - return true; - } - }); - return $$1.on(entryEl, 'click', () => ThreadWatcher.toggle(g.threads.get(`${g.BOARD}.${g.THREADID}`))); - }, - - addMenuEntries() { - const entries = []; - - // `Open all` entry - entries.push({ - text: 'Open all threads', - cb: ThreadWatcher.cb.openAll, - open() { - this.el.classList.toggle('disabled', !ThreadWatcher.list.firstElementChild); - return true; - } - }); - - // `Open Unread` entry - entries.push({ - text: 'Open unread threads', - cb: ThreadWatcher.cb.openUnread, - open() { - this.el.classList.toggle('disabled', !$$1('.replies-unread', ThreadWatcher.list)); - return true; - } - }); - - // `Open dead threads` entry - entries.push({ - text: 'Open dead threads', - cb: ThreadWatcher.cb.openDeads, - open() { - this.el.classList.toggle('disabled', !$$1('.dead-thread', ThreadWatcher.list)); - return true; - } - }); - - // `Prune dead threads` entry - entries.push({ - text: 'Prune dead threads', - cb: ThreadWatcher.cb.pruneDeads, - open() { - this.el.classList.toggle('disabled', !$$1('.dead-thread', ThreadWatcher.list)); - return true; - } - }); - - // `Dismiss posts quoting you` entry - entries.push({ - text: 'Dismiss posts quoting you', - title: 'Unhighlight the thread watcher icon and threads until there are new replies quoting you.', - cb: ThreadWatcher.cb.dismiss, - open() { - this.el.classList.toggle('disabled', !$$1.hasClass(ThreadWatcher.shortcut, 'replies-quoting-you')); - return true; - } - }); - - for (var {text, title, cb, open} of entries) { - var entry = { - el: $$1.el('a', { - textContent: text, - href: 'javascript:;' - } - ) - }; - if (title) { entry.el.title = title; } - $$1.on(entry.el, 'click', cb); - entry.open = open.bind(entry); - this.menu.addEntry(entry); - } - - // Settings checkbox entries: - for (var name in Config.threadWatcher) { - var conf = Config.threadWatcher[name]; - this.addCheckbox(name, conf[1]); - } - - }, - - addCheckbox(name, desc) { - const entry = { - type: 'thread watcher', - el: UI.checkbox(name, name.replace(' Thread Watcher', '')) - }; - entry.el.title = desc; - const input = entry.el.firstElementChild; - if ((name === 'Show Unread Count') && !ThreadWatcher.unreadEnabled) { - input.disabled = true; - $$1.addClass(entry.el, 'disabled'); - entry.el.title += '\n[Remember Last Read Post is disabled.]'; - } - $$1.on(input, 'change', $$1.cb.checked); - if (['Current Board', 'Show Page', 'Show Unread Count', 'Show Site Prefix'].includes(name)) { $$1.on(input, 'change', ThreadWatcher.refresh); } - if (['Show Page', 'Show Unread Count', 'Auto Update Thread Watcher'].includes(name)) { $$1.on(input, 'change', ThreadWatcher.fetchAuto); } - return this.menu.addEntry(entry); - } - } - }; + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + + var ThreadWatcher = { + init() { + let sc; + if (!(this.enabled = Conf['Thread Watcher'])) { return; } + + this.shortcut = (sc = $$1.el('a', { + id: 'watcher-link', + textContent: '👁︎', + title: 'Thread Watcher', + href: 'javascript:;', + } + )); + + this.db = new DataBoard('watchedThreads', this.refresh, true); + this.dbLM = new DataBoard('watcherLastModified', null, true); + this.dialog = UI.dialog('thread-watcher', { innerHTML: ThreadWatcherPage }); + this.status = $$1('#watcher-status', this.dialog); + this.list = this.dialog.lastElementChild; + this.refreshButton = $$1('.refresh', this.dialog); + this.closeButton = $$1('.move > .close', this.dialog); + this.unreaddb = Unread.db || UnreadIndex.db || new DataBoard('lastReadPosts'); + this.unreadEnabled = Conf['Remember Last Read Post']; + + $$1.on(d$1, 'QRPostSuccessful', this.cb.post); + $$1.on(sc, 'click', this.toggleWatcher); + $$1.on(this.refreshButton, 'click', this.buttonFetchAll); + $$1.on(this.closeButton, 'click', this.toggleWatcher); + + this.menu.addHeaderMenuEntry(); + $$1.onExists(doc$1, 'body', this.addDialog); + + switch (g.VIEW) { + case 'index': + $$1.on(d$1, 'IndexUpdate', this.cb.onIndexUpdate); + break; + case 'thread': + $$1.on(d$1, 'ThreadUpdate', this.cb.onThreadRefresh); + break; + } + + if (Conf['Fixed Thread Watcher']) { + $$1.addClass(doc$1, 'fixed-watcher'); + } + if (!Conf['Persistent Thread Watcher']) { + $$1.addClass(ThreadWatcher.shortcut, 'disabled'); + this.dialog.hidden = true; + } + + Header$1.addShortcut('watcher', sc, 510); + + ThreadWatcher.initLastModified(); + ThreadWatcher.fetchAuto(); + $$1.on(window, 'visibilitychange focus', () => $$1.queueTask(ThreadWatcher.fetchAuto)); + + if (Conf['Menu'] && Index$1.enabled) { + Menu.menu.addEntry({ + el: $$1.el('a', { + href: 'javascript:;', + className: 'has-shortcut-text' + } + , {innerHTML: 'Alt+click'}), + order: 6, + open({thread}) { + if (Conf['Index Mode'] !== 'catalog') { return false; } + this.el.firstElementChild.textContent = ThreadWatcher.isWatched(thread) ? + 'Unwatch' + : + 'Watch'; + if (this.cb) { $$1.off(this.el, 'click', this.cb); } + this.cb = function() { + $$1.event('CloseMenu'); + return ThreadWatcher.toggle(thread); + }; + $$1.on(this.el, 'click', this.cb); + return true; + } + }); + } + + if (!['index', 'thread'].includes(g.VIEW)) { return; } + + Callbacks.Post.push({ + name: 'Thread Watcher', + cb: this.node + }); + return Callbacks.CatalogThread.push({ + name: 'Thread Watcher', + cb: this.catalogNode + }); + }, + + isWatched(thread) { + return !!ThreadWatcher.db?.get({boardID: thread.board.ID, threadID: thread.ID}); + }, + + isWatchedRaw(boardID, threadID) { + return !!ThreadWatcher.db?.get({boardID, threadID}); + }, + + setToggler(toggler, isWatched) { + toggler.classList.toggle('watched', isWatched); + return toggler.title = `${isWatched ? 'Unwatch' : 'Watch'} Thread`; + }, + + node() { + let toggler; + if (this.isReply) { return; } + if (this.isClone) { + toggler = $$1('.watch-thread-link', this.nodes.info); + } else { + toggler = $$1.el('a', { + href: 'javascript:;', + className: 'watch-thread-link' + } + ); + $$1.before($$1('input', this.nodes.info), toggler); + } + const siteID = g.SITE.ID; + const boardID = this.board.ID; + const threadID = this.thread.ID; + const data = ThreadWatcher.db.get({siteID, boardID, threadID}); + ThreadWatcher.setToggler(toggler, !!data); + $$1.on(toggler, 'click', ThreadWatcher.cb.toggle); + // Add missing excerpt for threads added by Auto Watch + if (data && (data.excerpt == null)) { + return $$1.queueTask(() => { + return ThreadWatcher.update(siteID, boardID, threadID, {excerpt: Get$1.threadExcerpt(this.thread)}); + }); + } + }, + + catalogNode() { + if (ThreadWatcher.isWatched(this.thread)) { $$1.addClass(this.nodes.root, 'watched'); } + return $$1.on(this.nodes.root, 'mousedown click', e => { + if ((e.button !== 0) || !e.altKey) { return; } + if (e.type === 'click') { ThreadWatcher.toggle(this.thread); } + return e.preventDefault(); + }); + }, // Also on mousedown to prevent highlighting thumbnail in Firefox. + + addDialog() { + if (!Main$1.isThisPageLegit()) { return; } + ThreadWatcher.build(); + return $$1.prepend(d$1.body, ThreadWatcher.dialog); + }, + + toggleWatcher() { + $$1.toggleClass(ThreadWatcher.shortcut, 'disabled'); + return ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden; + }, + + cb: { + openAll() { + if ($$1.hasClass(this, 'disabled')) { return; } + for (var a of $$('a.watcher-link', ThreadWatcher.list)) { + $$1.open(a.href); + } + return $$1.event('CloseMenu'); + }, + openUnread() { + if ($$1.hasClass(this, 'disabled')) { return; } + for (var a of $$('.replies-unread > a.watcher-link', ThreadWatcher.list)) { + $$1.open(a.href); + } + return $$1.event('CloseMenu'); + }, + openDeads() { + if ($$1.hasClass(this, 'disabled')) { return; } + for (var a of $$('.dead-thread > a.watcher-link', ThreadWatcher.list)) { + $$1.open(a.href); + } + return $$1.event('CloseMenu'); + }, + pruneDeads() { + if ($$1.hasClass(this, 'disabled')) { return; } + for (var {siteID, boardID, threadID, data} of ThreadWatcher.getAll()) { + if (data.isDead) { + ThreadWatcher.db.delete({siteID, boardID, threadID}); + } + } + ThreadWatcher.refresh(); + return $$1.event('CloseMenu'); + }, + dismiss() { + for (var {siteID, boardID, threadID, data} of ThreadWatcher.getAll()) { + if (data.quotingYou) { + ThreadWatcher.update(siteID, boardID, threadID, {dismiss: data.quotingYou || 0}); + } + } + return $$1.event('CloseMenu'); + }, + toggle() { + const {thread} = Get$1.postFromNode(this); + return ThreadWatcher.toggle(thread); + }, + rm() { + const {siteID} = this.parentNode.dataset; + const [boardID, threadID] = this.parentNode.dataset.fullID.split('.'); + return ThreadWatcher.rm(siteID, boardID, +threadID); + }, + post(e) { + const {boardID, threadID, postID} = e.detail; + const cb = PostRedirect.delay(); + if (postID === threadID) { + if (Conf['Auto Watch']) { + return ThreadWatcher.addRaw(boardID, threadID, {}, cb); + } + } else if (Conf['Auto Watch Reply']) { + return ThreadWatcher.add((g.threads.get(boardID + '.' + threadID) || new Thread(threadID, g.boards[boardID] || new Board(boardID))), cb); + } + }, + onIndexUpdate(e) { + const {db} = ThreadWatcher; + const siteID = g.SITE.ID; + const boardID = g.BOARD.ID; + let nKilled = 0; + for (var threadID in db.data[siteID].boards[boardID]) { + // Don't prune threads that have yet to appear in index. + var data = db.data[siteID].boards[boardID][threadID]; + if (!data?.isDead && !e.detail.threads.includes(`${boardID}.${threadID}`)) { + if (!e.detail.threads.some(fullID => +fullID.split('.')[1] > threadID)) { continue; } + if (Conf['Auto Prune'] || !(data && (typeof data === 'object'))) { // corrupt data + db.delete({boardID, threadID}); + nKilled++; + } else { + ThreadWatcher.fetchStatus({siteID, boardID, threadID, data}); + } + } + } + if (nKilled) { return ThreadWatcher.refresh(); } + }, + onThreadRefresh(e) { + const thread = g.threads.get(e.detail.threadID); + if (!e.detail[404] || !ThreadWatcher.isWatched(thread)) { return; } + // Update dead status. + return ThreadWatcher.add(thread); + } + }, + + requests: [], + fetched: 0, + + fetch(url, {siteID, force}, args, cb) { + if (ThreadWatcher.requests.length === 0) { + ThreadWatcher.status.textContent = '...'; + $$1.addClass(ThreadWatcher.refreshButton, 'spin'); + } + const onloadend = function() { + if (this.finished) { return; } + this.finished = true; + ThreadWatcher.fetched++; + if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { + ThreadWatcher.clearRequests(); + } else { + ThreadWatcher.status.textContent = `${Math.round((ThreadWatcher.fetched / ThreadWatcher.requests.length) * 100)}%`; + } + return cb.apply(this, args); + }; + const ajax = siteID === g.SITE.ID ? $$1.ajax : CrossOrigin$1.ajax; + if (force) { + delete $$1.lastModified.ThreadWatcher?.[url]; + } + const req = $$1.whenModified( + url, + 'ThreadWatcher', + onloadend, + { timeout: MINUTE, ajax } + ); + return ThreadWatcher.requests.push(req); + }, + + clearRequests() { + ThreadWatcher.requests = []; + ThreadWatcher.fetched = 0; + ThreadWatcher.status.textContent = ''; + return $$1.rmClass(ThreadWatcher.refreshButton, 'spin'); + }, + + abort() { + delete ThreadWatcher.syncing; + for (var req of ThreadWatcher.requests) { + if (!req.finished) { + req.finished = true; + req.abort(); + } + } + return ThreadWatcher.clearRequests(); + }, + + initLastModified() { + const lm = ($$1.lastModified['ThreadWatcher'] || ($$1.lastModified['ThreadWatcher'] = dict())); + for (var siteID in ThreadWatcher.dbLM.data) { + var boards = ThreadWatcher.dbLM.data[siteID]; + for (var boardID in boards.boards) { + var data = boards.boards[boardID]; + if (ThreadWatcher.db.get({siteID, boardID})) { + for (var url in data) { + var date = data[url]; + lm[url] = date; + } + } else { + ThreadWatcher.dbLM.delete({siteID, boardID}); + } + } + } + }, + + fetchAuto() { + let middle; + clearTimeout(ThreadWatcher.timeout); + if (!Conf['Auto Update Thread Watcher']) { return; } + const {db} = ThreadWatcher; + const interval = Conf['Show Page'] || (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) ? 5 * MINUTE : 2 * HOUR; + const now = Date.now(); + if ((now - interval >= ((middle = db.data.lastChecked || 0)) || middle > now) && !d$1.hidden && !!d$1.hasFocus()) { + ThreadWatcher.fetchAllStatus(interval); + } + return ThreadWatcher.timeout = setTimeout(ThreadWatcher.fetchAuto, interval); + }, + + buttonFetchAll() { + if (ThreadWatcher.syncing || ThreadWatcher.requests.length) { + return ThreadWatcher.abort(); + } else { + return ThreadWatcher.fetchAllStatus(); + } + }, + + fetchAllStatus(interval=0) { + ThreadWatcher.status.textContent = '...'; + $$1.addClass(ThreadWatcher.refreshButton, 'spin'); + ThreadWatcher.syncing = true; + const dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter(x => x); + let n = 0; + return dbs.map((dbi) => + dbi.forceSync(function() { + if ((++n) === dbs.length) { + let middle; + if (!ThreadWatcher.syncing) { return; } // aborted + delete ThreadWatcher.syncing; + if (0 > (middle = Date.now() - (ThreadWatcher.db.data.lastChecked || 0)) || middle >= interval) { // not checked in another tab + // XXX On vichan boards, last_modified field of threads.json does not account for sage posts. + // Occasionally check replies field of catalog.json to find these posts. + let middle1; + const {db} = ThreadWatcher; + const now = Date.now(); + const deep = !(now - (2 * HOUR) < ((middle1 = db.data.lastChecked2 || 0)) && middle1 <= now); + const boards = ThreadWatcher.getAll(true); + for (var board of boards) { + ThreadWatcher.fetchBoard(board, deep); + } + db.setLastChecked(); + if (deep) { db.setLastChecked('lastChecked2'); } + } + if (ThreadWatcher.fetched === ThreadWatcher.requests.length) { + return ThreadWatcher.clearRequests(); + } + } + })); + }, + + fetchBoard(board, deep) { + if (!board.some(thread => !thread.data.isDead)) { return; } + let force = false; + for (var thread of board) { + var {data} = thread; + if (!data.isDead && (data.last !== -1)) { + if (Conf['Show Page'] && (data.page == null)) { force = true; } + if ((data.modified == null)) { force = (thread.force = true); } + } + } + const {siteID, boardID} = board[0]; + const site = g.sites[siteID]; + if (!site) { return; } + const urlF = deep && site.threadModTimeIgnoresSage ? 'catalogJSON' : 'threadsListJSON'; + const url = site.urls[urlF]?.({siteID, boardID}); + if (!url) { return; } + return ThreadWatcher.fetch(url, {siteID, force}, [board, url], ThreadWatcher.parseBoard); + }, + + parseBoard(board, url) { + let page, thread; + if (this.status !== 200) { return; } + const {siteID, boardID} = board[0]; + const lmDate = this.getResponseHeader('Last-Modified'); + ThreadWatcher.dbLM.extend({siteID, boardID, val: $$1.item(url, lmDate)}); + const threads = dict(); + let pageLength = 0; + let nThreads = 0; + let oldest = null; + try { + pageLength = this.response[0]?.threads.length || 0; + for (let i = 0; i < this.response.length; i++) { + page = this.response[i]; + for (var item of page.threads) { + threads[item.no] = { + page: i + 1, + index: nThreads, + modified: item.last_modified, + replies: item.replies + }; + nThreads++; + if ((oldest == null) || (item.no < oldest)) { + oldest = item.no; + } + } + } + } catch (error) { + for (thread of board) { + ThreadWatcher.fetchStatus(thread); + } + } + for (thread of board) { + var {threadID, data} = thread; + if (threads[threadID]) { + var index, modified, replies; + ({page, index, modified, replies} = threads[threadID]); + if (Conf['Show Page']) { + var lastPage = g.sites[siteID].isPrunedByAge?.({siteID, boardID}) ? + threadID === oldest + : + index >= (nThreads - pageLength); + ThreadWatcher.update(siteID, boardID, threadID, {page, lastPage}); + } + if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) { + if ((modified !== data.modified) || ((replies != null) && (replies !== data.replies))) { + (thread.newData || (thread.newData = {})).modified = modified; + ThreadWatcher.fetchStatus(thread); + } + } + } else { + ThreadWatcher.fetchStatus(thread); + } + } + }, + + fetchStatus(thread) { + const {siteID, boardID, threadID, data, force} = thread; + const url = g.sites[siteID]?.urls.threadJSON?.({siteID, boardID, threadID}); + if (!url) { return; } + if (data.isDead && !force) { return; } + if (data.last === -1) { return; } // 404 or no JSON API + return ThreadWatcher.fetch(url, {siteID, force}, [thread], ThreadWatcher.parseStatus); + }, + + parseStatus(thread, isArchiveURL) { + let isDead, last; + let {siteID, boardID, threadID, data, newData, force} = thread; + const site = g.sites[siteID]; + if ((this.status === 200) && this.response) { + let isArchived; + last = this.response.posts[this.response.posts.length-1].no; + const replies = this.response.posts.length-1; + isDead = (isArchived = !!(this.response.posts[0].archived || isArchiveURL)); + if (isDead && Conf['Auto Prune']) { + ThreadWatcher.rm(siteID, boardID, threadID); + return; + } + + if ((last === data.last) && (isDead === data.isDead) && (isArchived === data.isArchived)) { return; } + + const lastReadPost = ThreadWatcher.unreaddb.get({siteID, boardID, threadID, defaultValue: 0}); + let unread = data.unread || 0; + let quotingYou = data.quotingYou || 0; + const youOP = !!QuoteYou.db?.get({siteID, boardID, threadID, postID: threadID}); + + for (var postObj of this.response.posts) { + if ((postObj.no <= (data.last || 0)) || (postObj.no <= lastReadPost)) { continue; } + if (QuoteYou.db?.get({siteID, boardID, threadID, postID: postObj.no})) { continue; } + + var quotesYou = false; + if (!Conf['Require OP Quote Link'] && youOP) { + quotesYou = true; + } else if (QuoteYou.db && postObj.com) { + var match; + var regexp = site.regexp.quotelinkHTML; + regexp.lastIndex = 0; + while (match = regexp.exec(postObj.com)) { + if (QuoteYou.db.get({ + siteID, + boardID: match[1] ? encodeURIComponent(match[1]) : boardID, + threadID: match[2] || threadID, + postID: match[3] || match[2] || threadID + })) { + quotesYou = true; + break; + } + } + } + + if (!unread || (!quotingYou && quotesYou)) { + if (Filter.isHidden(site.Build.parseJSON(postObj, {siteID, boardID}))) { continue; } + } + + unread++; + if (quotesYou) { quotingYou = postObj.no; } + } + + if (!newData) { newData = {}; } + $$1.extend(newData, {last, replies, isDead, isArchived, unread, quotingYou}); + return ThreadWatcher.update(siteID, boardID, threadID, newData); + + } else if (this.status === 404) { + const archiveURL = g.sites[siteID]?.urls.archivedThreadJSON?.({siteID, boardID, threadID}); + if (!isArchiveURL && archiveURL) { + return ThreadWatcher.fetch(archiveURL, {siteID, force}, [thread, true], ThreadWatcher.parseStatus); + } else if (site.mayLackJSON && (data.last == null)) { + return ThreadWatcher.update(siteID, boardID, threadID, {last: -1}); + } else { + return ThreadWatcher.update(siteID, boardID, threadID, {isDead: true}); + } + } + }, + + getAll(groupByBoard) { + const all = []; + for (var siteID in ThreadWatcher.db.data) { + var boards = ThreadWatcher.db.data[siteID]; + for (var boardID in boards.boards) { + var cont; + var threads = boards.boards[boardID]; + if (Conf['Current Board'] && ((siteID !== g.SITE.ID) || (boardID !== g.BOARD.ID))) { + continue; + } + if (groupByBoard) { + all.push((cont = [])); + } + for (var threadID in threads) { + var data = threads[threadID]; + if (data && (typeof data === 'object')) { + (groupByBoard ? cont : all).push({siteID, boardID, threadID, data}); + } + } + } + } + return all; + }, + + makeLine(siteID, boardID, threadID, data) { + let page; + const x = $$1.el('a', { + textContent: '✕', + href: 'javascript:;' + } + ); + $$1.on(x, 'click', ThreadWatcher.cb.rm); + + let {excerpt, isArchived} = data; + if (!excerpt) { excerpt = `/${boardID}/ - No.${threadID}`; } + if (Conf['Show Site Prefix']) { excerpt = ThreadWatcher.prefixes[siteID] + excerpt; } + + const link = $$1.el('a', { + href: g.sites[siteID]?.urls.thread({siteID, boardID, threadID}, isArchived) || '', + title: excerpt, + className: 'watcher-link' + } + ); + + if (Conf['Show Page'] && (data.page != null)) { + page = $$1.el('span', { + textContent: `[${data.page}]`, + className: 'watcher-page' + } + ); + $$1.add(link, page); + } + + if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count'] && (data.unread != null)) { + const count = $$1.el('span', { + textContent: `(${data.unread})`, + className: 'watcher-unread' + } + ); + $$1.add(link, count); + } + + const title = $$1.el('span', { + textContent: excerpt, + className: 'watcher-title' + } + ); + $$1.add(link, title); + + const div = $$1.el('div'); + const fullID = `${boardID}.${threadID}`; + div.dataset.fullID = fullID; + div.dataset.siteID = siteID; + if ((g.VIEW === 'thread') && (fullID === `${g.BOARD}.${g.THREADID}`)) { $$1.addClass(div, 'current'); } + if (data.isDead) { $$1.addClass(div, 'dead-thread'); } + if (Conf['Show Page']) { + if (data.lastPage) { $$1.addClass(div, 'last-page'); } + if (data.page != null) { div.dataset.page = data.page; } + } + if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) { + if (data.unread === 0) { $$1.addClass(div, 'replies-read'); } + if (data.unread) { $$1.addClass(div, 'replies-unread'); } + if ((data.quotingYou || 0) > (data.dismiss || 0)) { $$1.addClass(div, 'replies-quoting-you'); } + } + $$1.add(div, [x, $$1.tn(' '), link]); + return div; + }, + + setPrefixes(threads) { + const prefixes = dict(); + for (var {siteID} of threads) { + if (siteID in prefixes) { continue; } + var len = 0; + var prefix = ''; + var conflicts = Object.keys(prefixes); + while (conflicts.length > 0) { + len++; + prefix = siteID.slice(0, len); + var conflicts2 = []; + for (var siteID2 of conflicts) { + if (siteID2.slice(0, len) === prefix) { + conflicts2.push(siteID2); + } else if (prefixes[siteID2].length < len) { + prefixes[siteID2] = siteID2.slice(0, len); + } + } + conflicts = conflicts2; + } + prefixes[siteID] = prefix; + } + return ThreadWatcher.prefixes = prefixes; + }, + + build() { + const nodes = []; + const threads = ThreadWatcher.getAll(); + ThreadWatcher.setPrefixes(threads); + for (var {siteID, boardID, threadID, data} of threads) { + // Add missing excerpt for threads added by Auto Watch + var thread; + if ((data.excerpt == null) && (siteID === g.SITE.ID) && (thread = g.threads.get(`${boardID}.${threadID}`)) && thread.OP) { + ThreadWatcher.db.extend({boardID, threadID, val: {excerpt: Get$1.threadExcerpt(thread)}}); + } + nodes.push(ThreadWatcher.makeLine(siteID, boardID, threadID, data)); + } + const {list} = ThreadWatcher; + $$1.rmAll(list); + $$1.add(list, nodes); + + return ThreadWatcher.refreshIcon(); + }, + + refresh() { + ThreadWatcher.build(); + + g.threads.forEach(function(thread) { + const isWatched = ThreadWatcher.isWatched(thread); + if (thread.OP) { + for (var post of [thread.OP, ...thread.OP.clones]) { + var toggler; + if (toggler = $$1('.watch-thread-link', post.nodes.info)) { + ThreadWatcher.setToggler(toggler, isWatched); + } + } + } + if (thread.catalogView) { return thread.catalogView.nodes.root.classList.toggle('watched', isWatched); } + }); + + if (Conf['Pin Watched Threads']) { + return $$1.event('SortIndex', {deferred: Conf['Index Mode'] !== 'catalog'}); + } + }, + + refreshIcon() { + for (var className of ['replies-unread', 'replies-quoting-you']) { + ThreadWatcher.shortcut.classList.toggle(className, !!$$1(`.${className}`, ThreadWatcher.dialog)); + } + }, + + update(siteID, boardID, threadID, newData) { + let data, key, line, val; + if (!(data = ThreadWatcher.db?.get({siteID, boardID, threadID}))) { return; } + if (newData.isDead && Conf['Auto Prune']) { + ThreadWatcher.rm(siteID, boardID, threadID); + return; + } + if (newData.isDead || (newData.last === -1)) { + for (key of ['isArchived', 'page', 'lastPage', 'unread', 'quotingyou']) { + if (!(key in newData)) { + newData[key] = undefined; + } + } + } + if ((newData.last != null) && (newData.last < data.last)) { + newData.modified = undefined; + } + let n = 0; + for (key in newData) { val = newData[key]; if (data[key] !== val) { n++; } } + if (!n) { return; } + ThreadWatcher.db.extend({siteID, boardID, threadID, val: newData}); + if (line = $$1(`#watched-threads > [data-site-i-d='${siteID}'][data-full-i-d='${boardID}.${threadID}']`, ThreadWatcher.dialog)) { + const newLine = ThreadWatcher.makeLine(siteID, boardID, threadID, data); + $$1.replace(line, newLine); + return ThreadWatcher.refreshIcon(); + } else { + return ThreadWatcher.refresh(); + } + }, + + set404(boardID, threadID, cb) { + let data; + if (!(data = ThreadWatcher.db?.get({boardID, threadID}))) { return cb(); } + if (Conf['Auto Prune']) { + ThreadWatcher.db.delete({boardID, threadID}); + return cb(); + } + if (data.isDead && !((data.isArchived != null) || (data.page != null) || (data.lastPage != null) || (data.unread != null) || (data.quotingYou != null))) { return cb(); } + return ThreadWatcher.db.extend({boardID, threadID, val: {isDead: true, isArchived: undefined, page: undefined, lastPage: undefined, unread: undefined, quotingYou: undefined}}, cb); + }, + + toggle(thread) { + const siteID = g.SITE.ID; + const boardID = thread.board.ID; + const threadID = thread.ID; + if (ThreadWatcher.db.get({boardID, threadID})) { + return ThreadWatcher.rm(siteID, boardID, threadID); + } else { + return ThreadWatcher.add(thread); + } + }, + + add(thread, cb) { + const data = {}; + const siteID = g.SITE.ID; + const boardID = thread.board.ID; + const threadID = thread.ID; + if (thread.isDead) { + if (Conf['Auto Prune'] && ThreadWatcher.db.get({boardID, threadID})) { + ThreadWatcher.rm(siteID, boardID, threadID, cb); + return; + } + data.isDead = true; + } + if (thread.OP) { data.excerpt = Get$1.threadExcerpt(thread); } + return ThreadWatcher.addRaw(boardID, threadID, data, cb); + }, + + addRaw(boardID, threadID, data, cb) { + const oldData = ThreadWatcher.db.get({ boardID, threadID, defaultValue: dict() }); + delete oldData.last; + delete oldData.modified; + $$1.extend(oldData, data); + ThreadWatcher.db.set({boardID, threadID, val: oldData}, cb); + ThreadWatcher.refresh(); + const thread = {siteID: g.SITE.ID, boardID, threadID, data, force: true}; + if (Conf['Show Page'] && !data.isDead) { + return ThreadWatcher.fetchBoard([thread]); + } else if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) { + return ThreadWatcher.fetchStatus(thread); + } + }, + + rm(siteID, boardID, threadID, cb) { + ThreadWatcher.db.delete({siteID, boardID, threadID}, cb); + return ThreadWatcher.refresh(); + }, + + menu: { + init() { + if (!Conf['Thread Watcher']) { return; } + const menu = (this.menu = new UI.Menu('thread watcher')); + $$1.on($$1('.menu-button', ThreadWatcher.dialog), 'click', function(e) { + return menu.toggle(e, this, ThreadWatcher); + }); + return this.addMenuEntries(); + }, + + addHeaderMenuEntry() { + if (g.VIEW !== 'thread') { return; } + const entryEl = $$1.el('a', + {href: 'javascript:;'}); + Header$1.menu.addEntry({ + el: entryEl, + order: 60, + open() { + const [addClass, rmClass, text] = !!ThreadWatcher.db.get({boardID: g.BOARD.ID, threadID: g.THREADID}) ? + ['unwatch-thread', 'watch-thread', 'Unwatch thread'] + : + ['watch-thread', 'unwatch-thread', 'Watch thread']; + $$1.addClass(entryEl, addClass); + $$1.rmClass(entryEl, rmClass); + entryEl.textContent = text; + return true; + } + }); + return $$1.on(entryEl, 'click', () => ThreadWatcher.toggle(g.threads.get(`${g.BOARD}.${g.THREADID}`))); + }, + + addMenuEntries() { + const entries = []; + + // `Open all` entry + entries.push({ + text: 'Open all threads', + cb: ThreadWatcher.cb.openAll, + open() { + this.el.classList.toggle('disabled', !ThreadWatcher.list.firstElementChild); + return true; + } + }); + + // `Open Unread` entry + entries.push({ + text: 'Open unread threads', + cb: ThreadWatcher.cb.openUnread, + open() { + this.el.classList.toggle('disabled', !$$1('.replies-unread', ThreadWatcher.list)); + return true; + } + }); + + // `Open dead threads` entry + entries.push({ + text: 'Open dead threads', + cb: ThreadWatcher.cb.openDeads, + open() { + this.el.classList.toggle('disabled', !$$1('.dead-thread', ThreadWatcher.list)); + return true; + } + }); + + // `Prune dead threads` entry + entries.push({ + text: 'Prune dead threads', + cb: ThreadWatcher.cb.pruneDeads, + open() { + this.el.classList.toggle('disabled', !$$1('.dead-thread', ThreadWatcher.list)); + return true; + } + }); + + // `Dismiss posts quoting you` entry + entries.push({ + text: 'Dismiss posts quoting you', + title: 'Unhighlight the thread watcher icon and threads until there are new replies quoting you.', + cb: ThreadWatcher.cb.dismiss, + open() { + this.el.classList.toggle('disabled', !$$1.hasClass(ThreadWatcher.shortcut, 'replies-quoting-you')); + return true; + } + }); + + for (var {text, title, cb, open} of entries) { + var entry = { + el: $$1.el('a', { + textContent: text, + href: 'javascript:;' + } + ) + }; + if (title) { entry.el.title = title; } + $$1.on(entry.el, 'click', cb); + entry.open = open.bind(entry); + this.menu.addEntry(entry); + } + + // Settings checkbox entries: + for (var name in Config.threadWatcher) { + var conf = Config.threadWatcher[name]; + this.addCheckbox(name, conf[1]); + } + + }, + + addCheckbox(name, desc) { + const entry = { + type: 'thread watcher', + el: UI.checkbox(name, name.replace(' Thread Watcher', '')) + }; + entry.el.title = desc; + const input = entry.el.firstElementChild; + if ((name === 'Show Unread Count') && !ThreadWatcher.unreadEnabled) { + input.disabled = true; + $$1.addClass(entry.el, 'disabled'); + entry.el.title += '\n[Remember Last Read Post is disabled.]'; + } + $$1.on(input, 'change', $$1.cb.checked); + if (['Current Board', 'Show Page', 'Show Unread Count', 'Show Site Prefix'].includes(name)) { $$1.on(input, 'change', ThreadWatcher.refresh); } + if (['Show Page', 'Show Unread Count', 'Auto Update Thread Watcher'].includes(name)) { $$1.on(input, 'change', ThreadWatcher.fetchAuto); } + return this.menu.addEntry(entry); + } + } + }; var ThreadWatcher$1 = ThreadWatcher; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * DS206: Consider reworking classes to avoid initClass - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - class Fetcher { - static initClass() { - - this.prototype.archiveTags = { - '\n': {innerHTML: "
"}, - '[b]': {innerHTML: ""}, - '[/b]': {innerHTML: ""}, - '[spoiler]': {innerHTML: ""}, - '[/spoiler]': {innerHTML: ""}, - '[code]': {innerHTML: "
"},
-        '[/code]':    {innerHTML: "
"}, - '[moot]': {innerHTML: "
"}, - '[/moot]': {innerHTML: "
"}, - '[banned]': {innerHTML: ""}, - '[/banned]': {innerHTML: ""}, - '[fortune]'(text) { return {innerHTML: ""}; }, - '[/fortune]': {innerHTML: ""}, - '[i]': {innerHTML: ""}, - '[/i]': {innerHTML: ""}, - '[red]': {innerHTML: ""}, - '[/red]': {innerHTML: ""}, - '[green]': {innerHTML: ""}, - '[/green]': {innerHTML: ""}, - '[blue]': {innerHTML: ""}, - '[/blue]': {innerHTML: ""} - }; - } - constructor(boardID, threadID, postID, root, quoter) { - let post, thread; - this.boardID = boardID; - this.threadID = threadID; - this.postID = postID; - this.root = root; - this.quoter = quoter; - if (post = g.posts.get(`${this.boardID}.${this.postID}`)) { - this.insert(post); - return; - } - - // 4chan X catalog data - if ((post = Index$1.replyData?.[`${this.boardID}.${this.postID}`]) && (thread = g.threads.get(`${this.boardID}.${this.threadID}`))) { - const board = g.boards[this.boardID]; - post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true}); - Main$1.callbackNodes('Post', [post]); - this.insert(post); - return; - } - - this.root.textContent = `Loading post No.${this.postID}...`; - if (this.threadID) { - const that = this; - $$1.cache(g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID}), function({isCached}) { - return that.fetchedPost(this, isCached); - }); - } else { - this.archivedPost(); - } - } - - insert(post) { - // Stop here if the container has been removed while loading. - if (!this.root.parentNode) { return; } - if (!this.quoter) { this.quoter = post; } - const clone = post.addClone(this.quoter.context, ($$1.hasClass(this.root, 'dialog'))); - Main$1.callbackNodes('Post', [clone]); - - // Get rid of the side arrows/stubs. - const {nodes} = clone; - $$1.rmAll(nodes.root); - $$1.add(nodes.root, nodes.post); - - // Indicate links to the containing post. - const quotes = [...clone.nodes.quotelinks, ...clone.nodes.backlinks]; - for (var quote of quotes) { - var {boardID, postID} = Get$1.postDataFromLink(quote); - if ((postID === this.quoter.ID) && (boardID === this.quoter.board.ID)) { - $$1.addClass(quote, 'forwardlink'); - } - } - - // Set up flag CSS for cross-board links to boards with flags - if (clone.nodes.flag && !(Fetcher.flagCSS || (Fetcher.flagCSS = $$1('link[href^="//s.4cdn.org/css/flags."]')))) { - const cssVersion = $$1('link[href^="//s.4cdn.org/css/"]')?.href.match(/\d+(?=\.css$)|$/)[0] || Date.now(); - Fetcher.flagCSS = $$1.el('link', { - rel: 'stylesheet', - href: `//s.4cdn.org/css/flags.${cssVersion}.css` - } - ); - $$1.add(d$1.head, Fetcher.flagCSS); - } - - $$1.rmAll(this.root); - $$1.add(this.root, nodes.root); - return $$1.event('PostsInserted', null, this.root); - } - - fetchedPost(req, isCached) { - // In case of multiple callbacks for the same request, - // don't parse the same original post more than once. - let post; - if (post = g.posts.get(`${this.boardID}.${this.postID}`)) { - this.insert(post); - return; - } - - const {status} = req; - if (status !== 200) { - // The thread can die by the time we check a quote. - if (status && this.archivedPost()) { return; } - - $$1.addClass(this.root, 'warning'); - this.root.textContent = - status === 404 ? - `Thread No.${this.threadID} 404'd.` - : !status ? - 'Connection Error' - : - `Error ${req.statusText} (${req.status}).`; - return; - } - - const {posts} = req.response; - g.SITE.Build.spoilerRange[this.boardID] = posts[0].custom_spoiler; - for (post of posts) { - if (post.no === this.postID) { break; } - } // we found it! - - if (post.no !== this.postID) { - // Cached requests can be stale and must be rechecked. - if (isCached) { - const api = g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID}); - $$1.cleanCache(url => url === api); - const that = this; - $$1.cache(api, function() { - return that.fetchedPost(this, false); - }); - return; - } - - // The post can be deleted by the time we check a quote. - if (this.archivedPost()) { return; } - - $$1.addClass(this.root, 'warning'); - this.root.textContent = `Post No.${this.postID} was not found.`; - return; - } - - const board = g.boards[this.boardID] || - new Board(this.boardID); - const thread = g.threads.get(`${this.boardID}.${this.threadID}`) || - new Thread(this.threadID, board); - post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true}); - Main$1.callbackNodes('Post', [post]); - return this.insert(post); - } - - archivedPost() { - let url; - if (!Conf['Resurrect Quotes']) { return false; } - if (!(url = Redirect$1.to('post', {boardID: this.boardID, postID: this.postID}))) { return false; } - const archive = Redirect$1.data.post[this.boardID]; - const encryptionOK = /^https:\/\//.test(url) || (location.protocol === 'http:'); - if (encryptionOK || Conf['Exempt Archives from Encryption']) { - const that = this; - CrossOrigin$1.cache(url, function() { - if (!encryptionOK && this.response?.media) { - const {media} = this.response; - for (var key in media) { - // Image/thumbnail URLs loaded over HTTP can be modified in transit. - // Require them to be from an HTTP host so that no referrer is sent to them from an HTTPS page. - if (/_link$/.test(key)) { - if (!$$1.getOwn(media, key)?.match(/^http:\/\//)) { delete media[key]; } - } - } - } - return that.parseArchivedPost(this.response, url, archive); - }); - return true; - } - return false; - } - - parseArchivedPost(data, url, archive) { - // In case of multiple callbacks for the same request, - // don't parse the same original post more than once. - let post; - if (post = g.posts.get(`${this.boardID}.${this.postID}`)) { - this.insert(post); - return; - } - - if (data == null) { - $$1.addClass(this.root, 'warning'); - this.root.textContent = `Error fetching Post No.${this.postID} from ${archive.name}.`; - return; - } - - if (data.error) { - $$1.addClass(this.root, 'warning'); - this.root.textContent = data.error; - return; - } - - // https://github.com/eksopl/asagi/blob/v0.4.0b74/src/main/java/net/easymodo/asagi/YotsubaAbstract.java#L82-L129 - // https://github.com/FoolCode/FoolFuuka/blob/800bd090835489e7e24371186db6e336f04b85c0/src/Model/Comment.php#L368-L428 - // https://github.com/bstats/b-stats/blob/6abe7bffaf6e5f523498d760e54b110df5331fbb/inc/classes/Yotsuba.php#L157-L168 - let comment = (data.comment || '').split(/(\n|\[\/?(?:b|spoiler|code|moot|banned|fortune(?: color="#\w+")?|i|red|green|blue)\])/); - comment = (() => { - const result = []; - for (let i = 0; i < comment.length; i++) { - var text = comment[i]; - if ((i % 2) === 1) { - var tag = Fetcher.archiveTags[text.replace(/\ .*\]/, ']')]; - if (typeof tag === 'function') { result.push(tag(text)); } else { result.push(tag); } - } else { - var greentext = text[0] === '>'; - text = text.replace(/(\[\/?[a-z]+):lit(\])/g, '$1$2'); - text = text.split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g).map((text2, j) => - {((j % 2) ? "" + E(text2) + "" : E(text2));}); - text = {innerHTML: ((greentext) ? "" + E.cat(text) + "" : E.cat(text))}; - result.push(text); - } - } - return result; - })(); - comment = {innerHTML: E.cat(comment)}; - - this.threadID = +data.thread_num; - const o = { - ID: this.postID, - threadID: this.threadID, - boardID: this.boardID, - isReply: this.postID !== this.threadID - }; - o.info = { - subject: data.title, - email: data.email, - name: data.name || '', - tripcode: data.trip, - capcode: (() => { switch (data.capcode) { - // https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/assets/themes/foolz/foolfuuka-theme-fuuka/src/Partial/Board.php#L77 - case 'M': return 'Mod'; - case 'A': return 'Admin'; - case 'D': return 'Developer'; - case 'V': return 'Verified'; - case 'F': return 'Founder'; - case 'G': return 'Manager'; - } })(), - uniqueID: data.poster_hash, - flagCode: data.poster_country, - flagCodeTroll: data.troll_country_code, - flag: data.poster_country_name || data.troll_country_name, - dateUTC: data.timestamp, - dateText: data.fourchan_date, - commentHTML: comment - }; - if (o.info.capcode) { delete o.info.uniqueID; } - if (data.media && !!+data.media.banned) { - o.fileDeleted = true; - } else if (data.media?.media_filename) { - let {thumb_link} = data.media; - // Fix URLs missing origin - if (thumb_link?.[0] === '/') { thumb_link = url.split('/', 3).join('/') + thumb_link; } - if (!Redirect$1.securityCheck(thumb_link)) { thumb_link = ''; } - let media_link = Redirect$1.to('file', {boardID: this.boardID, filename: data.media.media_orig}); - if (!Redirect$1.securityCheck(media_link)) { media_link = ''; } - o.file = { - name: data.media.media_filename, - url: media_link || - (this.boardID === 'f' ? - `${location.protocol}//${ImageHost.flashHost()}/${this.boardID}/${encodeURIComponent(E(data.media.media_filename))}` - : - `${location.protocol}//${ImageHost.host()}/${this.boardID}/${data.media.media_orig}`), - height: data.media.media_h, - width: data.media.media_w, - MD5: data.media.media_hash, - size: $$1.bytesToString(data.media.media_size), - thumbURL: thumb_link || `${location.protocol}//${ImageHost.thumbHost()}/${this.boardID}/${data.media.preview_orig}`, - theight: data.media.preview_h, - twidth: data.media.preview_w, - isSpoiler: data.media.spoiler === '1' - }; - if (!/\.pdf$/.test(o.file.url)) { o.file.dimensions = `${o.file.width}x${o.file.height}`; } - if ((this.boardID === 'f') && data.media.exif) { o.file.tag = JSON.parse(data.media.exif).Tag; } - } - o.extra = dict(); - - const board = g.boards[this.boardID] || - new Board(this.boardID); - const thread = g.threads.get(`${this.boardID}.${this.threadID}`) || - new Thread(this.threadID, board); - post = new Post(g.SITE.Build.post(o), thread, board, {isFetchedQuote: true}); - post.kill(); - if (post.file) { post.file.thumbURL = o.file.thumbURL; } - Main$1.callbackNodes('Post', [post]); - return this.insert(post); - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + class Fetcher { + static initClass() { + + this.prototype.archiveTags = { + '\n': {innerHTML: "
"}, + '[b]': {innerHTML: ""}, + '[/b]': {innerHTML: ""}, + '[spoiler]': {innerHTML: ""}, + '[/spoiler]': {innerHTML: ""}, + '[code]': {innerHTML: "
"},
+        '[/code]':    {innerHTML: "
"}, + '[moot]': {innerHTML: "
"}, + '[/moot]': {innerHTML: "
"}, + '[banned]': {innerHTML: ""}, + '[/banned]': {innerHTML: ""}, + '[fortune]'(text) { return {innerHTML: ""}; }, + '[/fortune]': {innerHTML: ""}, + '[i]': {innerHTML: ""}, + '[/i]': {innerHTML: ""}, + '[red]': {innerHTML: ""}, + '[/red]': {innerHTML: ""}, + '[green]': {innerHTML: ""}, + '[/green]': {innerHTML: ""}, + '[blue]': {innerHTML: ""}, + '[/blue]': {innerHTML: ""} + }; + } + constructor(boardID, threadID, postID, root, quoter) { + let post, thread; + this.boardID = boardID; + this.threadID = threadID; + this.postID = postID; + this.root = root; + this.quoter = quoter; + if (post = g.posts.get(`${this.boardID}.${this.postID}`)) { + this.insert(post); + return; + } + + // 4chan X catalog data + if ((post = Index$1.replyData?.[`${this.boardID}.${this.postID}`]) && (thread = g.threads.get(`${this.boardID}.${this.threadID}`))) { + const board = g.boards[this.boardID]; + post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true}); + Main$1.callbackNodes('Post', [post]); + this.insert(post); + return; + } + + this.root.textContent = `Loading post No.${this.postID}...`; + if (this.threadID) { + const that = this; + $$1.cache(g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID}), function({isCached}) { + return that.fetchedPost(this, isCached); + }); + } else { + this.archivedPost(); + } + } + + insert(post) { + // Stop here if the container has been removed while loading. + if (!this.root.parentNode) { return; } + if (!this.quoter) { this.quoter = post; } + const clone = post.addClone(this.quoter.context, ($$1.hasClass(this.root, 'dialog'))); + Main$1.callbackNodes('Post', [clone]); + + // Get rid of the side arrows/stubs. + const {nodes} = clone; + $$1.rmAll(nodes.root); + $$1.add(nodes.root, nodes.post); + + // Indicate links to the containing post. + const quotes = [...clone.nodes.quotelinks, ...clone.nodes.backlinks]; + for (var quote of quotes) { + var {boardID, postID} = Get$1.postDataFromLink(quote); + if ((postID === this.quoter.ID) && (boardID === this.quoter.board.ID)) { + $$1.addClass(quote, 'forwardlink'); + } + } + + // Set up flag CSS for cross-board links to boards with flags + if (clone.nodes.flag && !(Fetcher.flagCSS || (Fetcher.flagCSS = $$1('link[href^="//s.4cdn.org/css/flags."]')))) { + const cssVersion = $$1('link[href^="//s.4cdn.org/css/"]')?.href.match(/\d+(?=\.css$)|$/)[0] || Date.now(); + Fetcher.flagCSS = $$1.el('link', { + rel: 'stylesheet', + href: `//s.4cdn.org/css/flags.${cssVersion}.css` + } + ); + $$1.add(d$1.head, Fetcher.flagCSS); + } + + $$1.rmAll(this.root); + $$1.add(this.root, nodes.root); + return $$1.event('PostsInserted', null, this.root); + } + + fetchedPost(req, isCached) { + // In case of multiple callbacks for the same request, + // don't parse the same original post more than once. + let post; + if (post = g.posts.get(`${this.boardID}.${this.postID}`)) { + this.insert(post); + return; + } + + const {status} = req; + if (status !== 200) { + // The thread can die by the time we check a quote. + if (status && this.archivedPost()) { return; } + + $$1.addClass(this.root, 'warning'); + this.root.textContent = + status === 404 ? + `Thread No.${this.threadID} 404'd.` + : !status ? + 'Connection Error' + : + `Error ${req.statusText} (${req.status}).`; + return; + } + + const {posts} = req.response; + g.SITE.Build.spoilerRange[this.boardID] = posts[0].custom_spoiler; + for (post of posts) { + if (post.no === this.postID) { break; } + } // we found it! + + if (post.no !== this.postID) { + // Cached requests can be stale and must be rechecked. + if (isCached) { + const api = g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID}); + $$1.cleanCache(url => url === api); + const that = this; + $$1.cache(api, function() { + return that.fetchedPost(this, false); + }); + return; + } + + // The post can be deleted by the time we check a quote. + if (this.archivedPost()) { return; } + + $$1.addClass(this.root, 'warning'); + this.root.textContent = `Post No.${this.postID} was not found.`; + return; + } + + const board = g.boards[this.boardID] || + new Board(this.boardID); + const thread = g.threads.get(`${this.boardID}.${this.threadID}`) || + new Thread(this.threadID, board); + post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true}); + Main$1.callbackNodes('Post', [post]); + return this.insert(post); + } + + archivedPost() { + let url; + if (!Conf['Resurrect Quotes']) { return false; } + if (!(url = Redirect$1.to('post', {boardID: this.boardID, postID: this.postID}))) { return false; } + const archive = Redirect$1.data.post[this.boardID]; + const encryptionOK = /^https:\/\//.test(url) || (location.protocol === 'http:'); + if (encryptionOK || Conf['Exempt Archives from Encryption']) { + const that = this; + CrossOrigin$1.cache(url, function() { + if (!encryptionOK && this.response?.media) { + const {media} = this.response; + for (var key in media) { + // Image/thumbnail URLs loaded over HTTP can be modified in transit. + // Require them to be from an HTTP host so that no referrer is sent to them from an HTTPS page. + if (/_link$/.test(key)) { + if (!$$1.getOwn(media, key)?.match(/^http:\/\//)) { delete media[key]; } + } + } + } + return that.parseArchivedPost(this.response, url, archive); + }); + return true; + } + return false; + } + + parseArchivedPost(data, url, archive) { + // In case of multiple callbacks for the same request, + // don't parse the same original post more than once. + let post; + if (post = g.posts.get(`${this.boardID}.${this.postID}`)) { + this.insert(post); + return; + } + + if (data == null) { + $$1.addClass(this.root, 'warning'); + this.root.textContent = `Error fetching Post No.${this.postID} from ${archive.name}.`; + return; + } + + if (data.error) { + $$1.addClass(this.root, 'warning'); + this.root.textContent = data.error; + return; + } + + // https://github.com/eksopl/asagi/blob/v0.4.0b74/src/main/java/net/easymodo/asagi/YotsubaAbstract.java#L82-L129 + // https://github.com/FoolCode/FoolFuuka/blob/800bd090835489e7e24371186db6e336f04b85c0/src/Model/Comment.php#L368-L428 + // https://github.com/bstats/b-stats/blob/6abe7bffaf6e5f523498d760e54b110df5331fbb/inc/classes/Yotsuba.php#L157-L168 + let comment = (data.comment || '').split(/(\n|\[\/?(?:b|spoiler|code|moot|banned|fortune(?: color="#\w+")?|i|red|green|blue)\])/); + comment = (() => { + const result = []; + for (let i = 0; i < comment.length; i++) { + var text = comment[i]; + if ((i % 2) === 1) { + var tag = Fetcher.archiveTags[text.replace(/\ .*\]/, ']')]; + if (typeof tag === 'function') { result.push(tag(text)); } else { result.push(tag); } + } else { + var greentext = text[0] === '>'; + text = text.replace(/(\[\/?[a-z]+):lit(\])/g, '$1$2'); + text = text.split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g).map((text2, j) => + {((j % 2) ? "" + E(text2) + "" : E(text2));}); + text = {innerHTML: ((greentext) ? "" + E.cat(text) + "" : E.cat(text))}; + result.push(text); + } + } + return result; + })(); + comment = {innerHTML: E.cat(comment)}; + + this.threadID = +data.thread_num; + const o = { + ID: this.postID, + threadID: this.threadID, + boardID: this.boardID, + isReply: this.postID !== this.threadID + }; + o.info = { + subject: data.title, + email: data.email, + name: data.name || '', + tripcode: data.trip, + capcode: (() => { switch (data.capcode) { + // https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/assets/themes/foolz/foolfuuka-theme-fuuka/src/Partial/Board.php#L77 + case 'M': return 'Mod'; + case 'A': return 'Admin'; + case 'D': return 'Developer'; + case 'V': return 'Verified'; + case 'F': return 'Founder'; + case 'G': return 'Manager'; + } })(), + uniqueID: data.poster_hash, + flagCode: data.poster_country, + flagCodeTroll: data.troll_country_code, + flag: data.poster_country_name || data.troll_country_name, + dateUTC: data.timestamp, + dateText: data.fourchan_date, + commentHTML: comment + }; + if (o.info.capcode) { delete o.info.uniqueID; } + if (data.media && !!+data.media.banned) { + o.fileDeleted = true; + } else if (data.media?.media_filename) { + let {thumb_link} = data.media; + // Fix URLs missing origin + if (thumb_link?.[0] === '/') { thumb_link = url.split('/', 3).join('/') + thumb_link; } + if (!Redirect$1.securityCheck(thumb_link)) { thumb_link = ''; } + let media_link = Redirect$1.to('file', {boardID: this.boardID, filename: data.media.media_orig}); + if (!Redirect$1.securityCheck(media_link)) { media_link = ''; } + o.file = { + name: data.media.media_filename, + url: media_link || + (this.boardID === 'f' ? + `${location.protocol}//${ImageHost.flashHost()}/${this.boardID}/${encodeURIComponent(E(data.media.media_filename))}` + : + `${location.protocol}//${ImageHost.host()}/${this.boardID}/${data.media.media_orig}`), + height: data.media.media_h, + width: data.media.media_w, + MD5: data.media.media_hash, + size: $$1.bytesToString(data.media.media_size), + thumbURL: thumb_link || `${location.protocol}//${ImageHost.thumbHost()}/${this.boardID}/${data.media.preview_orig}`, + theight: data.media.preview_h, + twidth: data.media.preview_w, + isSpoiler: data.media.spoiler === '1' + }; + if (!/\.pdf$/.test(o.file.url)) { o.file.dimensions = `${o.file.width}x${o.file.height}`; } + if ((this.boardID === 'f') && data.media.exif) { o.file.tag = JSON.parse(data.media.exif).Tag; } + } + o.extra = dict(); + + const board = g.boards[this.boardID] || + new Board(this.boardID); + const thread = g.threads.get(`${this.boardID}.${this.threadID}`) || + new Thread(this.threadID, board); + post = new Post(g.SITE.Build.post(o), thread, board, {isFetchedQuote: true}); + post.kill(); + if (post.file) { post.file.thumbURL = o.file.thumbURL; } + Main$1.callbackNodes('Post', [post]); + return this.insert(post); + } + } Fetcher.initClass(); - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var QuotePreview = { - init() { - if (!Conf['Quote Previewing']) { return; } - - if (g.VIEW === 'archive') { - $$1.on(d$1, 'mouseover', function(e) { - if ((e.target.nodeName === 'A') && $$1.hasClass(e.target, 'quotelink')) { - return QuotePreview.mouseover.call(e.target, e); - } - }); - } - - if (!['index', 'thread'].includes(g.VIEW)) { return; } - - if (Conf['Comment Expansion']) { - ExpandComment.callbacks.push(this.node); - } - - return Callbacks.Post.push({ - name: 'Quote Previewing', - cb: this.node - }); - }, - - node() { - for (var link of this.nodes.quotelinks.concat([...this.nodes.backlinks], this.nodes.archivelinks)) { - $$1.on(link, 'mouseover', QuotePreview.mouseover); - } - }, - - mouseover(e) { - let origin; - if (($$1.hasClass(this, 'inlined') && !$$1.hasClass(doc, 'catalog-mode')) || !d$1.contains(this)) { return; } - - const {boardID, threadID, postID} = Get$1.postDataFromLink(this); - - const qp = $$1.el('div', { - id: 'qp', - className: 'dialog' - } - ); - - $$1.add(Header$1.hover, qp); - new Fetcher(boardID, threadID, postID, qp, Get$1.postFromNode(this)); - - UI.hover({ - root: this, - el: qp, - latestEvent: e, - endEvents: 'mouseout click', - cb: QuotePreview.mouseout - }); - - if (Conf['Quote Highlighting'] && (origin = g.posts.get(`${boardID}.${postID}`))) { - const posts = [origin].concat(origin.clones); - // Remove the clone that's in the qp from the array. - posts.pop(); - for (var post of posts) { - $$1.addClass(post.nodes.post, 'qphl'); - } - } - }, - - mouseout() { - // Stop if it only contains text. - let root; - if (!(root = this.el.firstElementChild)) { return; } - - $$1.event('PostsRemoved', null, Header$1.hover); - - const clone = Get$1.postFromRoot(root); - let post = clone.origin; - post.rmClone(root.dataset.clone); - - if (!Conf['Quote Highlighting']) { return; } - for (post of [post].concat(post.clones)) { - $$1.rmClass(post.nodes.post, 'qphl'); - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var QuotePreview = { + init() { + if (!Conf['Quote Previewing']) { return; } + + if (g.VIEW === 'archive') { + $$1.on(d$1, 'mouseover', function(e) { + if ((e.target.nodeName === 'A') && $$1.hasClass(e.target, 'quotelink')) { + return QuotePreview.mouseover.call(e.target, e); + } + }); + } + + if (!['index', 'thread'].includes(g.VIEW)) { return; } + + if (Conf['Comment Expansion']) { + ExpandComment.callbacks.push(this.node); + } + + return Callbacks.Post.push({ + name: 'Quote Previewing', + cb: this.node + }); + }, + + node() { + for (var link of this.nodes.quotelinks.concat([...this.nodes.backlinks], this.nodes.archivelinks)) { + $$1.on(link, 'mouseover', QuotePreview.mouseover); + } + }, + + mouseover(e) { + let origin; + if (($$1.hasClass(this, 'inlined') && !$$1.hasClass(doc, 'catalog-mode')) || !d$1.contains(this)) { return; } + + const {boardID, threadID, postID} = Get$1.postDataFromLink(this); + + const qp = $$1.el('div', { + id: 'qp', + className: 'dialog' + } + ); + + $$1.add(Header$1.hover, qp); + new Fetcher(boardID, threadID, postID, qp, Get$1.postFromNode(this)); + + UI.hover({ + root: this, + el: qp, + latestEvent: e, + endEvents: 'mouseout click', + cb: QuotePreview.mouseout + }); + + if (Conf['Quote Highlighting'] && (origin = g.posts.get(`${boardID}.${postID}`))) { + const posts = [origin].concat(origin.clones); + // Remove the clone that's in the qp from the array. + posts.pop(); + for (var post of posts) { + $$1.addClass(post.nodes.post, 'qphl'); + } + } + }, + + mouseout() { + // Stop if it only contains text. + let root; + if (!(root = this.el.firstElementChild)) { return; } + + $$1.event('PostsRemoved', null, Header$1.hover); + + const clone = Get$1.postFromRoot(root); + let post = clone.origin; + post.rmClone(root.dataset.clone); + + if (!Conf['Quote Highlighting']) { return; } + for (post of [post].concat(post.clones)) { + $$1.rmClass(post.nodes.post, 'qphl'); + } + } }; - var NavLinksPage = `Index -Catalog -Archive -Bottom - - -× - - - - - - - - + var NavLinksPage = `Index +Catalog +Archive +Bottom + + +× + + + + + + + + `; - var PageList = ` -
- - + var PageList = ` +
+ + `; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - - var Index = { - showHiddenThreads: false, - changed: {}, - - enabledOn({siteID, boardID}) { - return Conf['JSON Index'] && (g.sites[siteID].software === 'yotsuba') && (boardID !== 'f'); - }, - - init() { - let input, inputs, name; - if (g.VIEW !== 'index') { return; } - - // For IndexRefresh events - $$1.one(d$1, '4chanXInitFinished', this.cb.initFinished); - $$1.on(d$1, 'PostsInserted', this.cb.postsInserted); - - if (!this.enabledOn(g.BOARD)) { return; } - - this.enabled = true; - - Callbacks.Post.push({ - name: 'Index Page Numbers', - cb: this.node - }); - Callbacks.CatalogThread.push({ - name: 'Catalog Features', - cb: this.catalogNode - }); - - this.search = history.state?.searched || ''; - if (history.state?.mode) { - Conf['Index Mode'] = history.state?.mode; - } - this.currentSort = history.state?.sort; - if (!this.currentSort) { this.currentSort = typeof Conf['Index Sort'] === 'object' ? ( - Conf['Index Sort'][g.BOARD.ID] || 'bump' - ) : ( - Conf['Index Sort'] - ); } - this.currentPage = this.getCurrentPage(); - this.processHash(); - - $$1.addClass(doc$1, 'index-loading', `${Conf['Index Mode'].replace(/\ /g, '-')}-mode`); - $$1.on(window, 'popstate', this.cb.popstate); - $$1.on(d$1, 'scroll', this.scroll); - $$1.on(d$1, 'SortIndex', this.cb.resort); - - // Header refresh button - this.button = $$1.el('a', { - title: 'Refresh', - href: 'javascript:;', - textContent: '🗘' - } - ); - $$1.on(this.button, 'click', () => Index.update()); - Header$1.addShortcut('index-refresh', this.button, 590); - - // Header "Index Navigation" submenu - const entries = []; - this.inputs = (inputs = dict()); - for (name in Config.Index) { - var arr = Config.Index[name]; - if (arr instanceof Array) { - var label = UI.checkbox(name, `${name[0]}${name.slice(1).toLowerCase()}`); - label.title = arr[1]; - entries.push({el: label}); - input = label.firstChild; - $$1.on(input, 'change', $$1.cb.checked); - inputs[name] = input; - } - } - $$1.on(inputs['Show Replies'], 'change', this.cb.replies); - $$1.on(inputs['Catalog Hover Expand'], 'change', this.cb.hover); - $$1.on(inputs['Pin Watched Threads'], 'change', this.cb.resort); - $$1.on(inputs['Anchor Hidden Threads'], 'change', this.cb.resort); - - const watchSettings = function(e) { - if (input = $$1.getOwn(inputs, e.target.name)) { - input.checked = e.target.checked; - return $$1.event('change', null, input); - } - }; - $$1.on(d$1, 'OpenSettings', () => $$1.on($$1.id('fourchanx-settings'), 'change', watchSettings)); - - const sortEntry = UI.checkbox('Per-Board Sort Type', 'Per-board sort type', (typeof Conf['Index Sort'] === 'object')); - sortEntry.title = 'Set the sorting order of each board independently.'; - $$1.on(sortEntry.firstChild, 'change', this.cb.perBoardSort); - entries.splice(3, 0, {el: sortEntry}); - - Header$1.menu.addEntry({ - el: $$1.el('span', - {textContent: 'Index Navigation'}), - order: 100, - subEntries: entries - }); - - // Navigation links at top of index - this.navLinks = $$1.el('div', {className: 'navLinks json-index'}); - $$1.extend(this.navLinks, {innerHTML: NavLinksPage}); - $$1('.cataloglink a', this.navLinks).href = CatalogLinks.catalog(); - if (!BoardConfig.isArchived(g.BOARD.ID)) { $$1('.archlistlink', this.navLinks).hidden = true; } - $$1.on($$1('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront); - - // Search field - this.searchInput = $$1('#index-search', this.navLinks); - this.setupSearch(); - $$1.on(this.searchInput, 'input', this.onSearchInput); - $$1.on($$1('#index-search-clear', this.navLinks), 'click', this.clearSearch); - - // Hidden threads toggle - this.hideLabel = $$1('#hidden-label', this.navLinks); - $$1.on($$1('#hidden-toggle a', this.navLinks), 'click', this.cb.toggleHiddenThreads); - - // Drop-down menus and reverse sort toggle - this.selectRev = $$1('#index-rev', this.navLinks); - this.selectMode = $$1('#index-mode', this.navLinks); - this.selectSort = $$1('#index-sort', this.navLinks); - this.selectSize = $$1('#index-size', this.navLinks); - $$1.on(this.selectRev, 'change', this.cb.sort); - $$1.on(this.selectMode, 'change', this.cb.mode); - $$1.on(this.selectSort, 'change', this.cb.sort); - $$1.on(this.selectSize, 'change', $$1.cb.value); - $$1.on(this.selectSize, 'change', this.cb.size); - for (var select of [this.selectMode, this.selectSize]) { - select.value = Conf[select.name]; - } - this.selectRev.checked = /-rev$/.test(Index.currentSort); - this.selectSort.value = Index.currentSort.replace(/-rev$/, ''); - - // Last Long Reply options - this.lastLongOptions = $$1('#lastlong-options', this.navLinks); - this.lastLongInputs = $$('input', this.lastLongOptions); - this.lastLongThresholds = [0, 0]; - this.lastLongOptions.hidden = (this.selectSort.value !== 'lastlong'); - for (let i = 0; i < this.lastLongInputs.length; i++) { - input = this.lastLongInputs[i]; - $$1.on(input, 'change', this.cb.lastLongThresholds); - var tRaw = Conf[`Last Long Reply Thresholds ${i}`]; - input.value = (this.lastLongThresholds[i] = - typeof tRaw === 'object' ? (tRaw[g.BOARD.ID] ?? 100) : tRaw); - } - - // Thread container - this.root = $$1.el('div', {className: 'board json-index'}); - $$1.on(this.root, 'click', this.cb.hoverToggle); - this.cb.size(); - this.cb.hover(); - - // Page list - this.pagelist = $$1.el('div', {className: 'pagelist json-index'}); - $$1.extend(this.pagelist, {innerHTML: PageList}); - $$1('.cataloglink a', this.pagelist).href = CatalogLinks.catalog(); - $$1.on(this.pagelist, 'click', this.cb.pageNav); - - this.update(true); - - $$1.onExists(doc$1, 'title + *', () => d$1.title = d$1.title.replace(/\ -\ Page\ \d+/, '')); - - $$1.onExists(doc$1, '.board > .thread > .postContainer, .board + *', function() { - let el; - g.SITE.Build.hat = $$1('.board > .thread > img:first-child'); - if (g.SITE.Build.hat) { - g.BOARD.threads.forEach(function(thread) { - if (thread.nodes.root) { - return $$1.prepend(thread.nodes.root, g.SITE.Build.hat.cloneNode(false)); - } - }); - $$1.addClass(doc$1, 'hats-enabled'); - $$1.addStyle(`.catalog-thread::after {background-image: url(${g.SITE.Build.hat.src});}`); - } - - const board = $$1('.board'); - $$1.replace(board, Index.root); - if (Index.loaded) { - $$1.event('PostsInserted', null, Index.root); - } - // Hacks: - // - When removing an element from the document during page load, - // its ancestors will still be correctly created inside of it. - // - Creating loadable elements inside of an origin-less document - // will not download them. - // - Combine the two and you get a download canceller! - // Does not work on Firefox unfortunately. bugzil.la/939713 - try { - d$1.implementation.createDocument(null, null, null).appendChild(board); - } catch (error) {} - - for (el of $$('.navLinks')) { $$1.rm(el); } - $$1.rm($$1.id('ctrl-top')); - const topNavPos = $$1.id('delform').previousElementSibling; - $$1.before(topNavPos, $$1.el('hr')); - $$1.before(topNavPos, Index.navLinks); - const timeEl = $$1('#index-last-refresh time', Index.navLinks); - if (timeEl.dataset.utc) { return RelativeDates.update(timeEl); } - }); - - return Main$1.ready(function() { - let pagelist; - if (pagelist = $$1('.pagelist')) { - $$1.replace(pagelist, Index.pagelist); - } - return $$1.rmClass(doc$1, 'index-loading'); - }); - }, - - scroll() { - if (Index.req || !Index.liveThreadData || (Conf['Index Mode'] !== 'infinite') || (window.scrollY <= (doc$1.scrollHeight - (300 + window.innerHeight)))) { return; } - if (Index.pageNum == null) { Index.pageNum = Index.currentPage; } // Avoid having to pushState to keep track of the current page - - const pageNum = ++Index.pageNum; - if (pageNum > Index.pagesNum) { return Index.endNotice(); } - - const threadIDs = Index.threadsOnPage(pageNum); - return Index.buildStructure(threadIDs); - }, - - endNotice: (function() { - let notify = false; - const reset = () => notify = false; - return function() { - if (notify) { return; } - notify = true; - new Notice('info', "Last page reached.", 2); - return setTimeout(reset, 3 * SECOND); - }; - })(), - - menu: { - init() { - if ((g.VIEW !== 'index') || !Conf['Menu'] || !Conf['Thread Hiding Link'] || !Index.enabledOn(g.BOARD)) { return; } - - return Menu.menu.addEntry({ - el: $$1.el('a', { - href: 'javascript:;', - className: 'has-shortcut-text' - } - , {innerHTML: "Shift+click"}), - order: 20, - open({thread}) { - if (Conf['Index Mode'] !== 'catalog') { return false; } - this.el.firstElementChild.textContent = thread.isHidden ? - 'Unhide' - : - 'Hide'; - if (this.cb) { $$1.off(this.el, 'click', this.cb); } - this.cb = function() { - $$1.event('CloseMenu'); - return Index.toggleHide(thread); - }; - $$1.on(this.el, 'click', this.cb); - return true; - } - }); - } - }, - - node() { - if (this.isReply || this.isClone || (Index.threadPosition[this.ID] == null)) { return; } - return this.thread.setPage(Math.floor(Index.threadPosition[this.ID] / Index.threadsNumPerPage) + 1); - }, - - catalogNode() { - return $$1.on(this.nodes.root, 'mousedown click', e => { - if ((e.button !== 0) || !e.shiftKey) { return; } - if (e.type === 'click') { Index.toggleHide(this.thread); } - return e.preventDefault(); - }); - }, // Also on mousedown to prevent highlighting text. - - toggleHide(thread) { - if (Index.showHiddenThreads) { - ThreadHiding.show(thread); - if (!ThreadHiding.db.get({boardID: thread.board.ID, threadID: thread.ID})) { return; } - // Don't save when un-hiding filtered threads. - } else { - ThreadHiding.hide(thread); - } - return ThreadHiding.saveHiddenState(thread); - }, - - cycleSortType() { - let i; - const types = Index.selectSort.options.filter(option => !option.disabled); - for (i = 0; i < types.length; i++) { - var type = types[i]; - if (type.selected) { break; } - } - types[(i + 1) % types.length].selected = true; - return $$1.event('change', null, Index.selectSort); - }, - - cb: { - initFinished() { - Index.initFinishedFired = true; - return $$1.queueTask(() => Index.cb.postsInserted()); - }, - - postsInserted() { - if (!Index.initFinishedFired) { return; } - let n = 0; - g.posts.forEach(function(post) { - if (!post.isFetchedQuote && !post.indexRefreshSeen && doc$1.contains(post.nodes.root)) { - post.indexRefreshSeen = true; - return n++; - } - }); - if (n) { return $$1.event('IndexRefresh'); } - }, - - toggleHiddenThreads() { - $$1('#hidden-toggle a', Index.navLinks).textContent = (Index.showHiddenThreads = !Index.showHiddenThreads) ? - 'Hide' - : - 'Show'; - Index.sort(); - return Index.buildIndex(); - }, - - mode() { - Index.pushState({mode: this.value}); - return Index.pageLoad(false); - }, - - sort() { - const value = Index.selectRev.checked ? Index.selectSort.value + "-rev" : Index.selectSort.value; - Index.pushState({sort: value}); - return Index.pageLoad(false); - }, - - resort(e) { - Index.changed.order = true; - if (!e?.detail?.deferred) { return Index.pageLoad(false); } - }, - - perBoardSort() { - Conf['Index Sort'] = this.checked ? dict() : ''; - Index.saveSort(); - for (let i = 0; i < 2; i++) { - Conf[`Last Long Reply Thresholds ${i}`] = this.checked ? dict() : ''; - Index.saveLastLongThresholds(i); - } - }, - - lastLongThresholds() { - const i = [...this.parentNode.children].indexOf(this); - const value = +this.value; - if (!Number.isFinite(value)) { - this.value = Index.lastLongThresholds[i]; - return; - } - Index.lastLongThresholds[i] = value; - Index.saveLastLongThresholds(i); - Index.changed.order = true; - return Index.pageLoad(false); - }, - - size(e) { - if (Conf['Index Mode'] !== 'catalog') { - $$1.rmClass(Index.root, 'catalog-small'); - $$1.rmClass(Index.root, 'catalog-large'); - } else if (Conf['Index Size'] === 'small') { - $$1.addClass(Index.root, 'catalog-small'); - $$1.rmClass(Index.root, 'catalog-large'); - } else { - $$1.addClass(Index.root, 'catalog-large'); - $$1.rmClass(Index.root, 'catalog-small'); - } - if (e) { return Index.buildIndex(); } - }, - - replies() { - return Index.buildIndex(); - }, - - hover() { - return doc$1.classList.toggle('catalog-hover-expand', Conf['Catalog Hover Expand']); - }, - - hoverToggle(e) { - if (Conf['Catalog Hover Toggle'] && $$1.hasClass(doc$1, 'catalog-mode') && !$$1.modifiedClick(e) && !$$1.x('ancestor-or-self::a', e.target)) { - let thread; - const input = Index.inputs['Catalog Hover Expand']; - input.checked = !input.checked; - $$1.event('change', null, input); - if (thread = Get$1.threadFromNode(e.target)) { - Index.cb.catalogReplies.call(thread); - return Index.cb.hoverAdjust.call(thread.OP.nodes); - } - } - }, - - popstate(e) { - if (e?.state) { - const {searched, mode, sort} = e.state; - const page = Index.getCurrentPage(); - Index.setState({search: searched, mode, sort, page}); - return Index.pageLoad(false); - } else { - // page load or hash change - const nCommands = Index.processHash(); - if (Conf['Refreshed Navigation'] && nCommands) { - return Index.update(); - } else { - return Index.pageLoad(); - } - } - }, - - pageNav(e) { - let a; - if ($$1.modifiedClick(e)) { return; } - switch (e.target.nodeName) { - case 'BUTTON': - e.target.blur(); - a = e.target.parentNode; - break; - case 'A': - a = e.target; - break; - default: - return; - } - if (a.textContent === 'Catalog') { return; } - e.preventDefault(); - return Index.userPageNav(+a.pathname.split(/\/+/)[2] || 1); - }, - - refreshFront() { - Index.pushState({page: 1}); - return Index.update(); - }, - - catalogReplies() { - if (Conf['Show Replies'] && $$1.hasClass(doc$1, 'catalog-hover-expand') && !this.catalogView.nodes.replies) { - return Index.buildCatalogReplies(this); - } - }, - - hoverAdjust() { - // Prevent hovered catalog threads from going offscreen. - let x; - if (!$$1.hasClass(doc$1, 'catalog-hover-expand')) { return; } - const rect = this.post.getBoundingClientRect(); - if (x = $$1.minmax(0, -rect.left, doc$1.clientWidth - rect.right)) { - const {style} = this.post; - style.left = `${x}px`; - style.right = `${-x}px`; - return $$1.one(this.root, 'mouseleave', () => style.left = (style.right = null)); - } - } - }, - - scrollToIndex() { - // Scroll to navlinks, or top of board if navlinks are hidden. - return Header$1.scrollToIfNeeded((Index.navLinks.getBoundingClientRect().height ? Index.navLinks : Index.root)); - }, - - getCurrentPage() { - return +window.location.pathname.split(/\/+/)[2] || 1; - }, - - userPageNav(page) { - Index.pushState({page}); - if (Conf['Refreshed Navigation']) { - return Index.update(); - } else { - return Index.pageLoad(); - } - }, - - hashCommands: { - mode: { - 'paged': 'paged', - 'infinite-scrolling': 'infinite', - 'infinite': 'infinite', - 'all-threads': 'all pages', - 'all-pages': 'all pages', - 'catalog': 'catalog' - }, - sort: { - 'bump-order': 'bump', - 'last-reply': 'lastreply', - 'last-long-reply': 'lastlong', - 'creation-date': 'birth', - 'reply-count': 'replycount', - 'file-count': 'filecount', - 'posts-per-minute': 'activity' - } - }, - - processHash() { - // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=483304 - let hash = location.href.match(/#.*/)?.[0] || ''; - const state = - {replace: true}; - const commands = hash.slice(1).split('/'); - const leftover = []; - for (var command of commands) { - var mode, sort; - if (mode = $$1.getOwn(Index.hashCommands.mode, command)) { - state.mode = mode; - } else if (command === 'index') { - state.mode = Conf['Previous Index Mode']; - state.page = 1; - } else if (sort = $$1.getOwn(Index.hashCommands.sort, command.replace(/-rev$/, ''))) { - state.sort = sort; - if (/-rev$/.test(command)) { state.sort += '-rev'; } - } else if (/^s=/.test(command)) { - state.search = decodeURIComponent(command.slice(2)).replace(/\+/g, ' ').trim(); - } else { - leftover.push(command); - } - } - hash = leftover.join('/'); - if (hash) { state.hash = `#${hash}`; } - Index.pushState(state); - return commands.length - leftover.length; - }, - - pushState(state) { - let {search, hash, replace} = state; - let pageBeforeSearch = history.state?.oldpage; - if ((search != null) && (search !== Index.search)) { - state.page = search ? 1 : (pageBeforeSearch || 1); - if (!search) { - pageBeforeSearch = undefined; - } else if (!Index.search) { - pageBeforeSearch = Index.currentPage; - } - } - Index.setState(state); - const pathname = Index.currentPage === 1 ? `/${g.BOARD}/` : `/${g.BOARD}/${Index.currentPage}`; - if (!hash) { hash = ''; } - return history[replace ? 'replaceState' : 'pushState']({ - mode: Conf['Index Mode'], - sort: Index.currentSort, - searched: Index.search, - oldpage: pageBeforeSearch - } - , '', `${location.protocol}//${location.host}${pathname}${hash}`); - }, - - setState({search, mode, sort, page, hash}) { - if ((search != null) && (search !== Index.search)) { - Index.changed.search = true; - Index.search = search; - } - if ((mode != null) && (mode !== Conf['Index Mode'])) { - Index.changed.mode = true; - Conf['Index Mode'] = mode; - $$1.set('Index Mode', mode); - if ((mode !== 'catalog') && (Conf['Previous Index Mode'] !== mode)) { - Conf['Previous Index Mode'] = mode; - $$1.set('Previous Index Mode', mode); - } - } - if ((sort != null) && (sort !== Index.currentSort)) { - Index.changed.sort = true; - Index.currentSort = sort; - Index.saveSort(); - } - if (['all pages', 'catalog'].includes(Conf['Index Mode'])) { page = 1; } - if ((page != null) && (page !== Index.currentPage)) { - Index.changed.page = true; - Index.currentPage = page; - } - if (hash != null) { - return Index.changed.hash = true; - } - }, - - savePerBoard(key, value) { - if (typeof Conf[key] === 'object') { - Conf[key][g.BOARD.ID] = value; - } else { - Conf[key] = value; - } - return $$1.set(key, Conf[key]); - }, - - saveSort() { - return Index.savePerBoard('Index Sort', Index.currentSort); - }, - - saveLastLongThresholds(i) { - return Index.savePerBoard(`Last Long Reply Thresholds ${i}`, Index.lastLongThresholds[i]); - }, - - pageLoad(scroll=true) { - if (!Index.liveThreadData) { return; } - let {threads, order, search, mode, sort, page, hash} = Index.changed; - if (!threads) { threads = search; } - if (!order) { order = sort; } - if (threads || order) { Index.sort(); } - if (threads) { Index.buildPagelist(); } - if (search) { Index.setupSearch(); } - if (mode) { Index.setupMode(); } - if (sort) { Index.setupSort(); } - if (threads || mode || page || order) { Index.buildIndex(); } - if (threads || page) { Index.setPage(); } - if (scroll && !hash) { Index.scrollToIndex(); } - if (hash) { Header$1.hashScroll(); } - return Index.changed = {}; - }, - - setupMode() { - for (var mode of ['paged', 'infinite', 'all pages', 'catalog']) { - $$1[mode === Conf['Index Mode'] ? 'addClass' : 'rmClass'](doc$1, `${mode.replace(/\ /g, '-')}-mode`); - } - Index.selectMode.value = Conf['Index Mode']; - Index.cb.size(); - Index.showHiddenThreads = false; - return $$1('#hidden-toggle a', Index.navLinks).textContent = 'Show'; - }, - - setupSort() { - Index.selectRev.checked = /-rev$/.test(Index.currentSort); - Index.selectSort.value = Index.currentSort.replace(/-rev$/, ''); - return Index.lastLongOptions.hidden = (Index.selectSort.value !== 'lastlong'); - }, - - getPagesNum() { - if (Index.search) { - return Math.ceil(Index.sortedThreadIDs.length / Index.threadsNumPerPage); - } else { - return Index.pagesNum; - } - }, - - getMaxPageNum() { - return Math.max(1, Index.getPagesNum()); - }, - - buildPagelist() { - const pagesRoot = $$1('.pages', Index.pagelist); - const maxPageNum = Index.getMaxPageNum(); - if (pagesRoot.childElementCount !== maxPageNum) { - const nodes = []; - for (let i = 1, end = maxPageNum; i <= end; i++) { - var a = $$1.el('a', { - textContent: i, - href: i === 1 ? './' : i - } - ); - nodes.push($$1.tn('['), a, $$1.tn('] ')); - } - $$1.rmAll(pagesRoot); - return $$1.add(pagesRoot, nodes); - } - }, - - setPage() { - let a, strong; - const pageNum = Index.currentPage; - const maxPageNum = Index.getMaxPageNum(); - const pagesRoot = $$1('.pages', Index.pagelist); - - // Previous/Next buttons - const prev = pagesRoot.previousElementSibling.firstElementChild; - const next = pagesRoot.nextElementSibling.firstElementChild; - let href = Math.max(pageNum - 1, 1); - prev.href = href === 1 ? './' : href; - prev.firstElementChild.disabled = href === pageNum; - href = Math.min(pageNum + 1, maxPageNum); - next.href = href === 1 ? './' : href; - next.firstElementChild.disabled = href === pageNum; - - // current page - if (strong = $$1('strong', pagesRoot)) { - if (+strong.textContent === pageNum) { return; } - $$1.replace(strong, strong.firstChild); - } else { - strong = $$1.el('strong'); - } - - if (a = pagesRoot.children[pageNum - 1]) { - $$1.before(a, strong); - return $$1.add(strong, a); - } - }, - - updateHideLabel() { - if (!Index.hideLabel) { return; } - let hiddenCount = 0; - for (var threadID of Index.liveThreadIDs) { - if (Index.isHidden(threadID)) { - hiddenCount++; - } - } - if (!hiddenCount) { - Index.hideLabel.hidden = true; - if (Index.showHiddenThreads) { Index.cb.toggleHiddenThreads(); } - return; - } - Index.hideLabel.hidden = false; - return $$1('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ? - '1 hidden thread' - : - `${hiddenCount} hidden threads`; - }, - - update(firstTime) { - let oldReq; - if (oldReq = Index.req) { - delete Index.req; - oldReq.abort(); - } - - if (Conf['Index Refresh Notifications']) { - // Optional notification for manual refreshes - if (!Index.notice) { Index.notice = new Notice('info', 'Refreshing index...'); } - if (!Index.nTimeout) { Index.nTimeout = setTimeout(() => { - if (Index.notice) { - Index.notice.el.lastElementChild.textContent += ' (disable JSON Index if this takes too long)'; - } - } - , 3 * SECOND); } - } else { - // Also display notice if Index Refresh is taking too long - if (!Index.nTimeout) { Index.nTimeout = setTimeout(() => Index.notice || (Index.notice = new Notice('info', 'Refreshing index... (disable JSON Index if this takes too long)')) - , 3 * SECOND); } - } - - // Hard refresh in case of incomplete page load. - if (!firstTime && (d$1.readyState !== 'loading') && !$$1('.board + *')) { - location.reload(); - return; - } - - Index.req = $$1.whenModified( - g.SITE.urls.catalogJSON({boardID: g.BOARD.ID}), - 'Index', - Index.load - ); - return $$1.addClass(Index.button, 'spin'); - }, - - load() { - let err; - if (this !== Index.req) { return; } // aborted - - $$1.rmClass(Index.button, 'spin'); - const {notice, nTimeout} = Index; - if (nTimeout) { clearTimeout(nTimeout); } - delete Index.nTimeout; - delete Index.req; - delete Index.notice; - - if (![200, 304].includes(this.status)) { - err = `Index refresh failed. ${this.status ? `Error ${this.statusText} (${this.status})` : 'Connection Error'}`; - if (notice) { - notice.setType('warning'); - notice.el.lastElementChild.textContent = err; - setTimeout(notice.close, SECOND); - } else { - new Notice('warning', err, 1); - } - return; - } - - try { - if (this.status === 200) { - Index.parse(this.response); - } else if (this.status === 304) { - Index.pageLoad(); - } - } catch (error) { - err = error; - c.error(`Index failure: ${err.message}`, err.stack); - if (notice) { - notice.setType('error'); - notice.el.lastElementChild.textContent = 'Index refresh failed.'; - setTimeout(notice.close, SECOND); - } else { - new Notice('error', 'Index refresh failed.', 1); - } - return; - } - - if (notice) { - if (Conf['Index Refresh Notifications']) { - notice.setType('success'); - notice.el.lastElementChild.textContent = 'Index refreshed!'; - setTimeout(notice.close, SECOND); - } else { - notice.close(); - } - } - - const timeEl = $$1('#index-last-refresh time', Index.navLinks); - timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified')); - return RelativeDates.update(timeEl); - }, - - parse(pages) { - $$1.cleanCache(url => /^https?:\/\/a\.4cdn\.org\//.test(url)); - Index.parseThreadList(pages); - Index.changed.threads = true; - return Index.pageLoad(); - }, - - parseThreadList(pages) { - Index.pagesNum = pages.length; - Index.threadsNumPerPage = pages[0]?.threads.length || 1; - Index.liveThreadData = pages.reduce(((arr, next) => arr.concat(next.threads)), []); - Index.liveThreadIDs = Index.liveThreadData.map(data => data.no); - Index.liveThreadDict = dict(); - Index.threadPosition = dict(); - Index.parsedThreads = dict(); - Index.replyData = dict(); - for (let i = 0; i < Index.liveThreadData.length; i++) { - var obj, results; - var data = Index.liveThreadData[i]; - Index.liveThreadDict[data.no] = data; - Index.threadPosition[data.no] = i; - Index.parsedThreads[data.no] = (obj = g.SITE.Build.parseJSON(data, g.BOARD)); - obj.filterResults = (results = Filter.test(obj)); - obj.isOnTop = results.top; - obj.isHidden = results.hide || ThreadHiding.isHidden(obj.boardID, obj.threadID); - if (data.last_replies) { - for (var reply of data.last_replies) { - Index.replyData[`${g.BOARD}.${reply.no}`] = reply; - } - } - } - if (Index.liveThreadData[0]) { - g.SITE.Build.spoilerRange[g.BOARD.ID] = Index.liveThreadData[0].custom_spoiler; - } - g.BOARD.threads.forEach(function(thread) { - if (!Index.liveThreadIDs.includes(thread.ID)) { return thread.collect(); } - }); - $$1.event('IndexUpdate', - {threads: ((Index.liveThreadIDs.map((ID) => `${g.BOARD}.${ID}`)))}); - }, - - isHidden(threadID) { - let thread; - if ((thread = g.BOARD.threads.get(threadID)) && thread.OP && !thread.OP.isFetchedQuote) { - return thread.isHidden; - } else { - return Index.parsedThreads[threadID].isHidden; - } - }, - - isHiddenReply(threadID, replyData) { - return PostHiding.isHidden(g.BOARD.ID, threadID, replyData.no) || Filter.isHidden(g.SITE.Build.parseJSON(replyData, g.BOARD)); - }, - - buildThreads(threadIDs, isCatalog, withReplies) { - let errors; - const threads = []; - const newThreads = []; - let newPosts = []; - for (var ID of threadIDs) { - var opRoot, thread; - try { - var OP; - var threadData = Index.liveThreadDict[ID]; - - if (thread = g.BOARD.threads.get(ID)) { - var isStale = (thread.json !== threadData) && (JSON.stringify(thread.json) !== JSON.stringify(threadData)); - if (isStale) { - thread.setCount('post', threadData.replies + 1, threadData.bumplimit); - thread.setCount('file', threadData.images + !!threadData.ext, threadData.imagelimit); - thread.setStatus('Sticky', !!threadData.sticky); - thread.setStatus('Closed', !!threadData.closed); - } - if (thread.catalogView) { - $$1.rm(thread.catalogView.nodes.replies); - thread.catalogView.nodes.replies = null; - } - } else { - thread = new Thread(ID, g.BOARD); - newThreads.push(thread); - } - var lastPost = threadData.last_replies && threadData.last_replies.length ? threadData.last_replies[threadData.last_replies.length - 1].no : ID; - if (lastPost > thread.lastPost) { thread.lastPost = lastPost; } - thread.json = threadData; - threads.push(thread); - - if ((OP = thread.OP) && !OP.isFetchedQuote) { - OP.setCatalogOP(isCatalog); - thread.setPage(Math.floor(Index.threadPosition[ID] / Index.threadsNumPerPage) + 1); - } else { - var obj = Index.parsedThreads[ID]; - opRoot = g.SITE.Build.post(obj); - OP = new Post(opRoot, thread, g.BOARD); - OP.filterResults = obj.filterResults; - newPosts.push(OP); - } - - if (!isCatalog || !thread.nodes.root) { - g.SITE.Build.thread(thread, threadData, withReplies); - } - } catch (err) { - // Skip posts that we failed to parse. - if (!errors) { errors = []; } - errors.push({ - message: `Parsing of Thread No.${thread} failed. Thread will be skipped.`, - error: err, - html: opRoot?.outerHTML - }); - } - } - if (errors) { Main$1.handleErrors(errors); } - - if (withReplies) { - newPosts = newPosts.concat(Index.buildReplies(threads)); - } - - Main$1.callbackNodes('Thread', newThreads); - Main$1.callbackNodes('Post', newPosts); - Index.updateHideLabel(); - $$1.event('IndexRefreshInternal', {threadIDs: (threads.map((t) => t.fullID)), isCatalog}); - - return threads; - }, - - buildReplies(threads) { - let errors; - const posts = []; - for (var thread of threads) { - var lastReplies; - if (!(lastReplies = Index.liveThreadDict[thread.ID].last_replies)) { continue; } - var nodes = []; - for (var data of lastReplies) { - var node, post; - if ((post = thread.posts.get(data.no)) && !post.isFetchedQuote) { - nodes.push(post.nodes.root); - continue; - } - nodes.push(node = g.SITE.Build.postFromObject(data, thread.board.ID)); - try { - posts.push(new Post(node, thread, thread.board)); - } catch (err) { - // Skip posts that we failed to parse. - if (!errors) { errors = []; } - errors.push({ - message: `Parsing of Post No.${data.no} failed. Post will be skipped.`, - error: err, - html: node?.outerHTML - }); - } - } - $$1.add(thread.nodes.root, nodes); - } - - if (errors) { Main$1.handleErrors(errors); } - return posts; - }, - - buildCatalogViews(threads) { - const catalogThreads = []; - for (var thread of threads) { - if (!thread.catalogView) { - var {ID} = thread; - var page = Math.floor(Index.threadPosition[ID] / Index.threadsNumPerPage) + 1; - var root = g.SITE.Build.catalogThread(thread, Index.liveThreadDict[ID], page); - catalogThreads.push(new CatalogThread(root, thread)); - } - } - Main$1.callbackNodes('CatalogThread', catalogThreads); - }, - - sizeCatalogViews(threads) { - // XXX When browsers support CSS3 attr(), use it instead. - const size = Conf['Index Size'] === 'small' ? 150 : 250; - for (var thread of threads) { - var {thumb} = thread.catalogView.nodes; - var {width, height} = thumb.dataset; - if (!width) { continue; } - var ratio = size / Math.max(width, height); - thumb.style.width = (width * ratio) + 'px'; - thumb.style.height = (height * ratio) + 'px'; - } - }, - - buildCatalogReplies(thread) { - let lastReplies; - const {nodes} = thread.catalogView; - if (!(lastReplies = Index.liveThreadDict[thread.ID].last_replies)) { return; } - - const replies = []; - for (var data of lastReplies) { - if (Index.isHiddenReply(thread.ID, data)) { continue; } - var reply = g.SITE.Build.catalogReply(thread, data); - RelativeDates.update($$1('time', reply)); - $$1.on($$1('.catalog-reply-preview', reply), 'mouseover', QuotePreview.mouseover); - replies.push(reply); - } - - nodes.replies = $$1.el('div', {className: 'catalog-replies'}); - $$1.add(nodes.replies, replies); - $$1.add(thread.OP.nodes.post, nodes.replies); - }, - - sort() { - let threadIDs; - const {liveThreadIDs, liveThreadData} = Index; - if (!liveThreadData) { return; } - const tmp_time = new Date().getTime()/1000; - const sortType = Index.currentSort.replace(/-rev$/, ''); - Index.sortedThreadIDs = (() => { switch (sortType) { - case 'lastreply': case 'lastlong': - var repliesAvailable = liveThreadData.some(thread => thread.last_replies?.length); - var lastlong = function(thread) { - if (!repliesAvailable) { - return thread.last_modified; - } - const iterable = thread.last_replies || []; - for (let i = iterable.length - 1; i >= 0; i--) { - var r = iterable[i]; - if (Index.isHiddenReply(thread.no, r)) { continue; } - if (sortType === 'lastreply') { - return r; - } - var len = r.com ? g.SITE.Build.parseComment(r.com).replace(/[^a-z]/ig, '').length : 0; - if (len >= Index.lastLongThresholds[+!!r.ext]) { - return r; - } - } - if (thread.omitted_posts && thread.last_replies?.length) { return thread.last_replies[0]; } else { return thread; } - }; - var lastlongD = dict(); - for (var thread of liveThreadData) { - lastlongD[thread.no] = lastlong(thread).no; - } - return [...liveThreadData].sort((a, b) => lastlongD[b.no] - lastlongD[a.no]).map(post => post.no); - case 'bump': return liveThreadIDs; - case 'birth': return [...liveThreadIDs ].sort((a, b) => b - a); - case 'replycount': return [...liveThreadData].sort((a, b) => b.replies - a.replies).map(post => post.no); - case 'filecount': return [...liveThreadData].sort((a, b) => b.images - a.images).map(post => post.no); - case 'activity': return [...liveThreadData].sort((a, b) => ((tmp_time-a.time)/(a.replies+1)) - ((tmp_time-b.time)/(b.replies+1))).map(post => post.no); - default: return liveThreadIDs; - } })(); - if (/-rev$/.test(Index.currentSort)) { - Index.sortedThreadIDs.reverse(); - } - if (Index.search && (threadIDs = Index.querySearch(Index.search))) { - Index.sortedThreadIDs = threadIDs; - } - // Sticky threads - Index.sortOnTop(obj => obj.isSticky); - // Highlighted threads - Index.sortOnTop(obj => obj.isOnTop || (Conf['Pin Watched Threads'] && ThreadWatcher$1.isWatchedRaw(obj.boardID, obj.threadID))); - // Non-hidden threads - if (Conf['Anchor Hidden Threads']) { return Index.sortOnTop(obj => !Index.isHidden(obj.threadID)); } - }, - - sortOnTop(match) { - const topThreads = []; - const bottomThreads = []; - for (var ID of Index.sortedThreadIDs) { - (match(Index.parsedThreads[ID]) ? topThreads : bottomThreads).push(ID); - } - return Index.sortedThreadIDs = topThreads.concat(bottomThreads); - }, - - buildIndex() { - let threadIDs; - if (!Index.liveThreadData) { return; } - switch (Conf['Index Mode']) { - case 'all pages': - threadIDs = Index.sortedThreadIDs; - break; - case 'catalog': - threadIDs = Index.sortedThreadIDs.filter(ID => !Index.isHidden(ID) !== Index.showHiddenThreads); - break; - default: - threadIDs = Index.threadsOnPage(Index.currentPage); - } - delete Index.pageNum; - $$1.rmAll(Index.root); - $$1.rmAll(Header$1.hover); - if (Index.loaded && Index.root.parentNode) { - $$1.event('PostsRemoved', null, Index.root); - } - if (Conf['Index Mode'] === 'catalog') { - Index.buildCatalog(threadIDs); - } else { - Index.buildStructure(threadIDs); - } - }, - - threadsOnPage(pageNum) { - const nodesPerPage = Index.threadsNumPerPage; - const offset = nodesPerPage * (pageNum - 1); - return Index.sortedThreadIDs.slice(offset , offset + nodesPerPage); - }, - - buildStructure(threadIDs) { - const threads = Index.buildThreads(threadIDs, false, Conf['Show Replies']); - const nodes = []; - for (var thread of threads) { - nodes.push(thread.nodes.root, $$1.el('hr')); - } - $$1.add(Index.root, nodes); - if (Index.root.parentNode) { - $$1.event('PostsInserted', null, Index.root); - } - Index.loaded = true; - }, - - buildCatalog(threadIDs) { - let i = 0; - const n = threadIDs.length; - let node0 = null; - var fn = function() { - if (node0 && !node0.parentNode) { return; } // Index.root cleared - const j = (i > 0) && Index.root.parentNode ? n : i + 30; - node0 = Index.buildCatalogPart(threadIDs.slice(i, j))[0]; - i = j; - if (i < n) { - return $$1.queueTask(fn); - } else { - if (Index.root.parentNode) { - $$1.event('PostsInserted', null, Index.root); - } - return Index.loaded = true; - } - }; - fn(); - }, - - buildCatalogPart(threadIDs) { - const threads = Index.buildThreads(threadIDs, true); - Index.buildCatalogViews(threads); - Index.sizeCatalogViews(threads); - const nodes = []; - for (var thread of threads) { - thread.OP.setCatalogOP(true); - $$1.add(thread.catalogView.nodes.root, thread.OP.nodes.root); - nodes.push(thread.catalogView.nodes.root); - $$1.on(thread.catalogView.nodes.root, 'mouseenter', Index.cb.catalogReplies.bind(thread)); - $$1.on(thread.OP.nodes.root, 'mouseenter', Index.cb.hoverAdjust.bind(thread.OP.nodes)); - } - $$1.add(Index.root, nodes); - return nodes; - }, - - clearSearch() { - Index.searchInput.value = ''; - Index.onSearchInput(); - return Index.searchInput.focus(); - }, - - setupSearch() { - Index.searchInput.value = Index.search; - if (Index.search) { - return Index.searchInput.dataset.searching = 1; - } else { - // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289 - return Index.searchInput.removeAttribute('data-searching'); - } - }, - - onSearchInput() { - const search = Index.searchInput.value.trim(); - if (search === Index.search) { return; } - Index.pushState({ - search, - replace: !!search === !!Index.search - }); - return Index.pageLoad(false); - }, - - querySearch(query) { - let keywords, match; - if (match = query.match(/^([\w+]+):\/(.*)\/(\w*)$/)) { - let regexp; - try { - regexp = RegExp(match[2], match[3]); - } catch (error) { - return []; - } - return Index.sortedThreadIDs.filter(ID => regexp.test(Filter.values(match[1], Index.parsedThreads[ID]).join('\n'))); - } - if (!(keywords = query.toLowerCase().match(/\S+/g))) { return; } - return Index.sortedThreadIDs.filter(ID => Index.searchMatch(Index.parsedThreads[ID], keywords)); - }, - - searchMatch(obj, keywords) { - const {info, file} = obj; - if (info.comment == null) { info.comment = g.SITE.Build.parseComment(info.commentHTML.innerHTML); } - let text = []; - for (var key of ['comment', 'subject', 'name', 'tripcode']) { - if (key in info) { text.push(info[key]); } - } - if (file) { text.push(file.name); } - text = text.join(' ').toLowerCase(); - for (var keyword of keywords) { - if (-1 === text.indexOf(keyword)) { return false; } - } - return true; - } - }; + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + + var Index = { + showHiddenThreads: false, + changed: {}, + + enabledOn({siteID, boardID}) { + return Conf['JSON Index'] && (g.sites[siteID].software === 'yotsuba') && (boardID !== 'f'); + }, + + init() { + let input, inputs, name; + if (g.VIEW !== 'index') { return; } + + // For IndexRefresh events + $$1.one(d$1, '4chanXInitFinished', this.cb.initFinished); + $$1.on(d$1, 'PostsInserted', this.cb.postsInserted); + + if (!this.enabledOn(g.BOARD)) { return; } + + this.enabled = true; + + Callbacks.Post.push({ + name: 'Index Page Numbers', + cb: this.node + }); + Callbacks.CatalogThread.push({ + name: 'Catalog Features', + cb: this.catalogNode + }); + + this.search = history.state?.searched || ''; + if (history.state?.mode) { + Conf['Index Mode'] = history.state?.mode; + } + this.currentSort = history.state?.sort; + if (!this.currentSort) { this.currentSort = typeof Conf['Index Sort'] === 'object' ? ( + Conf['Index Sort'][g.BOARD.ID] || 'bump' + ) : ( + Conf['Index Sort'] + ); } + this.currentPage = this.getCurrentPage(); + this.processHash(); + + $$1.addClass(doc$1, 'index-loading', `${Conf['Index Mode'].replace(/\ /g, '-')}-mode`); + $$1.on(window, 'popstate', this.cb.popstate); + $$1.on(d$1, 'scroll', this.scroll); + $$1.on(d$1, 'SortIndex', this.cb.resort); + + // Header refresh button + this.button = $$1.el('a', { + title: 'Refresh', + href: 'javascript:;', + textContent: '🗘' + } + ); + $$1.on(this.button, 'click', () => Index.update()); + Header$1.addShortcut('index-refresh', this.button, 590); + + // Header "Index Navigation" submenu + const entries = []; + this.inputs = (inputs = dict()); + for (name in Config.Index) { + var arr = Config.Index[name]; + if (arr instanceof Array) { + var label = UI.checkbox(name, `${name[0]}${name.slice(1).toLowerCase()}`); + label.title = arr[1]; + entries.push({el: label}); + input = label.firstChild; + $$1.on(input, 'change', $$1.cb.checked); + inputs[name] = input; + } + } + $$1.on(inputs['Show Replies'], 'change', this.cb.replies); + $$1.on(inputs['Catalog Hover Expand'], 'change', this.cb.hover); + $$1.on(inputs['Pin Watched Threads'], 'change', this.cb.resort); + $$1.on(inputs['Anchor Hidden Threads'], 'change', this.cb.resort); + + const watchSettings = function(e) { + if (input = $$1.getOwn(inputs, e.target.name)) { + input.checked = e.target.checked; + return $$1.event('change', null, input); + } + }; + $$1.on(d$1, 'OpenSettings', () => $$1.on($$1.id('fourchanx-settings'), 'change', watchSettings)); + + const sortEntry = UI.checkbox('Per-Board Sort Type', 'Per-board sort type', (typeof Conf['Index Sort'] === 'object')); + sortEntry.title = 'Set the sorting order of each board independently.'; + $$1.on(sortEntry.firstChild, 'change', this.cb.perBoardSort); + entries.splice(3, 0, {el: sortEntry}); + + Header$1.menu.addEntry({ + el: $$1.el('span', + {textContent: 'Index Navigation'}), + order: 100, + subEntries: entries + }); + + // Navigation links at top of index + this.navLinks = $$1.el('div', {className: 'navLinks json-index'}); + $$1.extend(this.navLinks, {innerHTML: NavLinksPage}); + $$1('.cataloglink a', this.navLinks).href = CatalogLinks.catalog(); + if (!BoardConfig.isArchived(g.BOARD.ID)) { $$1('.archlistlink', this.navLinks).hidden = true; } + $$1.on($$1('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront); + + // Search field + this.searchInput = $$1('#index-search', this.navLinks); + this.setupSearch(); + $$1.on(this.searchInput, 'input', this.onSearchInput); + $$1.on($$1('#index-search-clear', this.navLinks), 'click', this.clearSearch); + + // Hidden threads toggle + this.hideLabel = $$1('#hidden-label', this.navLinks); + $$1.on($$1('#hidden-toggle a', this.navLinks), 'click', this.cb.toggleHiddenThreads); + + // Drop-down menus and reverse sort toggle + this.selectRev = $$1('#index-rev', this.navLinks); + this.selectMode = $$1('#index-mode', this.navLinks); + this.selectSort = $$1('#index-sort', this.navLinks); + this.selectSize = $$1('#index-size', this.navLinks); + $$1.on(this.selectRev, 'change', this.cb.sort); + $$1.on(this.selectMode, 'change', this.cb.mode); + $$1.on(this.selectSort, 'change', this.cb.sort); + $$1.on(this.selectSize, 'change', $$1.cb.value); + $$1.on(this.selectSize, 'change', this.cb.size); + for (var select of [this.selectMode, this.selectSize]) { + select.value = Conf[select.name]; + } + this.selectRev.checked = /-rev$/.test(Index.currentSort); + this.selectSort.value = Index.currentSort.replace(/-rev$/, ''); + + // Last Long Reply options + this.lastLongOptions = $$1('#lastlong-options', this.navLinks); + this.lastLongInputs = $$('input', this.lastLongOptions); + this.lastLongThresholds = [0, 0]; + this.lastLongOptions.hidden = (this.selectSort.value !== 'lastlong'); + for (let i = 0; i < this.lastLongInputs.length; i++) { + input = this.lastLongInputs[i]; + $$1.on(input, 'change', this.cb.lastLongThresholds); + var tRaw = Conf[`Last Long Reply Thresholds ${i}`]; + input.value = (this.lastLongThresholds[i] = + typeof tRaw === 'object' ? (tRaw[g.BOARD.ID] ?? 100) : tRaw); + } + + // Thread container + this.root = $$1.el('div', {className: 'board json-index'}); + $$1.on(this.root, 'click', this.cb.hoverToggle); + this.cb.size(); + this.cb.hover(); + + // Page list + this.pagelist = $$1.el('div', {className: 'pagelist json-index'}); + $$1.extend(this.pagelist, {innerHTML: PageList}); + $$1('.cataloglink a', this.pagelist).href = CatalogLinks.catalog(); + $$1.on(this.pagelist, 'click', this.cb.pageNav); + + this.update(true); + + $$1.onExists(doc$1, 'title + *', () => d$1.title = d$1.title.replace(/\ -\ Page\ \d+/, '')); + + $$1.onExists(doc$1, '.board > .thread > .postContainer, .board + *', function() { + let el; + g.SITE.Build.hat = $$1('.board > .thread > img:first-child'); + if (g.SITE.Build.hat) { + g.BOARD.threads.forEach(function(thread) { + if (thread.nodes.root) { + return $$1.prepend(thread.nodes.root, g.SITE.Build.hat.cloneNode(false)); + } + }); + $$1.addClass(doc$1, 'hats-enabled'); + $$1.addStyle(`.catalog-thread::after {background-image: url(${g.SITE.Build.hat.src});}`); + } + + const board = $$1('.board'); + $$1.replace(board, Index.root); + if (Index.loaded) { + $$1.event('PostsInserted', null, Index.root); + } + // Hacks: + // - When removing an element from the document during page load, + // its ancestors will still be correctly created inside of it. + // - Creating loadable elements inside of an origin-less document + // will not download them. + // - Combine the two and you get a download canceller! + // Does not work on Firefox unfortunately. bugzil.la/939713 + try { + d$1.implementation.createDocument(null, null, null).appendChild(board); + } catch (error) {} + + for (el of $$('.navLinks')) { $$1.rm(el); } + $$1.rm($$1.id('ctrl-top')); + const topNavPos = $$1.id('delform').previousElementSibling; + $$1.before(topNavPos, $$1.el('hr')); + $$1.before(topNavPos, Index.navLinks); + const timeEl = $$1('#index-last-refresh time', Index.navLinks); + if (timeEl.dataset.utc) { return RelativeDates.update(timeEl); } + }); + + return Main$1.ready(function() { + let pagelist; + if (pagelist = $$1('.pagelist')) { + $$1.replace(pagelist, Index.pagelist); + } + return $$1.rmClass(doc$1, 'index-loading'); + }); + }, + + scroll() { + if (Index.req || !Index.liveThreadData || (Conf['Index Mode'] !== 'infinite') || (window.scrollY <= (doc$1.scrollHeight - (300 + window.innerHeight)))) { return; } + if (Index.pageNum == null) { Index.pageNum = Index.currentPage; } // Avoid having to pushState to keep track of the current page + + const pageNum = ++Index.pageNum; + if (pageNum > Index.pagesNum) { return Index.endNotice(); } + + const threadIDs = Index.threadsOnPage(pageNum); + return Index.buildStructure(threadIDs); + }, + + endNotice: (function() { + let notify = false; + const reset = () => notify = false; + return function() { + if (notify) { return; } + notify = true; + new Notice('info', "Last page reached.", 2); + return setTimeout(reset, 3 * SECOND); + }; + })(), + + menu: { + init() { + if ((g.VIEW !== 'index') || !Conf['Menu'] || !Conf['Thread Hiding Link'] || !Index.enabledOn(g.BOARD)) { return; } + + return Menu.menu.addEntry({ + el: $$1.el('a', { + href: 'javascript:;', + className: 'has-shortcut-text' + } + , {innerHTML: "Shift+click"}), + order: 20, + open({thread}) { + if (Conf['Index Mode'] !== 'catalog') { return false; } + this.el.firstElementChild.textContent = thread.isHidden ? + 'Unhide' + : + 'Hide'; + if (this.cb) { $$1.off(this.el, 'click', this.cb); } + this.cb = function() { + $$1.event('CloseMenu'); + return Index.toggleHide(thread); + }; + $$1.on(this.el, 'click', this.cb); + return true; + } + }); + } + }, + + node() { + if (this.isReply || this.isClone || (Index.threadPosition[this.ID] == null)) { return; } + return this.thread.setPage(Math.floor(Index.threadPosition[this.ID] / Index.threadsNumPerPage) + 1); + }, + + catalogNode() { + return $$1.on(this.nodes.root, 'mousedown click', e => { + if ((e.button !== 0) || !e.shiftKey) { return; } + if (e.type === 'click') { Index.toggleHide(this.thread); } + return e.preventDefault(); + }); + }, // Also on mousedown to prevent highlighting text. + + toggleHide(thread) { + if (Index.showHiddenThreads) { + ThreadHiding.show(thread); + if (!ThreadHiding.db.get({boardID: thread.board.ID, threadID: thread.ID})) { return; } + // Don't save when un-hiding filtered threads. + } else { + ThreadHiding.hide(thread); + } + return ThreadHiding.saveHiddenState(thread); + }, + + cycleSortType() { + let i; + const types = Index.selectSort.options.filter(option => !option.disabled); + for (i = 0; i < types.length; i++) { + var type = types[i]; + if (type.selected) { break; } + } + types[(i + 1) % types.length].selected = true; + return $$1.event('change', null, Index.selectSort); + }, + + cb: { + initFinished() { + Index.initFinishedFired = true; + return $$1.queueTask(() => Index.cb.postsInserted()); + }, + + postsInserted() { + if (!Index.initFinishedFired) { return; } + let n = 0; + g.posts.forEach(function(post) { + if (!post.isFetchedQuote && !post.indexRefreshSeen && doc$1.contains(post.nodes.root)) { + post.indexRefreshSeen = true; + return n++; + } + }); + if (n) { return $$1.event('IndexRefresh'); } + }, + + toggleHiddenThreads() { + $$1('#hidden-toggle a', Index.navLinks).textContent = (Index.showHiddenThreads = !Index.showHiddenThreads) ? + 'Hide' + : + 'Show'; + Index.sort(); + return Index.buildIndex(); + }, + + mode() { + Index.pushState({mode: this.value}); + return Index.pageLoad(false); + }, + + sort() { + const value = Index.selectRev.checked ? Index.selectSort.value + "-rev" : Index.selectSort.value; + Index.pushState({sort: value}); + return Index.pageLoad(false); + }, + + resort(e) { + Index.changed.order = true; + if (!e?.detail?.deferred) { return Index.pageLoad(false); } + }, + + perBoardSort() { + Conf['Index Sort'] = this.checked ? dict() : ''; + Index.saveSort(); + for (let i = 0; i < 2; i++) { + Conf[`Last Long Reply Thresholds ${i}`] = this.checked ? dict() : ''; + Index.saveLastLongThresholds(i); + } + }, + + lastLongThresholds() { + const i = [...this.parentNode.children].indexOf(this); + const value = +this.value; + if (!Number.isFinite(value)) { + this.value = Index.lastLongThresholds[i]; + return; + } + Index.lastLongThresholds[i] = value; + Index.saveLastLongThresholds(i); + Index.changed.order = true; + return Index.pageLoad(false); + }, + + size(e) { + if (Conf['Index Mode'] !== 'catalog') { + $$1.rmClass(Index.root, 'catalog-small'); + $$1.rmClass(Index.root, 'catalog-large'); + } else if (Conf['Index Size'] === 'small') { + $$1.addClass(Index.root, 'catalog-small'); + $$1.rmClass(Index.root, 'catalog-large'); + } else { + $$1.addClass(Index.root, 'catalog-large'); + $$1.rmClass(Index.root, 'catalog-small'); + } + if (e) { return Index.buildIndex(); } + }, + + replies() { + return Index.buildIndex(); + }, + + hover() { + return doc$1.classList.toggle('catalog-hover-expand', Conf['Catalog Hover Expand']); + }, + + hoverToggle(e) { + if (Conf['Catalog Hover Toggle'] && $$1.hasClass(doc$1, 'catalog-mode') && !$$1.modifiedClick(e) && !$$1.x('ancestor-or-self::a', e.target)) { + let thread; + const input = Index.inputs['Catalog Hover Expand']; + input.checked = !input.checked; + $$1.event('change', null, input); + if (thread = Get$1.threadFromNode(e.target)) { + Index.cb.catalogReplies.call(thread); + return Index.cb.hoverAdjust.call(thread.OP.nodes); + } + } + }, + + popstate(e) { + if (e?.state) { + const {searched, mode, sort} = e.state; + const page = Index.getCurrentPage(); + Index.setState({search: searched, mode, sort, page}); + return Index.pageLoad(false); + } else { + // page load or hash change + const nCommands = Index.processHash(); + if (Conf['Refreshed Navigation'] && nCommands) { + return Index.update(); + } else { + return Index.pageLoad(); + } + } + }, + + pageNav(e) { + let a; + if ($$1.modifiedClick(e)) { return; } + switch (e.target.nodeName) { + case 'BUTTON': + e.target.blur(); + a = e.target.parentNode; + break; + case 'A': + a = e.target; + break; + default: + return; + } + if (a.textContent === 'Catalog') { return; } + e.preventDefault(); + return Index.userPageNav(+a.pathname.split(/\/+/)[2] || 1); + }, + + refreshFront() { + Index.pushState({page: 1}); + return Index.update(); + }, + + catalogReplies() { + if (Conf['Show Replies'] && $$1.hasClass(doc$1, 'catalog-hover-expand') && !this.catalogView.nodes.replies) { + return Index.buildCatalogReplies(this); + } + }, + + hoverAdjust() { + // Prevent hovered catalog threads from going offscreen. + let x; + if (!$$1.hasClass(doc$1, 'catalog-hover-expand')) { return; } + const rect = this.post.getBoundingClientRect(); + if (x = $$1.minmax(0, -rect.left, doc$1.clientWidth - rect.right)) { + const {style} = this.post; + style.left = `${x}px`; + style.right = `${-x}px`; + return $$1.one(this.root, 'mouseleave', () => style.left = (style.right = null)); + } + } + }, + + scrollToIndex() { + // Scroll to navlinks, or top of board if navlinks are hidden. + return Header$1.scrollToIfNeeded((Index.navLinks.getBoundingClientRect().height ? Index.navLinks : Index.root)); + }, + + getCurrentPage() { + return +window.location.pathname.split(/\/+/)[2] || 1; + }, + + userPageNav(page) { + Index.pushState({page}); + if (Conf['Refreshed Navigation']) { + return Index.update(); + } else { + return Index.pageLoad(); + } + }, + + hashCommands: { + mode: { + 'paged': 'paged', + 'infinite-scrolling': 'infinite', + 'infinite': 'infinite', + 'all-threads': 'all pages', + 'all-pages': 'all pages', + 'catalog': 'catalog' + }, + sort: { + 'bump-order': 'bump', + 'last-reply': 'lastreply', + 'last-long-reply': 'lastlong', + 'creation-date': 'birth', + 'reply-count': 'replycount', + 'file-count': 'filecount', + 'posts-per-minute': 'activity' + } + }, + + processHash() { + // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=483304 + let hash = location.href.match(/#.*/)?.[0] || ''; + const state = + {replace: true}; + const commands = hash.slice(1).split('/'); + const leftover = []; + for (var command of commands) { + var mode, sort; + if (mode = $$1.getOwn(Index.hashCommands.mode, command)) { + state.mode = mode; + } else if (command === 'index') { + state.mode = Conf['Previous Index Mode']; + state.page = 1; + } else if (sort = $$1.getOwn(Index.hashCommands.sort, command.replace(/-rev$/, ''))) { + state.sort = sort; + if (/-rev$/.test(command)) { state.sort += '-rev'; } + } else if (/^s=/.test(command)) { + state.search = decodeURIComponent(command.slice(2)).replace(/\+/g, ' ').trim(); + } else { + leftover.push(command); + } + } + hash = leftover.join('/'); + if (hash) { state.hash = `#${hash}`; } + Index.pushState(state); + return commands.length - leftover.length; + }, + + pushState(state) { + let {search, hash, replace} = state; + let pageBeforeSearch = history.state?.oldpage; + if ((search != null) && (search !== Index.search)) { + state.page = search ? 1 : (pageBeforeSearch || 1); + if (!search) { + pageBeforeSearch = undefined; + } else if (!Index.search) { + pageBeforeSearch = Index.currentPage; + } + } + Index.setState(state); + const pathname = Index.currentPage === 1 ? `/${g.BOARD}/` : `/${g.BOARD}/${Index.currentPage}`; + if (!hash) { hash = ''; } + return history[replace ? 'replaceState' : 'pushState']({ + mode: Conf['Index Mode'], + sort: Index.currentSort, + searched: Index.search, + oldpage: pageBeforeSearch + } + , '', `${location.protocol}//${location.host}${pathname}${hash}`); + }, + + setState({search, mode, sort, page, hash}) { + if ((search != null) && (search !== Index.search)) { + Index.changed.search = true; + Index.search = search; + } + if ((mode != null) && (mode !== Conf['Index Mode'])) { + Index.changed.mode = true; + Conf['Index Mode'] = mode; + $$1.set('Index Mode', mode); + if ((mode !== 'catalog') && (Conf['Previous Index Mode'] !== mode)) { + Conf['Previous Index Mode'] = mode; + $$1.set('Previous Index Mode', mode); + } + } + if ((sort != null) && (sort !== Index.currentSort)) { + Index.changed.sort = true; + Index.currentSort = sort; + Index.saveSort(); + } + if (['all pages', 'catalog'].includes(Conf['Index Mode'])) { page = 1; } + if ((page != null) && (page !== Index.currentPage)) { + Index.changed.page = true; + Index.currentPage = page; + } + if (hash != null) { + return Index.changed.hash = true; + } + }, + + savePerBoard(key, value) { + if (typeof Conf[key] === 'object') { + Conf[key][g.BOARD.ID] = value; + } else { + Conf[key] = value; + } + return $$1.set(key, Conf[key]); + }, + + saveSort() { + return Index.savePerBoard('Index Sort', Index.currentSort); + }, + + saveLastLongThresholds(i) { + return Index.savePerBoard(`Last Long Reply Thresholds ${i}`, Index.lastLongThresholds[i]); + }, + + pageLoad(scroll=true) { + if (!Index.liveThreadData) { return; } + let {threads, order, search, mode, sort, page, hash} = Index.changed; + if (!threads) { threads = search; } + if (!order) { order = sort; } + if (threads || order) { Index.sort(); } + if (threads) { Index.buildPagelist(); } + if (search) { Index.setupSearch(); } + if (mode) { Index.setupMode(); } + if (sort) { Index.setupSort(); } + if (threads || mode || page || order) { Index.buildIndex(); } + if (threads || page) { Index.setPage(); } + if (scroll && !hash) { Index.scrollToIndex(); } + if (hash) { Header$1.hashScroll(); } + return Index.changed = {}; + }, + + setupMode() { + for (var mode of ['paged', 'infinite', 'all pages', 'catalog']) { + $$1[mode === Conf['Index Mode'] ? 'addClass' : 'rmClass'](doc$1, `${mode.replace(/\ /g, '-')}-mode`); + } + Index.selectMode.value = Conf['Index Mode']; + Index.cb.size(); + Index.showHiddenThreads = false; + return $$1('#hidden-toggle a', Index.navLinks).textContent = 'Show'; + }, + + setupSort() { + Index.selectRev.checked = /-rev$/.test(Index.currentSort); + Index.selectSort.value = Index.currentSort.replace(/-rev$/, ''); + return Index.lastLongOptions.hidden = (Index.selectSort.value !== 'lastlong'); + }, + + getPagesNum() { + if (Index.search) { + return Math.ceil(Index.sortedThreadIDs.length / Index.threadsNumPerPage); + } else { + return Index.pagesNum; + } + }, + + getMaxPageNum() { + return Math.max(1, Index.getPagesNum()); + }, + + buildPagelist() { + const pagesRoot = $$1('.pages', Index.pagelist); + const maxPageNum = Index.getMaxPageNum(); + if (pagesRoot.childElementCount !== maxPageNum) { + const nodes = []; + for (let i = 1, end = maxPageNum; i <= end; i++) { + var a = $$1.el('a', { + textContent: i, + href: i === 1 ? './' : i + } + ); + nodes.push($$1.tn('['), a, $$1.tn('] ')); + } + $$1.rmAll(pagesRoot); + return $$1.add(pagesRoot, nodes); + } + }, + + setPage() { + let a, strong; + const pageNum = Index.currentPage; + const maxPageNum = Index.getMaxPageNum(); + const pagesRoot = $$1('.pages', Index.pagelist); + + // Previous/Next buttons + const prev = pagesRoot.previousElementSibling.firstElementChild; + const next = pagesRoot.nextElementSibling.firstElementChild; + let href = Math.max(pageNum - 1, 1); + prev.href = href === 1 ? './' : href; + prev.firstElementChild.disabled = href === pageNum; + href = Math.min(pageNum + 1, maxPageNum); + next.href = href === 1 ? './' : href; + next.firstElementChild.disabled = href === pageNum; + + // current page + if (strong = $$1('strong', pagesRoot)) { + if (+strong.textContent === pageNum) { return; } + $$1.replace(strong, strong.firstChild); + } else { + strong = $$1.el('strong'); + } + + if (a = pagesRoot.children[pageNum - 1]) { + $$1.before(a, strong); + return $$1.add(strong, a); + } + }, + + updateHideLabel() { + if (!Index.hideLabel) { return; } + let hiddenCount = 0; + for (var threadID of Index.liveThreadIDs) { + if (Index.isHidden(threadID)) { + hiddenCount++; + } + } + if (!hiddenCount) { + Index.hideLabel.hidden = true; + if (Index.showHiddenThreads) { Index.cb.toggleHiddenThreads(); } + return; + } + Index.hideLabel.hidden = false; + return $$1('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ? + '1 hidden thread' + : + `${hiddenCount} hidden threads`; + }, + + update(firstTime) { + let oldReq; + if (oldReq = Index.req) { + delete Index.req; + oldReq.abort(); + } + + if (Conf['Index Refresh Notifications']) { + // Optional notification for manual refreshes + if (!Index.notice) { Index.notice = new Notice('info', 'Refreshing index...'); } + if (!Index.nTimeout) { Index.nTimeout = setTimeout(() => { + if (Index.notice) { + Index.notice.el.lastElementChild.textContent += ' (disable JSON Index if this takes too long)'; + } + } + , 3 * SECOND); } + } else { + // Also display notice if Index Refresh is taking too long + if (!Index.nTimeout) { Index.nTimeout = setTimeout(() => Index.notice || (Index.notice = new Notice('info', 'Refreshing index... (disable JSON Index if this takes too long)')) + , 3 * SECOND); } + } + + // Hard refresh in case of incomplete page load. + if (!firstTime && (d$1.readyState !== 'loading') && !$$1('.board + *')) { + location.reload(); + return; + } + + Index.req = $$1.whenModified( + g.SITE.urls.catalogJSON({boardID: g.BOARD.ID}), + 'Index', + Index.load + ); + return $$1.addClass(Index.button, 'spin'); + }, + + load() { + let err; + if (this !== Index.req) { return; } // aborted + + $$1.rmClass(Index.button, 'spin'); + const {notice, nTimeout} = Index; + if (nTimeout) { clearTimeout(nTimeout); } + delete Index.nTimeout; + delete Index.req; + delete Index.notice; + + if (![200, 304].includes(this.status)) { + err = `Index refresh failed. ${this.status ? `Error ${this.statusText} (${this.status})` : 'Connection Error'}`; + if (notice) { + notice.setType('warning'); + notice.el.lastElementChild.textContent = err; + setTimeout(notice.close, SECOND); + } else { + new Notice('warning', err, 1); + } + return; + } + + try { + if (this.status === 200) { + Index.parse(this.response); + } else if (this.status === 304) { + Index.pageLoad(); + } + } catch (error) { + err = error; + c.error(`Index failure: ${err.message}`, err.stack); + if (notice) { + notice.setType('error'); + notice.el.lastElementChild.textContent = 'Index refresh failed.'; + setTimeout(notice.close, SECOND); + } else { + new Notice('error', 'Index refresh failed.', 1); + } + return; + } + + if (notice) { + if (Conf['Index Refresh Notifications']) { + notice.setType('success'); + notice.el.lastElementChild.textContent = 'Index refreshed!'; + setTimeout(notice.close, SECOND); + } else { + notice.close(); + } + } + + const timeEl = $$1('#index-last-refresh time', Index.navLinks); + timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified')); + return RelativeDates.update(timeEl); + }, + + parse(pages) { + $$1.cleanCache(url => /^https?:\/\/a\.4cdn\.org\//.test(url)); + Index.parseThreadList(pages); + Index.changed.threads = true; + return Index.pageLoad(); + }, + + parseThreadList(pages) { + Index.pagesNum = pages.length; + Index.threadsNumPerPage = pages[0]?.threads.length || 1; + Index.liveThreadData = pages.reduce(((arr, next) => arr.concat(next.threads)), []); + Index.liveThreadIDs = Index.liveThreadData.map(data => data.no); + Index.liveThreadDict = dict(); + Index.threadPosition = dict(); + Index.parsedThreads = dict(); + Index.replyData = dict(); + for (let i = 0; i < Index.liveThreadData.length; i++) { + var obj, results; + var data = Index.liveThreadData[i]; + Index.liveThreadDict[data.no] = data; + Index.threadPosition[data.no] = i; + Index.parsedThreads[data.no] = (obj = g.SITE.Build.parseJSON(data, g.BOARD)); + obj.filterResults = (results = Filter.test(obj)); + obj.isOnTop = results.top; + obj.isHidden = results.hide || ThreadHiding.isHidden(obj.boardID, obj.threadID); + if (data.last_replies) { + for (var reply of data.last_replies) { + Index.replyData[`${g.BOARD}.${reply.no}`] = reply; + } + } + } + if (Index.liveThreadData[0]) { + g.SITE.Build.spoilerRange[g.BOARD.ID] = Index.liveThreadData[0].custom_spoiler; + } + g.BOARD.threads.forEach(function(thread) { + if (!Index.liveThreadIDs.includes(thread.ID)) { return thread.collect(); } + }); + $$1.event('IndexUpdate', + {threads: ((Index.liveThreadIDs.map((ID) => `${g.BOARD}.${ID}`)))}); + }, + + isHidden(threadID) { + let thread; + if ((thread = g.BOARD.threads.get(threadID)) && thread.OP && !thread.OP.isFetchedQuote) { + return thread.isHidden; + } else { + return Index.parsedThreads[threadID].isHidden; + } + }, + + isHiddenReply(threadID, replyData) { + return PostHiding.isHidden(g.BOARD.ID, threadID, replyData.no) || Filter.isHidden(g.SITE.Build.parseJSON(replyData, g.BOARD)); + }, + + buildThreads(threadIDs, isCatalog, withReplies) { + let errors; + const threads = []; + const newThreads = []; + let newPosts = []; + for (var ID of threadIDs) { + var opRoot, thread; + try { + var OP; + var threadData = Index.liveThreadDict[ID]; + + if (thread = g.BOARD.threads.get(ID)) { + var isStale = (thread.json !== threadData) && (JSON.stringify(thread.json) !== JSON.stringify(threadData)); + if (isStale) { + thread.setCount('post', threadData.replies + 1, threadData.bumplimit); + thread.setCount('file', threadData.images + !!threadData.ext, threadData.imagelimit); + thread.setStatus('Sticky', !!threadData.sticky); + thread.setStatus('Closed', !!threadData.closed); + } + if (thread.catalogView) { + $$1.rm(thread.catalogView.nodes.replies); + thread.catalogView.nodes.replies = null; + } + } else { + thread = new Thread(ID, g.BOARD); + newThreads.push(thread); + } + var lastPost = threadData.last_replies && threadData.last_replies.length ? threadData.last_replies[threadData.last_replies.length - 1].no : ID; + if (lastPost > thread.lastPost) { thread.lastPost = lastPost; } + thread.json = threadData; + threads.push(thread); + + if ((OP = thread.OP) && !OP.isFetchedQuote) { + OP.setCatalogOP(isCatalog); + thread.setPage(Math.floor(Index.threadPosition[ID] / Index.threadsNumPerPage) + 1); + } else { + var obj = Index.parsedThreads[ID]; + opRoot = g.SITE.Build.post(obj); + OP = new Post(opRoot, thread, g.BOARD); + OP.filterResults = obj.filterResults; + newPosts.push(OP); + } + + if (!isCatalog || !thread.nodes.root) { + g.SITE.Build.thread(thread, threadData, withReplies); + } + } catch (err) { + // Skip posts that we failed to parse. + if (!errors) { errors = []; } + errors.push({ + message: `Parsing of Thread No.${thread} failed. Thread will be skipped.`, + error: err, + html: opRoot?.outerHTML + }); + } + } + if (errors) { Main$1.handleErrors(errors); } + + if (withReplies) { + newPosts = newPosts.concat(Index.buildReplies(threads)); + } + + Main$1.callbackNodes('Thread', newThreads); + Main$1.callbackNodes('Post', newPosts); + Index.updateHideLabel(); + $$1.event('IndexRefreshInternal', {threadIDs: (threads.map((t) => t.fullID)), isCatalog}); + + return threads; + }, + + buildReplies(threads) { + let errors; + const posts = []; + for (var thread of threads) { + var lastReplies; + if (!(lastReplies = Index.liveThreadDict[thread.ID].last_replies)) { continue; } + var nodes = []; + for (var data of lastReplies) { + var node, post; + if ((post = thread.posts.get(data.no)) && !post.isFetchedQuote) { + nodes.push(post.nodes.root); + continue; + } + nodes.push(node = g.SITE.Build.postFromObject(data, thread.board.ID)); + try { + posts.push(new Post(node, thread, thread.board)); + } catch (err) { + // Skip posts that we failed to parse. + if (!errors) { errors = []; } + errors.push({ + message: `Parsing of Post No.${data.no} failed. Post will be skipped.`, + error: err, + html: node?.outerHTML + }); + } + } + $$1.add(thread.nodes.root, nodes); + } + + if (errors) { Main$1.handleErrors(errors); } + return posts; + }, + + buildCatalogViews(threads) { + const catalogThreads = []; + for (var thread of threads) { + if (!thread.catalogView) { + var {ID} = thread; + var page = Math.floor(Index.threadPosition[ID] / Index.threadsNumPerPage) + 1; + var root = g.SITE.Build.catalogThread(thread, Index.liveThreadDict[ID], page); + catalogThreads.push(new CatalogThread(root, thread)); + } + } + Main$1.callbackNodes('CatalogThread', catalogThreads); + }, + + sizeCatalogViews(threads) { + // XXX When browsers support CSS3 attr(), use it instead. + const size = Conf['Index Size'] === 'small' ? 150 : 250; + for (var thread of threads) { + var {thumb} = thread.catalogView.nodes; + var {width, height} = thumb.dataset; + if (!width) { continue; } + var ratio = size / Math.max(width, height); + thumb.style.width = (width * ratio) + 'px'; + thumb.style.height = (height * ratio) + 'px'; + } + }, + + buildCatalogReplies(thread) { + let lastReplies; + const {nodes} = thread.catalogView; + if (!(lastReplies = Index.liveThreadDict[thread.ID].last_replies)) { return; } + + const replies = []; + for (var data of lastReplies) { + if (Index.isHiddenReply(thread.ID, data)) { continue; } + var reply = g.SITE.Build.catalogReply(thread, data); + RelativeDates.update($$1('time', reply)); + $$1.on($$1('.catalog-reply-preview', reply), 'mouseover', QuotePreview.mouseover); + replies.push(reply); + } + + nodes.replies = $$1.el('div', {className: 'catalog-replies'}); + $$1.add(nodes.replies, replies); + $$1.add(thread.OP.nodes.post, nodes.replies); + }, + + sort() { + let threadIDs; + const {liveThreadIDs, liveThreadData} = Index; + if (!liveThreadData) { return; } + const tmp_time = new Date().getTime()/1000; + const sortType = Index.currentSort.replace(/-rev$/, ''); + Index.sortedThreadIDs = (() => { switch (sortType) { + case 'lastreply': case 'lastlong': + var repliesAvailable = liveThreadData.some(thread => thread.last_replies?.length); + var lastlong = function(thread) { + if (!repliesAvailable) { + return thread.last_modified; + } + const iterable = thread.last_replies || []; + for (let i = iterable.length - 1; i >= 0; i--) { + var r = iterable[i]; + if (Index.isHiddenReply(thread.no, r)) { continue; } + if (sortType === 'lastreply') { + return r; + } + var len = r.com ? g.SITE.Build.parseComment(r.com).replace(/[^a-z]/ig, '').length : 0; + if (len >= Index.lastLongThresholds[+!!r.ext]) { + return r; + } + } + if (thread.omitted_posts && thread.last_replies?.length) { return thread.last_replies[0]; } else { return thread; } + }; + var lastlongD = dict(); + for (var thread of liveThreadData) { + lastlongD[thread.no] = lastlong(thread).no; + } + return [...liveThreadData].sort((a, b) => lastlongD[b.no] - lastlongD[a.no]).map(post => post.no); + case 'bump': return liveThreadIDs; + case 'birth': return [...liveThreadIDs ].sort((a, b) => b - a); + case 'replycount': return [...liveThreadData].sort((a, b) => b.replies - a.replies).map(post => post.no); + case 'filecount': return [...liveThreadData].sort((a, b) => b.images - a.images).map(post => post.no); + case 'activity': return [...liveThreadData].sort((a, b) => ((tmp_time-a.time)/(a.replies+1)) - ((tmp_time-b.time)/(b.replies+1))).map(post => post.no); + default: return liveThreadIDs; + } })(); + if (/-rev$/.test(Index.currentSort)) { + Index.sortedThreadIDs.reverse(); + } + if (Index.search && (threadIDs = Index.querySearch(Index.search))) { + Index.sortedThreadIDs = threadIDs; + } + // Sticky threads + Index.sortOnTop(obj => obj.isSticky); + // Highlighted threads + Index.sortOnTop(obj => obj.isOnTop || (Conf['Pin Watched Threads'] && ThreadWatcher$1.isWatchedRaw(obj.boardID, obj.threadID))); + // Non-hidden threads + if (Conf['Anchor Hidden Threads']) { return Index.sortOnTop(obj => !Index.isHidden(obj.threadID)); } + }, + + sortOnTop(match) { + const topThreads = []; + const bottomThreads = []; + for (var ID of Index.sortedThreadIDs) { + (match(Index.parsedThreads[ID]) ? topThreads : bottomThreads).push(ID); + } + return Index.sortedThreadIDs = topThreads.concat(bottomThreads); + }, + + buildIndex() { + let threadIDs; + if (!Index.liveThreadData) { return; } + switch (Conf['Index Mode']) { + case 'all pages': + threadIDs = Index.sortedThreadIDs; + break; + case 'catalog': + threadIDs = Index.sortedThreadIDs.filter(ID => !Index.isHidden(ID) !== Index.showHiddenThreads); + break; + default: + threadIDs = Index.threadsOnPage(Index.currentPage); + } + delete Index.pageNum; + $$1.rmAll(Index.root); + $$1.rmAll(Header$1.hover); + if (Index.loaded && Index.root.parentNode) { + $$1.event('PostsRemoved', null, Index.root); + } + if (Conf['Index Mode'] === 'catalog') { + Index.buildCatalog(threadIDs); + } else { + Index.buildStructure(threadIDs); + } + }, + + threadsOnPage(pageNum) { + const nodesPerPage = Index.threadsNumPerPage; + const offset = nodesPerPage * (pageNum - 1); + return Index.sortedThreadIDs.slice(offset , offset + nodesPerPage); + }, + + buildStructure(threadIDs) { + const threads = Index.buildThreads(threadIDs, false, Conf['Show Replies']); + const nodes = []; + for (var thread of threads) { + nodes.push(thread.nodes.root, $$1.el('hr')); + } + $$1.add(Index.root, nodes); + if (Index.root.parentNode) { + $$1.event('PostsInserted', null, Index.root); + } + Index.loaded = true; + }, + + buildCatalog(threadIDs) { + let i = 0; + const n = threadIDs.length; + let node0 = null; + var fn = function() { + if (node0 && !node0.parentNode) { return; } // Index.root cleared + const j = (i > 0) && Index.root.parentNode ? n : i + 30; + node0 = Index.buildCatalogPart(threadIDs.slice(i, j))[0]; + i = j; + if (i < n) { + return $$1.queueTask(fn); + } else { + if (Index.root.parentNode) { + $$1.event('PostsInserted', null, Index.root); + } + return Index.loaded = true; + } + }; + fn(); + }, + + buildCatalogPart(threadIDs) { + const threads = Index.buildThreads(threadIDs, true); + Index.buildCatalogViews(threads); + Index.sizeCatalogViews(threads); + const nodes = []; + for (var thread of threads) { + thread.OP.setCatalogOP(true); + $$1.add(thread.catalogView.nodes.root, thread.OP.nodes.root); + nodes.push(thread.catalogView.nodes.root); + $$1.on(thread.catalogView.nodes.root, 'mouseenter', Index.cb.catalogReplies.bind(thread)); + $$1.on(thread.OP.nodes.root, 'mouseenter', Index.cb.hoverAdjust.bind(thread.OP.nodes)); + } + $$1.add(Index.root, nodes); + return nodes; + }, + + clearSearch() { + Index.searchInput.value = ''; + Index.onSearchInput(); + return Index.searchInput.focus(); + }, + + setupSearch() { + Index.searchInput.value = Index.search; + if (Index.search) { + return Index.searchInput.dataset.searching = 1; + } else { + // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289 + return Index.searchInput.removeAttribute('data-searching'); + } + }, + + onSearchInput() { + const search = Index.searchInput.value.trim(); + if (search === Index.search) { return; } + Index.pushState({ + search, + replace: !!search === !!Index.search + }); + return Index.pageLoad(false); + }, + + querySearch(query) { + let keywords, match; + if (match = query.match(/^([\w+]+):\/(.*)\/(\w*)$/)) { + let regexp; + try { + regexp = RegExp(match[2], match[3]); + } catch (error) { + return []; + } + return Index.sortedThreadIDs.filter(ID => regexp.test(Filter.values(match[1], Index.parsedThreads[ID]).join('\n'))); + } + if (!(keywords = query.toLowerCase().match(/\S+/g))) { return; } + return Index.sortedThreadIDs.filter(ID => Index.searchMatch(Index.parsedThreads[ID], keywords)); + }, + + searchMatch(obj, keywords) { + const {info, file} = obj; + if (info.comment == null) { info.comment = g.SITE.Build.parseComment(info.commentHTML.innerHTML); } + let text = []; + for (var key of ['comment', 'subject', 'name', 'tripcode']) { + if (key in info) { text.push(info[key]); } + } + if (file) { text.push(file.name); } + text = text.join(' ').toLowerCase(); + for (var keyword of keywords) { + if (-1 === text.indexOf(keyword)) { return false; } + } + return true; + } + }; var Index$1 = Index; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var ThreadHiding = { - init() { - if (!['index', 'catalog'].includes(g.VIEW) || (!Conf['Thread Hiding Buttons'] && !(Conf['Menu'] && Conf['Thread Hiding Link']) && !Conf['JSON Index'])) { return; } - this.db = new DataBoard('hiddenThreads'); - if (g.VIEW === 'catalog') { return this.catalogWatch(); } - this.catalogSet(g.BOARD); - $$1.on(d$1, 'IndexRefreshInternal', this.onIndexRefresh); - if (Conf['Thread Hiding Buttons']) { - $$1.addClass(doc$1, 'thread-hide'); - } - return Callbacks.Post.push({ - name: 'Thread Hiding', - cb: this.node - }); - }, - - catalogSet(board) { - if (!$$1.hasStorage || (g.SITE.software !== 'yotsuba')) { return; } - const hiddenThreads = ThreadHiding.db.get({ - boardID: board.ID, - defaultValue: dict() - }); - for (var threadID in hiddenThreads) { hiddenThreads[threadID] = true; } - return localStorage.setItem(`4chan-hide-t-${board}`, JSON.stringify(hiddenThreads)); - }, - - catalogWatch() { - if (!$$1.hasStorage || (g.SITE.software !== 'yotsuba')) { return; } - this.hiddenThreads = JSON.parse(localStorage.getItem(`4chan-hide-t-${g.BOARD}`)) || {}; - return Main$1.ready(() => // 4chan's catalog sets the style to "display: none;" when hiding or unhiding a thread. - new MutationObserver(ThreadHiding.catalogSave).observe($$1.id('threads'), { - attributes: true, - subtree: true, - attributeFilter: ['style'] - })); - }, - - catalogSave() { - let threadID; - const hiddenThreads2 = JSON.parse(localStorage.getItem(`4chan-hide-t-${g.BOARD}`)) || {}; - for (threadID in hiddenThreads2) { - if (!$$1.hasOwn(ThreadHiding.hiddenThreads, threadID)) { - ThreadHiding.db.set({ - boardID: g.BOARD.ID, - threadID, - val: {makeStub: Conf['Stubs']}}); - } - } - for (threadID in ThreadHiding.hiddenThreads) { - if (!$$1.hasOwn(hiddenThreads2, threadID)) { - ThreadHiding.db.delete({ - boardID: g.BOARD.ID, - threadID - }); - } - } - return ThreadHiding.hiddenThreads = hiddenThreads2; - }, - - isHidden(boardID, threadID) { - return !!(ThreadHiding.db && ThreadHiding.db.get({boardID, threadID})); - }, - - node() { - let data; - if (this.isReply || this.isClone || this.isFetchedQuote) { return; } - - if (Conf['Thread Hiding Buttons']) { - $$1.prepend(this.nodes.root, ThreadHiding.makeButton(this.thread, 'hide')); - } - - if (data = ThreadHiding.db.get({boardID: this.board.ID, threadID: this.ID})) { - return ThreadHiding.hide(this.thread, data.makeStub); - } - }, - - onIndexRefresh() { - return g.BOARD.threads.forEach(function(thread) { - const {root} = thread.nodes; - if (thread.isHidden && thread.stub && !root.contains(thread.stub)) { - return ThreadHiding.makeStub(thread, root); - } - }); - }, - - menu: { - init() { - if ((g.VIEW !== 'index') || !Conf['Menu'] || !Conf['Thread Hiding Link']) { return; } - - let div = $$1.el('div', { - className: 'hide-thread-link', - textContent: 'Hide' - } - ); - - const apply = $$1.el('a', { - textContent: 'Apply', - href: 'javascript:;' - } - ); - $$1.on(apply, 'click', ThreadHiding.menu.hide); - - const makeStub = UI.checkbox('Stubs', 'Make stub'); - - Menu.menu.addEntry({ - el: div, - order: 20, - open({thread, isReply}) { - if (isReply || thread.isHidden || (Conf['JSON Index'] && (Conf['Index Mode'] === 'catalog'))) { - return false; - } - ThreadHiding.menu.thread = thread; - return true; - }, - subEntries: [{el: apply}, {el: makeStub}]}); - - div = $$1.el('a', { - className: 'show-thread-link', - textContent: 'Show', - href: 'javascript:;' - } - ); - $$1.on(div, 'click', ThreadHiding.menu.show); - - Menu.menu.addEntry({ - el: div, - order: 20, - open({thread, isReply}) { - if (isReply || !thread.isHidden || (Conf['JSON Index'] && (Conf['Index Mode'] === 'catalog'))) { - return false; - } - ThreadHiding.menu.thread = thread; - return true; - } - }); - - const hideStubLink = $$1.el('a', { - textContent: 'Hide stub', - href: 'javascript:;' - } - ); - $$1.on(hideStubLink, 'click', ThreadHiding.menu.hideStub); - - return Menu.menu.addEntry({ - el: hideStubLink, - order: 15, - open({thread, isReply}) { - if (isReply || !thread.isHidden || (Conf['JSON Index'] && (Conf['Index Mode'] === 'catalog'))) { - return false; - } - return ThreadHiding.menu.thread = thread; - } - }); - }, - - hide() { - const makeStub = $$1('input', this.parentNode).checked; - const {thread} = ThreadHiding.menu; - ThreadHiding.hide(thread, makeStub); - ThreadHiding.saveHiddenState(thread, makeStub); - return $$1.event('CloseMenu'); - }, - - show() { - const {thread} = ThreadHiding.menu; - ThreadHiding.show(thread); - ThreadHiding.saveHiddenState(thread); - return $$1.event('CloseMenu'); - }, - - hideStub() { - const {thread} = ThreadHiding.menu; - ThreadHiding.show(thread); - ThreadHiding.hide(thread, false); - ThreadHiding.saveHiddenState(thread, false); - $$1.event('CloseMenu'); - } - }, - - makeButton(thread, type) { - const a = $$1.el('a', { - className: `${type}-thread-button`, - href: 'javascript:;' - } - ); - $$1.extend(a, {textContent: type === "hide" ? '➖︎' : '➕︎' }); - a.dataset.fullID = thread.fullID; - $$1.on(a, 'click', ThreadHiding.toggle); - return a; - }, - - makeStub(thread, root) { - let summary, threadDivider; - let numReplies = $$(g.SITE.selectors.replyOriginal, root).length; - if (summary = $$1(g.SITE.selectors.summary, root)) { numReplies += +summary.textContent.match(/\d+/); } - - const a = ThreadHiding.makeButton(thread, 'show'); - $$1.add(a, $$1.tn(` ${thread.OP.info.nameBlock} (${numReplies === 1 ? '1 reply' : `${numReplies} replies`})`)); - thread.stub = $$1.el('div', - {className: 'stub'}); - if (Conf['Menu']) { - $$1.add(thread.stub, [a, Menu.makeButton(thread.OP)]); - } else { - $$1.add(thread.stub, a); - } - $$1.prepend(root, thread.stub); - - // Prevent hiding of thread divider on sites that put it inside the thread - if (threadDivider = $$1(g.SITE.selectors.threadDivider, root)) { - return $$1.addClass(threadDivider, 'threadDivider'); - } - }, - - saveHiddenState(thread, makeStub) { - if (thread.isHidden) { - ThreadHiding.db.set({ - boardID: thread.board.ID, - threadID: thread.ID, - val: {makeStub}}); - } else { - ThreadHiding.db.delete({ - boardID: thread.board.ID, - threadID: thread.ID - }); - } - return ThreadHiding.catalogSet(thread.board); - }, - - toggle(thread) { - if (!(thread instanceof Thread)) { - thread = g.threads.get(this.dataset.fullID); - } - if (thread.isHidden) { - ThreadHiding.show(thread); - } else { - ThreadHiding.hide(thread); - } - return ThreadHiding.saveHiddenState(thread); - }, - - hide(thread, makeStub=Conf['Stubs']) { - if (thread.isHidden) { return; } - const threadRoot = thread.nodes.root; - thread.isHidden = true; - Index$1.updateHideLabel(); - if (thread.catalogView && !Index$1.showHiddenThreads) { - $$1.rm(thread.catalogView.nodes.root); - $$1.event('PostsRemoved', null, Index$1.root); - } - - if (!makeStub) { return threadRoot.hidden = true; } - - return ThreadHiding.makeStub(thread, threadRoot); - }, - - show(thread) { - if (thread.stub) { - $$1.rm(thread.stub); - delete thread.stub; - } - const threadRoot = thread.nodes.root; - threadRoot.hidden = (thread.isHidden = false); - Index$1.updateHideLabel(); - if (thread.catalogView && Index$1.showHiddenThreads) { - $$1.rm(thread.catalogView.nodes.root); - return $$1.event('PostsRemoved', null, Index$1.root); - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var ThreadHiding = { + init() { + if (!['index', 'catalog'].includes(g.VIEW) || (!Conf['Thread Hiding Buttons'] && !(Conf['Menu'] && Conf['Thread Hiding Link']) && !Conf['JSON Index'])) { return; } + this.db = new DataBoard('hiddenThreads'); + if (g.VIEW === 'catalog') { return this.catalogWatch(); } + this.catalogSet(g.BOARD); + $$1.on(d$1, 'IndexRefreshInternal', this.onIndexRefresh); + if (Conf['Thread Hiding Buttons']) { + $$1.addClass(doc$1, 'thread-hide'); + } + return Callbacks.Post.push({ + name: 'Thread Hiding', + cb: this.node + }); + }, + + catalogSet(board) { + if (!$$1.hasStorage || (g.SITE.software !== 'yotsuba')) { return; } + const hiddenThreads = ThreadHiding.db.get({ + boardID: board.ID, + defaultValue: dict() + }); + for (var threadID in hiddenThreads) { hiddenThreads[threadID] = true; } + return localStorage.setItem(`4chan-hide-t-${board}`, JSON.stringify(hiddenThreads)); + }, + + catalogWatch() { + if (!$$1.hasStorage || (g.SITE.software !== 'yotsuba')) { return; } + this.hiddenThreads = JSON.parse(localStorage.getItem(`4chan-hide-t-${g.BOARD}`)) || {}; + return Main$1.ready(() => // 4chan's catalog sets the style to "display: none;" when hiding or unhiding a thread. + new MutationObserver(ThreadHiding.catalogSave).observe($$1.id('threads'), { + attributes: true, + subtree: true, + attributeFilter: ['style'] + })); + }, + + catalogSave() { + let threadID; + const hiddenThreads2 = JSON.parse(localStorage.getItem(`4chan-hide-t-${g.BOARD}`)) || {}; + for (threadID in hiddenThreads2) { + if (!$$1.hasOwn(ThreadHiding.hiddenThreads, threadID)) { + ThreadHiding.db.set({ + boardID: g.BOARD.ID, + threadID, + val: {makeStub: Conf['Stubs']}}); + } + } + for (threadID in ThreadHiding.hiddenThreads) { + if (!$$1.hasOwn(hiddenThreads2, threadID)) { + ThreadHiding.db.delete({ + boardID: g.BOARD.ID, + threadID + }); + } + } + return ThreadHiding.hiddenThreads = hiddenThreads2; + }, + + isHidden(boardID, threadID) { + return !!(ThreadHiding.db && ThreadHiding.db.get({boardID, threadID})); + }, + + node() { + let data; + if (this.isReply || this.isClone || this.isFetchedQuote) { return; } + + if (Conf['Thread Hiding Buttons']) { + $$1.prepend(this.nodes.root, ThreadHiding.makeButton(this.thread, 'hide')); + } + + if (data = ThreadHiding.db.get({boardID: this.board.ID, threadID: this.ID})) { + return ThreadHiding.hide(this.thread, data.makeStub); + } + }, + + onIndexRefresh() { + return g.BOARD.threads.forEach(function(thread) { + const {root} = thread.nodes; + if (thread.isHidden && thread.stub && !root.contains(thread.stub)) { + return ThreadHiding.makeStub(thread, root); + } + }); + }, + + menu: { + init() { + if ((g.VIEW !== 'index') || !Conf['Menu'] || !Conf['Thread Hiding Link']) { return; } + + let div = $$1.el('div', { + className: 'hide-thread-link', + textContent: 'Hide' + } + ); + + const apply = $$1.el('a', { + textContent: 'Apply', + href: 'javascript:;' + } + ); + $$1.on(apply, 'click', ThreadHiding.menu.hide); + + const makeStub = UI.checkbox('Stubs', 'Make stub'); + + Menu.menu.addEntry({ + el: div, + order: 20, + open({thread, isReply}) { + if (isReply || thread.isHidden || (Conf['JSON Index'] && (Conf['Index Mode'] === 'catalog'))) { + return false; + } + ThreadHiding.menu.thread = thread; + return true; + }, + subEntries: [{el: apply}, {el: makeStub}]}); + + div = $$1.el('a', { + className: 'show-thread-link', + textContent: 'Show', + href: 'javascript:;' + } + ); + $$1.on(div, 'click', ThreadHiding.menu.show); + + Menu.menu.addEntry({ + el: div, + order: 20, + open({thread, isReply}) { + if (isReply || !thread.isHidden || (Conf['JSON Index'] && (Conf['Index Mode'] === 'catalog'))) { + return false; + } + ThreadHiding.menu.thread = thread; + return true; + } + }); + + const hideStubLink = $$1.el('a', { + textContent: 'Hide stub', + href: 'javascript:;' + } + ); + $$1.on(hideStubLink, 'click', ThreadHiding.menu.hideStub); + + return Menu.menu.addEntry({ + el: hideStubLink, + order: 15, + open({thread, isReply}) { + if (isReply || !thread.isHidden || (Conf['JSON Index'] && (Conf['Index Mode'] === 'catalog'))) { + return false; + } + return ThreadHiding.menu.thread = thread; + } + }); + }, + + hide() { + const makeStub = $$1('input', this.parentNode).checked; + const {thread} = ThreadHiding.menu; + ThreadHiding.hide(thread, makeStub); + ThreadHiding.saveHiddenState(thread, makeStub); + return $$1.event('CloseMenu'); + }, + + show() { + const {thread} = ThreadHiding.menu; + ThreadHiding.show(thread); + ThreadHiding.saveHiddenState(thread); + return $$1.event('CloseMenu'); + }, + + hideStub() { + const {thread} = ThreadHiding.menu; + ThreadHiding.show(thread); + ThreadHiding.hide(thread, false); + ThreadHiding.saveHiddenState(thread, false); + $$1.event('CloseMenu'); + } + }, + + makeButton(thread, type) { + const a = $$1.el('a', { + className: `${type}-thread-button`, + href: 'javascript:;' + } + ); + $$1.extend(a, {textContent: type === "hide" ? '➖︎' : '➕︎' }); + a.dataset.fullID = thread.fullID; + $$1.on(a, 'click', ThreadHiding.toggle); + return a; + }, + + makeStub(thread, root) { + let summary, threadDivider; + let numReplies = $$(g.SITE.selectors.replyOriginal, root).length; + if (summary = $$1(g.SITE.selectors.summary, root)) { numReplies += +summary.textContent.match(/\d+/); } + + const a = ThreadHiding.makeButton(thread, 'show'); + $$1.add(a, $$1.tn(` ${thread.OP.info.nameBlock} (${numReplies === 1 ? '1 reply' : `${numReplies} replies`})`)); + thread.stub = $$1.el('div', + {className: 'stub'}); + if (Conf['Menu']) { + $$1.add(thread.stub, [a, Menu.makeButton(thread.OP)]); + } else { + $$1.add(thread.stub, a); + } + $$1.prepend(root, thread.stub); + + // Prevent hiding of thread divider on sites that put it inside the thread + if (threadDivider = $$1(g.SITE.selectors.threadDivider, root)) { + return $$1.addClass(threadDivider, 'threadDivider'); + } + }, + + saveHiddenState(thread, makeStub) { + if (thread.isHidden) { + ThreadHiding.db.set({ + boardID: thread.board.ID, + threadID: thread.ID, + val: {makeStub}}); + } else { + ThreadHiding.db.delete({ + boardID: thread.board.ID, + threadID: thread.ID + }); + } + return ThreadHiding.catalogSet(thread.board); + }, + + toggle(thread) { + if (!(thread instanceof Thread)) { + thread = g.threads.get(this.dataset.fullID); + } + if (thread.isHidden) { + ThreadHiding.show(thread); + } else { + ThreadHiding.hide(thread); + } + return ThreadHiding.saveHiddenState(thread); + }, + + hide(thread, makeStub=Conf['Stubs']) { + if (thread.isHidden) { return; } + const threadRoot = thread.nodes.root; + thread.isHidden = true; + Index$1.updateHideLabel(); + if (thread.catalogView && !Index$1.showHiddenThreads) { + $$1.rm(thread.catalogView.nodes.root); + $$1.event('PostsRemoved', null, Index$1.root); + } + + if (!makeStub) { return threadRoot.hidden = true; } + + return ThreadHiding.makeStub(thread, threadRoot); + }, + + show(thread) { + if (thread.stub) { + $$1.rm(thread.stub); + delete thread.stub; + } + const threadRoot = thread.nodes.root; + threadRoot.hidden = (thread.isHidden = false); + Index$1.updateHideLabel(); + if (thread.catalogView && Index$1.showHiddenThreads) { + $$1.rm(thread.catalogView.nodes.root); + return $$1.event('PostsRemoved', null, Index$1.root); + } + } }; /* @@ -8947,507 +8947,507 @@ https://*.hcaptcha.com h("div", { class: "section-container" }, h("section", null))); - var FilterGuidePage = `
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 and Unique ID filtering use 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. Use sfw and nsfw to reference all worksafe or not-worksafe boards.
    - For example: boards:a,jp;.
    - To specify boards on a particular site, put the beginning of the domain and a slash character before the list.
    - Any initial www. should not be included, and all 4chan domains are considered 4chan.org.
    - For example: boards:4:a,jp,sama:a,z;.
    - An asterisk can be used to specify all boards on a site.
    - For example: boards:4:*;.
    -
  • -
  • - Select boards to be excluded from the filter. The syntax is the same as for the boards: option above.
    - For example: exclude:vg,v;. -
  • -
  • - Filter OPs only along with their threads (\`only\`) or replies only (\`no\`).
    - For example: op:only; or op:no;. -
  • -
  • - Filter only posts with files (\`only\`) or only posts without files (\`no\`).
    - For example: file:only; or file:no;. -
  • -
  • - 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 the board index by default.
    - For example: top:yes; or top:no;. -
  • -
  • - Show a desktop notification instead of hiding.
    - For example: notify;. -
  • -
  • - Filters in the "General" section apply to multiple fields, by default subject,name,filename,comment.
    - The fields can be specified with the type option, separated by commas.
    - For example: type:@{filterTypes};.
    - Types can also be combined with a + sign; this indicates the filter applies to the given fields joined by newlines.
    - For example: type:filename+filesize+dimensions;.
    -
  • -
+ var FilterGuidePage = `
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 and Unique ID filtering use 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. Use sfw and nsfw to reference all worksafe or not-worksafe boards.
    + For example: boards:a,jp;.
    + To specify boards on a particular site, put the beginning of the domain and a slash character before the list.
    + Any initial www. should not be included, and all 4chan domains are considered 4chan.org.
    + For example: boards:4:a,jp,sama:a,z;.
    + An asterisk can be used to specify all boards on a site.
    + For example: boards:4:*;.
    +
  • +
  • + Select boards to be excluded from the filter. The syntax is the same as for the boards: option above.
    + For example: exclude:vg,v;. +
  • +
  • + Filter OPs only along with their threads (\`only\`) or replies only (\`no\`).
    + For example: op:only; or op:no;. +
  • +
  • + Filter only posts with files (\`only\`) or only posts without files (\`no\`).
    + For example: file:only; or file:no;. +
  • +
  • + 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 the board index by default.
    + For example: top:yes; or top:no;. +
  • +
  • + Show a desktop notification instead of hiding.
    + For example: notify;. +
  • +
  • + Filters in the "General" section apply to multiple fields, by default subject,name,filename,comment.
    + The fields can be specified with the type option, separated by commas.
    + For example: type:@{filterTypes};.
    + Types can also be combined with a + sign; this indicates the filter applies to the given fields joined by newlines.
    + For example: type:filename+filesize+dimensions;.
    +
  • +
`; - var SaucePage = `
Sauce is disabled.
- -
- -
These parameters will be replaced by their corresponding values in the URL and displayed text:
-
    -
  • %IMG: Full image URL for GIF, JPG, and PNG; thumbnail URL for other types.
  • -
  • %URL: Full image URL.
  • -
  • %TURL: Thumbnail URL.
  • -
  • %name: Original file name.
  • -
  • %board: Current board.
  • -
  • %MD5: MD5 hash in base64.
  • -
  • %sMD5: MD5 hash in base64 using - and _.
  • -
  • %hMD5: MD5 hash in hexadecimal.
  • -
  • %$0: Matched regular expression within the filename.
  • -
  • %$1, %$2, %$3, ... : Subexpressions within the matched regular expression.
  • -
  • %%, %semi: Literal % and ;.
  • -
-
Lines starting with a # will be ignored.
-
You can specify a display text by appending ;text:[text] to the URL.
-
You can specify the applicable boards/sites by appending ;boards:[board1],[board2]. See the Filter guide for details.
-
You can specify the applicable file types by appending ;types:[extension1],[extension2].
-
You can specify a regular expression the filename must match by appending ;regexp:[regular expression].
-
- + var SaucePage = `
Sauce is disabled.
+ +
+ +
These parameters will be replaced by their corresponding values in the URL and displayed text:
+
    +
  • %IMG: Full image URL for GIF, JPG, and PNG; thumbnail URL for other types.
  • +
  • %URL: Full image URL.
  • +
  • %TURL: Thumbnail URL.
  • +
  • %name: Original file name.
  • +
  • %board: Current board.
  • +
  • %MD5: MD5 hash in base64.
  • +
  • %sMD5: MD5 hash in base64 using - and _.
  • +
  • %hMD5: MD5 hash in hexadecimal.
  • +
  • %$0: Matched regular expression within the filename.
  • +
  • %$1, %$2, %$3, ... : Subexpressions within the matched regular expression.
  • +
  • %%, %semi: Literal % and ;.
  • +
+
Lines starting with a # will be ignored.
+
You can specify a display text by appending ;text:[text] to the URL.
+
You can specify the applicable boards/sites by appending ;boards:[board1],[board2]. See the Filter guide for details.
+
You can specify the applicable file types by appending ;types:[extension1],[extension2].
+
You can specify a regular expression the filename must match by appending ;regexp:[regular expression].
+
+ `; - var AdvancedPage = `
- Archives -
404 Redirect is disabled.
- - - - - - - - -
Thread redirectionPost fetchingFile redirection
-
-
- Archive Lists: Each line below should be an archive list in this format or a URL to load an archive list from.
- Archive properties can be overriden by another item with the same uid (or if absent, its name). -
- - Last updated: -
- -
- External Catalog -
External Catalog is disabled. This will be used only as a fallback.
-
- URLs of external catalog sites, where %board is to be replaced by the board name.
- Each URL should be followed by ;boards: and optionally ;exclude: and a list of supported/excluded boards in the format explained in the Filter guide. -
- -
- -
- Override 4chan Image Host -
Change 4chan image links to this domain. Leave blank for no change.
-
- -
- -
- Captcha Language -
Choose from list of language codes. Leave blank to autoselect.
-
-
- -
- Custom Board Navigation -
- New lines will be converted into spaces.

-
In the following examples for /g/, g can be changed to a different board ID (a, b, etc...), the current board (current), or the Twitter link (@).
-
Board link: g
-
Archive link: g-archive
-
Internal archive link: g-expired
-
Title link: g-title
-
Board link (Replace with title when on that board): g-replace
-
Full text link: g-full
-
Custom text link: g-text:"Install Gentoo"
-
Index-only link: g-index
-
Catalog-only link: g-catalog
-
Index mode: g-mode:"infinite scrolling"
-
Index sort: g-sort:"creation date rev"
-
External link: external-text:"Google","http://www.google.com"
-
Open in new tab: g-nt
-
Combinations are possible: g-index-text:"Technology Index"
-
Full board list toggle: toggle-all
-
-
- [ toggle-all ] [current-title] [g-title / a-title / jp-title] [x / wsg / h] [t-text:"Piracy"]
- will give you
- [ + ] [Technology] [Technology / Anime & Manga / Otaku Culture] [x / wsg / h] [Piracy]
- if you are on /g/. -
-
- -
- Time Formatting is disabled. -
:
- -
Day: %a, %A, %d, %e
-
Month: %m, %b, %B
-
Year: %y, %Y
-
Hour: %k, %H, %l, %I, %p, %P
-
Minute: %M
-
Second: %S
-
Literal %: %%
-
Language tag: (needs page reload)
-
- -
- Quote Backlinks formatting is disabled. -
:
-
- -
- Default pasted content filename -
.png
-
- -
- File Info Formatting is disabled. -
:
-
Link: %l (truncated), %L (untruncated), %T (4chan filename)
-
Filename: %n (truncated), %N (untruncated), %t (4chan filename)
-
Download button: %d
-
Quick filter MD5: %f
-
Spoiler indicator: %p
-
Size: %B (Bytes), %K (KB), %M (MB), %s (4chan default)
-
Resolution: %r (Displays 'PDF' for PDF files)
-
Tag: %g -
Literal %: %%
-
- -
- Quick Reply Personas - -

- One item per line.
- Items will be added in the relevant input's auto-completion list.
- Password items will always be used, since there is no password input.
- Lines starting with a # will be ignored. -

-
    You can use these settings with each item, separate them with semicolons: -
  • Possible items are: name, options (or equivalently email), subject and password.
  • -
  • Wrap values of items with quotes, like this: options:"sage".
  • -
  • Force values as defaults with the always keyword, for example: options:"sage";always.
  • -
  • Select specific boards for an item, separated with commas, for example: options:"sage";boards:jp;always.
  • -
-
- -
- Unread Favicon is disabled. - - -
- -
- Thread Updater is disabled. -
- Interval: seconds -
-
- -
- Custom Cooldown Time -
- Seconds: -
-
- -
- - - -
For more information about customizing 4chan X's CSS, see the styling guide.
- - -
- -
- Javascript Whitelist -
- Sources from which Javascript is allowed to be loaded by Content Security Policy.
- Lines starting with a # will be ignored. -
- -
- -
- Known Banners -
List of known banners, used for click-to-change feature.
- -
+ var AdvancedPage = `
+ Archives +
404 Redirect is disabled.
+ + + + + + + + +
Thread redirectionPost fetchingFile redirection
+
+
+ Archive Lists: Each line below should be an archive list in this format or a URL to load an archive list from.
+ Archive properties can be overriden by another item with the same uid (or if absent, its name). +
+ + Last updated: +
+ +
+ External Catalog +
External Catalog is disabled. This will be used only as a fallback.
+
+ URLs of external catalog sites, where %board is to be replaced by the board name.
+ Each URL should be followed by ;boards: and optionally ;exclude: and a list of supported/excluded boards in the format explained in the Filter guide. +
+ +
+ +
+ Override 4chan Image Host +
Change 4chan image links to this domain. Leave blank for no change.
+
+ +
+ +
+ Captcha Language +
Choose from list of language codes. Leave blank to autoselect.
+
+
+ +
+ Custom Board Navigation +
+ New lines will be converted into spaces.

+
In the following examples for /g/, g can be changed to a different board ID (a, b, etc...), the current board (current), or the Twitter link (@).
+
Board link: g
+
Archive link: g-archive
+
Internal archive link: g-expired
+
Title link: g-title
+
Board link (Replace with title when on that board): g-replace
+
Full text link: g-full
+
Custom text link: g-text:"Install Gentoo"
+
Index-only link: g-index
+
Catalog-only link: g-catalog
+
Index mode: g-mode:"infinite scrolling"
+
Index sort: g-sort:"creation date rev"
+
External link: external-text:"Google","http://www.google.com"
+
Open in new tab: g-nt
+
Combinations are possible: g-index-text:"Technology Index"
+
Full board list toggle: toggle-all
+
+
+ [ toggle-all ] [current-title] [g-title / a-title / jp-title] [x / wsg / h] [t-text:"Piracy"]
+ will give you
+ [ + ] [Technology] [Technology / Anime & Manga / Otaku Culture] [x / wsg / h] [Piracy]
+ if you are on /g/. +
+
+ +
+ Time Formatting is disabled. +
:
+ +
Day: %a, %A, %d, %e
+
Month: %m, %b, %B
+
Year: %y, %Y
+
Hour: %k, %H, %l, %I, %p, %P
+
Minute: %M
+
Second: %S
+
Literal %: %%
+
Language tag: (needs page reload)
+
+ +
+ Quote Backlinks formatting is disabled. +
:
+
+ +
+ Default pasted content filename +
.png
+
+ +
+ File Info Formatting is disabled. +
:
+
Link: %l (truncated), %L (untruncated), %T (4chan filename)
+
Filename: %n (truncated), %N (untruncated), %t (4chan filename)
+
Download button: %d
+
Quick filter MD5: %f
+
Spoiler indicator: %p
+
Size: %B (Bytes), %K (KB), %M (MB), %s (4chan default)
+
Resolution: %r (Displays 'PDF' for PDF files)
+
Tag: %g +
Literal %: %%
+
+ +
+ Quick Reply Personas + +

+ One item per line.
+ Items will be added in the relevant input's auto-completion list.
+ Password items will always be used, since there is no password input.
+ Lines starting with a # will be ignored. +

+
    You can use these settings with each item, separate them with semicolons: +
  • Possible items are: name, options (or equivalently email), subject and password.
  • +
  • Wrap values of items with quotes, like this: options:"sage".
  • +
  • Force values as defaults with the always keyword, for example: options:"sage";always.
  • +
  • Select specific boards for an item, separated with commas, for example: options:"sage";boards:jp;always.
  • +
+
+ +
+ Unread Favicon is disabled. + + +
+ +
+ Thread Updater is disabled. +
+ Interval: seconds +
+
+ +
+ Custom Cooldown Time +
+ Seconds: +
+
+ +
+ + + +
For more information about customizing 4chan X's CSS, see the styling guide.
+ + +
+ +
+ Javascript Whitelist +
+ Sources from which Javascript is allowed to be loaded by Content Security Policy.
+ Lines starting with a # will be ignored. +
+ +
+ +
+ Known Banners +
List of known banners, used for click-to-change feature.
+ +
`; - var KeybindsPage = `
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
+ var KeybindsPage = `
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
`; - var FilterSelectPage = ` -
+ var FilterSelectPage = ` +
`; - var burichan = `/* General */ -:root.burichan .dialog { - background-color: #D6DAF0; - border-color: #B7C5D9; -} -:root.burichan .field:focus, -:root.burichan .field.focus { - border-color: #98E; -} - -/* Header */ -:root.burichan #header-bar.dialog { - background-color: rgba(214,218,240,0.98); -} -:root.burichan:not(.fixed) #header-bar, :root.burichan #header-bar #notifications { - font-size: 11pt; -} -:root.burichan #header-bar, :root.burichan #header-bar #notifications { - color: #89A; -} -:root.burichan #header-bar a, :root.burichan #header-bar #notifications a { - color: #34345C; -} - -/* Settings */ -:root.burichan #fourchanx-settings fieldset, :root.burichan .section-main div::before { - border-color: #B7C5D9; -} -:root.burichan .suboption-list > div:last-of-type { - background-color: #D6DAF0; -} - -/* Catalog */ -:root.burichan.catalog-hover-expand .catalog-container:hover > .post { - background-color: #D6DAF0; -} -:root.burichan.werkTyme .catalog-thread:not(:hover), -:root.burichan.werkTyme:not(.catalog-hover-expand) .catalog-thread, -:root.burichan.catalog-hover-expand .catalog-container:hover > .post, -:root.burichan.catalog-hover-expand .catalog-container:hover .catalog-reply { - border-color: #B7C5D9; -} - -/* Quote */ -:root.burichan .backlink.deadlink { - color: #34345C !important; -} -:root.burichan .inline { - border-color: #B7C5D9; - background-color: rgba(255, 255, 255, .14); -} - -/* Fappe and Werk Tyme */ -:root.burichan .indicator { - color: #D6DAF0; -} - -/* Anonymize */ -:root.burichan.anonymize $site$info$name::before { - font-size: 12pt; -} - -/* QR */ -.burichan #dump-list::-webkit-scrollbar-thumb { - background-color: #D6DAF0; - border-color: #B7C5D9; -} -:root.burichan .qr-preview { - background-color: rgba(0, 0, 0, .15); -} -:root.burichan .qr-link { - border-color: rgb(199, 203, 225) rgb(199, 203, 225) rgb(184, 188, 210); - background: linear-gradient(#E5E9FF, #D6DAF0) repeat scroll 0% 0% transparent; -} -:root.burichan .qr-link:hover { - background: #D9DDF3; -} - -/* Menu */ -:root.burichan #menu { - color: #000000; -} -:root.burichan .entry { - font-size: 12pt; -} -:root.burichan .focused.entry { - background: rgba(255, 255, 255, .33); -} - -/* Unread */ -:root.burichan .unread-mark-read { - background-color: rgba(214,218,240,0.5); -} - -/* Thread Watcher */ -:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you, :root.burichan .last-page > a > .watcher-page { - color: #F00; -} - -/* Watcher Favicon */ -:root.burichan .watch-thread-link -{ - background-image: url("data:image/svg+xml,"); -} + var burichan = `/* General */ +:root.burichan .dialog { + background-color: #D6DAF0; + border-color: #B7C5D9; +} +:root.burichan .field:focus, +:root.burichan .field.focus { + border-color: #98E; +} + +/* Header */ +:root.burichan #header-bar.dialog { + background-color: rgba(214,218,240,0.98); +} +:root.burichan:not(.fixed) #header-bar, :root.burichan #header-bar #notifications { + font-size: 11pt; +} +:root.burichan #header-bar, :root.burichan #header-bar #notifications { + color: #89A; +} +:root.burichan #header-bar a, :root.burichan #header-bar #notifications a { + color: #34345C; +} + +/* Settings */ +:root.burichan #fourchanx-settings fieldset, :root.burichan .section-main div::before { + border-color: #B7C5D9; +} +:root.burichan .suboption-list > div:last-of-type { + background-color: #D6DAF0; +} + +/* Catalog */ +:root.burichan.catalog-hover-expand .catalog-container:hover > .post { + background-color: #D6DAF0; +} +:root.burichan.werkTyme .catalog-thread:not(:hover), +:root.burichan.werkTyme:not(.catalog-hover-expand) .catalog-thread, +:root.burichan.catalog-hover-expand .catalog-container:hover > .post, +:root.burichan.catalog-hover-expand .catalog-container:hover .catalog-reply { + border-color: #B7C5D9; +} + +/* Quote */ +:root.burichan .backlink.deadlink { + color: #34345C !important; +} +:root.burichan .inline { + border-color: #B7C5D9; + background-color: rgba(255, 255, 255, .14); +} + +/* Fappe and Werk Tyme */ +:root.burichan .indicator { + color: #D6DAF0; +} + +/* Anonymize */ +:root.burichan.anonymize $site$info$name::before { + font-size: 12pt; +} + +/* QR */ +.burichan #dump-list::-webkit-scrollbar-thumb { + background-color: #D6DAF0; + border-color: #B7C5D9; +} +:root.burichan .qr-preview { + background-color: rgba(0, 0, 0, .15); +} +:root.burichan .qr-link { + border-color: rgb(199, 203, 225) rgb(199, 203, 225) rgb(184, 188, 210); + background: linear-gradient(#E5E9FF, #D6DAF0) repeat scroll 0% 0% transparent; +} +:root.burichan .qr-link:hover { + background: #D9DDF3; +} + +/* Menu */ +:root.burichan #menu { + color: #000000; +} +:root.burichan .entry { + font-size: 12pt; +} +:root.burichan .focused.entry { + background: rgba(255, 255, 255, .33); +} + +/* Unread */ +:root.burichan .unread-mark-read { + background-color: rgba(214,218,240,0.5); +} + +/* Thread Watcher */ +:root.burichan .replies-quoting-you > a, :root.burichan #watcher-link.replies-quoting-you, :root.burichan .last-page > a > .watcher-page { + color: #F00; +} + +/* Watcher Favicon */ +:root.burichan .watch-thread-link +{ + background-image: url("data:image/svg+xml,"); +} `; - var futaba = `/* General */ -:root.futaba .dialog { - background-color: #F0E0D6; - border-color: #D9BFB7; -} -:root.futaba .field:focus, -:root.futaba .field.focus { - border-color: #EA8; -} - -/* Header */ -:root.futaba #header-bar.dialog { - background-color: rgba(240,224,214,0.98); -} -:root.futaba:not(.fixed) #header-bar, :root.futaba #notifications { - font-size: 11pt; -} -:root.futaba #header-bar, :root.futaba #notifications { - color: #B86; -} -:root.futaba #header-bar a, :root.futaba #notifications a { - color: #800000; -} - -/* Settings */ -:root.futaba #fourchanx-settings fieldset, :root.futaba .section-main div::before { - border-color: #D9BFB7; -} -:root.futaba .suboption-list > div:last-of-type { - background-color: #F0E0D6; -} - -/* Catalog */ -:root.futaba.catalog-hover-expand .catalog-container:hover > .post { - background-color: #F0E0D6; -} -:root.futaba.werkTyme .catalog-thread:not(:hover), -:root.futaba.werkTyme:not(.catalog-hover-expand) .catalog-thread, -:root.futaba.catalog-hover-expand .catalog-container:hover > .post, -:root.futaba.catalog-hover-expand .catalog-container:hover .catalog-reply { - border-color: #D9BFB7; -} - -/* Quote */ -:root.futaba .backlink.deadlink { - color: #00E !important; -} -:root.futaba .inline { - border-color: #D9BFB7; - background-color: rgba(255, 255, 255, .14); -} - -/* Fappe and Werk Tyme */ -:root.futaba .indicator { - color: #F0E0D6; -} - -/* Anonymize */ -:root.futaba.anonymize $site$info$name::before { - font-size: 12pt; -} - -/* QR */ -.futaba #dump-list::-webkit-scrollbar-thumb { - background-color: #F0E0D6; - border-color: #D9BFB7; -} -:root.futaba .qr-preview { - background-color: rgba(0, 0, 0, .15); -} -:root.futaba .qr-link { - border-color: rgb(225, 209, 199) rgb(225, 209, 199) rgb(210, 194, 184); - background: linear-gradient(#FFEFE5, #F0E0D6) repeat scroll 0% 0% transparent; -} -:root.futaba .qr-link:hover { - background: #F0E0D6; -} - -/* Menu */ -:root.futaba #menu { - color: #800000; -} -:root.futaba .entry { - font-size: 12pt; -} -:root.futaba .focused.entry { - background: rgba(255, 255, 255, .33); -} - -/* Unread */ -:root.futaba .unread-mark-read { - background-color: rgba(240,224,214,0.5); -} - -/* Thread Watcher */ -:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you, :root.futaba .last-page > a > .watcher-page { - color: #F00; -} - -/* Watcher Favicon */ -:root.futaba .watch-thread-link -{ - background-image: url("data:image/svg+xml,"); -} + var futaba = `/* General */ +:root.futaba .dialog { + background-color: #F0E0D6; + border-color: #D9BFB7; +} +:root.futaba .field:focus, +:root.futaba .field.focus { + border-color: #EA8; +} + +/* Header */ +:root.futaba #header-bar.dialog { + background-color: rgba(240,224,214,0.98); +} +:root.futaba:not(.fixed) #header-bar, :root.futaba #notifications { + font-size: 11pt; +} +:root.futaba #header-bar, :root.futaba #notifications { + color: #B86; +} +:root.futaba #header-bar a, :root.futaba #notifications a { + color: #800000; +} + +/* Settings */ +:root.futaba #fourchanx-settings fieldset, :root.futaba .section-main div::before { + border-color: #D9BFB7; +} +:root.futaba .suboption-list > div:last-of-type { + background-color: #F0E0D6; +} + +/* Catalog */ +:root.futaba.catalog-hover-expand .catalog-container:hover > .post { + background-color: #F0E0D6; +} +:root.futaba.werkTyme .catalog-thread:not(:hover), +:root.futaba.werkTyme:not(.catalog-hover-expand) .catalog-thread, +:root.futaba.catalog-hover-expand .catalog-container:hover > .post, +:root.futaba.catalog-hover-expand .catalog-container:hover .catalog-reply { + border-color: #D9BFB7; +} + +/* Quote */ +:root.futaba .backlink.deadlink { + color: #00E !important; +} +:root.futaba .inline { + border-color: #D9BFB7; + background-color: rgba(255, 255, 255, .14); +} + +/* Fappe and Werk Tyme */ +:root.futaba .indicator { + color: #F0E0D6; +} + +/* Anonymize */ +:root.futaba.anonymize $site$info$name::before { + font-size: 12pt; +} + +/* QR */ +.futaba #dump-list::-webkit-scrollbar-thumb { + background-color: #F0E0D6; + border-color: #D9BFB7; +} +:root.futaba .qr-preview { + background-color: rgba(0, 0, 0, .15); +} +:root.futaba .qr-link { + border-color: rgb(225, 209, 199) rgb(225, 209, 199) rgb(210, 194, 184); + background: linear-gradient(#FFEFE5, #F0E0D6) repeat scroll 0% 0% transparent; +} +:root.futaba .qr-link:hover { + background: #F0E0D6; +} + +/* Menu */ +:root.futaba #menu { + color: #800000; +} +:root.futaba .entry { + font-size: 12pt; +} +:root.futaba .focused.entry { + background: rgba(255, 255, 255, .33); +} + +/* Unread */ +:root.futaba .unread-mark-read { + background-color: rgba(240,224,214,0.5); +} + +/* Thread Watcher */ +:root.futaba .replies-quoting-you > a, :root.futaba #watcher-link.replies-quoting-you, :root.futaba .last-page > a > .watcher-page { + color: #F00; +} + +/* Watcher Favicon */ +:root.futaba .watch-thread-link +{ + background-image: url("data:image/svg+xml,"); +} `; var linkifyAudio = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAitJREFUOE9jYCAWKJWwavr0KyXWb/FIbDtUFFyzJx6nVofE2Xo5nXsj0rqPNSR0nVkR2Hjmgmfd+U9Otdf+m5Vf/6+SfeU/R9ChVVgNYDRtlfJuuPA/rPfe/4QpD/6nznj0P27Kw/9unff/69Xf+69c/+C/SO7N/0z+OAxgMmmRCe++/r9i3ev/KWvf/vdY8PK/bt/9/wrNV3/IN5y/IVt1YqNg4pGTTP4HsbuA2bhZ2qvpyn+xjIObxAp3VwqlrgngLFyryVy5nhPmZJHANS2cwYexG8BmVC/pWn3hP4NZlzWuQDJI3dIiFnUUuwEsQAOcq87jNcC7fHeLUtJxHF4AGmBWeAavAWH1+1rUUk7giAWjOknllON4DXAs2NEiG4/DBQxAF/CFHfrPYI4jDFSLuJVjNrUJhB/B7gIGo1pJRt99GAZYJK7wLJ1z7Xzl4vu/7aqv/GRBj0bjqAX2qb0nJ7mXH17C4HcUxQA+hymWtSue/C5a9up/9Ozn/7Vr7v1nRY7GqMb91T3b3v6vWvPmf/S0p/9ZQk+DDLCBRSOz06Jqk+o7/21nvfqvsebDf7kZL/5zBaxphkezd+OFn7HzXvz3Wvjmv9a8N//5Ek//ZTBpVYUrMG2X5wjcdl68+uI/wa5Lr3hSNjczGFeywOVZ/bbcVGp//F9izfv/Ql03f3P4LC/HSEQquYwMFnUCDJ7dzBhyjGZNQpye89M5gpfnMvtNUyE2h4PUAQBovvT7lyNljwAAAABJRU5ErkJggg=='; @@ -9492,3261 +9492,3261 @@ https://*.hcaptcha.com var linkifyYoutube = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAMCAYAAABr5z2BAAABIklEQVQoz53LvUrDUBjG8bOoOammSf1IoBSvoCB4JeIqOHgBLt6AIMRBBQelWurQ2kERnMRBsBUcIp5FJSBI5oQsJVkkUHh8W0o5nhaFHvjBgef/Mq+Q46RJBMkI/vE+aOus956tnEswIZe1LV0QyJ5sE2GzgZfVMtRNIdiDpccEssdlB1mW4bvTwdvWJtRdErM7U+8S/FJykCRJX5qm+KpVce8UMNLRLbulz4iSjTAMh6Iowsd5BeNadp3nUF0VlxAEwZBotXC0Usa4ll3meZdA1iguwvf9vpvDA2wvmKgYGtSud8suDB4TyGr2PF49D/vra9jRZ1BVdknMzgwuCGSnZEObwu6sBnVTCHZiaC7BhFx2PKdxUidiAH/4lLo9Mv0DELVs9qsOHXwAAAAASUVORK5CYII='; - var photon = `/* General */ -:root.photon .dialog { - background-color: #DDD; - border-color: #CCC; -} -:root.photon .field:focus, -:root.photon .field.focus { - border-color: #EA8; -} - -/* 4chan style fixes */ -:root.photon #arc-list tr:nth-of-type(odd) span.quote { - color: #C0E17A; -} -:root.photon.highlight-you .quotesYou$site$highlightable$reply { - border-left: 3px solid rgba(221, 0, 0, .8) !important; -} -:root.photon.highlight-own .yourPost$site$highlightable$reply { - border-left: 3px dashed rgba(221, 0, 0, .8) !important; -} - -/* Header */ -:root.photon #header-bar.dialog { - background-color: rgba(221,221,221,0.98); -} -:root.photon:not(.fixed) #header-bar, :root.photon #notifications { - font-size: 9pt; -} -:root.photon #header-bar, :root.photon #notifications { - color: #333; -} -:root.photon #header-bar a, :root.photon #notifications a { - color: #FF6600; -} - -/* Settings */ -:root.photon #fourchanx-settings fieldset, :root.photon .section-main div::before { - border-color: #CCC; -} -:root.photon .suboption-list > div:last-of-type { - background-color: #DDD; -} - -/* Catalog */ -:root.photon.catalog-hover-expand .catalog-container:hover > .post { - background-color: #DDD; -} -:root.photon.werkTyme .catalog-thread:not(:hover), -:root.photon.werkTyme:not(.catalog-hover-expand) .catalog-thread, -:root.photon.catalog-hover-expand .catalog-container:hover > .post, -:root.photon.catalog-hover-expand .catalog-container:hover .catalog-reply { - border-color: #CCC; -} - -/* Quote */ -:root.photon .backlink.deadlink { - color: #F60 !important; -} -:root.photon .inline { - border-color: #CCC; - background-color: rgba(255, 255, 255, .14); -} - -/* Fappe and Werk Tyme */ -:root.photon .indicator { - color: #DDD; -} - -/* QR */ -.photon #dump-list::-webkit-scrollbar-thumb { - background-color: #DDD; - border-color: #CCC; -} -:root.photon .qr-preview { - background-color: rgba(0, 0, 0, .15); -} -:root.photon .qr-link { - border-color: rgb(206, 206, 206) rgb(206, 206, 206) rgb(191, 191, 191); - background: linear-gradient(#ECECEC, #DDD) repeat scroll 0% 0% transparent; -} -:root.photon .qr-link:hover { - background: #DDDDDD; -} - -/* Menu */ -:root.photon #menu { - color: #333; -} -:root.photon .entry { - font-size: 10pt; -} -:root.photon .focused.entry { - background: rgba(255, 255, 255, .33); -} - -/* Unread */ -:root.photon .unread-mark-read { - background-color: rgba(221,221,221,0.5); -} - -/* Thread Watcher */ -:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you, :root.photon .last-page > a > .watcher-page { - color: #00F !important; -} - -/* Watcher Favicon */ -:root.photon .watch-thread-link -{ - background-image: url("data:image/svg+xml,"); -} + var photon = `/* General */ +:root.photon .dialog { + background-color: #DDD; + border-color: #CCC; +} +:root.photon .field:focus, +:root.photon .field.focus { + border-color: #EA8; +} + +/* 4chan style fixes */ +:root.photon #arc-list tr:nth-of-type(odd) span.quote { + color: #C0E17A; +} +:root.photon.highlight-you .quotesYou$site$highlightable$reply { + border-left: 3px solid rgba(221, 0, 0, .8) !important; +} +:root.photon.highlight-own .yourPost$site$highlightable$reply { + border-left: 3px dashed rgba(221, 0, 0, .8) !important; +} + +/* Header */ +:root.photon #header-bar.dialog { + background-color: rgba(221,221,221,0.98); +} +:root.photon:not(.fixed) #header-bar, :root.photon #notifications { + font-size: 9pt; +} +:root.photon #header-bar, :root.photon #notifications { + color: #333; +} +:root.photon #header-bar a, :root.photon #notifications a { + color: #FF6600; +} + +/* Settings */ +:root.photon #fourchanx-settings fieldset, :root.photon .section-main div::before { + border-color: #CCC; +} +:root.photon .suboption-list > div:last-of-type { + background-color: #DDD; +} + +/* Catalog */ +:root.photon.catalog-hover-expand .catalog-container:hover > .post { + background-color: #DDD; +} +:root.photon.werkTyme .catalog-thread:not(:hover), +:root.photon.werkTyme:not(.catalog-hover-expand) .catalog-thread, +:root.photon.catalog-hover-expand .catalog-container:hover > .post, +:root.photon.catalog-hover-expand .catalog-container:hover .catalog-reply { + border-color: #CCC; +} + +/* Quote */ +:root.photon .backlink.deadlink { + color: #F60 !important; +} +:root.photon .inline { + border-color: #CCC; + background-color: rgba(255, 255, 255, .14); +} + +/* Fappe and Werk Tyme */ +:root.photon .indicator { + color: #DDD; +} + +/* QR */ +.photon #dump-list::-webkit-scrollbar-thumb { + background-color: #DDD; + border-color: #CCC; +} +:root.photon .qr-preview { + background-color: rgba(0, 0, 0, .15); +} +:root.photon .qr-link { + border-color: rgb(206, 206, 206) rgb(206, 206, 206) rgb(191, 191, 191); + background: linear-gradient(#ECECEC, #DDD) repeat scroll 0% 0% transparent; +} +:root.photon .qr-link:hover { + background: #DDDDDD; +} + +/* Menu */ +:root.photon #menu { + color: #333; +} +:root.photon .entry { + font-size: 10pt; +} +:root.photon .focused.entry { + background: rgba(255, 255, 255, .33); +} + +/* Unread */ +:root.photon .unread-mark-read { + background-color: rgba(221,221,221,0.5); +} + +/* Thread Watcher */ +:root.photon .replies-quoting-you > a, :root.photon #watcher-link.replies-quoting-you, :root.photon .last-page > a > .watcher-page { + color: #00F !important; +} + +/* Watcher Favicon */ +:root.photon .watch-thread-link +{ + background-image: url("data:image/svg+xml,"); +} `; - var report = `#g-recaptcha, -:root:not(.js-enabled) #captchaContainerAlt { - height: auto; -} -#captchaContainerAlt td:nth-child(2) { - display: table-cell !important; -} - -/* Archive reports */ -#archive-report { - padding: 3px; -} -#archive-report-enabled { - vertical-align: middle; -} -#archive-report > label { - display: block; -} -#archive-report-reason { - display: block; - width: 98%; -} -.archive-report-success { - color: green; -} -.archive-report-error { - color: red; + var report = `#g-recaptcha, +:root:not(.js-enabled) #captchaContainerAlt { + height: auto; +} +#captchaContainerAlt td:nth-child(2) { + display: table-cell !important; +} + +/* Archive reports */ +#archive-report { + padding: 3px; +} +#archive-report-enabled { + vertical-align: middle; +} +#archive-report > label { + display: block; +} +#archive-report-reason { + display: block; + width: 98%; +} +.archive-report-success { + color: green; +} +.archive-report-error { + color: red; }`; - var spooky = `/* General */ -:root.spooky .dialog { - background-color: #171526; - border-color: #707070; -} -:root.spooky .field:focus, -:root.spooky .field.focus { - border-color: #98E; -} - -/* 4chan style fixes */ -:root.spooky #arc-list span.quote { - color: #634C2C; -} -:root.spooky.highlight-you .quotesYou$site$highlightable$reply { - border-left: 3px solid rgba(145, 182, 214, .8) !important; -} -:root.spooky.highlight-own .yourPost$site$highlightable$reply { - border-left: 3px dashed rgba(145, 182, 214, .8) !important; -} - -/* Header */ -:root.spooky #header-bar.dialog { - background-color: rgba(23,21,38,0.98); -} -:root.spooky:not(.fixed) #header-bar, :root.spooky #notifications { - font-size: 9pt; -} -:root.spooky #header-bar, :root.spooky #notifications { - color: #C49756; -} -:root.spooky #board-list a, :root.spooky #shortcuts a { - color: #FE9600; -} -:root.spooky.shortcut-icons .native-settings { - background-image: url('//s.4cdn.org/image/favicon-ws.ico'); -} - -/* Settings */ -:root.spooky #fourchanx-settings fieldset, :root.spooky .section-main div::before { - border-color: #707070; -} -:root.spooky .suboption-list > div:last-of-type { - background-color: #171526; -} - -/* Catalog */ -:root.spooky.catalog-hover-expand .catalog-container:hover > .post { - background-color: #171526; -} -:root.spooky.werkTyme .catalog-thread:not(:hover), -:root.spooky.werkTyme:not(.catalog-hover-expand) .catalog-thread, -:root.spooky.catalog-hover-expand .catalog-container:hover > .post, -:root.spooky.catalog-hover-expand .catalog-container:hover .catalog-reply { - border-color: #707070; -} - -/* Quote */ -:root.spooky .backlink.deadlink { - color: #FE9600 !important; -} -:root.spooky .inline { - border-color: #707070; - background-color: rgba(255, 255, 255, .14); -} - -/* Fappe and Werk Tyme */ -:root.spooky .indicator { - color: #171526; -} - -/* Highlighting */ -:root.spooky .qphl { - outline: 2px solid rgba(145, 182, 214, .8); -} -:root.spooky.highlight-you .quotesYou$site$highlightable$op, -:root.spooky.highlight-you .quotesYou$site$highlightable$reply { - border-left: 3px solid rgba(145, 182, 214, .8); -} -:root.spooky.highlight-own .yourPost$site$highlightable$op, -:root.spooky.highlight-own .yourPost$site$highlightable$reply { - border-left: 3px dashed rgba(145, 182, 214, .8); -} -:root.spooky .filter-highlight$site$highlightable$op, -:root.spooky .filter-highlight$site$highlightable$reply { - box-shadow: inset 5px 0 rgba(145, 182, 214, .5); -} -:root.spooky.highlight-own .yourPost > $site$sideArrows, -:root.spooky.highlight-you .quotesYou > $site$sideArrows, -:root.spooky .filter-highlight > $site$sideArrows { - color: rgb(155, 185, 210); -} - -/* QR */ -.spooky #dump-list::-webkit-scrollbar-thumb { - background-color: #171526; - border-color: #707070; -} -:root.spooky .qr-preview { - background-color: rgba(0, 0, 0, .15); -} -:root.spooky #qr .field { - background-color: rgb(26, 27, 29); - color: rgb(197,200,198); - border-color: rgb(40, 41, 42); -} -:root.spooky #qr .field:focus, -:root.spooky #qr .field.focus { - border-color: rgb(254, 150, 0) !important; - background-color: rgb(30,32,36); -} -:root.spooky .persona button { - background: linear-gradient(to bottom, #2E3035, #222427) no-repeat; - color: rgb(197,200,198); - border-color: rgb(40, 41, 42); - outline: none; -} -:root.spooky .persona button::-moz-focus-inner { - border: none; -} -:root.spooky .persona button:focus { - border-color: rgb(254, 150, 0); -} -:root.spooky #qr.sjis-preview #sjis-toggle, -:root.spooky #qr.tex-preview #tex-preview-button { - background: rgb(26, 27, 29); -} -:root.spooky #qr select, -:root.spooky #file-n-submit > input, -:root.spooky #qr-draw-button { - border-color: rgb(40, 41, 42); -} -:root.spooky #qr-filename { - color: rgb(197,200,198); -} - -:root.spooky .qr-link { - border-color: rgb(8, 6, 23) rgb(8, 6, 23) rgb(0, 0, 8); - background: linear-gradient(#262435, #171526) repeat scroll 0% 0% transparent; -} -:root.spooky .qr-link:hover { - background: #1A1829; -} - - -/* Menu */ -:root.spooky #menu { - color: #FE9600; -} -:root.spooky .entry { - font-size: 10pt; -} -:root.spooky .focused.entry { - background: rgba(255, 255, 255, .33); -} - -/* Unread */ -:root.spooky .unread-line { - border-color: rgb(197, 200, 198); - visibility: visible; - opacity: 1; -} -:root.spooky .unread-mark-read { - background-color: rgba(23,21,38,0.5); -} - -/* Thread Watcher */ -:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you, :root.spooky .last-page > a > .watcher-page { - color: #F00 !important; -} - -/* Watcher Favicon */ -:root.spooky .watch-thread-link -{ - background-image: url("data:image/svg+xml,"); -} + var spooky = `/* General */ +:root.spooky .dialog { + background-color: #171526; + border-color: #707070; +} +:root.spooky .field:focus, +:root.spooky .field.focus { + border-color: #98E; +} + +/* 4chan style fixes */ +:root.spooky #arc-list span.quote { + color: #634C2C; +} +:root.spooky.highlight-you .quotesYou$site$highlightable$reply { + border-left: 3px solid rgba(145, 182, 214, .8) !important; +} +:root.spooky.highlight-own .yourPost$site$highlightable$reply { + border-left: 3px dashed rgba(145, 182, 214, .8) !important; +} + +/* Header */ +:root.spooky #header-bar.dialog { + background-color: rgba(23,21,38,0.98); +} +:root.spooky:not(.fixed) #header-bar, :root.spooky #notifications { + font-size: 9pt; +} +:root.spooky #header-bar, :root.spooky #notifications { + color: #C49756; +} +:root.spooky #board-list a, :root.spooky #shortcuts a { + color: #FE9600; +} +:root.spooky.shortcut-icons .native-settings { + background-image: url('//s.4cdn.org/image/favicon-ws.ico'); +} + +/* Settings */ +:root.spooky #fourchanx-settings fieldset, :root.spooky .section-main div::before { + border-color: #707070; +} +:root.spooky .suboption-list > div:last-of-type { + background-color: #171526; +} + +/* Catalog */ +:root.spooky.catalog-hover-expand .catalog-container:hover > .post { + background-color: #171526; +} +:root.spooky.werkTyme .catalog-thread:not(:hover), +:root.spooky.werkTyme:not(.catalog-hover-expand) .catalog-thread, +:root.spooky.catalog-hover-expand .catalog-container:hover > .post, +:root.spooky.catalog-hover-expand .catalog-container:hover .catalog-reply { + border-color: #707070; +} + +/* Quote */ +:root.spooky .backlink.deadlink { + color: #FE9600 !important; +} +:root.spooky .inline { + border-color: #707070; + background-color: rgba(255, 255, 255, .14); +} + +/* Fappe and Werk Tyme */ +:root.spooky .indicator { + color: #171526; +} + +/* Highlighting */ +:root.spooky .qphl { + outline: 2px solid rgba(145, 182, 214, .8); +} +:root.spooky.highlight-you .quotesYou$site$highlightable$op, +:root.spooky.highlight-you .quotesYou$site$highlightable$reply { + border-left: 3px solid rgba(145, 182, 214, .8); +} +:root.spooky.highlight-own .yourPost$site$highlightable$op, +:root.spooky.highlight-own .yourPost$site$highlightable$reply { + border-left: 3px dashed rgba(145, 182, 214, .8); +} +:root.spooky .filter-highlight$site$highlightable$op, +:root.spooky .filter-highlight$site$highlightable$reply { + box-shadow: inset 5px 0 rgba(145, 182, 214, .5); +} +:root.spooky.highlight-own .yourPost > $site$sideArrows, +:root.spooky.highlight-you .quotesYou > $site$sideArrows, +:root.spooky .filter-highlight > $site$sideArrows { + color: rgb(155, 185, 210); +} + +/* QR */ +.spooky #dump-list::-webkit-scrollbar-thumb { + background-color: #171526; + border-color: #707070; +} +:root.spooky .qr-preview { + background-color: rgba(0, 0, 0, .15); +} +:root.spooky #qr .field { + background-color: rgb(26, 27, 29); + color: rgb(197,200,198); + border-color: rgb(40, 41, 42); +} +:root.spooky #qr .field:focus, +:root.spooky #qr .field.focus { + border-color: rgb(254, 150, 0) !important; + background-color: rgb(30,32,36); +} +:root.spooky .persona button { + background: linear-gradient(to bottom, #2E3035, #222427) no-repeat; + color: rgb(197,200,198); + border-color: rgb(40, 41, 42); + outline: none; +} +:root.spooky .persona button::-moz-focus-inner { + border: none; +} +:root.spooky .persona button:focus { + border-color: rgb(254, 150, 0); +} +:root.spooky #qr.sjis-preview #sjis-toggle, +:root.spooky #qr.tex-preview #tex-preview-button { + background: rgb(26, 27, 29); +} +:root.spooky #qr select, +:root.spooky #file-n-submit > input, +:root.spooky #qr-draw-button { + border-color: rgb(40, 41, 42); +} +:root.spooky #qr-filename { + color: rgb(197,200,198); +} + +:root.spooky .qr-link { + border-color: rgb(8, 6, 23) rgb(8, 6, 23) rgb(0, 0, 8); + background: linear-gradient(#262435, #171526) repeat scroll 0% 0% transparent; +} +:root.spooky .qr-link:hover { + background: #1A1829; +} + + +/* Menu */ +:root.spooky #menu { + color: #FE9600; +} +:root.spooky .entry { + font-size: 10pt; +} +:root.spooky .focused.entry { + background: rgba(255, 255, 255, .33); +} + +/* Unread */ +:root.spooky .unread-line { + border-color: rgb(197, 200, 198); + visibility: visible; + opacity: 1; +} +:root.spooky .unread-mark-read { + background-color: rgba(23,21,38,0.5); +} + +/* Thread Watcher */ +:root.spooky .replies-quoting-you > a, :root.spooky #watcher-link.replies-quoting-you, :root.spooky .last-page > a > .watcher-page { + color: #F00 !important; +} + +/* Watcher Favicon */ +:root.spooky .watch-thread-link +{ + background-image: url("data:image/svg+xml,"); +} `; - var style = `/* General */ -.dialog { - border: 1px solid; - display: block; - background-color: inherit; -} -.dialog:not(#qr):not(#thread-watcher):not(#header-bar) { - box-shadow: 0 1px 2px rgba(0, 0, 0, .15); -} -#qr, -#thread-watcher { - box-shadow: -1px 2px 2px rgba(0, 0, 0, 0.25); -} -.captcha-img, -.field { - background-color: #FFF; - border: 1px solid #CCC; - -moz-box-sizing: border-box; - box-sizing: border-box; - color: #333; - font: 13px sans-serif; - outline: none; - transition: color .25s, border-color .25s; -} -.field::-moz-placeholder { - color: #AAA; - font-size: 13px; - opacity: 1; -} -.captch-img:hover, -.field:hover { - border-color: #999; -} -.field:hover, .field:focus, .field.focus { - color: #000; -} -.field[disabled] { - background-color: #F2F2F2; - color: #888; -} -.field::-webkit-search-decoration { - display: none; -} -.move { - cursor: move; - overflow: hidden; -} -label { - cursor: pointer; -} -a[href="javascript:;"] { - text-decoration: none; -} -.warning { - color: red; -} -:root.sw-yotsuba #boardNavDesktop, :root.sw-yotsuba #boardNavMobile { - display: none !important; -} -:root.hide-bottom-board-list $site$boardListBottom { - display: none; -} -body.hasDropDownNav{ - margin-top: 5px; -} -:root:not(.keyboard-focus) a { - outline: none; -} -.painted { - border-radius: 3px; - padding: 0px 2px; -} -[hidden] { - display: none !important; -} - -/* 4chan style fixes */ -/* overrides 4chan CSS on div.opContainer, div.op */ -:root.sw-yotsuba .opContainer, :root.sw-yotsuba .op { - display: block; - overflow: visible; -} -:root.sw-yotsuba .reply > .file > .fileText { - margin: 0 20px; -} -:root.sw-yotsuba #arc-list span.quote { - color: #789922; -} -:root.sw-yotsuba .fileText a { - unicode-bidi: -moz-isolate; - unicode-bidi: -webkit-isolate; -} -:root.sw-yotsuba #g-recaptcha { - min-height: 78px; - height: auto; -} -:root.sw-yotsuba:not(.js-enabled) #postForm { - display: table; -} -:root.sw-yotsuba #captchaContainerAlt td:nth-child(2) { - display: table-cell !important; -} -:root.sw-yotsuba canvas#tegaki-canvas { - background: none; -} -/* Disable obnoxious captcha fade-in. */ -:root.sw-yotsuba > body > div:last-of-type { - transition: none !important; -} -/* Fix captcha scrolling to top of page. */ -:root.sw-yotsuba > body > div[style*=" top: -10000px;"] { - visibility: hidden !important; -} -/* Make long filenames wrap properly: https://github.com/ccd0/4chan-x/issues/1082 */ -:root.sw-yotsuba .post > .file { - /* currently nonstandard but may be added: https://lists.w3.org/Archives/Public/www-style/2016Mar/0352.html, https://bugzilla.mozilla.org/show_bug.cgi?id=1296042 */ - word-break: break-word; -} -:root.sw-yotsuba:not(.ua-webkit):not(.ua-blink) .fileText { - word-wrap: break-word; - max-width: calc(100vw - 90px); -} -:root.sw-yotsuba > body.is_catalog .thread > a > img { - display: inline-block; -} -/* Links to NSFW boards */ -:root.sw-yotsuba .nwsb { - display: inline; -} -:root.sw-yotsuba .fileText { - max-width: auto; - white-space: normal; -} - -/* Ads */ -:root.sw-yotsuba .ad-cnt > *, :root.sw-yotsuba .adg-rects > *, :root.sw-yotsuba .bsa-cnt { - height: auto !important; -} -:root.sw-yotsuba:not(.ads-loaded) hr.abovePostForm, -:root.sw-yotsuba:not(.ads-loaded) .adg-rects > hr, -:root.sw-yotsuba #adg-ol + hr, -:root.sw-yotsuba .danbo-slot:empty { - display: none; -} -:root.sw-yotsuba .adg-rects { - margin: 0; - font-size: 0; -} -:root.sw-yotsuba div.center[style] { - display: none !important; -} - -/* Tinyboard / vichan conflicts */ -#menu > .hide-thread-link { - width: auto; - height: auto; - overflow: visible; - background-image: none; -} -#menu label.entry { - display: block; -} -#fourchanx-settings label { - display: inline; -} -.intro a[href="javascript:;"], -#menu a { - margin: 0; -} -.gal-buttons.gal-buttons a { - font-size: inherit; -} -:root.sw-tinyboard.fixed.top-header:not(.autohide) .boardlist, -:root.sw-tinyboard.fixed.top-header:not(.autohide) .bar.top { - position: static; -} -:root.sw-tinyboard.fixed.top-header:not(.autohide) div.pages.top { - top: auto; - bottom: 0; -} -:root.sw-tinyboard.fixed.top-header.autohide .boardlist, -:root.sw-tinyboard.fixed.top-header.autohide .bar.top { - z-index: 3; -} - -/* Tinyboard site style conflicts */ -:root[data-host="fufufu.moe"].fixed.top-header:not(.autohide) div.pages.top { - top: 26px; - bottom: auto; -} -:root[data-host="merorin.com"].fixed.top-header:not(.autohide) span.settings { - top: 26px; -} -:root[data-host="fufufu.moe"]:not(.fixed) #header-bar { - margin-top: 38px; -} -:root[data-host="lainchan.org"]:not(.fixed) #header-bar { - margin-top: 17px; -} -:root[data-host="smuglo.li"]:not(.fixed) #header-bar { - margin-top: 8px; -} - -/* Anti-autoplay */ -audio.controls-added { - display: block; - margin: auto; - white-space: normal; -} -:root.anti-autoplay div.embed { - position: static; - width: auto; - height: auto; - text-align: center; -} -:root.anti-autoplay .autoplay-removed { - visibility: visible !important; - min-width: 640px; - min-height: 360px; -} - -/* fixed, z-index */ -#overlay, -#qp, #ihover, -#navlinks, .fixed #header-bar, -:root.float #updater, -:root.float #thread-stats, -#qr { - position: fixed; -} -#overlay { - z-index: 999; -} -#qp, #ihover { - z-index: 60; -} -#menu, .gal-buttons { - z-index: 50; -} -#updater, #thread-stats { - z-index: 40; -} -:root.fixed #header-bar, #notifications { - z-index: 35; -} -#a-gallery { - z-index: 30; -} -#navlinks { - z-index: 25; -} -#qr { - z-index: 20; -} -#embedding { - z-index: 11; -} -:root.fixed-watcher #thread-watcher { - z-index: 10; -} -:root.fixed:not(.gallery-open) #header-bar:not(:hover) { - z-index: 8; -} -#thread-watcher { - z-index: 5; -} - -/* Header */ -.fixed.top-header body { - padding-top: 2em; -} -.fixed.bottom-header body { - padding-bottom: 2em; -} -.fixed #header-bar { - right: 0; - left: 0; - padding: 3px 4px 4px; - font-size: 12px; -} -.fixed.top-header #header-bar { - top: 0; -} -.fixed.bottom-header #header-bar { - bottom: 0; -} -#header-bar { - border-width: 0; - transition: all .1s .05s ease-in-out; -} -:root.fixed #header-bar { - box-shadow: -5px 1px 10px rgba(0, 0, 0, 0.20); -} -:root.centered-links #shortcuts { - width: 300px; - text-align: right; -} -:root.centered-links #header-bar { - text-align: center; -} -#custom-board-list { - font-size: 13px; - vertical-align: middle; -} -#full-board-list { - vertical-align: middle; -} -:root.centered-links #custom-board-list { - position: relative; - left: 150px; -} -.fixed.top-header #header-bar { - border-bottom-width: 1px; -} -.fixed.bottom-header #header-bar { - box-shadow: 0 -1px 2px rgba(0, 0, 0, .15); - border-top-width: 1px; -} -.fixed.bottom-header #header-bar .menu-button i { - border-top: none; - border-bottom: 6px solid; -} -.fixed #header-bar.autohide:not(:hover) { - box-shadow: none; - transition: all .8s .6s cubic-bezier(.55, .055, .675, .19); -} -.fixed.top-header #header-bar.autohide:not(:hover) { - margin-bottom: -1em; - -webkit-transform: translateY(-100%); - transform: translateY(-100%); -} -.fixed.bottom-header #header-bar.autohide:not(:hover) { - -webkit-transform: translateY(100%); - transform: translateY(100%); -} -#scroll-marker { - left: 0; - right: 0; - height: 10px; - position: absolute; -} -#header-bar:not(.autohide) #scroll-marker { - pointer-events: none; -} -#header-bar #scroll-marker { - display: none; -} -.fixed #header-bar #scroll-marker { - display: block; -} -.fixed.top-header #header-bar #scroll-marker { - top: 100%; -} -.fixed.bottom-header #header-bar #scroll-marker { - bottom: 100%; -} -#board-list a, #shortcuts a:not(.entry) { - text-decoration: none; - padding: 1px; -} -#shortcuts:empty { - display: none; -} -.brackets-wrap::before { - content: "\\00a0["; -} -.brackets-wrap::after { - content: "]\\00a0"; -} -.dead-thread, -.disabled:not(.replies-quoting-you) { - opacity: .45; -} -#shortcuts { - float: right; -} -:root.autohiding-scrollbar #shortcuts { - margin-right: 12px; -} -.shortcut { - margin-left: 3px; - vertical-align: middle; -} -:root.shortcut-icons .native-settings { - font-size: 0; - color: transparent; - display: inline-block; - vertical-align: top; - height: 12px; - width: 14px; - background: url('//s.4cdn.org/image/favicon.ico') 0px -1px no-repeat; -} -#navbotright, -#navtopright { - display: none; -} -#toggleMsgBtn { - display: none !important; -} -.current, -:root.sw-yotsuba div#boardNavDesktopFoot a.current { - font-weight: bold; -} -@media (min-width: 1300px) { - :root.sw-yotsuba.fixed:not(.centered-links) #header-bar { - white-space: nowrap; - display: -webkit-flex; - display: flex; - -webkit-align-items: center; - align-items: center; - } - :root.sw-yotsuba.fixed:not(.centered-links) #board-list { - -webkit-flex: auto; - flex: auto; - } - :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list { - display: -webkit-flex; - display: flex; - } - :root.sw-yotsuba.fixed:not(.centered-links) .hide-board-list-container { - -webkit-flex: none; - flex: none; - margin-right: 5px; - } - :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList { - -webkit-flex: auto; - flex: auto; - display: -webkit-flex; - display: flex; - width: 0px; /* XXX Fixes Edge not shrinking the board list below default size when needed */ - } - :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > a, - :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > span:not(.space):not(.spacer) { - -webkit-flex: none; - flex: none; - padding: .17em; - margin: -.17em -.32em; - } - :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > span { - pointer-events: none; - } - :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > span.space { - -webkit-flex: 0 .63 .63em; - flex: 0 .63 .63em; - } - :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > span.spacer { - -webkit-flex: 0 .38 .38em; - flex: 0 .38 .38em; - } - :root.sw-yotsuba.fixed:not(.centered-links) #shortcuts { - float: initial; - -webkit-flex: none; - flex: none; - display: -webkit-flex; - display: flex; - -webkit-align-items: center; - align-items: center; - } -} -/* 4chan X link brackets */ -.brackets-wrap::before { - content: "["; -} -.brackets-wrap::after { - content: "]"; -} -/* Notifications */ -#notifications { - position: fixed; - top: 0; - height: 0; - text-align: center; - right: 0; - left: 0; - visibility: visible; -} -#notifications:empty { - display: none; -} -:root.fixed.top-header:not(.gallery-open) #header-bar #notifications, -:root.fixed.top-header #header-bar.autohide #notifications { - position: absolute; - top: 100%; -} -.notification { - color: #FFF; - font-weight: 700; - text-shadow: 0 1px 2px rgba(0, 0, 0, .5); - box-shadow: 0 1px 2px rgba(0, 0, 0, .15); - border-radius: 2px; - margin: 1px auto; - width: 550px; - max-width: 100%; - position: relative; - transition: all .25s ease-in-out; -} -.notification.error { - background-color: hsla(0, 100%, 38%, .9); -} -.notification.warning { - background-color: hsla(36, 100%, 38%, .9); -} -.notification.info { - background-color: hsla(200, 100%, 38%, .9); -} -.notification.success { - background-color: hsla(104, 100%, 38%, .9); -} -.notification a { - color: white; -} -.notification > .close { - padding: 7px; - top: 0px; - right: 5px; - position: absolute; -} -.notification > .fa-times::before { - font-size: 11px !important; -} -.message { - -moz-box-sizing: border-box; - box-sizing: border-box; - padding: 6px 20px; - max-height: 200px; - width: 100%; - overflow: auto; - white-space: pre-line; -} -.message a { - text-decoration: underline; -} -:root.tainted .report-error { - display: none; -} - -/* Settings */ -:root.fourchan-x body { - -moz-box-sizing: border-box; - box-sizing: border-box; -} -#overlay { - background-color: rgba(0, 0, 0, .5); - display: -webkit-flex; - display: flex; - top: 0; - left: 0; - height: 100%; - width: 100%; -} -#fourchanx-settings { - -moz-box-sizing: border-box; - box-sizing: border-box; - box-shadow: 0 0 15px rgba(0, 0, 0, .15); - height: 600px; - max-height: 100%; - width: 900px; - max-width: 100%; - margin: auto; - padding: 5px; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: column; - flex-direction: column; -} -#fourchanx-settings > nav { - padding: 2px 2px 8px; - display: -webkit-flex; - display: flex; -} -#fourchanx-settings > nav a { - text-decoration: underline; -} -#fourchanx-settings > nav a.close { - text-decoration: none; - padding: 0 2px; - margin: 0; -} -.section-container { - -webkit-flex: 1; - flex: 1; - position: relative; - overflow: auto; - padding-right: 5px; - overscroll-behavior: contain; -} -.sections-list { - -webkit-flex: 1; - flex: 1; -} -.export, .import, .reset { - cursor: pointer; - text-decoration: none !important; -} -.tab-selected { - font-weight: 700; -} -.section-sauce ul, -.section-advanced ul { - list-style: none; - margin: 0; -} -.section-sauce ul { - padding: 8px; -} -.section-advanced ul { - padding: 0px; -} -.section-sauce li, -.section-advanced li { - padding-left: 4px; -} -.section-main ul { - margin: 0; - padding: 0 0 0 16px; -} -.section-main li { - white-space: pre-line; - list-style: disc; -} -.section-main li:not(:first-of-type) { - margin-top: 4px; -} -.section-main label { - text-decoration: underline; -} -div[data-checked="false"] > .suboption-list { - display: none; -} -.suboption-list { - position: relative; -} -.suboption-list::before { - content: ""; - display: inline-block; - position: absolute; - left: .7em; - width: 0; - height: 100%; - border-left: 1px solid; -} -.suboption-list > div { - position: relative; - padding-left: 1.4em; -} -.suboption-list > div::before { - content: ""; - display: inline-block; - position: absolute; - left: .7em; - width: .7em; - height: .6em; - border-left: 1px solid; - border-bottom: 1px solid; -} -#fourchanx-settings .section-main p { - margin: .5em 0 0; -} -.section-filter ul { - padding: 0; -} -.section-filter li { - margin: 10px 40px; - list-style: disc; -} -.section-filter textarea { - height: 500px; -} -.section-main a, .section-filter a, .section-advanced a { - text-decoration: underline; -} -#sauce-doc-expand:not(:checked) ~ #sauce-doc { - max-height: 130px; - overflow: auto; -} -#sauce-doc > label { - float: right; - margin: 0 5px; -} -/* XXX for OneeChan */ -#sauce-doc-expand + .riceCheck { - display: none; -} -.section-sauce textarea { - height: 430px; -} -.section-advanced .field[name="boardnav"] { - width: 100%; -} -.section-advanced textarea { - height: 150px; -} -.section-advanced textarea[name="archiveLists"], -.section-advanced textarea[name="externalCatalogURLs"], -.section-advanced textarea[name="knownBanners"] { - height: 75px; -} -.section-advanced .archive-cell { - min-width: 160px; - text-align: center; -} -.section-advanced #archive-board-select { - position: absolute; -} -.section-advanced .note { - font-size: 0.8em; - font-style: italic; - margin-left: 10px; -} -.section-advanced .note code { - font-style: normal; - font-size: 11px; -} -.favicon-preview > img { - vertical-align: middle; -} -.favicon-preview > img:nth-of-type(3n+1) { - margin-left: 4px; -} -.section-keybinds .field { - font-family: monospace; -} -#fourchanx-settings fieldset { - border: 1px solid; - border-radius: 3px; - padding: 0.35em 0.625em 0.75em; - margin: 0px 2px; -} -#fourchanx-settings legend { - font-weight: 700; - color: inherit; -} -#fourchanx-settings textarea { - font-family: monospace; - width: 100%; - resize: vertical; -} -#fourchanx-settings code { - color: #000; - background-color: #FFF; - padding: 0 2px; -} -#fourchanx-settings th { - text-align: center; - font-weight: bold; -} -#fourchanx-settings p { - margin: 1em 0px; -} -#fourchanx-settings table { - margin: auto; -} - -/* Index */ -:root.index-loading .navLinks:not(.json-index), -:root.index-loading .board:not(.json-index), -:root.index-loading .pagelist:not(.json-index), -:root.infinite-mode .pagelist, -:root.all-pages-mode .pagelist, -:root.catalog-mode .pagelist, -:root:not(.catalog-mode) .indexlink, -:root.catalog-mode .cataloglink, -:root:not(.catalog-mode) #hidden-label, -:root:not(.catalog-mode) #index-size { - display: none; -} -#index-search { - padding-right: 1.5em; - width: 100px; - transition: color .25s, border-color .25s, width .25s; -} -#index-search:focus, -#index-search[data-searching] { - width: 200px; -} -#index-search-clear { - color: gray; - display: inline-block; - position: relative; - left: -1em; - width: 0; -} -/* \`\`::-webkit-*'' selectors break selector lists on Firefox. */ -#index-search::-webkit-search-cancel-button { - display: none; -} -#index-search:not([data-searching]) + #index-search-clear { - display: none; -} -#index-options { - float: right; -} -#lastlong-options { - display: inline-block; - vertical-align: middle; - height: 28px; - margin: -14px 0; -} -#lastlong-options > input { - padding: 0; - border: 0 !important; - text-align: center; - background: transparent; - display: block; - font-size: 12px; - height: 12px; - width: 30px; - margin: 1px 0; -} -.summary { - text-decoration: none; -} - -/* Catalog */ -:root.catalog-mode .board { - text-align: center; -} -.catalog-thread { - display: inline-block; - -moz-box-sizing: border-box; - box-sizing: border-box; - border: 1px solid transparent; - word-wrap: break-word; - vertical-align: top; - position: relative; -} -/* overrides 4chan CSS on div.thread */ -.catalog-thread.catalog-thread { - margin: 2px; -} -.catalog-small > .catalog-thread { - width: 165px; - height: 320px; -} -.catalog-large > .catalog-thread { - width: 270px; - height: 410px; -} -:root.catalog-hover-expand .catalog-thread:hover { - z-index: 1; -} -.catalog-container { - position: absolute; - top: -4px; - left: 0; - right: 0; - bottom: 0; -} -.catalog-container:not(:hover), -:root:not(.catalog-hover-expand) .catalog-container { - overflow: hidden; -} -.catalog-post { - position: absolute; - top: 4px; - left: 0; - right: 0; - border: 1px solid transparent; - padding-top: 20px; -} -/* overrides inline CSS from Index.cb.hoverAdjust */ -:root:not(.catalog-hover-expand) .catalog-post { - left: 0 !important; - right: 0 !important; -} -/* overrides 4chan CSS on div.post */ -.catalog-post.catalog-post { - margin: -21px -1px -1px; - overflow: visible; -} -.catalog-thread.noFile > * > .catalog-post { - margin-top: -7px; - padding-top: 6px; -} -:root.catalog-hover-expand .catalog-container:hover > .catalog-post { - margin-left: -61px; - margin-right: -61px; -} -:root.catalog-hover-expand .catalog-container:hover > * > :not(.catalog-replies) { - padding-left: 2px; - padding-right: 2px; -} -.catalog-link { - display: block; - position: relative; -} -.catalog-thumb { - border-radius: 2px; - box-shadow: 0 0 5px rgba(0, 0, 0, .25); - vertical-align: top; -} -.catalog-thumb.spoiler-file { - width: 100px; - height: 100px; -} -.catalog-thumb.deleted-file { - width: 127px; - height: 13px; - padding: 20px 11px; -} -.catalog-thumb.no-file { - width: 77px; - height: 13px; - padding: 20px 36px; -} -.catalog-icons > img, -.catalog-stats > .menu-button { - width: 1em; - height: 1em; - margin: 0; - vertical-align: text-top; - padding-left: 2px; -} -.catalog-stats > .menu-button { - font-weight: normal; -} -.catalog-stats > .menu-button > i::before { - line-height: 11px; -} -.catalog-stats { - font-size: 10px; - font-weight: 700; - padding-top: 2px; -} -.catalog-stats > [title] { - cursor: help; -} -.catalog-post > .postMessage { - margin: 0; - padding-bottom: .3em; -} -.catalog-container:not(:hover) > * > .file, -.catalog-container:not(:hover) > * > .postInfo > :not(.subject), -.catalog-container:not(:hover) > * > .catalog-replies, -.catalog-container:not(:hover) .extra-linebreak, -.catalog-container:not(:hover) .abbr, -:root:not(.catalog-hover-expand) .catalog-container > * > .file, -:root:not(.catalog-hover-expand) .catalog-container > * > .postInfo > :not(.subject), -:root:not(.catalog-hover-expand) .catalog-container > * > .catalog-replies, -:root:not(.catalog-hover-expand) .catalog-container .extra-linebreak, -:root:not(.catalog-hover-expand) .catalog-container .abbr, -.catalog-thread > .catalog-container > :not(.catalog-post), -.catalog-post > .file > :not(.fileText), -.catalog-post > * > .fileText > :not(:first-child), -.catalog-post > .postInfo > :not(.subject):not(.nameBlock):not(.dateTime), -.catalog-post > .postInfo > .nameBlock > .contact-links, -.catalog-post > * > * > .posteruid, -.catalog-post > * > * > .postJumper, -:root.bottom-backlinks .catalog-post > .container, -.post:not(.catalog-post) > .catalog-link, -.post:not(.catalog-post) > .catalog-stats, -.post:not(.catalog-post) > .catalog-replies { - display: none; -} -.catalog-post > .file { - position: absolute; - left: 0; - right: 0; - top: 0; - min-height: 20px; - background-color: inherit; -} -.catalog-post > * > .fileText { - position: relative; - padding: 2px; - background-color: inherit; -} -.catalog-small .catalog-post > * .fileText { - font-size: 10px; -} -.catalog-post > * > .fileText:not(:hover) { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.catalog-post > * > .fileText:hover { - z-index: 1; -} -/* overrides 4chan CSS on div.post div.postInfo */ -.catalog-post > .postInfo.postInfo { - width: auto; -} -.catalog-post > * > .subject { - display: block; -} -.catalog-post > * > .dateTime { - display: inline-block; - font-style: italic; -} -:root.catalog-hover-expand .catalog-container:hover > * > * > .nameBlock, -:root.catalog-hover-expand .catalog-container:hover > * > * > .dateTime, -:root.catalog-hover-expand .catalog-container:hover > * > .postMessage:not(:empty) { - padding-top: .3em; -} -.catalog-post .extra-linebreak { - content: ''; /* makes this work in Blink/WebKit */ - display: block; - margin-top: .3em; -} -.catalog-reply { - text-align: left; - white-space: nowrap; - border-top: 1px solid transparent; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: row; - flex-direction: row; - -webkit-align-items: stretch; - align-items: stretch; -} -.catalog-reply > * { - padding: 3px; - overflow: hidden; - -webkit-flex: none; - flex: none; -} -.catalog-reply > span { - font-style: italic; - font-weight: bold; -} -.catalog-reply-excerpt { - -webkit-flex: 1 1 auto; - flex: 1 1 auto; -} -.catalog-post .prettyprinted { - max-width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -.catalog-post .MathJax_Display { - text-align: center !important; -} -.catalog-container:not(:hover) .exif, -:root:not(.catalog-hover-expand) .catalog-container .exif { - display: none !important; -} -.catalog-post > * > .exif { - border-collapse: collapse; -} -:root.catalog-hover-expand .catalog-container:hover .exif[style*="display: block;"] { - display: inline-block !important; -} -.catalog-post > * > .exif, -.catalog-post > * > .exif > tbody { - background-color: inherit; -} -.catalog-post > * > .exif, -.catalog-post > * > .exif td { - min-width: 0; -} -.catalog-post > * > .exif td { - padding-top: 1px; -} -:root.hats-enabled .catalog-thread::after { - content: ''; - pointer-events: none; - position: absolute; - background-size: contain; -} -:root.hats-enabled .catalog-small > .catalog-thread::after { - left: -8px; - top: -59px; - width: 96px; - height: 96px; -} -:root.hats-enabled:not(.werkTyme) .catalog-small > .catalog-thread:not(.noFile)::after { - left: calc(67px - .3px * var(--tn-w)); -} -:root.hats-enabled .catalog-large > .catalog-thread::after { - left: -15px; - top: -98px; - width: 160px; - height: 160px; -} -:root.hats-enabled:not(.werkTyme) .catalog-large > .catalog-thread:not(.noFile)::after { - left: calc(110px - .5px * var(--tn-w)); -} - -/* Copy Text Link's textarea element */ -textarea.copy-text-element { - height: 0; - width: 0; - position: absolute; - top: -10000px; -} - -/* Announcement Hiding */ -:root.hide-announcement $site$psa { - display: none; -} -.hide-announcement-button { - opacity: 0.4; - float: left; -} - -/* Unread */ -.unread-line { - margin: 0; - border-color: rgb(255,0,0); -} -.unread-line + br { - display: none; -} -.unread-mark-read { - float: right; - clear: both; - width: 100%; - text-align: right; -} -:not(.unread-thread) > .unread-mark-read { - display: none; -} - -/* Thread Updater */ -#updater { - background: none; - border: none; - box-shadow: none; -} -#updater > .move { - position: absolute; - top: -5px; - bottom: -5px; - left: -5px; - right: -5px; - z-index: -1; -} -#updater > div:last-child { - text-align: center; -} -#updater input[type="number"] { - width: 4em; -} -:root.float #updater { - padding: 0px 3px; -} -:root:not(.float).shortcut-icons #updater { - display: inline-block; - min-width: 12pt; - text-align: right; -} -.new { - color: limegreen; -} -#update-status:not(.empty) + #update-timer:not(.empty):not(.loading) { - margin-left: 5px; -} -#update-timer { - cursor: pointer; -} - -/* Thread Watcher */ -#thread-watcher { - position: absolute; -} -#thread-watcher { - padding-bottom: 3px; - padding-left: 3px; - white-space: nowrap; - min-width: 146px; -} -#watched-threads { - overflow-x: hidden; - overflow-y: auto; -} -#thread-watcher .refresh { - padding: 0px 3px; -} -:root.fixed-watcher #thread-watcher { - position: fixed; -} -:root.fixed-watcher #watched-threads { - /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ - max-height: 85vh; - max-height: calc(100vh - 75px); -} -:root:not(.fixed-watcher) #watched-threads:not(:hover) { - max-height: 210px; - overflow-y: hidden; -} -#thread-watcher > .move { - padding-top: 3px; -} -#watched-threads > div { - padding-left: 3px; - padding-right: 3px; -} -#watched-threads .watcher-link { - max-width: 250px; - display: -webkit-inline-flex; - display: inline-flex; - -webkit-flex-direction: row; - flex-direction: row; -} -#watched-threads .watcher-page, -#watched-threads .watcher-unread { - -webkit-flex: 0 0 auto; - flex: 0 0 auto; - margin-right: 2px; -} -#watched-threads .watcher-title { - overflow: hidden; - text-overflow: ellipsis; - -webkit-flex: 0 1 auto; - flex: 0 1 auto; -} -#watched-threads .watcher-title:not(:first-child) { - margin-left: 2px; -} -.replies-quoting-you > a, #watcher-link.replies-quoting-you, .last-page > a > .watcher-page { - color: #F00; -} -#thread-watcher a { - text-decoration: none; -} -#thread-watcher .move > .close { - position: absolute; - right: 0px; - top: 0px; - padding: 0px 4px; -} -.watch-thread-link { - padding-top: 18px; - width: 18px; - height: 0px; - display: inline-block; - background-repeat: no-repeat; - opacity: 0.2; - position: relative; - top: 1px; - background-image: url("data:image/svg+xml,"); -} -.watch-thread-link.watched { - opacity: 1; -} - - -/* Thread Stats */ -#thread-stats { - background: none; - border: none; - box-shadow: none; -} -:root.float #thread-stats > .move > :not(#page-count) { - pointer-events: none; -} -:root.float #thread-stats { - padding: 0px 3px; -} -#page-count { - cursor: pointer; -} - -/* Quote */ -.hashlink::before { - content: ' '; - visibility: hidden; -} -.inline + .hashlink { - display: none !important; -} -:root.resurrect-quotes .deadlink { - text-decoration: none !important; -} -.catalog-post .qmark-ct { - display: none; -} -.backlink.deadlink:not(.forwardlink), -.quotelink.deadlink:not(.forwardlink) { - text-decoration: underline !important; -} -:root:not(.catalog-mode) .inlined { - opacity: .5; -} -#qp input, .forwarded { - display: none; -} -.quotelink.forwardlink, -.backlink.forwardlink { - text-decoration: none; - border-bottom: 1px dashed; -} -.filtered { - text-decoration: underline line-through; -} -:root.hide-backlinks .backlink.filtered, -:root.hide-backlinks .backlink.filtered + .hashlink.filtered { - display: none; -} -.postNum + .container::before { - content: " "; -} -:root.bottom-backlinks .container { - display: block; - clear: both; - margin: 0 4px; -} -:root.bottom-backlinks .backlink { - font-size: 90%; -} -.inline { - border: 1px solid; - display: table; - margin: 2px 0; -} -.container ~ .inline { - margin-left: 20px; -} -:root.catalog-mode .inline { - display: none; -} -.inline .post { - border: 0 !important; - background-color: transparent !important; - display: table !important; - margin: 0 !important; - padding: 1px 2px !important; -} -#qp > .opContainer::after { - content: ''; - clear: both; - display: table; -} -#qp .post { - border: none; - margin: 0; - padding: 2px 2px 5px; -} -#qp img { - max-height: 80vh; - max-width: 50vw; -} - -/* Quote Threading */ -.threadContainer { - margin-left: 20px; - border-left: 1px solid rgba(128,128,128,.3); -} -.threadOP { - clear: both; -} - -/* File */ -.expanded-image > .post > .file > .fileThumb { - display: flex; - flex-direction: column; -} -.fileText-original, -.fnswitch:hover > .fntrunc, -.fnswitch:not(:hover) > .fnfull, -.expanded-image > .post > .file > .fileThumb > video[data-md5], -.expanded-image > .post > .file > .fileThumb > img[data-md5] { - display: none; -} -.full-image[data-file-i-d] { - display: none; - cursor: pointer; -} -.expanded-image > .post > .file > .fileThumb > .full-image { - display: inline; -} -.expanded-image > .post > .file > .fileThumb > audio { - height: 30px; - width: 100%; - min-width: 300px; -} -.expanded-image { - clear: left; -} -.expanding { - opacity: .5; -} -:root.fit-height .full-image { - max-height: 100vh; -} -:root.fit-height.fixed .full-image { - /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ - max-height: 93vh; - max-height: calc(100vh - 35px); -} -:root.fit-width .full-image { - max-width: 100%; -} -:root.ua-gecko.fit-width .full-image { - width: 100%; -} -.fileThumb > .warning { - clear: both; -} -#ihover { - pointer-events: none; - /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ - max-height: 95vh; - max-height: calc(100vh - 25px); - max-width: 100vw; -} -/* WEBM Metadata */ -.webm-title > a::before { - content: "title"; - text-decoration: underline; -} -.webm-title.loading > a::after { - content: "..."; -} -.webm-title.error > a:hover::before, -.webm-title.error > a:focus::before { - content: "error"; - text-decoration: none; -} -.webm-title > span { - cursor: text; -} -.webm-title.not-found > span::before { - content: "not found"; -} -.webm-title:not(:hover):not(:focus) > span, -.webm-title:hover > span + a, -.webm-title:focus > span + a { - display: none; -} -/* Volume control */ -input[name="Default Volume"] { - width: 4em; - height: 1ex; - vertical-align: middle; - margin: 0px; -} -/* Fappe and Werk Tyme */ -:root.fappeTyme $site$replyOriginal.noFile, -:root.fappeTyme $site$replyOriginal.noFile + br { - display: none; -} -:root.werkTyme $site$thumbLink, -:root.werkTyme $site$file$thumb, -:root.werkTyme .catalog-thumb:not(.deleted-file):not(.no-file), -:root:not(.werkTyme) .werkTyme-filename { - display: none; -} -.werkTyme-filename { - font-weight: bold; - font-size: 110%; -} -:root.werkTyme .catalog-link { - box-shadow: 0 0 5px rgba(0, 0, 0, .25); - padding: 8px; - text-align: center; -} -:root.werkTyme .catalog-thumb { - box-shadow: none; - padding: 0; - vertical-align: middle; -} -.indicator { - background: rgba(255,0,0,0.8); - font-weight: bold; - display: inline-block; - min-width: 9px; - padding: 0px 2px; - margin: 0 1px; - text-align: center; - color: white; - border-radius: 2px; - cursor: pointer; -} -:root:not(.fappeTyme) #shortcut-fappe, -:root:not(.werkTyme) #shortcut-werk { - display: none; -} - -/* Index/Reply Navigation */ -#navlinks { - font-size: 16px; - top: 25px; - right: 10px; -} -:root.catalog-mode #navlinks { - display: none; -} - -/* Highlighting */ -.qphl { - outline: 2px solid rgba(216, 94, 49, .8); -} -:root.highlight-you .quotesYou$site$highlightable$op, -:root.highlight-you .quotesYou$site$highlightable$reply { - border-left: 3px solid rgba(221, 0, 0, .8); -} -:root.highlight-own .yourPost$site$highlightable$op, -:root.highlight-own .yourPost$site$highlightable$reply { - border-left: 3px dashed rgba(221, 0, 0, .8); -} -.filter-highlight$site$highlightable$op, -.filter-highlight$site$highlightable$reply { - box-shadow: inset 5px 0 rgba(221, 0, 0, .5); -} -:root.highlight-own .yourPost > $site$sideArrows, -:root.highlight-you .quotesYou > $site$sideArrows, -.filter-highlight > $site$sideArrows { - color: rgba(221, 0, 0, .8); -} -:root.highlight-own .yourPost$site$highlightable$op::after, -:root.highlight-you .quotesYou$site$highlightable$op::after, -.filter-highlight$site$highlightable$op::after { - content: ""; - display: block; - clear: both; -} -:root:not(.werkTyme) .catalog-thread.filter-highlight .catalog-thumb, -:root.werkTyme .catalog-thread.filter-highlight:not(:hover), -:root.werkTyme:not(.catalog-hover-expand) .catalog-thread.filter-highlight, -:root.werkTyme.catalog-hover-expand .catalog-thread.filter-highlight > .catalog-container:hover > .catalog-post, -:root.catalog $site$catalog$thread.filter-highlight$site$highlightable$catalog { - box-shadow: 0 0 3px 3px rgba(255, 0, 0, .5); -} -:root:not(.werkTyme) .catalog-thread.watched .catalog-thumb, -:root:root.werkTyme .catalog-thread.watched:not(:hover), -:root:root.werkTyme:not(.catalog-hover-expand) .catalog-thread.watched, -:root.werkTyme.catalog-hover-expand .catalog-thread.watched > .catalog-container:hover > .catalog-post { - border: 2px solid rgba(255, 0, 0, .75); -} - -/* Spoiler text */ -:root.reveal-spoilers $site$spoiler, -:root.reveal-spoilers $site$spoiler > a { - color: white !important; -} -:root.reveal-spoilers .removed-spoiler::before { - content: "[spoiler]"; -} -:root.reveal-spoilers .removed-spoiler::after { - content: "[/spoiler]"; -} - -/* Thread & Reply Hiding */ -.hide-thread-button, -.hide-reply-button { - float: left; - margin-right: 4px; - padding: 2px; -} -$site$infoRoot a.hide-reply-button { - margin-right: 6px; - padding: 0; -} -.replacedSideArrows { - float: left; -} -.hide-thread-button:not(:hover), -.hide-reply-button:not(:hover) { - opacity: 0.4; -} -.threadContainer .hide-reply-button { - margin-left: 2px !important; - position: relative; - left: 1px; -} -.hide-thread-button { - margin-top: -1px; - width: 11px; -} -.stub ~ :not(.threadDivider) { - display: none !important; -} -.stub input { - display: inline-block; -} -$site$thread[hidden] + hr { - display: none; -} -:root.reply-hide $site$sideArrows { - display: none; -} -:root.sw-yotsuba.thread-hide .party-hat { - left: 19px; -} - -/* Anonymize */ -:root.anonymize $site$info$name, -:root.sw-yotsuba.anonymize .post-author:not([class*=capcode]) { - font-size: 0; -} -:root.anonymize $site$info$tripcode, -:root.sw-yotsuba.anonymize .n-pu { - display: none; -} -:root.anonymize $site$info$name::before, -:root.sw-yotsuba.anonymize .post-author:not([class*=capcode])::before { - content: "Anonymous"; - font-size: 10pt; -} -:root.sw-yotsuba.anonymize .flashListing .name::before, -:root.sw-yotsuba.anonymize .post-last > .post-author:not([class*=capcode])::before { - font-size: 9pt; -} - -/* QR */ -:root.hide-original-post-form #togglePostFormLink, -#qr.autohide:not(.focus):not(:hover):not(:active) > form, -:root.thread-view #qr:not(.show-new-thread-option) select[data-name="thread"], -#file-n-submit:not(.has-file) #qr-filerm { - display: none; -} -:root.hide-original-post-form #postForm { - display: none !important; -} -#qr select, -#qr-filename-container > a, -.remove, -.captcha-img { - cursor: pointer; -} -#qr { - position: fixed; - padding: 1px; - border: 1px solid transparent; - min-width: 300px; - border-radius: 3px 3px 0 0; -} -#qr > form { - /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ - max-height: 85vh; - max-height: calc(100vh - 75px); - overflow-y: auto; - overflow-x: hidden; -} -#qrtab { - border-radius: 3px 3px 0 0; -} -#qrtab { - margin-bottom: 1px; -} -#qr .close { - float: right; - padding: 0 3px; -} -.qr-link-container { - text-align: center; - margin: 16px 0; -} -.qr-link-container-bottom { - width: 200px; - position: absolute; - left: -100px; - margin-left: 50%; - text-align: center; -} -.qr-link { - border-radius: 3px; - padding: 6px 10px 5px; - font-weight: bold; - vertical-align: middle; - border-style: solid; - border-width: 1px; - font-size: 10pt; -} -.qr-link-container + #togglePostFormLink { - font-size: 10pt; - font-weight: normal; - margin: -8px 0 3.5px; -} -.persona { - width: 100%; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: row; - flex-direction: row; -} -.persona .field { - -webkit-flex: 1; - flex: 1; - width: 0; -} -#qr.forced-anon input[data-name="name"]:not(.force-show), -#qr.forced-anon input[data-name="sub"]:not(.force-show), -#qr.reply-to-thread input[data-name="sub"]:not(.force-show), -body:not(.board_f) #qr select[name="filetag"], -#qr.reply-to-thread select[name="filetag"], -#qr:not(.has-sjis) #sjis-toggle, -#qr:not(.has-math) #tex-preview-button, -#qr.tex-preview .textarea > :not(#tex-preview), -#qr:not(.tex-preview) #tex-preview { - display: none; -} -.persona button { - -webkit-flex: 0 0 23px; - flex: 0 0 23px; - -webkit-align-self: stretch; - align-self: stretch; - border: 1px solid #BBB; - padding: 0; - background: linear-gradient(to bottom, #F8F8F8, #DCDCDC) no-repeat; - color: #000; -} -#qr.sjis-preview #sjis-toggle, #qr.tex-preview #tex-preview-button { - background: #DCDCDC; -} -#sjis-toggle, #qr.sjis-preview textarea.field { - font-family: "IPAMonaPGothic","Mona","MS PGothic",monospace; - font-size: 16px; - line-height: 17px; -} -#tex-preview-button { - font-size: 10px; -} -#tex-preview { - white-space: pre-line; -} -#qr textarea.field { - height: 14.8em; - min-height: 9em; -} -#qr.has-captcha textarea.field { - height: 9em; -} -input.field.tripped:not(:hover):not(:focus) { - color: transparent !important; - text-shadow: none !important; -} -#qr textarea { - min-width: 300px; - resize: both; -} -.field { - -moz-box-sizing: border-box; - box-sizing: border-box; - margin: 0px; - padding: 2px 4px 3px; -} -#qr label input[type="checkbox"] { - position: relative; - top: 2px; -} - -/* Recaptcha v2 */ -#qr .captcha-root { - position: relative; -} -#qr .captcha-container > div { - margin: auto; - width: 304px; -} -/* XXX scrollable with scroll bar hidden; prevents scroll on space press */ -:root.ua-blink #qr .captcha-container > div, -:root.ua-edge #qr .captcha-container > div { - overflow: hidden; -} -:root.ua-blink #qr .captcha-container > div > div:first-of-type, -:root.ua-edge #qr .captcha-container > div > div:first-of-type { - overflow-y: scroll; - overflow-x: hidden; - padding-right: 30px; - height: 99%; - width: 100%; -} -#qr .captcha-counter { - display: block; - width: 100%; - text-align: center; - pointer-events: none; -} -#qr.captcha-open .captcha-counter { - position: absolute; - bottom: 3px; -} -#qr .captcha-counter > a { - pointer-events: auto; - display: inline-block; /* XXX https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8851747/ */ -} -#qr:not(.captcha-open) .captcha-counter > a { - display: block; - width: 100%; -} -#qr.captcha-v2 #qr-captcha-iframe { - width: 302px; - height: 423px; - border: 0; - display: block; - margin: auto; -} -.goog-bubble-content { - max-width: 100vw; - max-height: 100vh; - overflow: auto; -} -.goog-bubble-content iframe { - position: static !important; -} - -/* File Input, Submit Button, Oekaki */ -#file-n-submit, #qr .oekaki { - display: -webkit-flex; - display: flex; - -webkit-align-items: stretch; - align-items: stretch; - height: 25px; - margin-top: 1px; -} -#file-n-submit > input, #qr-draw-button { - background: linear-gradient(to bottom, #F8F8F8, #DCDCDC) no-repeat; - border: 1px solid #BBB; - border-radius: 2px; - height: 100%; -} -#qr-file-button, #qr-draw-button { - width: 15%; -} -#file-n-submit input[type="submit"] { - width: 25%; -} -#qr-filename-container { - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - width: 0; - display: -webkit-flex; - display: flex; - -webkit-align-items: center; - align-items: center; - position: relative; - padding: 1px; -} -input#qr-filename { - border: none !important; - background: none !important; - outline: none; -} -#qr-filename, -.has-file #qr-no-file { - display: none; -} -#qr-no-file, -.has-file #qr-filename { - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - width: 0px; /* XXX Fixes filename not shrinking to allow space for buttons in Edge */ - display: inline-block; - padding: 0; - padding-left: 3px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -#qr-no-file { - color: #AAA; -} -#qr .oekaki.has-file { - display: none; -} -#qr .oekaki > label { - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - width: 0; - display: -webkit-flex; - display: flex; - -webkit-align-items: center; - align-items: center; - height: 100%; -} -#qr .oekaki > label > span { - margin: 0 3px; -} -#qr .oekaki > label > input { - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - width: 0; - height: 100%; -} -#qr .oekaki-bg { - position: relative; - display: inline-block; - height: 100%; - width: 10%; - margin-left: 3px; -} -#qr .oekaki-bg > * { - position: absolute; - top: 0; - left: 0; - margin: 0; -} -#qr .oekaki-bg > :not([name="oekaki-bgcolor"]) { - z-index: 1; -} -#qr [name="oekaki-bgcolor"] { - height: 100%; - width: 100%; - border: none; - padding: 0; -} -#qr [name="oekaki-bg"]:not(:checked) ~ [name="oekaki-bgcolor"] { - visibility: hidden; -} -#qr input[type="file"] { - visibility: hidden; - position: absolute; -} - -/* Spoiler Checkbox, QR Icons */ -#qr-filename-container > label, #qr-filename-container > a { - -webkit-flex: none; - flex: none; - margin: 0; - margin-right: 3px; -} -#qr:not(.has-spoiler) #qr-spoiler-label, -#file-n-submit:not(.has-file) #qr-spoiler-label, -.has-file #paste-area, -.has-file #url-button, -#file-n-submit:not(.custom-cooldown) #custom-cooldown-button { - display: none; -} -#qr-filename-container > label { - position: relative; -} -#qr-filename-container input[type="checkbox"] { - margin: 0; -} -.checkbox-letter { - font-size: 13px; - font-weight: bold; -} -#qr-filename-container label:not(:hover) > input[type="checkbox"]:not(:focus):not(:checked), -#qr-filename-container label:not(:hover) > input[type="checkbox"]:not(:focus):not(:checked) ~ :not(.checkbox-letter), -#qr-filename-container label:hover > .checkbox-letter, -input[type="checkbox"]:focus ~ .checkbox-letter, -input[type="checkbox"]:checked ~ .checkbox-letter { - /* not displayed but still focusable */ - position: absolute; - opacity: 0; - pointer-events: none; -} -.checkbox-letter, #paste-area, #url-button, #custom-cooldown-button, #dump-button { - opacity: 0.6; -} -#paste-area { - font-size: 0; -} -#paste-area:focus { - opacity: 1; -} -#custom-cooldown-button.disabled { - opacity: 0.27; -} - -/* Thread and Flash Tag Select */ -#qr select { - background: white; - border: 1px solid #CCC; -} -#qr select[data-name="thread"] { - float: right; -} -#qr > form > select { - margin-top: 1px; -} - -/* Dumping UI */ -.dump #dump-list-container { - display: block; -} -#dump-list-container { - display: none; - position: relative; - overflow-y: hidden; - margin-top: 1px; -} -#dump-list { - overflow-x: auto; - overflow-y: auto; - white-space: nowrap; - width: 248px; - max-height: 248px; - min-height: 90px; - max-width: 100%; - min-width: 100%; - display: -webkit-flex; - display: flex; - -webkit-flex-wrap: wrap; - flex-wrap: wrap; -} -#dump-list:hover { - overflow-x: auto; -} -.qr-preview { - -moz-box-sizing: border-box; - box-sizing: border-box; - counter-increment: thumbnails; - cursor: move; - display: inline-block; - height: 90px; - width: 90px; - padding: 2px; - opacity: .5; - overflow: hidden; - position: relative; - text-shadow: 0 0 2px #000; - -webkit-transition: opacity .25s ease-in-out, -webkit-transform .25s ease-in-out; - transition: opacity .25s ease-in-out, transform .25s ease-in-out, -webkit-transform .25s ease-in-out; - vertical-align: top; - background-size: cover; - -webkit-flex: none; - flex: none; -} -.qr-preview:hover, -.qr-preview:focus { - opacity: .9; -} -.qr-preview::before { - content: counter(thumbnails); - color: #fff; - position: absolute; - top: 3px; - right: 3px; - text-shadow: 0 0 3px #000, 0 0 8px #000; -} -.qr-preview#selected { - opacity: 1; -} -.qr-preview.drag { - box-shadow: 0 0 10px rgba(0,0,0,.5); - -webkit-transform: scale(.8); - transform: scale(.8); -} -.qr-preview.over { - border-color: #fff; - -webkit-transform: scale(1.1); - transform: scale(1.1); - opacity: 0.9; - z-index: 10; -} -.qr-preview > span { - color: #fff; -} -.remove { - background: none; - color: #e00; - padding: 1px; -} -a:only-of-type > .remove { - display: none; -} -.remove:hover::after { - content: " Remove"; -} -.qr-preview:not(.has-file) label, -#qr:not(.has-spoiler) .qr-preview-spoiler { - display: none; -} -.qr-preview > label { - background: rgba(0,0,0,.5); - color: #fff; - right: 0; - bottom: 0; - left: 0; - position: absolute; - text-align: center; -} -.qr-preview > label > input { - margin: 0; -} -#add-post { - cursor: pointer; - font-size: 2em; - position: absolute; - bottom: 20px; - right: 10px; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} -.textarea { - position: relative; - display: -webkit-flex; - display: flex; -} -#char-count { - color: #000; - background: hsla(0, 0%, 100%, .5); - font-size: 8pt; - position: absolute; - bottom: 1px; - right: 1px; - pointer-events: none; -} -#char-count.warning { - color: red; -} - -/* Menu */ -.menu-button:not(.fa-bars) { - display: inline-block; - position: relative; - cursor: pointer; -} -#header-bar .menu-button i { - border-top: 6px solid; - border-right: 4px solid transparent; - border-left: 4px solid transparent; - display: inline-block; - margin: 2px; - vertical-align: middle; -} -.postInfo > .menu-button, -#thread-watcher .menu-button { - width: 18px; - height: 15px; - text-align: center; -} -#menu { - position: fixed; - outline: none; - font-weight: normal; -} -#menu, .submenu { - border-radius: 3px; - padding-top: 1px; - padding-bottom: 3px; -} -.entry { - cursor: pointer; - display: block; - outline: none; - padding: 2px 10px; - position: relative; - text-decoration: none; - white-space: nowrap; - min-width: 70px; - text-align: left; - text-shadow: none; - font-size: 10pt; -} -.left>.entry.has-submenu { - padding-right: 17px !important; -} -.entry input[type="checkbox"], -.entry input[type="radio"] { - margin: 0px; - position: relative; - top: 2px; -} -.entry input[type="number"] { - width: 4.5em; -} -.entry.has-shortcut-text { - display: flex; - justify-content: space-between; - align-items: center; -} -.entry .shortcut-text { - opacity: 0.5; - font-size: 70%; - margin-left: 5px; -} -.has-submenu::after { - content: ""; - border-left: .5em solid; - border-top: .3em solid transparent; - border-bottom: .3em solid transparent; - display: inline-block; - margin: .3em; - position: absolute; - right: 3px; -} -.left .has-submenu::after { - border-left: 0; - border-right: .5em solid; -} -.submenu { - display: none; - position: absolute; - left: 100%; - top: -1px; - margin-left: 0px; - margin-top: -2px; -} -.focused > .submenu { - display: block; -} -.imp-exp-result { - position: absolute; - text-align: center; - margin: auto; - right: 0px; - left: 0px; - width: 200px; -} - -/* Custom Board Titles */ -.boardTitle, .boardSubtitle { - white-space: pre-line; -} -.boardTitle[contenteditable="true"], -.boardSubtitle[contenteditable="true"] { - cursor: text !important; -} - -/* Embedding */ -.embedder:not(.embedded) > span { - display: none; -} -#embedding { - padding: 1px 4px 1px 4px; - position: fixed; -} -#embedding.empty { - display: none; -} -#embedding > div:first-child { - display: -webkit-flex; - display: flex; -} -#embedding .move { - -webkit-flex: 1; - flex: 1; -} -#embedding .jump { - margin: -1px 4px; - text-decoration: none; -} - -/* Gallery */ -#a-gallery { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: row; - flex-direction: row; - background: rgba(0,0,0,0.7); -} -.gal-viewport { - display: -webkit-flex; - display: flex; - -webkit-align-items: stretch; - align-items: stretch; - -webkit-flex-direction: row; - flex-direction: row; - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - overflow: hidden; -} -.gal-thumbnails { - -webkit-flex: 0 0 150px; - flex: 0 0 150px; - overflow-y: auto; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: column; - flex-direction: column; - -webkit-align-items: stretch; - align-items: stretch; - text-align: center; - background: rgba(0,0,0,.5); - border-left: 1px solid #222; -} -.gal-hide-thumbnails .gal-thumbnails { - display: none; -} -.gal-thumb img, -.gal-thumb video { - max-width: 125px; - max-height: 125px; - height: auto; - width: auto; -} -.gal-thumb { - -webkit-flex: 0 0 auto; - flex: 0 0 auto; - padding: 3px; - line-height: 0; - transition: background .2s linear; -} -.gal-highlight { - background: rgba(0, 190, 255,.8); -} -.gal-prev { - border-right: 1px solid #222; -} -.gal-next { - border-left: 1px solid #222; -} -.gal-prev, -.gal-next { - -webkit-flex: 0 0 20px; - flex: 0 0 20px; - position: relative; - cursor: pointer; - opacity: 0.7; - background-color: rgba(0, 0, 0, 0.3); -} -.gal-prev:hover, -.gal-next:hover { - opacity: 1; -} -.gal-prev::after, -.gal-next::after { - position: absolute; - top: 48.6%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - display: inline-block; - border-top: 11px solid transparent; - border-bottom: 11px solid transparent; - content: ""; -} -.gal-prev::after { - border-right: 12px solid #fff; - right: 5px; -} -.gal-next::after { - border-left: 12px solid #fff; - right: 3px; -} -.gal-image { - -webkit-flex: 1 0 auto; - flex: 1 0 auto; - display: -webkit-flex; - display: flex; - -webkit-align-items: flex-start; - align-items: flex-start; - -webkit-justify-content: space-around; - justify-content: space-around; - overflow: hidden; - /* Flex > Non-Flex child max-width and overflow fix (Firefox only?) */ - width: 1%; -} -:root:not(.gal-fit-height):not(.gal-pdf) .gal-image { - overflow-y: scroll !important; -} -:root:not(.gal-fit-width):not(.gal-pdf) .gal-image { - overflow-x: scroll !important; -} -.gal-image a { - display: -webkit-flex; - display: flex; - -webkit-align-items: flex-start; - align-items: flex-start; - margin: auto; - line-height: 0; - max-width: 100%; -} -:root.gal-pdf .gal-image a { - width: 100%; - height: 100%; -} -.gal-image img, -.gal-image video { - -webkit-flex: none; - flex: none; -} -.gal-fit-width .gal-image img, -.gal-fit-width .gal-image video { - max-width: 100%; -} -.gal-fit-height .gal-image img, -.gal-fit-height .gal-image video { - /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ - max-height: 95vh; - max-height: calc(100vh - 25px); -} -.gal-image iframe { - width: 100%; - height: 100%; -} -.gal-buttons { - font-size: 2em; - margin-right: 3px; - padding-left: 7px; - padding-right: 7px; - top: 5px; -} -:root.gal-pdf .gal-buttons { - top: 40px; - background: rgba(0,0,0,0.6) !important; - border-radius: 3px; -} -.gal-buttons a { - color: #ffffff; - text-shadow: 0px 0px 1px #000000; -} -.gal-buttons i { - display: inline-block; - margin: 2px; - position: relative; -} -.gal-start i { - border-left: 10px solid; - border-top: 6px solid transparent; - border-bottom: 6px solid transparent; - bottom: 1px; -} -.gal-stop i { - border: 5px solid; - bottom: 2px; -} -.gal-buttons.gal-playing > .gal-start, -.gal-buttons:not(.gal-playing) > .gal-stop { - display: none; -} -.gal-buttons .menu-button i { - border-top: 10px solid; - border-right: 6px solid transparent; - border-left: 6px solid transparent; - bottom: 2px; - vertical-align: baseline; -} -.gal-labels { - position: fixed; - bottom: 6px; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: column; - flex-direction: column; - -webkit-align-items: flex-end; - align-items: flex-end; - -} -:root:not(.show-sauce) .gal-sauce { - display: none; -} -.gal-name, -.gal-count, -.gal-sauce { - background: rgba(0,0,0,0.6) !important; - border-radius: 3px; - padding: 1px 5px 2px 5px; - margin-top: 3px; - color: #ffffff !important; - text-decoration: none !important; -} -.gal-sauce a { - color: #ffffff !important; -} -.gal-name:hover, -.gal-buttons a:hover, -.gal-sauce a:hover { - color: rgb(95, 95, 101) !important; -} -:root.gal-pdf .gal-buttons a:hover { - color: rgb(204, 204, 204) !important; -} -.gal-buttons, -.gal-labels { - position: fixed; - right: 195px; -} -.gal-hide-thumbnails .gal-buttons, -.gal-hide-thumbnails .gal-labels { - right: 44px; -} -:root:not(.gal-fit-width):not(.gal-pdf) .gal-labels { - bottom: 23px !important; -} -:root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-buttons, -:root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-labels { - right: 178px !important; -} -:root.gal-hide-thumbnails.gal-fit-height:not(.gal-pdf) .gal-buttons, -:root.gal-hide-thumbnails.gal-fit-height:not(.gal-pdf) .gal-labels { - right: 28px !important; -} -:root.gallery-open.fixed #header-bar:not(.autohide), -:root.gallery-open.fixed #header-bar:not(.autohide) #shortcuts .fa::before { - visibility: hidden; -} - -/* Mod Contact Links */ -.contact-links { - margin-left: 2px; -} -.move-note > a { - text-decoration: underline; -} -.invisible { - font-size: 0; -} - -/* PostJumper */ -.postJumper > .prev, -.postJumper > .next { - font-size: 120%; -} - -/* PSA */ -.fcx-announcement { - text-align: center; -} -.fcx-announcement a { - text-decoration: underline; -} - -@keyframes spin { - 0% {transform:rotate(0deg);} - 100% {transform:rotate(359deg);} -} - -.spin { - animation:spin 2s infinite linear; -} + var style = `/* General */ +.dialog { + border: 1px solid; + display: block; + background-color: inherit; +} +.dialog:not(#qr):not(#thread-watcher):not(#header-bar) { + box-shadow: 0 1px 2px rgba(0, 0, 0, .15); +} +#qr, +#thread-watcher { + box-shadow: -1px 2px 2px rgba(0, 0, 0, 0.25); +} +.captcha-img, +.field { + background-color: #FFF; + border: 1px solid #CCC; + -moz-box-sizing: border-box; + box-sizing: border-box; + color: #333; + font: 13px sans-serif; + outline: none; + transition: color .25s, border-color .25s; +} +.field::-moz-placeholder { + color: #AAA; + font-size: 13px; + opacity: 1; +} +.captch-img:hover, +.field:hover { + border-color: #999; +} +.field:hover, .field:focus, .field.focus { + color: #000; +} +.field[disabled] { + background-color: #F2F2F2; + color: #888; +} +.field::-webkit-search-decoration { + display: none; +} +.move { + cursor: move; + overflow: hidden; +} +label { + cursor: pointer; +} +a[href="javascript:;"] { + text-decoration: none; +} +.warning { + color: red; +} +:root.sw-yotsuba #boardNavDesktop, :root.sw-yotsuba #boardNavMobile { + display: none !important; +} +:root.hide-bottom-board-list $site$boardListBottom { + display: none; +} +body.hasDropDownNav{ + margin-top: 5px; +} +:root:not(.keyboard-focus) a { + outline: none; +} +.painted { + border-radius: 3px; + padding: 0px 2px; +} +[hidden] { + display: none !important; +} + +/* 4chan style fixes */ +/* overrides 4chan CSS on div.opContainer, div.op */ +:root.sw-yotsuba .opContainer, :root.sw-yotsuba .op { + display: block; + overflow: visible; +} +:root.sw-yotsuba .reply > .file > .fileText { + margin: 0 20px; +} +:root.sw-yotsuba #arc-list span.quote { + color: #789922; +} +:root.sw-yotsuba .fileText a { + unicode-bidi: -moz-isolate; + unicode-bidi: -webkit-isolate; +} +:root.sw-yotsuba #g-recaptcha { + min-height: 78px; + height: auto; +} +:root.sw-yotsuba:not(.js-enabled) #postForm { + display: table; +} +:root.sw-yotsuba #captchaContainerAlt td:nth-child(2) { + display: table-cell !important; +} +:root.sw-yotsuba canvas#tegaki-canvas { + background: none; +} +/* Disable obnoxious captcha fade-in. */ +:root.sw-yotsuba > body > div:last-of-type { + transition: none !important; +} +/* Fix captcha scrolling to top of page. */ +:root.sw-yotsuba > body > div[style*=" top: -10000px;"] { + visibility: hidden !important; +} +/* Make long filenames wrap properly: https://github.com/ccd0/4chan-x/issues/1082 */ +:root.sw-yotsuba .post > .file { + /* currently nonstandard but may be added: https://lists.w3.org/Archives/Public/www-style/2016Mar/0352.html, https://bugzilla.mozilla.org/show_bug.cgi?id=1296042 */ + word-break: break-word; +} +:root.sw-yotsuba:not(.ua-webkit):not(.ua-blink) .fileText { + word-wrap: break-word; + max-width: calc(100vw - 90px); +} +:root.sw-yotsuba > body.is_catalog .thread > a > img { + display: inline-block; +} +/* Links to NSFW boards */ +:root.sw-yotsuba .nwsb { + display: inline; +} +:root.sw-yotsuba .fileText { + max-width: auto; + white-space: normal; +} + +/* Ads */ +:root.sw-yotsuba .ad-cnt > *, :root.sw-yotsuba .adg-rects > *, :root.sw-yotsuba .bsa-cnt { + height: auto !important; +} +:root.sw-yotsuba:not(.ads-loaded) hr.abovePostForm, +:root.sw-yotsuba:not(.ads-loaded) .adg-rects > hr, +:root.sw-yotsuba #adg-ol + hr, +:root.sw-yotsuba .danbo-slot:empty { + display: none; +} +:root.sw-yotsuba .adg-rects { + margin: 0; + font-size: 0; +} +:root.sw-yotsuba div.center[style] { + display: none !important; +} + +/* Tinyboard / vichan conflicts */ +#menu > .hide-thread-link { + width: auto; + height: auto; + overflow: visible; + background-image: none; +} +#menu label.entry { + display: block; +} +#fourchanx-settings label { + display: inline; +} +.intro a[href="javascript:;"], +#menu a { + margin: 0; +} +.gal-buttons.gal-buttons a { + font-size: inherit; +} +:root.sw-tinyboard.fixed.top-header:not(.autohide) .boardlist, +:root.sw-tinyboard.fixed.top-header:not(.autohide) .bar.top { + position: static; +} +:root.sw-tinyboard.fixed.top-header:not(.autohide) div.pages.top { + top: auto; + bottom: 0; +} +:root.sw-tinyboard.fixed.top-header.autohide .boardlist, +:root.sw-tinyboard.fixed.top-header.autohide .bar.top { + z-index: 3; +} + +/* Tinyboard site style conflicts */ +:root[data-host="fufufu.moe"].fixed.top-header:not(.autohide) div.pages.top { + top: 26px; + bottom: auto; +} +:root[data-host="merorin.com"].fixed.top-header:not(.autohide) span.settings { + top: 26px; +} +:root[data-host="fufufu.moe"]:not(.fixed) #header-bar { + margin-top: 38px; +} +:root[data-host="lainchan.org"]:not(.fixed) #header-bar { + margin-top: 17px; +} +:root[data-host="smuglo.li"]:not(.fixed) #header-bar { + margin-top: 8px; +} + +/* Anti-autoplay */ +audio.controls-added { + display: block; + margin: auto; + white-space: normal; +} +:root.anti-autoplay div.embed { + position: static; + width: auto; + height: auto; + text-align: center; +} +:root.anti-autoplay .autoplay-removed { + visibility: visible !important; + min-width: 640px; + min-height: 360px; +} + +/* fixed, z-index */ +#overlay, +#qp, #ihover, +#navlinks, .fixed #header-bar, +:root.float #updater, +:root.float #thread-stats, +#qr { + position: fixed; +} +#overlay { + z-index: 999; +} +#qp, #ihover { + z-index: 60; +} +#menu, .gal-buttons { + z-index: 50; +} +#updater, #thread-stats { + z-index: 40; +} +:root.fixed #header-bar, #notifications { + z-index: 35; +} +#a-gallery { + z-index: 30; +} +#navlinks { + z-index: 25; +} +#qr { + z-index: 20; +} +#embedding { + z-index: 11; +} +:root.fixed-watcher #thread-watcher { + z-index: 10; +} +:root.fixed:not(.gallery-open) #header-bar:not(:hover) { + z-index: 8; +} +#thread-watcher { + z-index: 5; +} + +/* Header */ +.fixed.top-header body { + padding-top: 2em; +} +.fixed.bottom-header body { + padding-bottom: 2em; +} +.fixed #header-bar { + right: 0; + left: 0; + padding: 3px 4px 4px; + font-size: 12px; +} +.fixed.top-header #header-bar { + top: 0; +} +.fixed.bottom-header #header-bar { + bottom: 0; +} +#header-bar { + border-width: 0; + transition: all .1s .05s ease-in-out; +} +:root.fixed #header-bar { + box-shadow: -5px 1px 10px rgba(0, 0, 0, 0.20); +} +:root.centered-links #shortcuts { + width: 300px; + text-align: right; +} +:root.centered-links #header-bar { + text-align: center; +} +#custom-board-list { + font-size: 13px; + vertical-align: middle; +} +#full-board-list { + vertical-align: middle; +} +:root.centered-links #custom-board-list { + position: relative; + left: 150px; +} +.fixed.top-header #header-bar { + border-bottom-width: 1px; +} +.fixed.bottom-header #header-bar { + box-shadow: 0 -1px 2px rgba(0, 0, 0, .15); + border-top-width: 1px; +} +.fixed.bottom-header #header-bar .menu-button i { + border-top: none; + border-bottom: 6px solid; +} +.fixed #header-bar.autohide:not(:hover) { + box-shadow: none; + transition: all .8s .6s cubic-bezier(.55, .055, .675, .19); +} +.fixed.top-header #header-bar.autohide:not(:hover) { + margin-bottom: -1em; + -webkit-transform: translateY(-100%); + transform: translateY(-100%); +} +.fixed.bottom-header #header-bar.autohide:not(:hover) { + -webkit-transform: translateY(100%); + transform: translateY(100%); +} +#scroll-marker { + left: 0; + right: 0; + height: 10px; + position: absolute; +} +#header-bar:not(.autohide) #scroll-marker { + pointer-events: none; +} +#header-bar #scroll-marker { + display: none; +} +.fixed #header-bar #scroll-marker { + display: block; +} +.fixed.top-header #header-bar #scroll-marker { + top: 100%; +} +.fixed.bottom-header #header-bar #scroll-marker { + bottom: 100%; +} +#board-list a, #shortcuts a:not(.entry) { + text-decoration: none; + padding: 1px; +} +#shortcuts:empty { + display: none; +} +.brackets-wrap::before { + content: "\\00a0["; +} +.brackets-wrap::after { + content: "]\\00a0"; +} +.dead-thread, +.disabled:not(.replies-quoting-you) { + opacity: .45; +} +#shortcuts { + float: right; +} +:root.autohiding-scrollbar #shortcuts { + margin-right: 12px; +} +.shortcut { + margin-left: 3px; + vertical-align: middle; +} +:root.shortcut-icons .native-settings { + font-size: 0; + color: transparent; + display: inline-block; + vertical-align: top; + height: 12px; + width: 14px; + background: url('//s.4cdn.org/image/favicon.ico') 0px -1px no-repeat; +} +#navbotright, +#navtopright { + display: none; +} +#toggleMsgBtn { + display: none !important; +} +.current, +:root.sw-yotsuba div#boardNavDesktopFoot a.current { + font-weight: bold; +} +@media (min-width: 1300px) { + :root.sw-yotsuba.fixed:not(.centered-links) #header-bar { + white-space: nowrap; + display: -webkit-flex; + display: flex; + -webkit-align-items: center; + align-items: center; + } + :root.sw-yotsuba.fixed:not(.centered-links) #board-list { + -webkit-flex: auto; + flex: auto; + } + :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list { + display: -webkit-flex; + display: flex; + } + :root.sw-yotsuba.fixed:not(.centered-links) .hide-board-list-container { + -webkit-flex: none; + flex: none; + margin-right: 5px; + } + :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList { + -webkit-flex: auto; + flex: auto; + display: -webkit-flex; + display: flex; + width: 0px; /* XXX Fixes Edge not shrinking the board list below default size when needed */ + } + :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > a, + :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > span:not(.space):not(.spacer) { + -webkit-flex: none; + flex: none; + padding: .17em; + margin: -.17em -.32em; + } + :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > span { + pointer-events: none; + } + :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > span.space { + -webkit-flex: 0 .63 .63em; + flex: 0 .63 .63em; + } + :root.sw-yotsuba.fixed:not(.centered-links) #full-board-list > .boardList > span.spacer { + -webkit-flex: 0 .38 .38em; + flex: 0 .38 .38em; + } + :root.sw-yotsuba.fixed:not(.centered-links) #shortcuts { + float: initial; + -webkit-flex: none; + flex: none; + display: -webkit-flex; + display: flex; + -webkit-align-items: center; + align-items: center; + } +} +/* 4chan X link brackets */ +.brackets-wrap::before { + content: "["; +} +.brackets-wrap::after { + content: "]"; +} +/* Notifications */ +#notifications { + position: fixed; + top: 0; + height: 0; + text-align: center; + right: 0; + left: 0; + visibility: visible; +} +#notifications:empty { + display: none; +} +:root.fixed.top-header:not(.gallery-open) #header-bar #notifications, +:root.fixed.top-header #header-bar.autohide #notifications { + position: absolute; + top: 100%; +} +.notification { + color: #FFF; + font-weight: 700; + text-shadow: 0 1px 2px rgba(0, 0, 0, .5); + box-shadow: 0 1px 2px rgba(0, 0, 0, .15); + border-radius: 2px; + margin: 1px auto; + width: 550px; + max-width: 100%; + position: relative; + transition: all .25s ease-in-out; +} +.notification.error { + background-color: hsla(0, 100%, 38%, .9); +} +.notification.warning { + background-color: hsla(36, 100%, 38%, .9); +} +.notification.info { + background-color: hsla(200, 100%, 38%, .9); +} +.notification.success { + background-color: hsla(104, 100%, 38%, .9); +} +.notification a { + color: white; +} +.notification > .close { + padding: 7px; + top: 0px; + right: 5px; + position: absolute; +} +.notification > .fa-times::before { + font-size: 11px !important; +} +.message { + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 6px 20px; + max-height: 200px; + width: 100%; + overflow: auto; + white-space: pre-line; +} +.message a { + text-decoration: underline; +} +:root.tainted .report-error { + display: none; +} + +/* Settings */ +:root.fourchan-x body { + -moz-box-sizing: border-box; + box-sizing: border-box; +} +#overlay { + background-color: rgba(0, 0, 0, .5); + display: -webkit-flex; + display: flex; + top: 0; + left: 0; + height: 100%; + width: 100%; +} +#fourchanx-settings { + -moz-box-sizing: border-box; + box-sizing: border-box; + box-shadow: 0 0 15px rgba(0, 0, 0, .15); + height: 600px; + max-height: 100%; + width: 900px; + max-width: 100%; + margin: auto; + padding: 5px; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; +} +#fourchanx-settings > nav { + padding: 2px 2px 8px; + display: -webkit-flex; + display: flex; +} +#fourchanx-settings > nav a { + text-decoration: underline; +} +#fourchanx-settings > nav a.close { + text-decoration: none; + padding: 0 2px; + margin: 0; +} +.section-container { + -webkit-flex: 1; + flex: 1; + position: relative; + overflow: auto; + padding-right: 5px; + overscroll-behavior: contain; +} +.sections-list { + -webkit-flex: 1; + flex: 1; +} +.export, .import, .reset { + cursor: pointer; + text-decoration: none !important; +} +.tab-selected { + font-weight: 700; +} +.section-sauce ul, +.section-advanced ul { + list-style: none; + margin: 0; +} +.section-sauce ul { + padding: 8px; +} +.section-advanced ul { + padding: 0px; +} +.section-sauce li, +.section-advanced li { + padding-left: 4px; +} +.section-main ul { + margin: 0; + padding: 0 0 0 16px; +} +.section-main li { + white-space: pre-line; + list-style: disc; +} +.section-main li:not(:first-of-type) { + margin-top: 4px; +} +.section-main label { + text-decoration: underline; +} +div[data-checked="false"] > .suboption-list { + display: none; +} +.suboption-list { + position: relative; +} +.suboption-list::before { + content: ""; + display: inline-block; + position: absolute; + left: .7em; + width: 0; + height: 100%; + border-left: 1px solid; +} +.suboption-list > div { + position: relative; + padding-left: 1.4em; +} +.suboption-list > div::before { + content: ""; + display: inline-block; + position: absolute; + left: .7em; + width: .7em; + height: .6em; + border-left: 1px solid; + border-bottom: 1px solid; +} +#fourchanx-settings .section-main p { + margin: .5em 0 0; +} +.section-filter ul { + padding: 0; +} +.section-filter li { + margin: 10px 40px; + list-style: disc; +} +.section-filter textarea { + height: 500px; +} +.section-main a, .section-filter a, .section-advanced a { + text-decoration: underline; +} +#sauce-doc-expand:not(:checked) ~ #sauce-doc { + max-height: 130px; + overflow: auto; +} +#sauce-doc > label { + float: right; + margin: 0 5px; +} +/* XXX for OneeChan */ +#sauce-doc-expand + .riceCheck { + display: none; +} +.section-sauce textarea { + height: 430px; +} +.section-advanced .field[name="boardnav"] { + width: 100%; +} +.section-advanced textarea { + height: 150px; +} +.section-advanced textarea[name="archiveLists"], +.section-advanced textarea[name="externalCatalogURLs"], +.section-advanced textarea[name="knownBanners"] { + height: 75px; +} +.section-advanced .archive-cell { + min-width: 160px; + text-align: center; +} +.section-advanced #archive-board-select { + position: absolute; +} +.section-advanced .note { + font-size: 0.8em; + font-style: italic; + margin-left: 10px; +} +.section-advanced .note code { + font-style: normal; + font-size: 11px; +} +.favicon-preview > img { + vertical-align: middle; +} +.favicon-preview > img:nth-of-type(3n+1) { + margin-left: 4px; +} +.section-keybinds .field { + font-family: monospace; +} +#fourchanx-settings fieldset { + border: 1px solid; + border-radius: 3px; + padding: 0.35em 0.625em 0.75em; + margin: 0px 2px; +} +#fourchanx-settings legend { + font-weight: 700; + color: inherit; +} +#fourchanx-settings textarea { + font-family: monospace; + width: 100%; + resize: vertical; +} +#fourchanx-settings code { + color: #000; + background-color: #FFF; + padding: 0 2px; +} +#fourchanx-settings th { + text-align: center; + font-weight: bold; +} +#fourchanx-settings p { + margin: 1em 0px; +} +#fourchanx-settings table { + margin: auto; +} + +/* Index */ +:root.index-loading .navLinks:not(.json-index), +:root.index-loading .board:not(.json-index), +:root.index-loading .pagelist:not(.json-index), +:root.infinite-mode .pagelist, +:root.all-pages-mode .pagelist, +:root.catalog-mode .pagelist, +:root:not(.catalog-mode) .indexlink, +:root.catalog-mode .cataloglink, +:root:not(.catalog-mode) #hidden-label, +:root:not(.catalog-mode) #index-size { + display: none; +} +#index-search { + padding-right: 1.5em; + width: 100px; + transition: color .25s, border-color .25s, width .25s; +} +#index-search:focus, +#index-search[data-searching] { + width: 200px; +} +#index-search-clear { + color: gray; + display: inline-block; + position: relative; + left: -1em; + width: 0; +} +/* \`\`::-webkit-*'' selectors break selector lists on Firefox. */ +#index-search::-webkit-search-cancel-button { + display: none; +} +#index-search:not([data-searching]) + #index-search-clear { + display: none; +} +#index-options { + float: right; +} +#lastlong-options { + display: inline-block; + vertical-align: middle; + height: 28px; + margin: -14px 0; +} +#lastlong-options > input { + padding: 0; + border: 0 !important; + text-align: center; + background: transparent; + display: block; + font-size: 12px; + height: 12px; + width: 30px; + margin: 1px 0; +} +.summary { + text-decoration: none; +} + +/* Catalog */ +:root.catalog-mode .board { + text-align: center; +} +.catalog-thread { + display: inline-block; + -moz-box-sizing: border-box; + box-sizing: border-box; + border: 1px solid transparent; + word-wrap: break-word; + vertical-align: top; + position: relative; +} +/* overrides 4chan CSS on div.thread */ +.catalog-thread.catalog-thread { + margin: 2px; +} +.catalog-small > .catalog-thread { + width: 165px; + height: 320px; +} +.catalog-large > .catalog-thread { + width: 270px; + height: 410px; +} +:root.catalog-hover-expand .catalog-thread:hover { + z-index: 1; +} +.catalog-container { + position: absolute; + top: -4px; + left: 0; + right: 0; + bottom: 0; +} +.catalog-container:not(:hover), +:root:not(.catalog-hover-expand) .catalog-container { + overflow: hidden; +} +.catalog-post { + position: absolute; + top: 4px; + left: 0; + right: 0; + border: 1px solid transparent; + padding-top: 20px; +} +/* overrides inline CSS from Index.cb.hoverAdjust */ +:root:not(.catalog-hover-expand) .catalog-post { + left: 0 !important; + right: 0 !important; +} +/* overrides 4chan CSS on div.post */ +.catalog-post.catalog-post { + margin: -21px -1px -1px; + overflow: visible; +} +.catalog-thread.noFile > * > .catalog-post { + margin-top: -7px; + padding-top: 6px; +} +:root.catalog-hover-expand .catalog-container:hover > .catalog-post { + margin-left: -61px; + margin-right: -61px; +} +:root.catalog-hover-expand .catalog-container:hover > * > :not(.catalog-replies) { + padding-left: 2px; + padding-right: 2px; +} +.catalog-link { + display: block; + position: relative; +} +.catalog-thumb { + border-radius: 2px; + box-shadow: 0 0 5px rgba(0, 0, 0, .25); + vertical-align: top; +} +.catalog-thumb.spoiler-file { + width: 100px; + height: 100px; +} +.catalog-thumb.deleted-file { + width: 127px; + height: 13px; + padding: 20px 11px; +} +.catalog-thumb.no-file { + width: 77px; + height: 13px; + padding: 20px 36px; +} +.catalog-icons > img, +.catalog-stats > .menu-button { + width: 1em; + height: 1em; + margin: 0; + vertical-align: text-top; + padding-left: 2px; +} +.catalog-stats > .menu-button { + font-weight: normal; +} +.catalog-stats > .menu-button > i::before { + line-height: 11px; +} +.catalog-stats { + font-size: 10px; + font-weight: 700; + padding-top: 2px; +} +.catalog-stats > [title] { + cursor: help; +} +.catalog-post > .postMessage { + margin: 0; + padding-bottom: .3em; +} +.catalog-container:not(:hover) > * > .file, +.catalog-container:not(:hover) > * > .postInfo > :not(.subject), +.catalog-container:not(:hover) > * > .catalog-replies, +.catalog-container:not(:hover) .extra-linebreak, +.catalog-container:not(:hover) .abbr, +:root:not(.catalog-hover-expand) .catalog-container > * > .file, +:root:not(.catalog-hover-expand) .catalog-container > * > .postInfo > :not(.subject), +:root:not(.catalog-hover-expand) .catalog-container > * > .catalog-replies, +:root:not(.catalog-hover-expand) .catalog-container .extra-linebreak, +:root:not(.catalog-hover-expand) .catalog-container .abbr, +.catalog-thread > .catalog-container > :not(.catalog-post), +.catalog-post > .file > :not(.fileText), +.catalog-post > * > .fileText > :not(:first-child), +.catalog-post > .postInfo > :not(.subject):not(.nameBlock):not(.dateTime), +.catalog-post > .postInfo > .nameBlock > .contact-links, +.catalog-post > * > * > .posteruid, +.catalog-post > * > * > .postJumper, +:root.bottom-backlinks .catalog-post > .container, +.post:not(.catalog-post) > .catalog-link, +.post:not(.catalog-post) > .catalog-stats, +.post:not(.catalog-post) > .catalog-replies { + display: none; +} +.catalog-post > .file { + position: absolute; + left: 0; + right: 0; + top: 0; + min-height: 20px; + background-color: inherit; +} +.catalog-post > * > .fileText { + position: relative; + padding: 2px; + background-color: inherit; +} +.catalog-small .catalog-post > * .fileText { + font-size: 10px; +} +.catalog-post > * > .fileText:not(:hover) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.catalog-post > * > .fileText:hover { + z-index: 1; +} +/* overrides 4chan CSS on div.post div.postInfo */ +.catalog-post > .postInfo.postInfo { + width: auto; +} +.catalog-post > * > .subject { + display: block; +} +.catalog-post > * > .dateTime { + display: inline-block; + font-style: italic; +} +:root.catalog-hover-expand .catalog-container:hover > * > * > .nameBlock, +:root.catalog-hover-expand .catalog-container:hover > * > * > .dateTime, +:root.catalog-hover-expand .catalog-container:hover > * > .postMessage:not(:empty) { + padding-top: .3em; +} +.catalog-post .extra-linebreak { + content: ''; /* makes this work in Blink/WebKit */ + display: block; + margin-top: .3em; +} +.catalog-reply { + text-align: left; + white-space: nowrap; + border-top: 1px solid transparent; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-align-items: stretch; + align-items: stretch; +} +.catalog-reply > * { + padding: 3px; + overflow: hidden; + -webkit-flex: none; + flex: none; +} +.catalog-reply > span { + font-style: italic; + font-weight: bold; +} +.catalog-reply-excerpt { + -webkit-flex: 1 1 auto; + flex: 1 1 auto; +} +.catalog-post .prettyprinted { + max-width: 100%; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.catalog-post .MathJax_Display { + text-align: center !important; +} +.catalog-container:not(:hover) .exif, +:root:not(.catalog-hover-expand) .catalog-container .exif { + display: none !important; +} +.catalog-post > * > .exif { + border-collapse: collapse; +} +:root.catalog-hover-expand .catalog-container:hover .exif[style*="display: block;"] { + display: inline-block !important; +} +.catalog-post > * > .exif, +.catalog-post > * > .exif > tbody { + background-color: inherit; +} +.catalog-post > * > .exif, +.catalog-post > * > .exif td { + min-width: 0; +} +.catalog-post > * > .exif td { + padding-top: 1px; +} +:root.hats-enabled .catalog-thread::after { + content: ''; + pointer-events: none; + position: absolute; + background-size: contain; +} +:root.hats-enabled .catalog-small > .catalog-thread::after { + left: -8px; + top: -59px; + width: 96px; + height: 96px; +} +:root.hats-enabled:not(.werkTyme) .catalog-small > .catalog-thread:not(.noFile)::after { + left: calc(67px - .3px * var(--tn-w)); +} +:root.hats-enabled .catalog-large > .catalog-thread::after { + left: -15px; + top: -98px; + width: 160px; + height: 160px; +} +:root.hats-enabled:not(.werkTyme) .catalog-large > .catalog-thread:not(.noFile)::after { + left: calc(110px - .5px * var(--tn-w)); +} + +/* Copy Text Link's textarea element */ +textarea.copy-text-element { + height: 0; + width: 0; + position: absolute; + top: -10000px; +} + +/* Announcement Hiding */ +:root.hide-announcement $site$psa { + display: none; +} +.hide-announcement-button { + opacity: 0.4; + float: left; +} + +/* Unread */ +.unread-line { + margin: 0; + border-color: rgb(255,0,0); +} +.unread-line + br { + display: none; +} +.unread-mark-read { + float: right; + clear: both; + width: 100%; + text-align: right; +} +:not(.unread-thread) > .unread-mark-read { + display: none; +} + +/* Thread Updater */ +#updater { + background: none; + border: none; + box-shadow: none; +} +#updater > .move { + position: absolute; + top: -5px; + bottom: -5px; + left: -5px; + right: -5px; + z-index: -1; +} +#updater > div:last-child { + text-align: center; +} +#updater input[type="number"] { + width: 4em; +} +:root.float #updater { + padding: 0px 3px; +} +:root:not(.float).shortcut-icons #updater { + display: inline-block; + min-width: 12pt; + text-align: right; +} +.new { + color: limegreen; +} +#update-status:not(.empty) + #update-timer:not(.empty):not(.loading) { + margin-left: 5px; +} +#update-timer { + cursor: pointer; +} + +/* Thread Watcher */ +#thread-watcher { + position: absolute; +} +#thread-watcher { + padding-bottom: 3px; + padding-left: 3px; + white-space: nowrap; + min-width: 146px; +} +#watched-threads { + overflow-x: hidden; + overflow-y: auto; +} +#thread-watcher .refresh { + padding: 0px 3px; +} +:root.fixed-watcher #thread-watcher { + position: fixed; +} +:root.fixed-watcher #watched-threads { + /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ + max-height: 85vh; + max-height: calc(100vh - 75px); +} +:root:not(.fixed-watcher) #watched-threads:not(:hover) { + max-height: 210px; + overflow-y: hidden; +} +#thread-watcher > .move { + padding-top: 3px; +} +#watched-threads > div { + padding-left: 3px; + padding-right: 3px; +} +#watched-threads .watcher-link { + max-width: 250px; + display: -webkit-inline-flex; + display: inline-flex; + -webkit-flex-direction: row; + flex-direction: row; +} +#watched-threads .watcher-page, +#watched-threads .watcher-unread { + -webkit-flex: 0 0 auto; + flex: 0 0 auto; + margin-right: 2px; +} +#watched-threads .watcher-title { + overflow: hidden; + text-overflow: ellipsis; + -webkit-flex: 0 1 auto; + flex: 0 1 auto; +} +#watched-threads .watcher-title:not(:first-child) { + margin-left: 2px; +} +.replies-quoting-you > a, #watcher-link.replies-quoting-you, .last-page > a > .watcher-page { + color: #F00; +} +#thread-watcher a { + text-decoration: none; +} +#thread-watcher .move > .close { + position: absolute; + right: 0px; + top: 0px; + padding: 0px 4px; +} +.watch-thread-link { + padding-top: 18px; + width: 18px; + height: 0px; + display: inline-block; + background-repeat: no-repeat; + opacity: 0.2; + position: relative; + top: 1px; + background-image: url("data:image/svg+xml,"); +} +.watch-thread-link.watched { + opacity: 1; +} + + +/* Thread Stats */ +#thread-stats { + background: none; + border: none; + box-shadow: none; +} +:root.float #thread-stats > .move > :not(#page-count) { + pointer-events: none; +} +:root.float #thread-stats { + padding: 0px 3px; +} +#page-count { + cursor: pointer; +} + +/* Quote */ +.hashlink::before { + content: ' '; + visibility: hidden; +} +.inline + .hashlink { + display: none !important; +} +:root.resurrect-quotes .deadlink { + text-decoration: none !important; +} +.catalog-post .qmark-ct { + display: none; +} +.backlink.deadlink:not(.forwardlink), +.quotelink.deadlink:not(.forwardlink) { + text-decoration: underline !important; +} +:root:not(.catalog-mode) .inlined { + opacity: .5; +} +#qp input, .forwarded { + display: none; +} +.quotelink.forwardlink, +.backlink.forwardlink { + text-decoration: none; + border-bottom: 1px dashed; +} +.filtered { + text-decoration: underline line-through; +} +:root.hide-backlinks .backlink.filtered, +:root.hide-backlinks .backlink.filtered + .hashlink.filtered { + display: none; +} +.postNum + .container::before { + content: " "; +} +:root.bottom-backlinks .container { + display: block; + clear: both; + margin: 0 4px; +} +:root.bottom-backlinks .backlink { + font-size: 90%; +} +.inline { + border: 1px solid; + display: table; + margin: 2px 0; +} +.container ~ .inline { + margin-left: 20px; +} +:root.catalog-mode .inline { + display: none; +} +.inline .post { + border: 0 !important; + background-color: transparent !important; + display: table !important; + margin: 0 !important; + padding: 1px 2px !important; +} +#qp > .opContainer::after { + content: ''; + clear: both; + display: table; +} +#qp .post { + border: none; + margin: 0; + padding: 2px 2px 5px; +} +#qp img { + max-height: 80vh; + max-width: 50vw; +} + +/* Quote Threading */ +.threadContainer { + margin-left: 20px; + border-left: 1px solid rgba(128,128,128,.3); +} +.threadOP { + clear: both; +} + +/* File */ +.expanded-image > .post > .file > .fileThumb { + display: flex; + flex-direction: column; +} +.fileText-original, +.fnswitch:hover > .fntrunc, +.fnswitch:not(:hover) > .fnfull, +.expanded-image > .post > .file > .fileThumb > video[data-md5], +.expanded-image > .post > .file > .fileThumb > img[data-md5] { + display: none; +} +.full-image[data-file-i-d] { + display: none; + cursor: pointer; +} +.expanded-image > .post > .file > .fileThumb > .full-image { + display: inline; +} +.expanded-image > .post > .file > .fileThumb > audio { + height: 30px; + width: 100%; + min-width: 300px; +} +.expanded-image { + clear: left; +} +.expanding { + opacity: .5; +} +:root.fit-height .full-image { + max-height: 100vh; +} +:root.fit-height.fixed .full-image { + /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ + max-height: 93vh; + max-height: calc(100vh - 35px); +} +:root.fit-width .full-image { + max-width: 100%; +} +:root.ua-gecko.fit-width .full-image { + width: 100%; +} +.fileThumb > .warning { + clear: both; +} +#ihover { + pointer-events: none; + /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ + max-height: 95vh; + max-height: calc(100vh - 25px); + max-width: 100vw; +} +/* WEBM Metadata */ +.webm-title > a::before { + content: "title"; + text-decoration: underline; +} +.webm-title.loading > a::after { + content: "..."; +} +.webm-title.error > a:hover::before, +.webm-title.error > a:focus::before { + content: "error"; + text-decoration: none; +} +.webm-title > span { + cursor: text; +} +.webm-title.not-found > span::before { + content: "not found"; +} +.webm-title:not(:hover):not(:focus) > span, +.webm-title:hover > span + a, +.webm-title:focus > span + a { + display: none; +} +/* Volume control */ +input[name="Default Volume"] { + width: 4em; + height: 1ex; + vertical-align: middle; + margin: 0px; +} +/* Fappe and Werk Tyme */ +:root.fappeTyme $site$replyOriginal.noFile, +:root.fappeTyme $site$replyOriginal.noFile + br { + display: none; +} +:root.werkTyme $site$thumbLink, +:root.werkTyme $site$file$thumb, +:root.werkTyme .catalog-thumb:not(.deleted-file):not(.no-file), +:root:not(.werkTyme) .werkTyme-filename { + display: none; +} +.werkTyme-filename { + font-weight: bold; + font-size: 110%; +} +:root.werkTyme .catalog-link { + box-shadow: 0 0 5px rgba(0, 0, 0, .25); + padding: 8px; + text-align: center; +} +:root.werkTyme .catalog-thumb { + box-shadow: none; + padding: 0; + vertical-align: middle; +} +.indicator { + background: rgba(255,0,0,0.8); + font-weight: bold; + display: inline-block; + min-width: 9px; + padding: 0px 2px; + margin: 0 1px; + text-align: center; + color: white; + border-radius: 2px; + cursor: pointer; +} +:root:not(.fappeTyme) #shortcut-fappe, +:root:not(.werkTyme) #shortcut-werk { + display: none; +} + +/* Index/Reply Navigation */ +#navlinks { + font-size: 16px; + top: 25px; + right: 10px; +} +:root.catalog-mode #navlinks { + display: none; +} + +/* Highlighting */ +.qphl { + outline: 2px solid rgba(216, 94, 49, .8); +} +:root.highlight-you .quotesYou$site$highlightable$op, +:root.highlight-you .quotesYou$site$highlightable$reply { + border-left: 3px solid rgba(221, 0, 0, .8); +} +:root.highlight-own .yourPost$site$highlightable$op, +:root.highlight-own .yourPost$site$highlightable$reply { + border-left: 3px dashed rgba(221, 0, 0, .8); +} +.filter-highlight$site$highlightable$op, +.filter-highlight$site$highlightable$reply { + box-shadow: inset 5px 0 rgba(221, 0, 0, .5); +} +:root.highlight-own .yourPost > $site$sideArrows, +:root.highlight-you .quotesYou > $site$sideArrows, +.filter-highlight > $site$sideArrows { + color: rgba(221, 0, 0, .8); +} +:root.highlight-own .yourPost$site$highlightable$op::after, +:root.highlight-you .quotesYou$site$highlightable$op::after, +.filter-highlight$site$highlightable$op::after { + content: ""; + display: block; + clear: both; +} +:root:not(.werkTyme) .catalog-thread.filter-highlight .catalog-thumb, +:root.werkTyme .catalog-thread.filter-highlight:not(:hover), +:root.werkTyme:not(.catalog-hover-expand) .catalog-thread.filter-highlight, +:root.werkTyme.catalog-hover-expand .catalog-thread.filter-highlight > .catalog-container:hover > .catalog-post, +:root.catalog $site$catalog$thread.filter-highlight$site$highlightable$catalog { + box-shadow: 0 0 3px 3px rgba(255, 0, 0, .5); +} +:root:not(.werkTyme) .catalog-thread.watched .catalog-thumb, +:root:root.werkTyme .catalog-thread.watched:not(:hover), +:root:root.werkTyme:not(.catalog-hover-expand) .catalog-thread.watched, +:root.werkTyme.catalog-hover-expand .catalog-thread.watched > .catalog-container:hover > .catalog-post { + border: 2px solid rgba(255, 0, 0, .75); +} + +/* Spoiler text */ +:root.reveal-spoilers $site$spoiler, +:root.reveal-spoilers $site$spoiler > a { + color: white !important; +} +:root.reveal-spoilers .removed-spoiler::before { + content: "[spoiler]"; +} +:root.reveal-spoilers .removed-spoiler::after { + content: "[/spoiler]"; +} + +/* Thread & Reply Hiding */ +.hide-thread-button, +.hide-reply-button { + float: left; + margin-right: 4px; + padding: 2px; +} +$site$infoRoot a.hide-reply-button { + margin-right: 6px; + padding: 0; +} +.replacedSideArrows { + float: left; +} +.hide-thread-button:not(:hover), +.hide-reply-button:not(:hover) { + opacity: 0.4; +} +.threadContainer .hide-reply-button { + margin-left: 2px !important; + position: relative; + left: 1px; +} +.hide-thread-button { + margin-top: -1px; + width: 11px; +} +.stub ~ :not(.threadDivider) { + display: none !important; +} +.stub input { + display: inline-block; +} +$site$thread[hidden] + hr { + display: none; +} +:root.reply-hide $site$sideArrows { + display: none; +} +:root.sw-yotsuba.thread-hide .party-hat { + left: 19px; +} + +/* Anonymize */ +:root.anonymize $site$info$name, +:root.sw-yotsuba.anonymize .post-author:not([class*=capcode]) { + font-size: 0; +} +:root.anonymize $site$info$tripcode, +:root.sw-yotsuba.anonymize .n-pu { + display: none; +} +:root.anonymize $site$info$name::before, +:root.sw-yotsuba.anonymize .post-author:not([class*=capcode])::before { + content: "Anonymous"; + font-size: 10pt; +} +:root.sw-yotsuba.anonymize .flashListing .name::before, +:root.sw-yotsuba.anonymize .post-last > .post-author:not([class*=capcode])::before { + font-size: 9pt; +} + +/* QR */ +:root.hide-original-post-form #togglePostFormLink, +#qr.autohide:not(.focus):not(:hover):not(:active) > form, +:root.thread-view #qr:not(.show-new-thread-option) select[data-name="thread"], +#file-n-submit:not(.has-file) #qr-filerm { + display: none; +} +:root.hide-original-post-form #postForm { + display: none !important; +} +#qr select, +#qr-filename-container > a, +.remove, +.captcha-img { + cursor: pointer; +} +#qr { + position: fixed; + padding: 1px; + border: 1px solid transparent; + min-width: 300px; + border-radius: 3px 3px 0 0; +} +#qr > form { + /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ + max-height: 85vh; + max-height: calc(100vh - 75px); + overflow-y: auto; + overflow-x: hidden; +} +#qrtab { + border-radius: 3px 3px 0 0; +} +#qrtab { + margin-bottom: 1px; +} +#qr .close { + float: right; + padding: 0 3px; +} +.qr-link-container { + text-align: center; + margin: 16px 0; +} +.qr-link-container-bottom { + width: 200px; + position: absolute; + left: -100px; + margin-left: 50%; + text-align: center; +} +.qr-link { + border-radius: 3px; + padding: 6px 10px 5px; + font-weight: bold; + vertical-align: middle; + border-style: solid; + border-width: 1px; + font-size: 10pt; +} +.qr-link-container + #togglePostFormLink { + font-size: 10pt; + font-weight: normal; + margin: -8px 0 3.5px; +} +.persona { + width: 100%; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; +} +.persona .field { + -webkit-flex: 1; + flex: 1; + width: 0; +} +#qr.forced-anon input[data-name="name"]:not(.force-show), +#qr.forced-anon input[data-name="sub"]:not(.force-show), +#qr.reply-to-thread input[data-name="sub"]:not(.force-show), +body:not(.board_f) #qr select[name="filetag"], +#qr.reply-to-thread select[name="filetag"], +#qr:not(.has-sjis) #sjis-toggle, +#qr:not(.has-math) #tex-preview-button, +#qr.tex-preview .textarea > :not(#tex-preview), +#qr:not(.tex-preview) #tex-preview { + display: none; +} +.persona button { + -webkit-flex: 0 0 23px; + flex: 0 0 23px; + -webkit-align-self: stretch; + align-self: stretch; + border: 1px solid #BBB; + padding: 0; + background: linear-gradient(to bottom, #F8F8F8, #DCDCDC) no-repeat; + color: #000; +} +#qr.sjis-preview #sjis-toggle, #qr.tex-preview #tex-preview-button { + background: #DCDCDC; +} +#sjis-toggle, #qr.sjis-preview textarea.field { + font-family: "IPAMonaPGothic","Mona","MS PGothic",monospace; + font-size: 16px; + line-height: 17px; +} +#tex-preview-button { + font-size: 10px; +} +#tex-preview { + white-space: pre-line; +} +#qr textarea.field { + height: 14.8em; + min-height: 9em; +} +#qr.has-captcha textarea.field { + height: 9em; +} +input.field.tripped:not(:hover):not(:focus) { + color: transparent !important; + text-shadow: none !important; +} +#qr textarea { + min-width: 300px; + resize: both; +} +.field { + -moz-box-sizing: border-box; + box-sizing: border-box; + margin: 0px; + padding: 2px 4px 3px; +} +#qr label input[type="checkbox"] { + position: relative; + top: 2px; +} + +/* Recaptcha v2 */ +#qr .captcha-root { + position: relative; +} +#qr .captcha-container > div { + margin: auto; + width: 304px; +} +/* XXX scrollable with scroll bar hidden; prevents scroll on space press */ +:root.ua-blink #qr .captcha-container > div, +:root.ua-edge #qr .captcha-container > div { + overflow: hidden; +} +:root.ua-blink #qr .captcha-container > div > div:first-of-type, +:root.ua-edge #qr .captcha-container > div > div:first-of-type { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 30px; + height: 99%; + width: 100%; +} +#qr .captcha-counter { + display: block; + width: 100%; + text-align: center; + pointer-events: none; +} +#qr.captcha-open .captcha-counter { + position: absolute; + bottom: 3px; +} +#qr .captcha-counter > a { + pointer-events: auto; + display: inline-block; /* XXX https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8851747/ */ +} +#qr:not(.captcha-open) .captcha-counter > a { + display: block; + width: 100%; +} +#qr.captcha-v2 #qr-captcha-iframe { + width: 302px; + height: 423px; + border: 0; + display: block; + margin: auto; +} +.goog-bubble-content { + max-width: 100vw; + max-height: 100vh; + overflow: auto; +} +.goog-bubble-content iframe { + position: static !important; +} + +/* File Input, Submit Button, Oekaki */ +#file-n-submit, #qr .oekaki { + display: -webkit-flex; + display: flex; + -webkit-align-items: stretch; + align-items: stretch; + height: 25px; + margin-top: 1px; +} +#file-n-submit > input, #qr-draw-button { + background: linear-gradient(to bottom, #F8F8F8, #DCDCDC) no-repeat; + border: 1px solid #BBB; + border-radius: 2px; + height: 100%; +} +#qr-file-button, #qr-draw-button { + width: 15%; +} +#file-n-submit input[type="submit"] { + width: 25%; +} +#qr-filename-container { + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + width: 0; + display: -webkit-flex; + display: flex; + -webkit-align-items: center; + align-items: center; + position: relative; + padding: 1px; +} +input#qr-filename { + border: none !important; + background: none !important; + outline: none; +} +#qr-filename, +.has-file #qr-no-file { + display: none; +} +#qr-no-file, +.has-file #qr-filename { + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + width: 0px; /* XXX Fixes filename not shrinking to allow space for buttons in Edge */ + display: inline-block; + padding: 0; + padding-left: 3px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +#qr-no-file { + color: #AAA; +} +#qr .oekaki.has-file { + display: none; +} +#qr .oekaki > label { + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + width: 0; + display: -webkit-flex; + display: flex; + -webkit-align-items: center; + align-items: center; + height: 100%; +} +#qr .oekaki > label > span { + margin: 0 3px; +} +#qr .oekaki > label > input { + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + width: 0; + height: 100%; +} +#qr .oekaki-bg { + position: relative; + display: inline-block; + height: 100%; + width: 10%; + margin-left: 3px; +} +#qr .oekaki-bg > * { + position: absolute; + top: 0; + left: 0; + margin: 0; +} +#qr .oekaki-bg > :not([name="oekaki-bgcolor"]) { + z-index: 1; +} +#qr [name="oekaki-bgcolor"] { + height: 100%; + width: 100%; + border: none; + padding: 0; +} +#qr [name="oekaki-bg"]:not(:checked) ~ [name="oekaki-bgcolor"] { + visibility: hidden; +} +#qr input[type="file"] { + visibility: hidden; + position: absolute; +} + +/* Spoiler Checkbox, QR Icons */ +#qr-filename-container > label, #qr-filename-container > a { + -webkit-flex: none; + flex: none; + margin: 0; + margin-right: 3px; +} +#qr:not(.has-spoiler) #qr-spoiler-label, +#file-n-submit:not(.has-file) #qr-spoiler-label, +.has-file #paste-area, +.has-file #url-button, +#file-n-submit:not(.custom-cooldown) #custom-cooldown-button { + display: none; +} +#qr-filename-container > label { + position: relative; +} +#qr-filename-container input[type="checkbox"] { + margin: 0; +} +.checkbox-letter { + font-size: 13px; + font-weight: bold; +} +#qr-filename-container label:not(:hover) > input[type="checkbox"]:not(:focus):not(:checked), +#qr-filename-container label:not(:hover) > input[type="checkbox"]:not(:focus):not(:checked) ~ :not(.checkbox-letter), +#qr-filename-container label:hover > .checkbox-letter, +input[type="checkbox"]:focus ~ .checkbox-letter, +input[type="checkbox"]:checked ~ .checkbox-letter { + /* not displayed but still focusable */ + position: absolute; + opacity: 0; + pointer-events: none; +} +.checkbox-letter, #paste-area, #url-button, #custom-cooldown-button, #dump-button { + opacity: 0.6; +} +#paste-area { + font-size: 0; +} +#paste-area:focus { + opacity: 1; +} +#custom-cooldown-button.disabled { + opacity: 0.27; +} + +/* Thread and Flash Tag Select */ +#qr select { + background: white; + border: 1px solid #CCC; +} +#qr select[data-name="thread"] { + float: right; +} +#qr > form > select { + margin-top: 1px; +} + +/* Dumping UI */ +.dump #dump-list-container { + display: block; +} +#dump-list-container { + display: none; + position: relative; + overflow-y: hidden; + margin-top: 1px; +} +#dump-list { + overflow-x: auto; + overflow-y: auto; + white-space: nowrap; + width: 248px; + max-height: 248px; + min-height: 90px; + max-width: 100%; + min-width: 100%; + display: -webkit-flex; + display: flex; + -webkit-flex-wrap: wrap; + flex-wrap: wrap; +} +#dump-list:hover { + overflow-x: auto; +} +.qr-preview { + -moz-box-sizing: border-box; + box-sizing: border-box; + counter-increment: thumbnails; + cursor: move; + display: inline-block; + height: 90px; + width: 90px; + padding: 2px; + opacity: .5; + overflow: hidden; + position: relative; + text-shadow: 0 0 2px #000; + -webkit-transition: opacity .25s ease-in-out, -webkit-transform .25s ease-in-out; + transition: opacity .25s ease-in-out, transform .25s ease-in-out, -webkit-transform .25s ease-in-out; + vertical-align: top; + background-size: cover; + -webkit-flex: none; + flex: none; +} +.qr-preview:hover, +.qr-preview:focus { + opacity: .9; +} +.qr-preview::before { + content: counter(thumbnails); + color: #fff; + position: absolute; + top: 3px; + right: 3px; + text-shadow: 0 0 3px #000, 0 0 8px #000; +} +.qr-preview#selected { + opacity: 1; +} +.qr-preview.drag { + box-shadow: 0 0 10px rgba(0,0,0,.5); + -webkit-transform: scale(.8); + transform: scale(.8); +} +.qr-preview.over { + border-color: #fff; + -webkit-transform: scale(1.1); + transform: scale(1.1); + opacity: 0.9; + z-index: 10; +} +.qr-preview > span { + color: #fff; +} +.remove { + background: none; + color: #e00; + padding: 1px; +} +a:only-of-type > .remove { + display: none; +} +.remove:hover::after { + content: " Remove"; +} +.qr-preview:not(.has-file) label, +#qr:not(.has-spoiler) .qr-preview-spoiler { + display: none; +} +.qr-preview > label { + background: rgba(0,0,0,.5); + color: #fff; + right: 0; + bottom: 0; + left: 0; + position: absolute; + text-align: center; +} +.qr-preview > label > input { + margin: 0; +} +#add-post { + cursor: pointer; + font-size: 2em; + position: absolute; + bottom: 20px; + right: 10px; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); +} +.textarea { + position: relative; + display: -webkit-flex; + display: flex; +} +#char-count { + color: #000; + background: hsla(0, 0%, 100%, .5); + font-size: 8pt; + position: absolute; + bottom: 1px; + right: 1px; + pointer-events: none; +} +#char-count.warning { + color: red; +} + +/* Menu */ +.menu-button:not(.fa-bars) { + display: inline-block; + position: relative; + cursor: pointer; +} +#header-bar .menu-button i { + border-top: 6px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + display: inline-block; + margin: 2px; + vertical-align: middle; +} +.postInfo > .menu-button, +#thread-watcher .menu-button { + width: 18px; + height: 15px; + text-align: center; +} +#menu { + position: fixed; + outline: none; + font-weight: normal; +} +#menu, .submenu { + border-radius: 3px; + padding-top: 1px; + padding-bottom: 3px; +} +.entry { + cursor: pointer; + display: block; + outline: none; + padding: 2px 10px; + position: relative; + text-decoration: none; + white-space: nowrap; + min-width: 70px; + text-align: left; + text-shadow: none; + font-size: 10pt; +} +.left>.entry.has-submenu { + padding-right: 17px !important; +} +.entry input[type="checkbox"], +.entry input[type="radio"] { + margin: 0px; + position: relative; + top: 2px; +} +.entry input[type="number"] { + width: 4.5em; +} +.entry.has-shortcut-text { + display: flex; + justify-content: space-between; + align-items: center; +} +.entry .shortcut-text { + opacity: 0.5; + font-size: 70%; + margin-left: 5px; +} +.has-submenu::after { + content: ""; + border-left: .5em solid; + border-top: .3em solid transparent; + border-bottom: .3em solid transparent; + display: inline-block; + margin: .3em; + position: absolute; + right: 3px; +} +.left .has-submenu::after { + border-left: 0; + border-right: .5em solid; +} +.submenu { + display: none; + position: absolute; + left: 100%; + top: -1px; + margin-left: 0px; + margin-top: -2px; +} +.focused > .submenu { + display: block; +} +.imp-exp-result { + position: absolute; + text-align: center; + margin: auto; + right: 0px; + left: 0px; + width: 200px; +} + +/* Custom Board Titles */ +.boardTitle, .boardSubtitle { + white-space: pre-line; +} +.boardTitle[contenteditable="true"], +.boardSubtitle[contenteditable="true"] { + cursor: text !important; +} + +/* Embedding */ +.embedder:not(.embedded) > span { + display: none; +} +#embedding { + padding: 1px 4px 1px 4px; + position: fixed; +} +#embedding.empty { + display: none; +} +#embedding > div:first-child { + display: -webkit-flex; + display: flex; +} +#embedding .move { + -webkit-flex: 1; + flex: 1; +} +#embedding .jump { + margin: -1px 4px; + text-decoration: none; +} + +/* Gallery */ +#a-gallery { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + background: rgba(0,0,0,0.7); +} +.gal-viewport { + display: -webkit-flex; + display: flex; + -webkit-align-items: stretch; + align-items: stretch; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + overflow: hidden; +} +.gal-thumbnails { + -webkit-flex: 0 0 150px; + flex: 0 0 150px; + overflow-y: auto; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + -webkit-align-items: stretch; + align-items: stretch; + text-align: center; + background: rgba(0,0,0,.5); + border-left: 1px solid #222; +} +.gal-hide-thumbnails .gal-thumbnails { + display: none; +} +.gal-thumb img, +.gal-thumb video { + max-width: 125px; + max-height: 125px; + height: auto; + width: auto; +} +.gal-thumb { + -webkit-flex: 0 0 auto; + flex: 0 0 auto; + padding: 3px; + line-height: 0; + transition: background .2s linear; +} +.gal-highlight { + background: rgba(0, 190, 255,.8); +} +.gal-prev { + border-right: 1px solid #222; +} +.gal-next { + border-left: 1px solid #222; +} +.gal-prev, +.gal-next { + -webkit-flex: 0 0 20px; + flex: 0 0 20px; + position: relative; + cursor: pointer; + opacity: 0.7; + background-color: rgba(0, 0, 0, 0.3); +} +.gal-prev:hover, +.gal-next:hover { + opacity: 1; +} +.gal-prev::after, +.gal-next::after { + position: absolute; + top: 48.6%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + display: inline-block; + border-top: 11px solid transparent; + border-bottom: 11px solid transparent; + content: ""; +} +.gal-prev::after { + border-right: 12px solid #fff; + right: 5px; +} +.gal-next::after { + border-left: 12px solid #fff; + right: 3px; +} +.gal-image { + -webkit-flex: 1 0 auto; + flex: 1 0 auto; + display: -webkit-flex; + display: flex; + -webkit-align-items: flex-start; + align-items: flex-start; + -webkit-justify-content: space-around; + justify-content: space-around; + overflow: hidden; + /* Flex > Non-Flex child max-width and overflow fix (Firefox only?) */ + width: 1%; +} +:root:not(.gal-fit-height):not(.gal-pdf) .gal-image { + overflow-y: scroll !important; +} +:root:not(.gal-fit-width):not(.gal-pdf) .gal-image { + overflow-x: scroll !important; +} +.gal-image a { + display: -webkit-flex; + display: flex; + -webkit-align-items: flex-start; + align-items: flex-start; + margin: auto; + line-height: 0; + max-width: 100%; +} +:root.gal-pdf .gal-image a { + width: 100%; + height: 100%; +} +.gal-image img, +.gal-image video { + -webkit-flex: none; + flex: none; +} +.gal-fit-width .gal-image img, +.gal-fit-width .gal-image video { + max-width: 100%; +} +.gal-fit-height .gal-image img, +.gal-fit-height .gal-image video { + /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */ + max-height: 95vh; + max-height: calc(100vh - 25px); +} +.gal-image iframe { + width: 100%; + height: 100%; +} +.gal-buttons { + font-size: 2em; + margin-right: 3px; + padding-left: 7px; + padding-right: 7px; + top: 5px; +} +:root.gal-pdf .gal-buttons { + top: 40px; + background: rgba(0,0,0,0.6) !important; + border-radius: 3px; +} +.gal-buttons a { + color: #ffffff; + text-shadow: 0px 0px 1px #000000; +} +.gal-buttons i { + display: inline-block; + margin: 2px; + position: relative; +} +.gal-start i { + border-left: 10px solid; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + bottom: 1px; +} +.gal-stop i { + border: 5px solid; + bottom: 2px; +} +.gal-buttons.gal-playing > .gal-start, +.gal-buttons:not(.gal-playing) > .gal-stop { + display: none; +} +.gal-buttons .menu-button i { + border-top: 10px solid; + border-right: 6px solid transparent; + border-left: 6px solid transparent; + bottom: 2px; + vertical-align: baseline; +} +.gal-labels { + position: fixed; + bottom: 6px; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + -webkit-align-items: flex-end; + align-items: flex-end; + +} +:root:not(.show-sauce) .gal-sauce { + display: none; +} +.gal-name, +.gal-count, +.gal-sauce { + background: rgba(0,0,0,0.6) !important; + border-radius: 3px; + padding: 1px 5px 2px 5px; + margin-top: 3px; + color: #ffffff !important; + text-decoration: none !important; +} +.gal-sauce a { + color: #ffffff !important; +} +.gal-name:hover, +.gal-buttons a:hover, +.gal-sauce a:hover { + color: rgb(95, 95, 101) !important; +} +:root.gal-pdf .gal-buttons a:hover { + color: rgb(204, 204, 204) !important; +} +.gal-buttons, +.gal-labels { + position: fixed; + right: 195px; +} +.gal-hide-thumbnails .gal-buttons, +.gal-hide-thumbnails .gal-labels { + right: 44px; +} +:root:not(.gal-fit-width):not(.gal-pdf) .gal-labels { + bottom: 23px !important; +} +:root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-buttons, +:root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-labels { + right: 178px !important; +} +:root.gal-hide-thumbnails.gal-fit-height:not(.gal-pdf) .gal-buttons, +:root.gal-hide-thumbnails.gal-fit-height:not(.gal-pdf) .gal-labels { + right: 28px !important; +} +:root.gallery-open.fixed #header-bar:not(.autohide), +:root.gallery-open.fixed #header-bar:not(.autohide) #shortcuts .fa::before { + visibility: hidden; +} + +/* Mod Contact Links */ +.contact-links { + margin-left: 2px; +} +.move-note > a { + text-decoration: underline; +} +.invisible { + font-size: 0; +} + +/* PostJumper */ +.postJumper > .prev, +.postJumper > .next { + font-size: 120%; +} + +/* PSA */ +.fcx-announcement { + text-align: center; +} +.fcx-announcement a { + text-decoration: underline; +} + +@keyframes spin { + 0% {transform:rotate(0deg);} + 100% {transform:rotate(359deg);} +} + +.spin { + animation:spin 2s infinite linear; +} `; - var supports = `/* XXX Moved to end of stylesheet to avoid breaking whole stylesheet in Maxthon. */ -@supports (text-decoration-style: dashed) or (-moz-text-decoration-style: dashed) { - .quotelink.forwardlink, - .backlink.forwardlink { - text-decoration: underline; - -moz-text-decoration-style: dashed; - text-decoration-style: dashed; - border-bottom: none; - } -} + var supports = `/* XXX Moved to end of stylesheet to avoid breaking whole stylesheet in Maxthon. */ +@supports (text-decoration-style: dashed) or (-moz-text-decoration-style: dashed) { + .quotelink.forwardlink, + .backlink.forwardlink { + text-decoration: underline; + -moz-text-decoration-style: dashed; + text-decoration-style: dashed; + border-bottom: none; + } +} `; - var tomorrow = `/* General */ -:root.tomorrow .dialog { - background-color: #282A2E; - border-color: #111; -} - -/* 4chan style fixes */ -:root.tomorrow #arc-list span.quote { - color: #B5BD68; -} -:root.tomorrow.highlight-you .quotesYou$site$highlightable$reply { - border-left: 3px solid rgba(145, 182, 214, .8) !important; -} -:root.tomorrow.highlight-own .yourPost$site$highlightable$reply { - border-left: 3px dashed rgba(145, 182, 214, .8) !important; -} - -/* Header */ -:root.tomorrow #header-bar.dialog { - background-color: rgba(40,42,46,0.9); -} -:root.tomorrow:not(.fixed) #header-bar, :root.tomorrow #notifications { - font-size: 9pt; -} -:root.tomorrow #header-bar, :root.tomorrow #notifications { - color: #C5C8C6; -} -:root.tomorrow #header-bar a, :root.tomorrow #notifications a { - color: #81A2BE; -} -:root.tomorrow.shortcut-icons .native-settings { - background-image: url('//s.4cdn.org/image/favicon-ws.ico'); -} - -/* Settings */ -:root.tomorrow #fourchanx-settings fieldset, :root.tomorrow .section-main div::before { - border-color: #111; -} -:root.tomorrow .suboption-list > div:last-of-type { - background-color: #282A2E; -} - -/* Catalog */ -:root.tomorrow.catalog-hover-expand .catalog-container:hover > .post { - background-color: #282A2E; -} -:root.tomorrow.werkTyme .catalog-thread:not(:hover), -:root.tomorrow.werkTyme:not(.catalog-hover-expand) .catalog-thread, -:root.tomorrow.catalog-hover-expand .catalog-container:hover > .post, -:root.tomorrow.catalog-hover-expand .catalog-container:hover .catalog-reply { - border-color: #111; -} - -/* Quote */ -:root.tomorrow .backlink.deadlink { - color: #81A2BE !important; -} -:root.tomorrow .inline { - border-color: #111; - background-color: rgba(0, 0, 0, .14); -} - -/* Fappe and Werk Tyme */ -:root.tomorrow .indicator { - color: #282A2E; -} - -/* Highlighting */ -:root.tomorrow .qphl { - outline: 2px solid rgba(145, 182, 214, .8); -} -:root.tomorrow.highlight-you .quotesYou$site$highlightable$op, -:root.tomorrow.highlight-you .quotesYou$site$highlightable$reply { - border-left: 3px solid rgba(145, 182, 214, .8); -} -:root.tomorrow.highlight-own .yourPost$site$highlightable$op, -:root.tomorrow.highlight-own .yourPost$site$highlightable$reply { - border-left: 3px dashed rgba(145, 182, 214, .8); -} -:root.tomorrow .filter-highlight$site$highlightable$op, -:root.tomorrow .filter-highlight$site$highlightable$reply { - box-shadow: inset 5px 0 rgba(145, 182, 214, .5); -} -:root.tomorrow.highlight-own .yourPost > $site$sideArrows, -:root.tomorrow.highlight-you .quotesYou > $site$sideArrows, -:root.tomorrow .filter-highlight > $site$sideArrows { - color: rgb(155, 185, 210); -} -:root.tomorrow .catalog-thread.filter-highlight .catalog-thumb, -:root.tomorrow.werkTyme .catalog-thread.filter-highlight:not(:hover), -:root.tomorrow.werkTyme:not(.catalog-hover-expand) .catalog-thread.filter-highlight, -:root.tomorrow.werkTyme.catalog-hover-expand .catalog-thread.filter-highlight > .catalog-container:hover > .catalog-post { - box-shadow: 0 0 3px 3px rgba(64, 192, 255, .7); -} -:root.tomorrow .catalog-thread.watched .catalog-thumb, -:root.tomorrow.werkTyme .catalog-thread.watched:not(:hover), -:root.tomorrow.werkTyme:not(.catalog-hover-expand) .catalog-thread.watched, -:root.tomorrow.werkTyme.catalog-hover-expand .catalog-thread.watched > .catalog-container:hover > .catalog-post { - border: 2px solid rgb(64, 192, 255); -} - - -/* QR */ -.tomorrow #dump-list::-webkit-scrollbar-thumb { - background-color: #282A2E; - border-color: #111; -} -:root.tomorrow .qr-preview { - background-color: rgba(255, 255, 255, .15); -} -:root.tomorrow #qr .field { - background-color: rgb(26, 27, 29); - color: rgb(197,200,198); - border-color: rgb(40, 41, 42); -} -:root.tomorrow #qr .field:focus, -:root.tomorrow #qr .field.focus { - border-color: rgb(129, 162, 190) !important; - background-color: rgb(30,32,36); -} -:root.tomorrow .persona button { - background: linear-gradient(to bottom, #2E3035, #222427) no-repeat; - color: rgb(197,200,198); - border-color: rgb(40, 41, 42); - outline: none; -} -:root.tomorrow .persona button::-moz-focus-inner { - border: none; -} -:root.tomorrow .persona button:focus { - border-color: rgb(129, 162, 190); -} -:root.tomorrow #qr.sjis-preview #sjis-toggle, -:root.tomorrow #qr.tex-preview #tex-preview-button { - background: rgb(26, 27, 29); -} -:root.tomorrow #qr select, -:root.tomorrow #file-n-submit > input, -:root.tomorrow #qr-draw-button { - border-color: rgb(40, 41, 42); -} -:root.tomorrow #qr-filename { - color: rgb(197,200,198); -} -:root.tomorrow .qr-link { - border-color: rgb(25, 27, 31) rgb(25, 27, 31) rgb(10, 12, 16); - background: linear-gradient(#37393D, #282A2E) repeat scroll 0% 0% transparent; -} -:root.tomorrow .qr-link:hover { - background: #282A2E; -} - -/* Menu */ -:root.tomorrow #menu { - color: #C5C8C6; -} -:root.tomorrow .entry { - font-size: 10pt; -} -:root.tomorrow .focused.entry { - background: rgba(0, 0, 0, .33); -} - -/* Unread */ -:root.tomorrow .unread-line { - border-color: rgb(197, 200, 198); -} -:root.tomorrow .unread-mark-read { - background-color: rgba(40,42,46,0.5); -} - -/* Thread Watcher */ -:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you, :root.tomorrow .last-page > a > .watcher-page { - color: #F00 !important; -} - -/* Watcher Favicon */ -:root.tomorrow .watch-thread-link -{ - background-image: url("data:image/svg+xml,"); -} + var tomorrow = `/* General */ +:root.tomorrow .dialog { + background-color: #282A2E; + border-color: #111; +} + +/* 4chan style fixes */ +:root.tomorrow #arc-list span.quote { + color: #B5BD68; +} +:root.tomorrow.highlight-you .quotesYou$site$highlightable$reply { + border-left: 3px solid rgba(145, 182, 214, .8) !important; +} +:root.tomorrow.highlight-own .yourPost$site$highlightable$reply { + border-left: 3px dashed rgba(145, 182, 214, .8) !important; +} + +/* Header */ +:root.tomorrow #header-bar.dialog { + background-color: rgba(40,42,46,0.9); +} +:root.tomorrow:not(.fixed) #header-bar, :root.tomorrow #notifications { + font-size: 9pt; +} +:root.tomorrow #header-bar, :root.tomorrow #notifications { + color: #C5C8C6; +} +:root.tomorrow #header-bar a, :root.tomorrow #notifications a { + color: #81A2BE; +} +:root.tomorrow.shortcut-icons .native-settings { + background-image: url('//s.4cdn.org/image/favicon-ws.ico'); +} + +/* Settings */ +:root.tomorrow #fourchanx-settings fieldset, :root.tomorrow .section-main div::before { + border-color: #111; +} +:root.tomorrow .suboption-list > div:last-of-type { + background-color: #282A2E; +} + +/* Catalog */ +:root.tomorrow.catalog-hover-expand .catalog-container:hover > .post { + background-color: #282A2E; +} +:root.tomorrow.werkTyme .catalog-thread:not(:hover), +:root.tomorrow.werkTyme:not(.catalog-hover-expand) .catalog-thread, +:root.tomorrow.catalog-hover-expand .catalog-container:hover > .post, +:root.tomorrow.catalog-hover-expand .catalog-container:hover .catalog-reply { + border-color: #111; +} + +/* Quote */ +:root.tomorrow .backlink.deadlink { + color: #81A2BE !important; +} +:root.tomorrow .inline { + border-color: #111; + background-color: rgba(0, 0, 0, .14); +} + +/* Fappe and Werk Tyme */ +:root.tomorrow .indicator { + color: #282A2E; +} + +/* Highlighting */ +:root.tomorrow .qphl { + outline: 2px solid rgba(145, 182, 214, .8); +} +:root.tomorrow.highlight-you .quotesYou$site$highlightable$op, +:root.tomorrow.highlight-you .quotesYou$site$highlightable$reply { + border-left: 3px solid rgba(145, 182, 214, .8); +} +:root.tomorrow.highlight-own .yourPost$site$highlightable$op, +:root.tomorrow.highlight-own .yourPost$site$highlightable$reply { + border-left: 3px dashed rgba(145, 182, 214, .8); +} +:root.tomorrow .filter-highlight$site$highlightable$op, +:root.tomorrow .filter-highlight$site$highlightable$reply { + box-shadow: inset 5px 0 rgba(145, 182, 214, .5); +} +:root.tomorrow.highlight-own .yourPost > $site$sideArrows, +:root.tomorrow.highlight-you .quotesYou > $site$sideArrows, +:root.tomorrow .filter-highlight > $site$sideArrows { + color: rgb(155, 185, 210); +} +:root.tomorrow .catalog-thread.filter-highlight .catalog-thumb, +:root.tomorrow.werkTyme .catalog-thread.filter-highlight:not(:hover), +:root.tomorrow.werkTyme:not(.catalog-hover-expand) .catalog-thread.filter-highlight, +:root.tomorrow.werkTyme.catalog-hover-expand .catalog-thread.filter-highlight > .catalog-container:hover > .catalog-post { + box-shadow: 0 0 3px 3px rgba(64, 192, 255, .7); +} +:root.tomorrow .catalog-thread.watched .catalog-thumb, +:root.tomorrow.werkTyme .catalog-thread.watched:not(:hover), +:root.tomorrow.werkTyme:not(.catalog-hover-expand) .catalog-thread.watched, +:root.tomorrow.werkTyme.catalog-hover-expand .catalog-thread.watched > .catalog-container:hover > .catalog-post { + border: 2px solid rgb(64, 192, 255); +} + + +/* QR */ +.tomorrow #dump-list::-webkit-scrollbar-thumb { + background-color: #282A2E; + border-color: #111; +} +:root.tomorrow .qr-preview { + background-color: rgba(255, 255, 255, .15); +} +:root.tomorrow #qr .field { + background-color: rgb(26, 27, 29); + color: rgb(197,200,198); + border-color: rgb(40, 41, 42); +} +:root.tomorrow #qr .field:focus, +:root.tomorrow #qr .field.focus { + border-color: rgb(129, 162, 190) !important; + background-color: rgb(30,32,36); +} +:root.tomorrow .persona button { + background: linear-gradient(to bottom, #2E3035, #222427) no-repeat; + color: rgb(197,200,198); + border-color: rgb(40, 41, 42); + outline: none; +} +:root.tomorrow .persona button::-moz-focus-inner { + border: none; +} +:root.tomorrow .persona button:focus { + border-color: rgb(129, 162, 190); +} +:root.tomorrow #qr.sjis-preview #sjis-toggle, +:root.tomorrow #qr.tex-preview #tex-preview-button { + background: rgb(26, 27, 29); +} +:root.tomorrow #qr select, +:root.tomorrow #file-n-submit > input, +:root.tomorrow #qr-draw-button { + border-color: rgb(40, 41, 42); +} +:root.tomorrow #qr-filename { + color: rgb(197,200,198); +} +:root.tomorrow .qr-link { + border-color: rgb(25, 27, 31) rgb(25, 27, 31) rgb(10, 12, 16); + background: linear-gradient(#37393D, #282A2E) repeat scroll 0% 0% transparent; +} +:root.tomorrow .qr-link:hover { + background: #282A2E; +} + +/* Menu */ +:root.tomorrow #menu { + color: #C5C8C6; +} +:root.tomorrow .entry { + font-size: 10pt; +} +:root.tomorrow .focused.entry { + background: rgba(0, 0, 0, .33); +} + +/* Unread */ +:root.tomorrow .unread-line { + border-color: rgb(197, 200, 198); +} +:root.tomorrow .unread-mark-read { + background-color: rgba(40,42,46,0.5); +} + +/* Thread Watcher */ +:root.tomorrow .replies-quoting-you > a, :root.tomorrow #watcher-link.replies-quoting-you, :root.tomorrow .last-page > a > .watcher-page { + color: #F00 !important; +} + +/* Watcher Favicon */ +:root.tomorrow .watch-thread-link +{ + background-image: url("data:image/svg+xml,"); +} `; - var www = `#captcha-cnt { - height: auto; -} -:root:not(.js-enabled) #form { - display: block; -} -#bd > div[style], #bd > div[style] > * { - height: auto !important; - margin: 0 !important; - font-size: 0; -} + var www = `#captcha-cnt { + height: auto; +} +:root:not(.js-enabled) #form { + display: block; +} +#bd > div[style], #bd > div[style] > * { + height: auto !important; + margin: 0 !important; + font-size: 0; +} `; - var yotsubaB = `/* General */ -:root.yotsuba-b .dialog { - background-color: #D6DAF0; - border-color: #B7C5D9; -} -:root.yotsuba-b .field:focus, -:root.yotsuba-b .field.focus { - border-color: #98E; -} - -/* 4chan style fixes */ -:root.yotsuba-b.highlight-you .quotesYou$site$highlightable$reply { - border-left: 3px solid rgba(221, 0, 0, .8) !important; -} -:root.yotsuba-b.highlight-own .yourPost$site$highlightable$reply { - border-left: 3px dashed rgba(221, 0, 0, .8) !important; -} - -/* Header */ -:root.yotsuba-b #header-bar.dialog { - background-color: rgba(214,218,240,0.98); -} -:root.yotsuba-b:not(.fixed) #header-bar, :root.yotsuba-b #notifications { - font-size: 9pt; -} -:root.yotsuba-b #header-bar, :root.yotsuba-b #notifications { - color: #89A; -} -:root.yotsuba-b #board-list a, :root.yotsuba-b #shortcuts a { - color: #34345C; -} - -/* Settings */ -:root.yotsuba-b #fourchanx-settings fieldset, :root.yotsuba-b .section-main div::before { - border-color: #B7C5D9; -} -:root.yotsuba-b .suboption-list > div:last-of-type { - background-color: #D6DAF0; -} - -/* Catalog */ -:root.yotsuba-b.catalog-hover-expand .catalog-container:hover > .post { - background-color: #D6DAF0; -} -:root.yotsuba-b.werkTyme .catalog-thread:not(:hover), -:root.yotsuba-b.werkTyme:not(.catalog-hover-expand) .catalog-thread, -:root.yotsuba-b.catalog-hover-expand .catalog-container:hover > .post, -:root.yotsuba-b.catalog-hover-expand .catalog-container:hover .catalog-reply { - border-color: #B7C5D9; -} - -/* Quote */ -:root.yotsuba-b .backlink.deadlink { - color: #34345C !important; -} -:root.yotsuba-b .inline { - border-color: #B7C5D9; - background-color: rgba(255, 255, 255, .14); -} - -/* Fappe and Werk Tyme */ -:root.yotsuba-b .indicator { - color: #D6DAF0; -} - -/* QR */ -.yotsuba-b #dump-list::-webkit-scrollbar-thumb { - background-color: #D6DAF0; - border-color: #B7C5D9; -} -:root.yotsuba-b .qr-preview { - background-color: rgba(0, 0, 0, .15); -} -:root.yotsuba-b .qr-link { - border-color: rgb(199, 203, 225) rgb(199, 203, 225) rgb(184, 188, 210); - background: linear-gradient(#E5E9FF, #D6DAF0) repeat scroll 0% 0% transparent; -} -:root.yotsuba-b .qr-link:hover { - background: #D9DDF3; -} - - -/* Menu */ -:root.yotsuba-b #menu { - color: #000; -} -:root.yotsuba-b .entry { - font-size: 10pt; -} -:root.yotsuba-b .focused.entry { - background: rgba(255, 255, 255, .33); -} - -/* Unread */ -:root.yotsuba-b .unread-mark-read { - background-color: rgba(214,218,240,0.5); -} - -/* Thread Watcher */ -:root.yotsuba-b .replies-quoting-you > a, :root.yotsuba-b #watcher-link.replies-quoting-you { - color: #F00; -} - -/* Watcher Favicon */ -:root.yotsuba-b .watch-thread-link -{ - background-image: url("data:image/svg+xml,"); -} + var yotsubaB = `/* General */ +:root.yotsuba-b .dialog { + background-color: #D6DAF0; + border-color: #B7C5D9; +} +:root.yotsuba-b .field:focus, +:root.yotsuba-b .field.focus { + border-color: #98E; +} + +/* 4chan style fixes */ +:root.yotsuba-b.highlight-you .quotesYou$site$highlightable$reply { + border-left: 3px solid rgba(221, 0, 0, .8) !important; +} +:root.yotsuba-b.highlight-own .yourPost$site$highlightable$reply { + border-left: 3px dashed rgba(221, 0, 0, .8) !important; +} + +/* Header */ +:root.yotsuba-b #header-bar.dialog { + background-color: rgba(214,218,240,0.98); +} +:root.yotsuba-b:not(.fixed) #header-bar, :root.yotsuba-b #notifications { + font-size: 9pt; +} +:root.yotsuba-b #header-bar, :root.yotsuba-b #notifications { + color: #89A; +} +:root.yotsuba-b #board-list a, :root.yotsuba-b #shortcuts a { + color: #34345C; +} + +/* Settings */ +:root.yotsuba-b #fourchanx-settings fieldset, :root.yotsuba-b .section-main div::before { + border-color: #B7C5D9; +} +:root.yotsuba-b .suboption-list > div:last-of-type { + background-color: #D6DAF0; +} + +/* Catalog */ +:root.yotsuba-b.catalog-hover-expand .catalog-container:hover > .post { + background-color: #D6DAF0; +} +:root.yotsuba-b.werkTyme .catalog-thread:not(:hover), +:root.yotsuba-b.werkTyme:not(.catalog-hover-expand) .catalog-thread, +:root.yotsuba-b.catalog-hover-expand .catalog-container:hover > .post, +:root.yotsuba-b.catalog-hover-expand .catalog-container:hover .catalog-reply { + border-color: #B7C5D9; +} + +/* Quote */ +:root.yotsuba-b .backlink.deadlink { + color: #34345C !important; +} +:root.yotsuba-b .inline { + border-color: #B7C5D9; + background-color: rgba(255, 255, 255, .14); +} + +/* Fappe and Werk Tyme */ +:root.yotsuba-b .indicator { + color: #D6DAF0; +} + +/* QR */ +.yotsuba-b #dump-list::-webkit-scrollbar-thumb { + background-color: #D6DAF0; + border-color: #B7C5D9; +} +:root.yotsuba-b .qr-preview { + background-color: rgba(0, 0, 0, .15); +} +:root.yotsuba-b .qr-link { + border-color: rgb(199, 203, 225) rgb(199, 203, 225) rgb(184, 188, 210); + background: linear-gradient(#E5E9FF, #D6DAF0) repeat scroll 0% 0% transparent; +} +:root.yotsuba-b .qr-link:hover { + background: #D9DDF3; +} + + +/* Menu */ +:root.yotsuba-b #menu { + color: #000; +} +:root.yotsuba-b .entry { + font-size: 10pt; +} +:root.yotsuba-b .focused.entry { + background: rgba(255, 255, 255, .33); +} + +/* Unread */ +:root.yotsuba-b .unread-mark-read { + background-color: rgba(214,218,240,0.5); +} + +/* Thread Watcher */ +:root.yotsuba-b .replies-quoting-you > a, :root.yotsuba-b #watcher-link.replies-quoting-you { + color: #F00; +} + +/* Watcher Favicon */ +:root.yotsuba-b .watch-thread-link +{ + background-image: url("data:image/svg+xml,"); +} `; - var yotsuba = `/* General */ -:root.yotsuba .dialog { - background-color: #F0E0D6; - border-color: #D9BFB7; -} -:root.yotsuba .field:focus, -:root.yotsuba .field.focus { - border-color: #EA8; -} - -/* 4chan style fixes */ -:root.yotsuba.highlight-you .quotesYou$site$highlightable$reply { - border-left: 3px solid rgba(221, 0, 0, .8) !important; -} -:root.yotsuba.highlight-own .yourPost$site$highlightable$reply { - border-left: 3px dashed rgba(221, 0, 0, .8) !important; -} - -/* Header */ -:root.yotsuba #header-bar.dialog { - background-color: rgba(240,224,214,0.98); -} -:root.yotsuba:not(.fixed) #header-bar, :root.yotsuba #notifications { - font-size: 9pt; -} -:root.yotsuba #header-bar, :root.yotsuba #notifications { - color: #B86; -} -:root.yotsuba #board-list a, :root.yotsuba #shortcuts a { - color: #800000; -} - -/* Settings */ -:root.yotsuba #fourchanx-settings fieldset, :root.yotsuba .section-main div::before { - border-color: #D9BFB7; -} -:root.yotsuba .suboption-list > div:last-of-type { - background-color: #F0E0D6; -} - -/* Catalog */ -:root.yotsuba.catalog-hover-expand .catalog-container:hover > .post { - background-color: #F0E0D6; -} -:root.yotsuba.werkTyme .catalog-thread:not(:hover), -:root.yotsuba.werkTyme:not(.catalog-hover-expand) .catalog-thread, -:root.yotsuba.catalog-hover-expand .catalog-container:hover > .post, -:root.yotsuba.catalog-hover-expand .catalog-container:hover .catalog-reply { - border-color: #D9BFB7; -} - -/* Quote */ -:root.yotsuba .backlink.deadlink { - color: #00E !important; -} -:root.yotsuba .inline { - border-color: #D9BFB7; - background-color: rgba(255, 255, 255, .14); -} - -/* Fappe and Werk Tyme */ -:root.yotsuba .indicator { - color: #F0E0D6; -} - -/* QR */ -.yotsuba #dump-list::-webkit-scrollbar-thumb { - background-color: #F0E0D6; - border-color: #D9BFB7; -} -:root.yotsuba .qr-preview { - background-color: rgba(0, 0, 0, .15); -} -:root.yotsuba .qr-link { - border-color: rgb(225, 209, 199) rgb(225, 209, 199) rgb(210, 194, 184); - background: linear-gradient(#FFEFE5, #F0E0D6) repeat scroll 0% 0% transparent; -} -:root.yotsuba .qr-link:hover { - background: #F0E0D6; -} - -/* Menu */ -:root.yotsuba #menu { - color: #800000; -} -:root.yotsuba .entry { - font-size: 10pt; -} -:root.yotsuba .focused.entry { - background: rgba(255, 255, 255, .33); -} - -/* Unread */ -:root.yotsuba .unread-mark-read { - background-color: rgba(240,224,214,0.5); -} - -/* Thread Watcher */ -:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you, :root.yotsuba .last-page > a > .watcher-page { - color: #F00; -} - -/* Watcher Favicon */ -:root.yotsuba .watch-thread-link -{ - background-image: url("data:image/svg+xml,"); -} + var yotsuba = `/* General */ +:root.yotsuba .dialog { + background-color: #F0E0D6; + border-color: #D9BFB7; +} +:root.yotsuba .field:focus, +:root.yotsuba .field.focus { + border-color: #EA8; +} + +/* 4chan style fixes */ +:root.yotsuba.highlight-you .quotesYou$site$highlightable$reply { + border-left: 3px solid rgba(221, 0, 0, .8) !important; +} +:root.yotsuba.highlight-own .yourPost$site$highlightable$reply { + border-left: 3px dashed rgba(221, 0, 0, .8) !important; +} + +/* Header */ +:root.yotsuba #header-bar.dialog { + background-color: rgba(240,224,214,0.98); +} +:root.yotsuba:not(.fixed) #header-bar, :root.yotsuba #notifications { + font-size: 9pt; +} +:root.yotsuba #header-bar, :root.yotsuba #notifications { + color: #B86; +} +:root.yotsuba #board-list a, :root.yotsuba #shortcuts a { + color: #800000; +} + +/* Settings */ +:root.yotsuba #fourchanx-settings fieldset, :root.yotsuba .section-main div::before { + border-color: #D9BFB7; +} +:root.yotsuba .suboption-list > div:last-of-type { + background-color: #F0E0D6; +} + +/* Catalog */ +:root.yotsuba.catalog-hover-expand .catalog-container:hover > .post { + background-color: #F0E0D6; +} +:root.yotsuba.werkTyme .catalog-thread:not(:hover), +:root.yotsuba.werkTyme:not(.catalog-hover-expand) .catalog-thread, +:root.yotsuba.catalog-hover-expand .catalog-container:hover > .post, +:root.yotsuba.catalog-hover-expand .catalog-container:hover .catalog-reply { + border-color: #D9BFB7; +} + +/* Quote */ +:root.yotsuba .backlink.deadlink { + color: #00E !important; +} +:root.yotsuba .inline { + border-color: #D9BFB7; + background-color: rgba(255, 255, 255, .14); +} + +/* Fappe and Werk Tyme */ +:root.yotsuba .indicator { + color: #F0E0D6; +} + +/* QR */ +.yotsuba #dump-list::-webkit-scrollbar-thumb { + background-color: #F0E0D6; + border-color: #D9BFB7; +} +:root.yotsuba .qr-preview { + background-color: rgba(0, 0, 0, .15); +} +:root.yotsuba .qr-link { + border-color: rgb(225, 209, 199) rgb(225, 209, 199) rgb(210, 194, 184); + background: linear-gradient(#FFEFE5, #F0E0D6) repeat scroll 0% 0% transparent; +} +:root.yotsuba .qr-link:hover { + background: #F0E0D6; +} + +/* Menu */ +:root.yotsuba #menu { + color: #800000; +} +:root.yotsuba .entry { + font-size: 10pt; +} +:root.yotsuba .focused.entry { + background: rgba(255, 255, 255, .33); +} + +/* Unread */ +:root.yotsuba .unread-mark-read { + background-color: rgba(240,224,214,0.5); +} + +/* Thread Watcher */ +:root.yotsuba .replies-quoting-you > a, :root.yotsuba #watcher-link.replies-quoting-you, :root.yotsuba .last-page > a > .watcher-page { + color: #F00; +} + +/* Watcher Favicon */ +:root.yotsuba .watch-thread-link +{ + background-image: url("data:image/svg+xml,"); +} `; // == Create CSS for Link Title Favicons == // const icons = (data) => ('/* Link Title Favicons */\n' + - data.map(({ name, data }) => `.linkify.${name}::before { - content: ""; - background: transparent url('data:image/png;base64,${data}') center left no-repeat!important; - padding-left: 18px; -} + data.map(({ name, data }) => `.linkify.${name}::before { + content: ""; + background: transparent url('data:image/png;base64,${data}') center left no-repeat!important; + padding-left: 18px; +} `).join('')); // cSpell:ignore installGentoo, webfont @@ -12804,332 +12804,332 @@ a:only-of-type > .remove { } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - const CustomCSS = { - init() { - if (!Conf['Custom CSS']) { return; } - return this.addStyle(); - }, - - addStyle() { - return this.style = $$1.addStyle(CSS.sub(Conf['usercss']), 'custom-css', '#fourchanx-css'); - }, - - rmStyle() { - if (this.style) { - $$1.rm(this.style); - return delete this.style; - } - }, - - update() { - if (!this.style) { - return this.addStyle(); - } - return this.style.textContent = CSS.sub(Conf['usercss']); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + const CustomCSS = { + init() { + if (!Conf['Custom CSS']) { return; } + return this.addStyle(); + }, + + addStyle() { + return this.style = $$1.addStyle(CSS.sub(Conf['usercss']), 'custom-css', '#fourchanx-css'); + }, + + rmStyle() { + if (this.style) { + $$1.rm(this.style); + return delete this.style; + } + }, + + update() { + if (!this.style) { + return this.addStyle(); + } + return this.style.textContent = CSS.sub(Conf['usercss']); + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - const SWTinyboard = { - isOPContainerThread: true, - mayLackJSON: true, - threadModTimeIgnoresSage: true, - - disabledFeatures: [ - 'Resurrect Quotes', - 'Quick Reply Personas', - 'Quick Reply', - 'Cooldown', - 'Report Link', - 'Delete Link', - 'Edit Link', - 'Quote Inlining', - 'Quote Previewing', - 'Quote Backlinks', - 'File Info Formatting', - 'Image Expansion', - 'Image Expansion (Menu)', - 'Comment Expansion', - 'Thread Expansion', - 'Favicon', - 'Quote Threading', - 'Thread Updater', - 'Banner', - 'Flash Features', - 'Reply Pruning' - ], - - detect() { - for (var script of $$('script:not([src])', d$1.head)) { - var m; - if (m = script.textContent.match(/\bvar configRoot=(".*?")/)) { - var properties = dict(); - try { - var root = JSON.parse(m[1]); - if (root[0] === '/') { - properties.root = location.origin + root; - } else if (/^https?:/.test(root)) { - properties.root = root; - } - } catch (error) {} - return properties; - } - } - return false; - }, - - awaitBoard(cb) { - if ($$1.id('react-ui')) { - const s = (this.selectors = Object.create(this.selectors)); - s.boardFor = {index: '.page-container'}; - s.thread = 'div[id^="thread_"]'; - return Main$1.mounted(cb); - } else { - return cb(); - } - }, - - urls: { - thread({siteID, boardID, threadID}, isArchived) { - return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.html`; - }, - post({postID}) { return `#${postID}`; }, - index({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/`; }, - catalog({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/catalog.html`; }, - threadJSON({siteID, boardID, threadID}, isArchived) { - const root = Conf['siteProperties'][siteID]?.root; - if (root) { return `${root}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.json`; } else { return ''; } - }, - archivedThreadJSON(thread) { - return SWTinyboard.urls.threadJSON(thread, true); - }, - threadsListJSON({siteID, boardID}) { - const root = Conf['siteProperties'][siteID]?.root; - if (root) { return `${root}${boardID}/threads.json`; } else { return ''; } - }, - archiveListJSON({siteID, boardID}) { - const root = Conf['siteProperties'][siteID]?.root; - if (root) { return `${root}${boardID}/archive/archive.json`; } else { return ''; } - }, - catalogJSON({siteID, boardID}) { - const root = Conf['siteProperties'][siteID]?.root; - if (root) { return `${root}${boardID}/catalog.json`; } else { return ''; } - }, - file({siteID, boardID}, filename) { - return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${filename}`; - }, - thumb(board, filename) { - return SWTinyboard.urls.file(board, filename); - } - }, - - selectors: { - board: 'form[name="postcontrols"]', - thread: 'input[name="board"] ~ div[id^="thread_"]', - threadDivider: 'div[id^="thread_"] > hr:last-child', - summary: '.omitted', - postContainer: 'div[id^="reply_"]:not(.hidden)', // postContainer is thread for OP - opBottom: '.op', - replyOriginal: 'div[id^="reply_"]:not(.hidden)', - infoRoot: '.intro', - info: { - subject: '.subject', - name: '.name', - email: '.email', - tripcode: '.trip', - uniqueID: '.poster_id', - capcode: '.capcode', - flag: '.flag', - date: 'time', - nameBlock: 'label', - quote: 'a[href*="#q"]', - reply: 'a[href*="/res/"]:not([href*="#"])' - }, - icons: { - isSticky: '.fa-thumb-tack', - isClosed: '.fa-lock' - }, - file: { - text: '.fileinfo', - link: '.fileinfo > a', - thumb: 'a > .post-image' - }, - thumbLink: '.file > a', - multifile: '.files > .file', - highlightable: { - op: ' > .op', - reply: '.reply', - catalog: ' > .thread' - }, - comment: '.body', - spoiler: '.spoiler', - quotelink: 'a[onclick*="highlightReply("]', - catalog: { - board: '#Grid', - thread: '.mix', - thumb: '.thread-image' - }, - boardList: '.boardlist', - boardListBottom: '.boardlist.bottom', - styleSheet: '#stylesheet', - psa: '.blotter', - nav: { - prev: '.pages > form > [value=Previous]', - next: '.pages > form > [value=Next]' - } - }, - - classes: { - highlight: 'highlighted' - }, - - xpath: { - thread: 'div[starts-with(@id,"thread_")]', - postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]', - replyContainer: 'div[starts-with(@id,"reply_")]' - }, - - regexp: { - quotelink: - new RegExp(`\ -/\ -([^/]+)\ -/res/\ -(\\d+)\ -(?:\\.\\w+)?#\ -(\\d+)\ -$\ -`), - quotelinkHTML: - /]*\bhref="[^"]*\/([^\/]+)\/res\/(\d+)(?:\.\w+)?#(\d+)"/g - }, - - Build: { - parseJSON(data, board) { - const o = this.parseJSON(data, board); - if (data.ext === 'deleted') { - delete o.file; - $$1.extend(o, { - files: [], - fileDeleted: true, - filesDeleted: [0] - }); - } - if (data.extra_files) { - let file; - for (let i = 0; i < data.extra_files.length; i++) { - var extra_file = data.extra_files[i]; - if (extra_file.ext === 'deleted') { - o.filesDeleted.push(i); - } else { - file = this.parseJSONFile(data, board); - o.files.push(file); - } - } - if (o.files.length) { - o.file = o.files[0]; - } - } - return o; - }, - - parseComment(html) { - html = html - .replace(//gi, '\n') - .replace(/<[^>]*>/g, ''); - return $$1.unescape(html); - } - }, - - bgColoredEl() { - return $$1.el('div', {className: 'post reply'}); - }, - - isFileURL(url) { - return /\/src\/[^\/]+/.test(url.pathname); - }, - - preParsingFixes(board) { - // fixes effects of unclosed link in announcement - let broken; - if (broken = $$1('a > input[name="board"]', board)) { - return $$1.before(broken.parentNode, broken); - } - }, - - parseNodes(post, nodes) { - // Add vichan's span.poster_id around the ID if not already present. - let m; - if (nodes.uniqueID) { return; } - let text = ''; - let node = nodes.nameBlock.nextSibling; - while (node && (node.nodeType === 3)) { - text += node.textContent; - node = node.nextSibling; - } - if (m = text.match(/(\s*ID:\s*)(\S+)/)) { - let uniqueID; - nodes.info.normalize(); - let {nextSibling} = nodes.nameBlock; - nextSibling = nextSibling.splitText(m[1].length); - nextSibling.splitText(m[2].length); - nodes.uniqueID = (uniqueID = $$1.el('span', {className: 'poster_id'})); - $$1.replace(nextSibling, uniqueID); - return $$1.add(uniqueID, nextSibling); - } - }, - - parseDate(node) { - let date = Date.parse(node.getAttribute('datetime')?.trim()); - if (!isNaN(date)) { return new Date(date); } - date = Date.parse(node.textContent.trim() + ' UTC'); // e.g. onesixtwo.club - if (!isNaN(date)) { return new Date(date); } - return undefined; - }, - - parseFile(post, file) { - let info, infoNode; - const {text, link, thumb} = file; - if ($$1.x(`ancestor::${this.xpath.postContainer}[1]`, text) !== post.nodes.root) { return false; } // file belongs to a reply - if (!(infoNode = link.nextSibling?.textContent.includes('(') ? link.nextSibling : link.nextElementSibling)) { return false; } - if (!(info = infoNode.textContent.match(/\((.*,\s*)?([\d.]+ ?[KMG]?B).*\)/))) { return false; } - const nameNode = $$1('.postfilename', text); - $$1.extend(file, { - name: nameNode ? (nameNode.title || nameNode.textContent) : link.pathname.match(/[^/]*$/)[0], - size: info[2], - dimensions: info[0].match(/\d+x\d+/)?.[0] - }); - if (thumb) { - $$1.extend(file, { - thumbURL: /\/static\//.test(thumb.src) && $$1.isImage(link.href) ? link.href : thumb.src, - isSpoiler: /^Spoiler/i.test(info[1] || '') || (link.textContent === 'Spoiler Image') - } - ); - } - return true; - }, - - isThumbExpanded(file) { - // Detect old Tinyboard image expansion that changes src attribute on thumbnail. - return $$1.hasClass(file.thumb.parentNode, 'expanded') || (file.thumb.parentNode.dataset.expanded === 'true'); - }, - - isLinkified(link) { - return /\bnofollow\b/.test(link.rel); - }, - - catalogPin(threadRoot) { - return threadRoot.dataset.sticky = 'true'; - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + const SWTinyboard = { + isOPContainerThread: true, + mayLackJSON: true, + threadModTimeIgnoresSage: true, + + disabledFeatures: [ + 'Resurrect Quotes', + 'Quick Reply Personas', + 'Quick Reply', + 'Cooldown', + 'Report Link', + 'Delete Link', + 'Edit Link', + 'Quote Inlining', + 'Quote Previewing', + 'Quote Backlinks', + 'File Info Formatting', + 'Image Expansion', + 'Image Expansion (Menu)', + 'Comment Expansion', + 'Thread Expansion', + 'Favicon', + 'Quote Threading', + 'Thread Updater', + 'Banner', + 'Flash Features', + 'Reply Pruning' + ], + + detect() { + for (var script of $$('script:not([src])', d$1.head)) { + var m; + if (m = script.textContent.match(/\bvar configRoot=(".*?")/)) { + var properties = dict(); + try { + var root = JSON.parse(m[1]); + if (root[0] === '/') { + properties.root = location.origin + root; + } else if (/^https?:/.test(root)) { + properties.root = root; + } + } catch (error) {} + return properties; + } + } + return false; + }, + + awaitBoard(cb) { + if ($$1.id('react-ui')) { + const s = (this.selectors = Object.create(this.selectors)); + s.boardFor = {index: '.page-container'}; + s.thread = 'div[id^="thread_"]'; + return Main$1.mounted(cb); + } else { + return cb(); + } + }, + + urls: { + thread({siteID, boardID, threadID}, isArchived) { + return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.html`; + }, + post({postID}) { return `#${postID}`; }, + index({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/`; }, + catalog({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/catalog.html`; }, + threadJSON({siteID, boardID, threadID}, isArchived) { + const root = Conf['siteProperties'][siteID]?.root; + if (root) { return `${root}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.json`; } else { return ''; } + }, + archivedThreadJSON(thread) { + return SWTinyboard.urls.threadJSON(thread, true); + }, + threadsListJSON({siteID, boardID}) { + const root = Conf['siteProperties'][siteID]?.root; + if (root) { return `${root}${boardID}/threads.json`; } else { return ''; } + }, + archiveListJSON({siteID, boardID}) { + const root = Conf['siteProperties'][siteID]?.root; + if (root) { return `${root}${boardID}/archive/archive.json`; } else { return ''; } + }, + catalogJSON({siteID, boardID}) { + const root = Conf['siteProperties'][siteID]?.root; + if (root) { return `${root}${boardID}/catalog.json`; } else { return ''; } + }, + file({siteID, boardID}, filename) { + return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${filename}`; + }, + thumb(board, filename) { + return SWTinyboard.urls.file(board, filename); + } + }, + + selectors: { + board: 'form[name="postcontrols"]', + thread: 'input[name="board"] ~ div[id^="thread_"]', + threadDivider: 'div[id^="thread_"] > hr:last-child', + summary: '.omitted', + postContainer: 'div[id^="reply_"]:not(.hidden)', // postContainer is thread for OP + opBottom: '.op', + replyOriginal: 'div[id^="reply_"]:not(.hidden)', + infoRoot: '.intro', + info: { + subject: '.subject', + name: '.name', + email: '.email', + tripcode: '.trip', + uniqueID: '.poster_id', + capcode: '.capcode', + flag: '.flag', + date: 'time', + nameBlock: 'label', + quote: 'a[href*="#q"]', + reply: 'a[href*="/res/"]:not([href*="#"])' + }, + icons: { + isSticky: '.fa-thumb-tack', + isClosed: '.fa-lock' + }, + file: { + text: '.fileinfo', + link: '.fileinfo > a', + thumb: 'a > .post-image' + }, + thumbLink: '.file > a', + multifile: '.files > .file', + highlightable: { + op: ' > .op', + reply: '.reply', + catalog: ' > .thread' + }, + comment: '.body', + spoiler: '.spoiler', + quotelink: 'a[onclick*="highlightReply("]', + catalog: { + board: '#Grid', + thread: '.mix', + thumb: '.thread-image' + }, + boardList: '.boardlist', + boardListBottom: '.boardlist.bottom', + styleSheet: '#stylesheet', + psa: '.blotter', + nav: { + prev: '.pages > form > [value=Previous]', + next: '.pages > form > [value=Next]' + } + }, + + classes: { + highlight: 'highlighted' + }, + + xpath: { + thread: 'div[starts-with(@id,"thread_")]', + postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]', + replyContainer: 'div[starts-with(@id,"reply_")]' + }, + + regexp: { + quotelink: + new RegExp(`\ +/\ +([^/]+)\ +/res/\ +(\\d+)\ +(?:\\.\\w+)?#\ +(\\d+)\ +$\ +`), + quotelinkHTML: + /]*\bhref="[^"]*\/([^\/]+)\/res\/(\d+)(?:\.\w+)?#(\d+)"/g + }, + + Build: { + parseJSON(data, board) { + const o = this.parseJSON(data, board); + if (data.ext === 'deleted') { + delete o.file; + $$1.extend(o, { + files: [], + fileDeleted: true, + filesDeleted: [0] + }); + } + if (data.extra_files) { + let file; + for (let i = 0; i < data.extra_files.length; i++) { + var extra_file = data.extra_files[i]; + if (extra_file.ext === 'deleted') { + o.filesDeleted.push(i); + } else { + file = this.parseJSONFile(data, board); + o.files.push(file); + } + } + if (o.files.length) { + o.file = o.files[0]; + } + } + return o; + }, + + parseComment(html) { + html = html + .replace(//gi, '\n') + .replace(/<[^>]*>/g, ''); + return $$1.unescape(html); + } + }, + + bgColoredEl() { + return $$1.el('div', {className: 'post reply'}); + }, + + isFileURL(url) { + return /\/src\/[^\/]+/.test(url.pathname); + }, + + preParsingFixes(board) { + // fixes effects of unclosed link in announcement + let broken; + if (broken = $$1('a > input[name="board"]', board)) { + return $$1.before(broken.parentNode, broken); + } + }, + + parseNodes(post, nodes) { + // Add vichan's span.poster_id around the ID if not already present. + let m; + if (nodes.uniqueID) { return; } + let text = ''; + let node = nodes.nameBlock.nextSibling; + while (node && (node.nodeType === 3)) { + text += node.textContent; + node = node.nextSibling; + } + if (m = text.match(/(\s*ID:\s*)(\S+)/)) { + let uniqueID; + nodes.info.normalize(); + let {nextSibling} = nodes.nameBlock; + nextSibling = nextSibling.splitText(m[1].length); + nextSibling.splitText(m[2].length); + nodes.uniqueID = (uniqueID = $$1.el('span', {className: 'poster_id'})); + $$1.replace(nextSibling, uniqueID); + return $$1.add(uniqueID, nextSibling); + } + }, + + parseDate(node) { + let date = Date.parse(node.getAttribute('datetime')?.trim()); + if (!isNaN(date)) { return new Date(date); } + date = Date.parse(node.textContent.trim() + ' UTC'); // e.g. onesixtwo.club + if (!isNaN(date)) { return new Date(date); } + return undefined; + }, + + parseFile(post, file) { + let info, infoNode; + const {text, link, thumb} = file; + if ($$1.x(`ancestor::${this.xpath.postContainer}[1]`, text) !== post.nodes.root) { return false; } // file belongs to a reply + if (!(infoNode = link.nextSibling?.textContent.includes('(') ? link.nextSibling : link.nextElementSibling)) { return false; } + if (!(info = infoNode.textContent.match(/\((.*,\s*)?([\d.]+ ?[KMG]?B).*\)/))) { return false; } + const nameNode = $$1('.postfilename', text); + $$1.extend(file, { + name: nameNode ? (nameNode.title || nameNode.textContent) : link.pathname.match(/[^/]*$/)[0], + size: info[2], + dimensions: info[0].match(/\d+x\d+/)?.[0] + }); + if (thumb) { + $$1.extend(file, { + thumbURL: /\/static\//.test(thumb.src) && $$1.isImage(link.href) ? link.href : thumb.src, + isSpoiler: /^Spoiler/i.test(info[1] || '') || (link.textContent === 'Spoiler Image') + } + ); + } + return true; + }, + + isThumbExpanded(file) { + // Detect old Tinyboard image expansion that changes src attribute on thumbnail. + return $$1.hasClass(file.thumb.parentNode, 'expanded') || (file.thumb.parentNode.dataset.expanded === 'true'); + }, + + isLinkified(link) { + return /\bnofollow\b/.test(link.rel); + }, + + catalogPin(threadRoot) { + return threadRoot.dataset.sticky = 'true'; + } }; const passMessagePage = h("div", { class: "box-inner" }, @@ -13142,205 +13142,205 @@ $\ h("a", { href: `${meta.captchaFAQ}#alternatives`, target: "_blank", rel: "noopener" }, "alternative solutions"), ".")); - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - - const PassMessage = { - init() { - if (Conf['passMessageClosed']) { return; } - const msg = $$1.el('div', - {className: 'box-outer top-box'} - , - passMessagePage); - msg.style.cssText = 'padding-bottom: 0;'; - const close = $$1('a', msg); - $$1.on(close, 'click', function() { - $$1.rm(msg); - return $$1.set('passMessageClosed', true); - }); - return $$1.ready(function() { - let hd; - if (hd = $$1.id('hd')) { - return $$1.after(hd, msg); - } else { - return $$1.prepend(d$1.body, msg); - } - }); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + + const PassMessage = { + init() { + if (Conf['passMessageClosed']) { return; } + const msg = $$1.el('div', + {className: 'box-outer top-box'} + , + passMessagePage); + msg.style.cssText = 'padding-bottom: 0;'; + const close = $$1('a', msg); + $$1.on(close, 'click', function() { + $$1.rm(msg); + return $$1.set('passMessageClosed', true); + }); + return $$1.ready(function() { + let hd; + if (hd = $$1.id('hd')) { + return $$1.after(hd, msg); + } else { + return $$1.prepend(d$1.body, msg); + } + }); + } }; - var ReportPage = ` - - - + var ReportPage = ` + + + `; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - - var Report = { - init() { - let match; - if (!(match = location.search.match(/\bno=(\d+)/))) { return; } - Captcha.replace.init(); - this.postID = +match[1]; - return $$1.ready(this.ready); - }, - - ready() { - $$1.addStyle(CSS.report); - - if (Conf['Archive Report']) { Report.archive(); } - - new MutationObserver(function() { - Report.fit('iframe[src^="https://www.google.com/recaptcha/api2/frame"]'); - return Report.fit('body'); - }).observe(d$1.body, { - childList: true, - attributes: true, - subtree: true - } - ); - return Report.fit('body'); - }, - - fit(selector) { - let el; - if (!((el = $$1(selector, doc)) && (getComputedStyle(el).visibility !== 'hidden'))) { return; } - const dy = (el.getBoundingClientRect().bottom - doc.clientHeight) + 8; - if (dy > 0) { return window.resizeBy(0, dy); } - }, - - archive() { - let match, urls; - if (!(urls = Redirect$1.report(g.BOARD.ID)).length) { return; } - - const form = $$1('form'); - const types = $$1.id('reportTypes'); - const message = $$1('h3'); - - const fieldset = $$1.el('fieldset', { - id: 'archive-report', - hidden: true - } - , - { innerHTML: ReportPage }); - const enabled = $$1('#archive-report-enabled', fieldset); - const reason = $$1('#archive-report-reason', fieldset); - const submit = $$1('#archive-report-submit', fieldset); - - $$1.on(enabled, 'change', function() { - return reason.disabled = !this.checked; - }); - - if (form && types) { - fieldset.hidden = !$$1('[value="31"]', types).checked; - $$1.on(types, 'change', function(e) { - fieldset.hidden = (e.target.value !== '31'); - return Report.fit('body'); - }); - $$1.after(types, fieldset); - Report.fit('body'); - $$1.one(form, 'submit', function(e) { - if (!fieldset.hidden && enabled.checked) { - e.preventDefault(); - return Report.archiveSubmit(urls, reason.value, results => { - this.action = '#archiveresults=' + encodeURIComponent(JSON.stringify(results)); - return this.submit(); - }); - } - }); - } else if (message) { - fieldset.hidden = /Report submitted!/.test(message.textContent); - $$1.on(enabled, 'change', function() { - return submit.hidden = !this.checked; - }); - $$1.after(message, fieldset); - $$1.on(submit, 'click', () => Report.archiveSubmit(urls, reason.value, Report.archiveResults)); - } - - if (match = location.hash.match(/^#archiveresults=(.*)$/)) { - try { - return Report.archiveResults(JSON.parse(decodeURIComponent(match[1]))); - } catch (error) {} - } - }, - - archiveSubmit(urls, reason, cb) { - const form = $$1.formData({ - board: g.BOARD.ID, - num: Report.postID, - reason - }); - const results = []; - for (var [name, url] of urls) { - (function(name, url) { - return $$1.ajax(url, { - onloadend() { - results.push([name, this.response || {error: ''}]); - if (results.length === urls.length) { - return cb(results); - } - }, - form - }); - })(name, url); - } - }, - - archiveResults(results) { - const fieldset = $$1.id('archive-report'); - for (var [name, response] of results) { - var line = $$1.el('h3', - {className: 'archive-report-response'}); - if ('success' in response) { - $$1.addClass(line, 'archive-report-success'); - line.textContent = `${name}: ${response.success}`; - } else { - $$1.addClass(line, 'archive-report-error'); - line.textContent = `${name}: ${response.error || 'Error reporting post.'}`; - } - if (fieldset) { - $$1.before(fieldset, line); - } else { - $$1.add(d$1.body, line); - } - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + + var Report = { + init() { + let match; + if (!(match = location.search.match(/\bno=(\d+)/))) { return; } + Captcha.replace.init(); + this.postID = +match[1]; + return $$1.ready(this.ready); + }, + + ready() { + $$1.addStyle(CSS.report); + + if (Conf['Archive Report']) { Report.archive(); } + + new MutationObserver(function() { + Report.fit('iframe[src^="https://www.google.com/recaptcha/api2/frame"]'); + return Report.fit('body'); + }).observe(d$1.body, { + childList: true, + attributes: true, + subtree: true + } + ); + return Report.fit('body'); + }, + + fit(selector) { + let el; + if (!((el = $$1(selector, doc)) && (getComputedStyle(el).visibility !== 'hidden'))) { return; } + const dy = (el.getBoundingClientRect().bottom - doc.clientHeight) + 8; + if (dy > 0) { return window.resizeBy(0, dy); } + }, + + archive() { + let match, urls; + if (!(urls = Redirect$1.report(g.BOARD.ID)).length) { return; } + + const form = $$1('form'); + const types = $$1.id('reportTypes'); + const message = $$1('h3'); + + const fieldset = $$1.el('fieldset', { + id: 'archive-report', + hidden: true + } + , + { innerHTML: ReportPage }); + const enabled = $$1('#archive-report-enabled', fieldset); + const reason = $$1('#archive-report-reason', fieldset); + const submit = $$1('#archive-report-submit', fieldset); + + $$1.on(enabled, 'change', function() { + return reason.disabled = !this.checked; + }); + + if (form && types) { + fieldset.hidden = !$$1('[value="31"]', types).checked; + $$1.on(types, 'change', function(e) { + fieldset.hidden = (e.target.value !== '31'); + return Report.fit('body'); + }); + $$1.after(types, fieldset); + Report.fit('body'); + $$1.one(form, 'submit', function(e) { + if (!fieldset.hidden && enabled.checked) { + e.preventDefault(); + return Report.archiveSubmit(urls, reason.value, results => { + this.action = '#archiveresults=' + encodeURIComponent(JSON.stringify(results)); + return this.submit(); + }); + } + }); + } else if (message) { + fieldset.hidden = /Report submitted!/.test(message.textContent); + $$1.on(enabled, 'change', function() { + return submit.hidden = !this.checked; + }); + $$1.after(message, fieldset); + $$1.on(submit, 'click', () => Report.archiveSubmit(urls, reason.value, Report.archiveResults)); + } + + if (match = location.hash.match(/^#archiveresults=(.*)$/)) { + try { + return Report.archiveResults(JSON.parse(decodeURIComponent(match[1]))); + } catch (error) {} + } + }, + + archiveSubmit(urls, reason, cb) { + const form = $$1.formData({ + board: g.BOARD.ID, + num: Report.postID, + reason + }); + const results = []; + for (var [name, url] of urls) { + (function(name, url) { + return $$1.ajax(url, { + onloadend() { + results.push([name, this.response || {error: ''}]); + if (results.length === urls.length) { + return cb(results); + } + }, + form + }); + })(name, url); + } + }, + + archiveResults(results) { + const fieldset = $$1.id('archive-report'); + for (var [name, response] of results) { + var line = $$1.el('h3', + {className: 'archive-report-response'}); + if ('success' in response) { + $$1.addClass(line, 'archive-report-success'); + line.textContent = `${name}: ${response.success}`; + } else { + $$1.addClass(line, 'archive-report-error'); + line.textContent = `${name}: ${response.error || 'Error reporting post.'}`; + } + if (fieldset) { + $$1.before(fieldset, line); + } else { + $$1.add(d$1.body, line); + } + } + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - const PostSuccessful = { - init() { - if (!Conf['Remember Your Posts']) { return; } - return $$1.ready(this.ready); - }, - - ready() { - if (d$1.title !== 'Post successful!') { return; } - - let [_, threadID, postID] = $$1('h1').nextSibling.textContent.match(/thread:(\d+),no:(\d+)/); - postID = +postID; - threadID = +threadID || postID; - - const db = new DataBoard('yourPosts'); - return db.set({ - boardID: g.BOARD.ID, - threadID, - postID, - val: true - }); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + const PostSuccessful = { + init() { + if (!Conf['Remember Your Posts']) { return; } + return $$1.ready(this.ready); + }, + + ready() { + if (d$1.title !== 'Post successful!') { return; } + + let [_, threadID, postID] = $$1('h1').nextSibling.textContent.match(/thread:(\d+),no:(\d+)/); + postID = +postID; + threadID = +threadID || postID; + + const db = new DataBoard('yourPosts'); + return db.set({ + boardID: g.BOARD.ID, + threadID, + postID, + val: true + }); + } }; function generatePostInfoHtml(ID, o, subject, capcode, email, name, tripcode, pass, capcodeLC, capcodePlural, staticPath, gifIcon, capcodeDescription, uniqueID, flag, flagCode, flagCodeTroll, dateUTC, dateText, postLink, quoteLink, boardID, threadID) { @@ -13583,16 +13583,16 @@ $\ replyContainer: 'div[contains(@class,"replyContainer")]' }, regexp: { - quotelink: new RegExp(`\ -^https?://boards\\.4chan(?:nel)?\\.org/+\ -([^/]+)\ -/+thread/+\ -(\\d+)\ -(?:[/?][^#]*)?\ -(?:#p\ -(\\d+)\ -)?\ -$\ + quotelink: new RegExp(`\ +^https?://boards\\.4chan(?:nel)?\\.org/+\ +([^/]+)\ +/+thread/+\ +(\\d+)\ +(?:[/?][^#]*)?\ +(?:#p\ +(\\d+)\ +)?\ +$\ `), quotelinkHTML: /]*\bhref="(?:(?:\/\/boards\.4chan(?:nel)?\.org)?\/([^\/]+)\/thread\/)?(\d+)?(?:#p(\d+))?"/g, pass: /^https?:\/\/www\.4chan(?:nel)?\.org\/+pass(?:$|[?#])/, @@ -14284,7 +14284,8 @@ $\ a() { let formatter = Time.formatterCache.get('a'); if (!formatter) { - formatter = Intl.DateTimeFormat(Conf['timeLocale'], { weekday: 'short' }); + // || undefined to fall back to browser locale, an empty string gives an error + formatter = Intl.DateTimeFormat(Conf['timeLocale'] || undefined, { weekday: 'short' }); Time.formatterCache.set('a', formatter); } return formatter.format(this); @@ -14292,7 +14293,7 @@ $\ A() { let formatter = Time.formatterCache.get('A'); if (!formatter) { - formatter = Intl.DateTimeFormat(Conf['timeLocale'], { weekday: 'long' }); + formatter = Intl.DateTimeFormat(Conf['timeLocale'] || undefined, { weekday: 'long' }); Time.formatterCache.set('A', formatter); } return formatter.format(this); @@ -14300,7 +14301,7 @@ $\ b() { let formatter = Time.formatterCache.get('b'); if (!formatter) { - formatter = Intl.DateTimeFormat(Conf['timeLocale'], { month: 'short' }); + formatter = Intl.DateTimeFormat(Conf['timeLocale'] || undefined, { month: 'short' }); Time.formatterCache.set('b', formatter); } return formatter.format(this); @@ -14308,7 +14309,7 @@ $\ B() { let formatter = Time.formatterCache.get('B'); if (!formatter) { - formatter = Intl.DateTimeFormat(Conf['timeLocale'], { month: 'long' }); + formatter = Intl.DateTimeFormat(Conf['timeLocale'] || undefined, { month: 'long' }); Time.formatterCache.set('B', formatter); } return formatter.format(this); @@ -14324,7 +14325,7 @@ $\ p() { let formatter = Time.formatterCache.get('p'); if (!formatter) { - formatter = Intl.DateTimeFormat(Conf['timeLocale'], { hour: 'numeric', hour12: true }); + formatter = Intl.DateTimeFormat(Conf['timeLocale'] || undefined, { hour: 'numeric', hour12: true }); Time.formatterCache.set('p', formatter); } const parts = formatter.formatToParts(this); @@ -14340,829 +14341,829 @@ $\ var Beep = 'UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA'; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var ReplyPruning = { - init() { - if ((g.VIEW !== 'thread') || !Conf['Reply Pruning']) { return; } - - this.container = $$1.frag(); - - this.summary = $$1.el('span', { - hidden: true, - className: 'summary' - } - ); - this.summary.style.cursor = 'pointer'; - $$1.on(this.summary, 'click', () => { - this.inputs.enabled.checked = !this.inputs.enabled.checked; - return $$1.event('change', null, this.inputs.enabled); - }); - - const label = UI.checkbox('Prune Replies', 'Show Last', Conf['Prune All Threads']); - const el = $$1.el('span', - {title: 'Maximum number of replies to show.'} - , - {innerHTML: " "}); - $$1.prepend(el, label); - - this.inputs = { - enabled: label.firstElementChild, - replies: el.lastElementChild - }; - - this.setEnabled.call(this.inputs.enabled); - $$1.on(this.inputs.enabled, 'change', this.setEnabled); - $$1.on(this.inputs.replies, 'change', $$1.cb.value); - - Header$1.menu.addEntry({ - el, - order: 190 - }); - - return Callbacks.Thread.push({ - name: 'Reply Pruning', - cb: this.node - }); - }, - - position: 0, - hidden: 0, - hiddenFiles: 0, - total: 0, - totalFiles: 0, - - setEnabled() { - const other = QuoteThreading.input; - if (this.checked && other?.checked) { - other.checked = false; - $$1.event('change', null, other); - } - return ReplyPruning.active = this.checked; - }, - - showIfHidden(id) { - if (ReplyPruning.container && $$1(`#${id}`, ReplyPruning.container)) { - ReplyPruning.inputs.enabled.checked = false; - return $$1.event('change', null, ReplyPruning.inputs.enabled); - } - }, - - node() { - let middle; - ReplyPruning.thread = this; - - if (this.isSticky) { - ReplyPruning.active = (ReplyPruning.inputs.enabled.checked = true); - if (QuoteThreading.input) { - // Disable Quote Threading for this thread but don't save the setting. - Conf['Thread Quotes'] = (QuoteThreading.input.checked = false); - } - } - - this.posts.forEach(function(post) { - if (post.isReply) { - ReplyPruning.total++; - if (post.file) { return ReplyPruning.totalFiles++; } - } - }); - - // If we're linked to a post that we would hide, don't hide the posts in the first place. - if ( - ReplyPruning.active && - /^#p\d+$/.test(location.hash) && - (1 <= (middle = this.posts.keys.indexOf(location.hash.slice(2))) && middle < 1 + Math.max(ReplyPruning.total - +Conf["Max Replies"], 0)) - ) { - ReplyPruning.active = (ReplyPruning.inputs.enabled.checked = false); - } - - $$1.after(this.OP.nodes.root, ReplyPruning.summary); - - $$1.on(ReplyPruning.inputs.enabled, 'change', ReplyPruning.update); - $$1.on(ReplyPruning.inputs.replies, 'change', ReplyPruning.update); - $$1.on(d$1, 'ThreadUpdate', ReplyPruning.updateCount); - $$1.on(d$1, 'ThreadUpdate', ReplyPruning.update); - - return ReplyPruning.update(); - }, - - updateCount(e) { - if (e.detail[404]) { return; } - for (var fullID of e.detail.newPosts) { - ReplyPruning.total++; - if (g.posts.get(fullID).file) { ReplyPruning.totalFiles++; } - } - }, - - update() { - let boardTop, node, post; - const hidden1 = ReplyPruning.hidden; - const hidden2 = ReplyPruning.active ? - Math.max(ReplyPruning.total - +Conf["Max Replies"], 0) - : - 0; - - // Record position from bottom of document - const oldPos = d$1.body.clientHeight - window.scrollY; - - const {posts} = ReplyPruning.thread; - - if (ReplyPruning.hidden < hidden2) { - while ((ReplyPruning.hidden < hidden2) && (ReplyPruning.position < posts.keys.length)) { - post = posts.get(posts.keys[ReplyPruning.position++]); - if (post.isReply && !post.isFetchedQuote) { - while ((node = ReplyPruning.summary.nextSibling) && (node !== post.nodes.root)) { $$1.add(ReplyPruning.container, node); } - $$1.add(ReplyPruning.container, post.nodes.root); - ReplyPruning.hidden++; - if (post.file) { ReplyPruning.hiddenFiles++; } - } - } - - } else if (ReplyPruning.hidden > hidden2) { - const frag = $$1.frag(); - while ((ReplyPruning.hidden > hidden2) && (ReplyPruning.position > 0)) { - post = posts.get(posts.keys[--ReplyPruning.position]); - if (post.isReply && !post.isFetchedQuote) { - while ((node = ReplyPruning.container.lastChild) && (node !== post.nodes.root)) { $$1.prepend(frag, node); } - $$1.prepend(frag, post.nodes.root); - ReplyPruning.hidden--; - if (post.file) { ReplyPruning.hiddenFiles--; } - } - } - $$1.after(ReplyPruning.summary, frag); - $$1.event('PostsInserted', null, ReplyPruning.summary.parentNode); - } - - ReplyPruning.summary.textContent = ReplyPruning.active ? - g.SITE.Build.summaryText('+', ReplyPruning.hidden, ReplyPruning.hiddenFiles) - : - g.SITE.Build.summaryText('-', ReplyPruning.total, ReplyPruning.totalFiles); - ReplyPruning.summary.hidden = (ReplyPruning.total <= +Conf["Max Replies"]); - - // Maintain position in thread when posts are added/removed above - if ((hidden1 !== hidden2) && ((boardTop = Header$1.getTopOf($$1('.board'))) < 0)) { - return window.scrollBy(0, Math.max(d$1.body.clientHeight - oldPos, window.scrollY + boardTop) - window.scrollY); - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var ReplyPruning = { + init() { + if ((g.VIEW !== 'thread') || !Conf['Reply Pruning']) { return; } + + this.container = $$1.frag(); + + this.summary = $$1.el('span', { + hidden: true, + className: 'summary' + } + ); + this.summary.style.cursor = 'pointer'; + $$1.on(this.summary, 'click', () => { + this.inputs.enabled.checked = !this.inputs.enabled.checked; + return $$1.event('change', null, this.inputs.enabled); + }); + + const label = UI.checkbox('Prune Replies', 'Show Last', Conf['Prune All Threads']); + const el = $$1.el('span', + {title: 'Maximum number of replies to show.'} + , + {innerHTML: " "}); + $$1.prepend(el, label); + + this.inputs = { + enabled: label.firstElementChild, + replies: el.lastElementChild + }; + + this.setEnabled.call(this.inputs.enabled); + $$1.on(this.inputs.enabled, 'change', this.setEnabled); + $$1.on(this.inputs.replies, 'change', $$1.cb.value); + + Header$1.menu.addEntry({ + el, + order: 190 + }); + + return Callbacks.Thread.push({ + name: 'Reply Pruning', + cb: this.node + }); + }, + + position: 0, + hidden: 0, + hiddenFiles: 0, + total: 0, + totalFiles: 0, + + setEnabled() { + const other = QuoteThreading.input; + if (this.checked && other?.checked) { + other.checked = false; + $$1.event('change', null, other); + } + return ReplyPruning.active = this.checked; + }, + + showIfHidden(id) { + if (ReplyPruning.container && $$1(`#${id}`, ReplyPruning.container)) { + ReplyPruning.inputs.enabled.checked = false; + return $$1.event('change', null, ReplyPruning.inputs.enabled); + } + }, + + node() { + let middle; + ReplyPruning.thread = this; + + if (this.isSticky) { + ReplyPruning.active = (ReplyPruning.inputs.enabled.checked = true); + if (QuoteThreading.input) { + // Disable Quote Threading for this thread but don't save the setting. + Conf['Thread Quotes'] = (QuoteThreading.input.checked = false); + } + } + + this.posts.forEach(function(post) { + if (post.isReply) { + ReplyPruning.total++; + if (post.file) { return ReplyPruning.totalFiles++; } + } + }); + + // If we're linked to a post that we would hide, don't hide the posts in the first place. + if ( + ReplyPruning.active && + /^#p\d+$/.test(location.hash) && + (1 <= (middle = this.posts.keys.indexOf(location.hash.slice(2))) && middle < 1 + Math.max(ReplyPruning.total - +Conf["Max Replies"], 0)) + ) { + ReplyPruning.active = (ReplyPruning.inputs.enabled.checked = false); + } + + $$1.after(this.OP.nodes.root, ReplyPruning.summary); + + $$1.on(ReplyPruning.inputs.enabled, 'change', ReplyPruning.update); + $$1.on(ReplyPruning.inputs.replies, 'change', ReplyPruning.update); + $$1.on(d$1, 'ThreadUpdate', ReplyPruning.updateCount); + $$1.on(d$1, 'ThreadUpdate', ReplyPruning.update); + + return ReplyPruning.update(); + }, + + updateCount(e) { + if (e.detail[404]) { return; } + for (var fullID of e.detail.newPosts) { + ReplyPruning.total++; + if (g.posts.get(fullID).file) { ReplyPruning.totalFiles++; } + } + }, + + update() { + let boardTop, node, post; + const hidden1 = ReplyPruning.hidden; + const hidden2 = ReplyPruning.active ? + Math.max(ReplyPruning.total - +Conf["Max Replies"], 0) + : + 0; + + // Record position from bottom of document + const oldPos = d$1.body.clientHeight - window.scrollY; + + const {posts} = ReplyPruning.thread; + + if (ReplyPruning.hidden < hidden2) { + while ((ReplyPruning.hidden < hidden2) && (ReplyPruning.position < posts.keys.length)) { + post = posts.get(posts.keys[ReplyPruning.position++]); + if (post.isReply && !post.isFetchedQuote) { + while ((node = ReplyPruning.summary.nextSibling) && (node !== post.nodes.root)) { $$1.add(ReplyPruning.container, node); } + $$1.add(ReplyPruning.container, post.nodes.root); + ReplyPruning.hidden++; + if (post.file) { ReplyPruning.hiddenFiles++; } + } + } + + } else if (ReplyPruning.hidden > hidden2) { + const frag = $$1.frag(); + while ((ReplyPruning.hidden > hidden2) && (ReplyPruning.position > 0)) { + post = posts.get(posts.keys[--ReplyPruning.position]); + if (post.isReply && !post.isFetchedQuote) { + while ((node = ReplyPruning.container.lastChild) && (node !== post.nodes.root)) { $$1.prepend(frag, node); } + $$1.prepend(frag, post.nodes.root); + ReplyPruning.hidden--; + if (post.file) { ReplyPruning.hiddenFiles--; } + } + } + $$1.after(ReplyPruning.summary, frag); + $$1.event('PostsInserted', null, ReplyPruning.summary.parentNode); + } + + ReplyPruning.summary.textContent = ReplyPruning.active ? + g.SITE.Build.summaryText('+', ReplyPruning.hidden, ReplyPruning.hiddenFiles) + : + g.SITE.Build.summaryText('-', ReplyPruning.total, ReplyPruning.totalFiles); + ReplyPruning.summary.hidden = (ReplyPruning.total <= +Conf["Max Replies"]); + + // Maintain position in thread when posts are added/removed above + if ((hidden1 !== hidden2) && ((boardTop = Header$1.getTopOf($$1('.board'))) < 0)) { + return window.scrollBy(0, Math.max(d$1.body.clientHeight - oldPos, window.scrollY + boardTop) - window.scrollY); + } + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - /* - <3 aeosynth - */ - - var QuoteThreading = { - init() { - if (!Conf['Quote Threading'] || (g.VIEW !== 'thread')) { return; } - - this.controls = $$1.el('label', - {innerHTML: " Threading"}); - - this.threadNewLink = $$1.el('span', { - className: 'brackets-wrap threadnewlink', - hidden: true - } - ); - $$1.extend(this.threadNewLink, {innerHTML: "Thread New Posts"}); - - this.input = $$1('input', this.controls); - this.input.checked = Conf['Thread Quotes']; - - $$1.on(this.input, 'change', this.setEnabled); - $$1.on(this.input, 'change', this.rethread); - $$1.on(this.threadNewLink.firstElementChild, 'click', this.rethread); - $$1.on(d$1, '4chanXInitFinished', () => { return this.ready = true; }); - - Header$1.menu.addEntry(this.entry = { - el: this.controls, - order: 99 - } - ); - - Callbacks.Thread.push({ - name: 'Quote Threading', - cb: this.setThread - }); - - return Callbacks.Post.push({ - name: 'Quote Threading', - cb: this.node - }); - }, - - parent: dict(), - children: dict(), - inserted: dict(), - - toggleThreading() { - return this.setThreadingState(!Conf['Thread Quotes']); - }, - - setThreadingState(enabled) { - this.input.checked = enabled; - this.setEnabled.call(this.input); - return this.rethread.call(this.input); - }, - - setEnabled() { - if (this.checked) { - $$1.set('Prune All Threads', false); - const other = ReplyPruning.inputs?.enabled; - if (other?.checked) { - other.checked = false; - $$1.event('change', null, other); - } - } - return $$1.cb.checked.call(this); - }, - - setThread() { - QuoteThreading.thread = this; - return $$1.asap((() => !Conf['Thread Updater'] || $$1('.navLinksBot > .updatelink')), function() { - let navLinksBot; - if (navLinksBot = $$1('.navLinksBot')) { return $$1.add(navLinksBot, [$$1.tn(' '), QuoteThreading.threadNewLink]); } - }); - }, - - node() { - let parent; - if (this.isFetchedQuote || this.isClone || !this.isReply) { return; } - - const parents = new Set(); - let lastParent = null; - for (var quote of this.quotes) { - if ((parent = g.posts.get(quote))) { - if (!parent.isFetchedQuote && parent.isReply && (parent.ID < this.ID)) { - parents.add(parent.ID); - if (!lastParent || (parent.ID > lastParent.ID)) { lastParent = parent; } - } - } - } - - if (!lastParent) { return; } - - let ancestor = lastParent; - while ((ancestor = QuoteThreading.parent[ancestor.fullID])) { - parents.delete(ancestor.ID); - } - - if (parents.size === 1) { - return QuoteThreading.parent[this.fullID] = lastParent; - } - }, - - descendants(post) { - let children; - let posts = [post]; - if (children = QuoteThreading.children[post.fullID]) { - for (var child of children) { - posts = posts.concat(QuoteThreading.descendants(child)); - } - } - return posts; - }, - - insert(post) { - let parent, x; - if (!( - Conf['Thread Quotes'] && - (parent = QuoteThreading.parent[post.fullID]) && - !QuoteThreading.inserted[post.fullID] - )) { return false; } - - const descendants = QuoteThreading.descendants(post); - if (!Unread.posts.has(parent.ID)) { - if ((function() { for (var x of descendants) { if (Unread.posts.has(x.ID)) { return true; } } })()) { - QuoteThreading.threadNewLink.hidden = false; - return false; - } - } - - const {order} = Unread; - const children = (QuoteThreading.children[parent.fullID] || (QuoteThreading.children[parent.fullID] = [])); - const threadContainer = parent.nodes.threadContainer || $$1.el('div', {className: 'threadContainer'}); - const nodes = [post.nodes.root]; - if (post.nodes.threadContainer) { nodes.push(post.nodes.threadContainer); } - - let i = children.length; - for (let j = children.length - 1; j >= 0; j--) { var child = children[j]; if (child.ID >= post.ID) { i--; } } - if (i !== children.length) { - const next = children[i]; - for (x of descendants) { order.before(order[next.ID], order[x.ID]); } - children.splice(i, 0, post); - $$1.before(next.nodes.root, nodes); - } else { - let prev2; - let prev = parent; - while ((prev2 = QuoteThreading.children[prev.fullID]) && prev2.length) { - prev = prev2[prev2.length-1]; - } - for (let k = descendants.length - 1; k >= 0; k--) { x = descendants[k]; order.after(order[prev.ID], order[x.ID]); } - children.push(post); - $$1.add(threadContainer, nodes); - } - - QuoteThreading.inserted[post.fullID] = true; - - if (!parent.nodes.threadContainer) { - parent.nodes.threadContainer = threadContainer; - $$1.addClass(parent.nodes.root, 'threadOP'); - $$1.after(parent.nodes.root, threadContainer); - } - - return true; - }, - - rethread() { - if (!QuoteThreading.ready) { return; } - const {thread} = QuoteThreading; - const {posts} = thread; - - QuoteThreading.threadNewLink.hidden = true; - - if (Conf['Thread Quotes']) { - posts.forEach(QuoteThreading.insert); - } else { - const nodes = []; - Unread.order = new RandomAccessList(); - QuoteThreading.inserted = dict(); - posts.forEach(function(post) { - if (post.isFetchedQuote) { return; } - Unread.order.push(post); - if (post.isReply) { nodes.push(post.nodes.root); } - if (QuoteThreading.children[post.fullID]) { - delete QuoteThreading.children[post.fullID]; - $$1.rmClass(post.nodes.root, 'threadOP'); - $$1.rm(post.nodes.threadContainer); - return delete post.nodes.threadContainer; - } - }); - $$1.add(thread.nodes.root, nodes); - } - - Unread.position = Unread.order.first; - Unread.updatePosition(); - Unread.setLine(true); - Unread.read(); - return Unread.update(); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + /* + <3 aeosynth + */ + + var QuoteThreading = { + init() { + if (!Conf['Quote Threading'] || (g.VIEW !== 'thread')) { return; } + + this.controls = $$1.el('label', + {innerHTML: " Threading"}); + + this.threadNewLink = $$1.el('span', { + className: 'brackets-wrap threadnewlink', + hidden: true + } + ); + $$1.extend(this.threadNewLink, {innerHTML: "Thread New Posts"}); + + this.input = $$1('input', this.controls); + this.input.checked = Conf['Thread Quotes']; + + $$1.on(this.input, 'change', this.setEnabled); + $$1.on(this.input, 'change', this.rethread); + $$1.on(this.threadNewLink.firstElementChild, 'click', this.rethread); + $$1.on(d$1, '4chanXInitFinished', () => { return this.ready = true; }); + + Header$1.menu.addEntry(this.entry = { + el: this.controls, + order: 99 + } + ); + + Callbacks.Thread.push({ + name: 'Quote Threading', + cb: this.setThread + }); + + return Callbacks.Post.push({ + name: 'Quote Threading', + cb: this.node + }); + }, + + parent: dict(), + children: dict(), + inserted: dict(), + + toggleThreading() { + return this.setThreadingState(!Conf['Thread Quotes']); + }, + + setThreadingState(enabled) { + this.input.checked = enabled; + this.setEnabled.call(this.input); + return this.rethread.call(this.input); + }, + + setEnabled() { + if (this.checked) { + $$1.set('Prune All Threads', false); + const other = ReplyPruning.inputs?.enabled; + if (other?.checked) { + other.checked = false; + $$1.event('change', null, other); + } + } + return $$1.cb.checked.call(this); + }, + + setThread() { + QuoteThreading.thread = this; + return $$1.asap((() => !Conf['Thread Updater'] || $$1('.navLinksBot > .updatelink')), function() { + let navLinksBot; + if (navLinksBot = $$1('.navLinksBot')) { return $$1.add(navLinksBot, [$$1.tn(' '), QuoteThreading.threadNewLink]); } + }); + }, + + node() { + let parent; + if (this.isFetchedQuote || this.isClone || !this.isReply) { return; } + + const parents = new Set(); + let lastParent = null; + for (var quote of this.quotes) { + if ((parent = g.posts.get(quote))) { + if (!parent.isFetchedQuote && parent.isReply && (parent.ID < this.ID)) { + parents.add(parent.ID); + if (!lastParent || (parent.ID > lastParent.ID)) { lastParent = parent; } + } + } + } + + if (!lastParent) { return; } + + let ancestor = lastParent; + while ((ancestor = QuoteThreading.parent[ancestor.fullID])) { + parents.delete(ancestor.ID); + } + + if (parents.size === 1) { + return QuoteThreading.parent[this.fullID] = lastParent; + } + }, + + descendants(post) { + let children; + let posts = [post]; + if (children = QuoteThreading.children[post.fullID]) { + for (var child of children) { + posts = posts.concat(QuoteThreading.descendants(child)); + } + } + return posts; + }, + + insert(post) { + let parent, x; + if (!( + Conf['Thread Quotes'] && + (parent = QuoteThreading.parent[post.fullID]) && + !QuoteThreading.inserted[post.fullID] + )) { return false; } + + const descendants = QuoteThreading.descendants(post); + if (!Unread.posts.has(parent.ID)) { + if ((function() { for (var x of descendants) { if (Unread.posts.has(x.ID)) { return true; } } })()) { + QuoteThreading.threadNewLink.hidden = false; + return false; + } + } + + const {order} = Unread; + const children = (QuoteThreading.children[parent.fullID] || (QuoteThreading.children[parent.fullID] = [])); + const threadContainer = parent.nodes.threadContainer || $$1.el('div', {className: 'threadContainer'}); + const nodes = [post.nodes.root]; + if (post.nodes.threadContainer) { nodes.push(post.nodes.threadContainer); } + + let i = children.length; + for (let j = children.length - 1; j >= 0; j--) { var child = children[j]; if (child.ID >= post.ID) { i--; } } + if (i !== children.length) { + const next = children[i]; + for (x of descendants) { order.before(order[next.ID], order[x.ID]); } + children.splice(i, 0, post); + $$1.before(next.nodes.root, nodes); + } else { + let prev2; + let prev = parent; + while ((prev2 = QuoteThreading.children[prev.fullID]) && prev2.length) { + prev = prev2[prev2.length-1]; + } + for (let k = descendants.length - 1; k >= 0; k--) { x = descendants[k]; order.after(order[prev.ID], order[x.ID]); } + children.push(post); + $$1.add(threadContainer, nodes); + } + + QuoteThreading.inserted[post.fullID] = true; + + if (!parent.nodes.threadContainer) { + parent.nodes.threadContainer = threadContainer; + $$1.addClass(parent.nodes.root, 'threadOP'); + $$1.after(parent.nodes.root, threadContainer); + } + + return true; + }, + + rethread() { + if (!QuoteThreading.ready) { return; } + const {thread} = QuoteThreading; + const {posts} = thread; + + QuoteThreading.threadNewLink.hidden = true; + + if (Conf['Thread Quotes']) { + posts.forEach(QuoteThreading.insert); + } else { + const nodes = []; + Unread.order = new RandomAccessList(); + QuoteThreading.inserted = dict(); + posts.forEach(function(post) { + if (post.isFetchedQuote) { return; } + Unread.order.push(post); + if (post.isReply) { nodes.push(post.nodes.root); } + if (QuoteThreading.children[post.fullID]) { + delete QuoteThreading.children[post.fullID]; + $$1.rmClass(post.nodes.root, 'threadOP'); + $$1.rm(post.nodes.threadContainer); + return delete post.nodes.threadContainer; + } + }); + $$1.add(thread.nodes.root, nodes); + } + + Unread.position = Unread.order.first; + Unread.updatePosition(); + Unread.setLine(true); + Unread.read(); + return Unread.update(); + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS201: Simplify complex destructure assignments - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - - var ThreadUpdater = { - init() { - let el, name, sc; - if ((g.VIEW !== 'thread') || !Conf['Thread Updater']) { return; } - this.enabled = true; - - // Chromium won't play audio created in an inactive tab until the tab has been focused, so set it up now. - // XXX Sometimes the loading stalls in Firefox, esp. when opening in private browsing window followed by normal window. - // Don't let it keep the loading icon on indefinitely. - this.audio = $$1.el('audio'); - if ($$1.engine !== 'gecko') { this.audio.src = this.beep; } - - if (Conf['Updater and Stats in Header']) { - this.dialog = (sc = $$1.el('span', - {id: 'updater'})); - $$1.extend(sc, {innerHTML: ''}); - Header$1.addShortcut('updater', sc, 100); - } else { - this.dialog = (sc = UI.dialog('updater', - {innerHTML: '
'})); - $$1.addClass(doc$1, 'float'); - $$1.ready(() => $$1.add(d$1.body, sc)); - } - - this.checkPostCount = 0; - - this.timer = $$1('#update-timer', sc); - this.status = $$1('#update-status', sc); - - $$1.on(this.timer, 'click', this.update); - $$1.on(this.status, 'click', this.update); - - const updateLink = $$1.el('span', - {className: 'brackets-wrap updatelink'}); - $$1.extend(updateLink, {innerHTML: 'Update'}); - Main$1.ready(function() { - let navLinksBot; - if (navLinksBot = $$1('.navLinksBot')) { return $$1.add(navLinksBot, [$$1.tn(' '), updateLink]); } - }); - $$1.on(updateLink.firstElementChild, 'click', this.update); - - const subEntries = []; - for (name in Config.updater.checkbox) { - var conf = Config.updater.checkbox[name]; - el = UI.checkbox(name, name); - el.title = conf[1]; - var input = el.firstElementChild; - $$1.on(input, 'change', $$1.cb.checked); - if (input.name === 'Scroll BG') { - $$1.on(input, 'change', this.cb.scrollBG); - this.cb.scrollBG(); - } else if (input.name === 'Auto Update') { - $$1.on(input, 'change', this.setInterval); - } - subEntries.push({el}); - } - - this.settings = $$1.el('span', - {innerHTML: 'Interval'}); - - $$1.on(this.settings, 'click', this.intervalShortcut); - - subEntries.push({el: this.settings}); - - Header$1.menu.addEntry(this.entry = { - el: $$1.el('span', - {textContent: 'Updater'}), - order: 110, - subEntries - } - ); - - return Callbacks.Thread.push({ - name: 'Thread Updater', - cb: this.node - }); - }, - - node() { - ThreadUpdater.thread = this; - ThreadUpdater.root = this.nodes.root; - ThreadUpdater.outdateCount = 0; - - // We must keep track of our own list of live posts/files - // to provide an accurate deletedPosts/deletedFiles on update - // as posts may be `kill`ed elsewhere. - ThreadUpdater.postIDs = []; - ThreadUpdater.fileIDs = []; - this.posts.forEach(function(post) { - ThreadUpdater.postIDs.push(post.ID); - if (post.file) { return ThreadUpdater.fileIDs.push(post.ID); } - }); - - ThreadUpdater.cb.interval.call($$1.el('input', {value: Conf['Interval']})); - - $$1.on(d$1, 'QRPostSuccessful', ThreadUpdater.cb.checkpost); - $$1.on(d$1, 'visibilitychange', ThreadUpdater.cb.visibility); - - return ThreadUpdater.setInterval(); - }, - - /* - http://freesound.org/people/pierrecartoons1979/sounds/90112/ - cc-by-nc-3.0 - */ - beep: `data:audio/wav;base64,${Beep}`, - - playBeep() { - const {audio} = ThreadUpdater; - if (!audio.src) { audio.src = ThreadUpdater.beep; } - if (audio.paused) { - return audio.play(); - } else { - return $$1.one(audio, 'ended', ThreadUpdater.playBeep); - } - }, - - cb: { - checkpost(e) { - if (e.detail.threadID !== ThreadUpdater.thread.ID) { return; } - ThreadUpdater.postID = e.detail.postID; - ThreadUpdater.checkPostCount = 0; - ThreadUpdater.outdateCount = 0; - return ThreadUpdater.setInterval(); - }, - - visibility() { - if (d$1.hidden) { return; } - // Reset the counter when we focus this tab. - ThreadUpdater.outdateCount = 0; - if (ThreadUpdater.seconds > ThreadUpdater.interval) { - return ThreadUpdater.setInterval(); - } - }, - - scrollBG() { - return ThreadUpdater.scrollBG = Conf['Scroll BG'] ? - () => true - : - () => !d$1.hidden; - }, - - interval(e) { - let val = parseInt(this.value, 10); - if (val < 1) { val = 1; } - ThreadUpdater.interval = (this.value = val); - if (e) { return $$1.cb.value.call(this); } - }, - - load() { - if (this !== ThreadUpdater.req) { return; } // aborted - switch (this.status) { - case 200: - ThreadUpdater.parse(this); - if (ThreadUpdater.thread.isArchived) { - return ThreadUpdater.kill(); - } else { - return ThreadUpdater.setInterval(); - } - case 404: - // XXX workaround for 4chan sending false 404s - return $$1.ajax(g.SITE.urls.catalogJSON({boardID: ThreadUpdater.thread.board.ID}), { onloadend() { - let confirmed; - if (this.status === 200) { - confirmed = true; - for (var page of this.response) { - for (var thread of page.threads) { - if (thread.no === ThreadUpdater.thread.ID) { - confirmed = false; - break; - } - } - } - } else { - confirmed = false; - } - if (confirmed) { - return ThreadUpdater.kill(); - } else { - return ThreadUpdater.error(this); - } - } - } - ); - default: - return ThreadUpdater.error(this); - } - } - }, - - kill() { - ThreadUpdater.thread.kill(); - ThreadUpdater.setInterval(); - return $$1.event('ThreadUpdate', { - 404: true, - threadID: ThreadUpdater.thread.fullID - } - ); - }, - - error(req) { - if (req.status === 304) { - ThreadUpdater.set('status', ''); - } - ThreadUpdater.setInterval(); - if (!req.status) { - return ThreadUpdater.set('status', 'Connection Error', 'warning'); - } else if (req.status !== 304) { - return ThreadUpdater.set('status', `${req.statusText} (${req.status})`, 'warning'); - } - }, - - setInterval() { - clearTimeout(ThreadUpdater.timeoutID); - - if (ThreadUpdater.thread.isDead) { - ThreadUpdater.set('status', (ThreadUpdater.thread.isArchived ? 'Archived' : '404'), 'warning'); - ThreadUpdater.set('timer', ''); - return; - } - - // Fetching your own posts after posting - if (ThreadUpdater.postID && (ThreadUpdater.checkPostCount < 5)) { - ThreadUpdater.set('timer', '...', 'loading'); - ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.update, ++ThreadUpdater.checkPostCount * SECOND); - return; - } - - if (!Conf['Auto Update']) { - ThreadUpdater.set('timer', 'Update'); - return; - } - - const {interval} = ThreadUpdater; - if (Conf['Optional Increase']) { - // Lower the max refresh rate limit on visible tabs. - const limit = d$1.hidden ? 10 : 5; - const j = Math.min(ThreadUpdater.outdateCount, limit); - - // 1 second to 100, 30 to 300. - const cur = (Math.floor(interval * 0.1) || 1) * j * j; - ThreadUpdater.seconds = $$1.minmax(cur, interval, 300); - } else { - ThreadUpdater.seconds = interval; - } - - return ThreadUpdater.timeout(); - }, - - intervalShortcut() { - Settings.open('Advanced'); - const settings = $$1.id('fourchanx-settings'); - return $$1('input[name=Interval]', settings).focus(); - }, - - set(name, text, klass) { - let node; - const 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; - } - return el.className = klass ?? (text === '' ? 'empty' : ''); - }, - - timeout() { - if (ThreadUpdater.seconds) { - ThreadUpdater.set('timer', ThreadUpdater.seconds); - ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); - } else { - ThreadUpdater.outdateCount++; - ThreadUpdater.update(); - } - return ThreadUpdater.seconds--; - }, - - update() { - let oldReq; - clearTimeout(ThreadUpdater.timeoutID); - ThreadUpdater.set('timer', '...', 'loading'); - if (oldReq = ThreadUpdater.req) { - delete ThreadUpdater.req; - oldReq.abort(); - } - return ThreadUpdater.req = $$1.whenModified( - g.SITE.urls.threadJSON({boardID: ThreadUpdater.thread.board.ID, threadID: ThreadUpdater.thread.ID}), - 'ThreadUpdater', - ThreadUpdater.cb.load, - { timeout: MINUTE } - ); - }, - - updateThreadStatus(type, status) { - if (!(ThreadUpdater.thread[`is${type}`] !== status)) { return; } - ThreadUpdater.thread.setStatus(type, status); - if ((type === 'Closed') && ThreadUpdater.thread.isArchived) { return; } - const change = type === 'Sticky' ? - status ? - 'now a sticky' - : - 'not a sticky anymore' - : - status ? - 'now closed' - : - 'not closed anymore'; - return new Notice('info', `The thread is ${change}.`, 30); - }, - - parse(req) { - let ID, ipCountEl, post; - const postObjects = req.response.posts; - const OP = postObjects[0]; - const {thread} = ThreadUpdater; - const {board} = thread; - const lastPost = ThreadUpdater.postIDs[ThreadUpdater.postIDs.length - 1]; - - // XXX Reject updates that falsely delete the last post. - if ((postObjects[postObjects.length-1].no < lastPost) && - ((new Date(req.getResponseHeader('Last-Modified')) - thread.posts.get(lastPost).info.date) < (30 * SECOND))) { return; } - - g.SITE.Build.spoilerRange[board] = OP.custom_spoiler; - thread.setStatus('Archived', !!OP.archived); - ThreadUpdater.updateThreadStatus('Sticky', !!OP.sticky); - ThreadUpdater.updateThreadStatus('Closed', !!OP.closed); - thread.postLimit = !!OP.bumplimit; - thread.fileLimit = !!OP.imagelimit; - if (OP.unique_ips != null) { thread.ipCount = OP.unique_ips; } - - const posts = []; // new post objects - const index = []; // existing posts - const files = []; // existing files - const newPosts = []; // new post fullID list for API - - // Build the index, create posts. - for (var postObject of postObjects) { - ID = postObject.no; - index.push(ID); - if (postObject.fsize) { files.push(ID); } - - // Insert new posts, not older ones. - if (ID <= lastPost) { continue; } - - // XXX Resurrect wrongly deleted posts. - if ((post = thread.posts.get(ID)) && !post.isFetchedQuote) { - post.resurrect(); - continue; - } - - newPosts.push(`${board}.${ID}`); - var node = g.SITE.Build.postFromObject(postObject, board.ID); - posts.push(new Post(node, thread, board)); - // Fetching your own posts after posting - if (ThreadUpdater.postID === ID) { delete ThreadUpdater.postID; } - } - - // Check for deleted posts. - const deletedPosts = []; - for (ID of ThreadUpdater.postIDs) { - if (!index.includes(ID)) { - thread.posts.get(ID).kill(); - deletedPosts.push(`${board}.${ID}`); - } - } - ThreadUpdater.postIDs = index; - - // Check for deleted files. - const deletedFiles = []; - for (ID of ThreadUpdater.fileIDs) { - if (!(files.includes(ID) || deletedPosts.includes(`${board}.${ID}`))) { - thread.posts.get(ID).kill(true); - deletedFiles.push(`${board}.${ID}`); - } - } - ThreadUpdater.fileIDs = files; - - if (!posts.length) { - ThreadUpdater.set('status', ''); - } else { - ThreadUpdater.set('status', `+${posts.length}`, 'new'); - ThreadUpdater.outdateCount = 0; - - const unreadCount = Unread.posts?.size; - const unreadQYCount = Unread.postsQuotingYou?.size; - - Main$1.callbackNodes('Post', posts); - - if (d$1.hidden || !d$1.hasFocus()) { - if (Conf['Beep Quoting You'] && (Unread.postsQuotingYou?.size > unreadQYCount)) { - ThreadUpdater.playBeep(); - if (Conf['Beep']) { ThreadUpdater.playBeep(); } - } else if (Conf['Beep'] && (Unread.posts?.size > 0) && (unreadCount === 0)) { - ThreadUpdater.playBeep(); - } - } - - const scroll = Conf['Auto Scroll'] && ThreadUpdater.scrollBG() && - ((ThreadUpdater.root.getBoundingClientRect().bottom - doc$1.clientHeight) < 25); - - let firstPost = null; - for (post of posts) { - if (!QuoteThreading.insert(post)) { - if (!firstPost) { firstPost = post.nodes.root; } - $$1.add(ThreadUpdater.root, post.nodes.root); - } - } - $$1.event('PostsInserted', null, ThreadUpdater.root); - - if (scroll) { - if (Conf['Bottom Scroll']) { - window.scrollTo(0, d$1.body.clientHeight); - } else { - if (firstPost) { Header$1.scrollTo(firstPost); } - } - } - } - - // Update IP count in original post form. - if ((OP.unique_ips != null) && (ipCountEl = $$1.id('unique-ips'))) { - ipCountEl.textContent = OP.unique_ips; - ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\b(?:is|are)\b/, OP.unique_ips === 1 ? 'is' : 'are'); - ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\bposters?\b/, OP.unique_ips === 1 ? 'poster' : 'posters'); - } - - return $$1.event('ThreadUpdate', { - 404: false, - threadID: thread.fullID, - newPosts, - deletedPosts, - deletedFiles, - postCount: OP.replies + 1, - fileCount: OP.images + !!OP.fsize, - ipCount: OP.unique_ips - } - ); - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS201: Simplify complex destructure assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + + var ThreadUpdater = { + init() { + let el, name, sc; + if ((g.VIEW !== 'thread') || !Conf['Thread Updater']) { return; } + this.enabled = true; + + // Chromium won't play audio created in an inactive tab until the tab has been focused, so set it up now. + // XXX Sometimes the loading stalls in Firefox, esp. when opening in private browsing window followed by normal window. + // Don't let it keep the loading icon on indefinitely. + this.audio = $$1.el('audio'); + if ($$1.engine !== 'gecko') { this.audio.src = this.beep; } + + if (Conf['Updater and Stats in Header']) { + this.dialog = (sc = $$1.el('span', + {id: 'updater'})); + $$1.extend(sc, {innerHTML: ''}); + Header$1.addShortcut('updater', sc, 100); + } else { + this.dialog = (sc = UI.dialog('updater', + {innerHTML: '
'})); + $$1.addClass(doc$1, 'float'); + $$1.ready(() => $$1.add(d$1.body, sc)); + } + + this.checkPostCount = 0; + + this.timer = $$1('#update-timer', sc); + this.status = $$1('#update-status', sc); + + $$1.on(this.timer, 'click', this.update); + $$1.on(this.status, 'click', this.update); + + const updateLink = $$1.el('span', + {className: 'brackets-wrap updatelink'}); + $$1.extend(updateLink, {innerHTML: 'Update'}); + Main$1.ready(function() { + let navLinksBot; + if (navLinksBot = $$1('.navLinksBot')) { return $$1.add(navLinksBot, [$$1.tn(' '), updateLink]); } + }); + $$1.on(updateLink.firstElementChild, 'click', this.update); + + const subEntries = []; + for (name in Config.updater.checkbox) { + var conf = Config.updater.checkbox[name]; + el = UI.checkbox(name, name); + el.title = conf[1]; + var input = el.firstElementChild; + $$1.on(input, 'change', $$1.cb.checked); + if (input.name === 'Scroll BG') { + $$1.on(input, 'change', this.cb.scrollBG); + this.cb.scrollBG(); + } else if (input.name === 'Auto Update') { + $$1.on(input, 'change', this.setInterval); + } + subEntries.push({el}); + } + + this.settings = $$1.el('span', + {innerHTML: 'Interval'}); + + $$1.on(this.settings, 'click', this.intervalShortcut); + + subEntries.push({el: this.settings}); + + Header$1.menu.addEntry(this.entry = { + el: $$1.el('span', + {textContent: 'Updater'}), + order: 110, + subEntries + } + ); + + return Callbacks.Thread.push({ + name: 'Thread Updater', + cb: this.node + }); + }, + + node() { + ThreadUpdater.thread = this; + ThreadUpdater.root = this.nodes.root; + ThreadUpdater.outdateCount = 0; + + // We must keep track of our own list of live posts/files + // to provide an accurate deletedPosts/deletedFiles on update + // as posts may be `kill`ed elsewhere. + ThreadUpdater.postIDs = []; + ThreadUpdater.fileIDs = []; + this.posts.forEach(function(post) { + ThreadUpdater.postIDs.push(post.ID); + if (post.file) { return ThreadUpdater.fileIDs.push(post.ID); } + }); + + ThreadUpdater.cb.interval.call($$1.el('input', {value: Conf['Interval']})); + + $$1.on(d$1, 'QRPostSuccessful', ThreadUpdater.cb.checkpost); + $$1.on(d$1, 'visibilitychange', ThreadUpdater.cb.visibility); + + return ThreadUpdater.setInterval(); + }, + + /* + http://freesound.org/people/pierrecartoons1979/sounds/90112/ + cc-by-nc-3.0 + */ + beep: `data:audio/wav;base64,${Beep}`, + + playBeep() { + const {audio} = ThreadUpdater; + if (!audio.src) { audio.src = ThreadUpdater.beep; } + if (audio.paused) { + return audio.play(); + } else { + return $$1.one(audio, 'ended', ThreadUpdater.playBeep); + } + }, + + cb: { + checkpost(e) { + if (e.detail.threadID !== ThreadUpdater.thread.ID) { return; } + ThreadUpdater.postID = e.detail.postID; + ThreadUpdater.checkPostCount = 0; + ThreadUpdater.outdateCount = 0; + return ThreadUpdater.setInterval(); + }, + + visibility() { + if (d$1.hidden) { return; } + // Reset the counter when we focus this tab. + ThreadUpdater.outdateCount = 0; + if (ThreadUpdater.seconds > ThreadUpdater.interval) { + return ThreadUpdater.setInterval(); + } + }, + + scrollBG() { + return ThreadUpdater.scrollBG = Conf['Scroll BG'] ? + () => true + : + () => !d$1.hidden; + }, + + interval(e) { + let val = parseInt(this.value, 10); + if (val < 1) { val = 1; } + ThreadUpdater.interval = (this.value = val); + if (e) { return $$1.cb.value.call(this); } + }, + + load() { + if (this !== ThreadUpdater.req) { return; } // aborted + switch (this.status) { + case 200: + ThreadUpdater.parse(this); + if (ThreadUpdater.thread.isArchived) { + return ThreadUpdater.kill(); + } else { + return ThreadUpdater.setInterval(); + } + case 404: + // XXX workaround for 4chan sending false 404s + return $$1.ajax(g.SITE.urls.catalogJSON({boardID: ThreadUpdater.thread.board.ID}), { onloadend() { + let confirmed; + if (this.status === 200) { + confirmed = true; + for (var page of this.response) { + for (var thread of page.threads) { + if (thread.no === ThreadUpdater.thread.ID) { + confirmed = false; + break; + } + } + } + } else { + confirmed = false; + } + if (confirmed) { + return ThreadUpdater.kill(); + } else { + return ThreadUpdater.error(this); + } + } + } + ); + default: + return ThreadUpdater.error(this); + } + } + }, + + kill() { + ThreadUpdater.thread.kill(); + ThreadUpdater.setInterval(); + return $$1.event('ThreadUpdate', { + 404: true, + threadID: ThreadUpdater.thread.fullID + } + ); + }, + + error(req) { + if (req.status === 304) { + ThreadUpdater.set('status', ''); + } + ThreadUpdater.setInterval(); + if (!req.status) { + return ThreadUpdater.set('status', 'Connection Error', 'warning'); + } else if (req.status !== 304) { + return ThreadUpdater.set('status', `${req.statusText} (${req.status})`, 'warning'); + } + }, + + setInterval() { + clearTimeout(ThreadUpdater.timeoutID); + + if (ThreadUpdater.thread.isDead) { + ThreadUpdater.set('status', (ThreadUpdater.thread.isArchived ? 'Archived' : '404'), 'warning'); + ThreadUpdater.set('timer', ''); + return; + } + + // Fetching your own posts after posting + if (ThreadUpdater.postID && (ThreadUpdater.checkPostCount < 5)) { + ThreadUpdater.set('timer', '...', 'loading'); + ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.update, ++ThreadUpdater.checkPostCount * SECOND); + return; + } + + if (!Conf['Auto Update']) { + ThreadUpdater.set('timer', 'Update'); + return; + } + + const {interval} = ThreadUpdater; + if (Conf['Optional Increase']) { + // Lower the max refresh rate limit on visible tabs. + const limit = d$1.hidden ? 10 : 5; + const j = Math.min(ThreadUpdater.outdateCount, limit); + + // 1 second to 100, 30 to 300. + const cur = (Math.floor(interval * 0.1) || 1) * j * j; + ThreadUpdater.seconds = $$1.minmax(cur, interval, 300); + } else { + ThreadUpdater.seconds = interval; + } + + return ThreadUpdater.timeout(); + }, + + intervalShortcut() { + Settings.open('Advanced'); + const settings = $$1.id('fourchanx-settings'); + return $$1('input[name=Interval]', settings).focus(); + }, + + set(name, text, klass) { + let node; + const 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; + } + return el.className = klass ?? (text === '' ? 'empty' : ''); + }, + + timeout() { + if (ThreadUpdater.seconds) { + ThreadUpdater.set('timer', ThreadUpdater.seconds); + ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000); + } else { + ThreadUpdater.outdateCount++; + ThreadUpdater.update(); + } + return ThreadUpdater.seconds--; + }, + + update() { + let oldReq; + clearTimeout(ThreadUpdater.timeoutID); + ThreadUpdater.set('timer', '...', 'loading'); + if (oldReq = ThreadUpdater.req) { + delete ThreadUpdater.req; + oldReq.abort(); + } + return ThreadUpdater.req = $$1.whenModified( + g.SITE.urls.threadJSON({boardID: ThreadUpdater.thread.board.ID, threadID: ThreadUpdater.thread.ID}), + 'ThreadUpdater', + ThreadUpdater.cb.load, + { timeout: MINUTE } + ); + }, + + updateThreadStatus(type, status) { + if (!(ThreadUpdater.thread[`is${type}`] !== status)) { return; } + ThreadUpdater.thread.setStatus(type, status); + if ((type === 'Closed') && ThreadUpdater.thread.isArchived) { return; } + const change = type === 'Sticky' ? + status ? + 'now a sticky' + : + 'not a sticky anymore' + : + status ? + 'now closed' + : + 'not closed anymore'; + return new Notice('info', `The thread is ${change}.`, 30); + }, + + parse(req) { + let ID, ipCountEl, post; + const postObjects = req.response.posts; + const OP = postObjects[0]; + const {thread} = ThreadUpdater; + const {board} = thread; + const lastPost = ThreadUpdater.postIDs[ThreadUpdater.postIDs.length - 1]; + + // XXX Reject updates that falsely delete the last post. + if ((postObjects[postObjects.length-1].no < lastPost) && + ((new Date(req.getResponseHeader('Last-Modified')) - thread.posts.get(lastPost).info.date) < (30 * SECOND))) { return; } + + g.SITE.Build.spoilerRange[board] = OP.custom_spoiler; + thread.setStatus('Archived', !!OP.archived); + ThreadUpdater.updateThreadStatus('Sticky', !!OP.sticky); + ThreadUpdater.updateThreadStatus('Closed', !!OP.closed); + thread.postLimit = !!OP.bumplimit; + thread.fileLimit = !!OP.imagelimit; + if (OP.unique_ips != null) { thread.ipCount = OP.unique_ips; } + + const posts = []; // new post objects + const index = []; // existing posts + const files = []; // existing files + const newPosts = []; // new post fullID list for API + + // Build the index, create posts. + for (var postObject of postObjects) { + ID = postObject.no; + index.push(ID); + if (postObject.fsize) { files.push(ID); } + + // Insert new posts, not older ones. + if (ID <= lastPost) { continue; } + + // XXX Resurrect wrongly deleted posts. + if ((post = thread.posts.get(ID)) && !post.isFetchedQuote) { + post.resurrect(); + continue; + } + + newPosts.push(`${board}.${ID}`); + var node = g.SITE.Build.postFromObject(postObject, board.ID); + posts.push(new Post(node, thread, board)); + // Fetching your own posts after posting + if (ThreadUpdater.postID === ID) { delete ThreadUpdater.postID; } + } + + // Check for deleted posts. + const deletedPosts = []; + for (ID of ThreadUpdater.postIDs) { + if (!index.includes(ID)) { + thread.posts.get(ID).kill(); + deletedPosts.push(`${board}.${ID}`); + } + } + ThreadUpdater.postIDs = index; + + // Check for deleted files. + const deletedFiles = []; + for (ID of ThreadUpdater.fileIDs) { + if (!(files.includes(ID) || deletedPosts.includes(`${board}.${ID}`))) { + thread.posts.get(ID).kill(true); + deletedFiles.push(`${board}.${ID}`); + } + } + ThreadUpdater.fileIDs = files; + + if (!posts.length) { + ThreadUpdater.set('status', ''); + } else { + ThreadUpdater.set('status', `+${posts.length}`, 'new'); + ThreadUpdater.outdateCount = 0; + + const unreadCount = Unread.posts?.size; + const unreadQYCount = Unread.postsQuotingYou?.size; + + Main$1.callbackNodes('Post', posts); + + if (d$1.hidden || !d$1.hasFocus()) { + if (Conf['Beep Quoting You'] && (Unread.postsQuotingYou?.size > unreadQYCount)) { + ThreadUpdater.playBeep(); + if (Conf['Beep']) { ThreadUpdater.playBeep(); } + } else if (Conf['Beep'] && (Unread.posts?.size > 0) && (unreadCount === 0)) { + ThreadUpdater.playBeep(); + } + } + + const scroll = Conf['Auto Scroll'] && ThreadUpdater.scrollBG() && + ((ThreadUpdater.root.getBoundingClientRect().bottom - doc$1.clientHeight) < 25); + + let firstPost = null; + for (post of posts) { + if (!QuoteThreading.insert(post)) { + if (!firstPost) { firstPost = post.nodes.root; } + $$1.add(ThreadUpdater.root, post.nodes.root); + } + } + $$1.event('PostsInserted', null, ThreadUpdater.root); + + if (scroll) { + if (Conf['Bottom Scroll']) { + window.scrollTo(0, d$1.body.clientHeight); + } else { + if (firstPost) { Header$1.scrollTo(firstPost); } + } + } + } + + // Update IP count in original post form. + if ((OP.unique_ips != null) && (ipCountEl = $$1.id('unique-ips'))) { + ipCountEl.textContent = OP.unique_ips; + ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\b(?:is|are)\b/, OP.unique_ips === 1 ? 'is' : 'are'); + ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\bposters?\b/, OP.unique_ips === 1 ? 'poster' : 'posters'); + } + + return $$1.event('ThreadUpdate', { + 404: false, + threadID: thread.fullID, + newPosts, + deletedPosts, + deletedFiles, + postCount: OP.replies + 1, + fileCount: OP.images + !!OP.fsize, + ipCount: OP.unique_ips + } + ); + } }; /* @@ -15286,9 +15287,9 @@ $\ if ($$1.cantSync) { const why = $$1.cantSet ? 'save your settings' : 'synchronize settings between tabs'; return cb($$1.el('li', { - textContent: `\ -${meta.name} needs local storage to ${why}. -Enable it on boards.${location.hostname.split('.')[1]}.org in your browser's privacy settings (may be listed as part of "local data" or "cookies").\ + textContent: `\ +${meta.name} needs local storage to ${why}. +Enable it on boards.${location.hostname.split('.')[1]}.org in your browser's privacy settings (may be listed as part of "local data" or "cookies").\ ` })); } @@ -15875,24 +15876,24 @@ Enable it on boards.${location.hostname.split('.')[1]}.org in your browser's pri } if (compareString < '00001.00014.00012.00008') { if (data['boardnav'] == null) { - set('boardnav', `\ -[ toggle-all ] -a-replace -c-replace -g-replace -k-replace -v-replace -vg-replace -vr-replace -ck-replace -co-replace -fit-replace -jp-replace -mu-replace -sp-replace -tv-replace -vp-replace -[external-text:"FAQ","${meta.faq}"]\ + set('boardnav', `\ +[ toggle-all ] +a-replace +c-replace +g-replace +k-replace +v-replace +vg-replace +vr-replace +ck-replace +co-replace +fit-replace +jp-replace +mu-replace +sp-replace +tv-replace +vp-replace +[external-text:"FAQ","${meta.faq}"]\ `); } } @@ -16267,5803 +16268,5803 @@ vp-replace } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var FappeTyme = { - init() { - if ((!Conf['Fappe Tyme'] && !Conf['Werk Tyme']) || !['index', 'thread', 'archive'].includes(g.VIEW)) { return; } - - this.nodes = {}; - this.enabled = { - fappe: false, - werk: Conf['werk'] - }; - - for (var type of ["Fappe", "Werk"]) { - if (Conf[`${type} Tyme`]) { - var lc = type.toLowerCase(); - var el = UI.checkbox(lc, `${type} Tyme`, false); - el.title = `${type} Tyme`; - - this.nodes[lc] = el.firstElementChild; - if (Conf[lc]) { this.set(lc, true); } - $$1.on(this.nodes[lc], 'change', this.toggle.bind(this, lc)); - - Header$1.menu.addEntry({ - el, - order: 97 - }); - - var indicator = $$1.el('span', { - className: 'indicator', - textContent: type[0], - title: `${type} Tyme active` - } - ); - $$1.on(indicator, 'click', function() { - const check = $$1.getOwn(FappeTyme.nodes, this.parentNode.id.replace('shortcut-', '')); - check.checked = !check.checked; - return $$1.event('change', null, check); - }); - Header$1.addShortcut(lc, indicator, 410); - } - } - - if (Conf['Werk Tyme']) { - $$1.sync('werk', this.set.bind(this, 'werk')); - } - - Callbacks.Post.push({ - name: 'Fappe Tyme', - cb: this.node - }); - - return Callbacks.CatalogThread.push({ - name: 'Werk Tyme', - cb: this.catalogNode - }); - }, - - node() { - return this.nodes.root.classList.toggle('noFile', !this.files.length); - }, - - catalogNode() { - const file = this.thread.OP.files[0]; - if (!file) { return; } - const filename = $$1.el('div', { - textContent: file.name, - className: 'werkTyme-filename' - } - ); - return $$1.add(this.nodes.thumb.parentNode, filename); - }, - - set(type, enabled) { - this.enabled[type] = (this.nodes[type].checked = enabled); - return $$1[`${enabled ? 'add' : 'rm'}Class`](doc$1, `${type}Tyme`); - }, - - toggle(type) { - this.set(type, !this.enabled[type]); - if (type === 'werk') { return $$1.cb.checked.call(this.nodes[type]); } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var FappeTyme = { + init() { + if ((!Conf['Fappe Tyme'] && !Conf['Werk Tyme']) || !['index', 'thread', 'archive'].includes(g.VIEW)) { return; } + + this.nodes = {}; + this.enabled = { + fappe: false, + werk: Conf['werk'] + }; + + for (var type of ["Fappe", "Werk"]) { + if (Conf[`${type} Tyme`]) { + var lc = type.toLowerCase(); + var el = UI.checkbox(lc, `${type} Tyme`, false); + el.title = `${type} Tyme`; + + this.nodes[lc] = el.firstElementChild; + if (Conf[lc]) { this.set(lc, true); } + $$1.on(this.nodes[lc], 'change', this.toggle.bind(this, lc)); + + Header$1.menu.addEntry({ + el, + order: 97 + }); + + var indicator = $$1.el('span', { + className: 'indicator', + textContent: type[0], + title: `${type} Tyme active` + } + ); + $$1.on(indicator, 'click', function() { + const check = $$1.getOwn(FappeTyme.nodes, this.parentNode.id.replace('shortcut-', '')); + check.checked = !check.checked; + return $$1.event('change', null, check); + }); + Header$1.addShortcut(lc, indicator, 410); + } + } + + if (Conf['Werk Tyme']) { + $$1.sync('werk', this.set.bind(this, 'werk')); + } + + Callbacks.Post.push({ + name: 'Fappe Tyme', + cb: this.node + }); + + return Callbacks.CatalogThread.push({ + name: 'Werk Tyme', + cb: this.catalogNode + }); + }, + + node() { + return this.nodes.root.classList.toggle('noFile', !this.files.length); + }, + + catalogNode() { + const file = this.thread.OP.files[0]; + if (!file) { return; } + const filename = $$1.el('div', { + textContent: file.name, + className: 'werkTyme-filename' + } + ); + return $$1.add(this.nodes.thumb.parentNode, filename); + }, + + set(type, enabled) { + this.enabled[type] = (this.nodes[type].checked = enabled); + return $$1[`${enabled ? 'add' : 'rm'}Class`](doc$1, `${type}Tyme`); + }, + + toggle(type) { + this.set(type, !this.enabled[type]); + if (type === 'werk') { return $$1.cb.checked.call(this.nodes[type]); } + } }; - var galleryPage = `
- - - - - × - -
- - / - - - -
-
-
- -
-
-
-
+ var galleryPage = `
+ + + + + × + +
+ + / + + + +
+
+
+ +
+
+
+
`; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS204: Change includes calls to have a more natural evaluation order - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var Sauce = { - init() { - let link; - if (!['index', 'thread'].includes(g.VIEW) || !Conf['Sauce']) { return; } - $$1.addClass(doc$1, 'show-sauce'); - - const links = []; - for (link of Conf['sauces'].split('\n')) { - var linkData; - if ((link[0] !== '#') && (linkData = this.parseLink(link))) { - links.push(linkData); - } - } - if (!links.length) { return; } - - this.links = links; - this.link = $$1.el('a', { - target: '_blank', - className: 'sauce' - } - ); - return Callbacks.Post.push({ - name: 'Sauce', - cb: this.node - }); - }, - - parseLink(link) { - if (!(link = link.trim())) { return null; } - const parts = dict(); - const iterable = link.split(/;(?=(?:text|boards|types|regexp|sandbox):?)/); - for (let i = 0; i < iterable.length; i++) { - var part = iterable[i]; - if (i === 0) { - parts['url'] = part; - } else { - var m = part.match(/^(\w*):?(.*)$/); - parts[m[1]] = m[2]; - } - } - if (!parts['text']) { parts['text'] = parts['url'].match(/(\w+)\.\w+\//)?.[1] || '?'; } - if ('boards' in parts) { - parts['boards'] = Filter.parseBoards(parts['boards']); - } - if ('regexp' in parts) { - try { - let regexp; - if (regexp = parts['regexp'].match(/^\/(.*)\/(\w*)$/)) { - parts['regexp'] = RegExp(regexp[1], regexp[2]); - } else { - parts['regexp'] = RegExp(parts['regexp']); - } - } catch (err) { - new Notice('warning', [ - $$1.tn("Invalid regexp for Sauce link:"), - $$1.el('br'), - $$1.tn(link), - $$1.el('br'), - $$1.tn(err.message) - ], 60); - return null; - } - } - return parts; - }, - - createSauceLink(link, post, file) { - let a, matches, needle; - const ext = file.url.match(/[^.]*$/)[0]; - const parts = dict(); - $$1.extend(parts, link); - - if (!!parts['boards'] && !parts['boards'][`${post.siteID}/${post.boardID}`] && !parts['boards'][`${post.siteID}/*`]) { return null; } - if (!!parts['types'] && (needle = ext, !parts['types'].split(',').includes(needle))) { return null; } - if (!!parts['regexp'] && (!(matches = file.name.match(parts['regexp'])))) { return null; } - - const missing = []; - for (var key of ['url', 'text']) { - parts[key] = parts[key].replace(/%(T?URL|IMG|[sh]?MD5|board|name|%|semi|\$\d+)/g, function(orig, parameter) { - let type; - if (parameter[0] === '$') { - if (!matches) { return orig; } - type = matches[parameter.slice(1)] || ''; - } else { - type = Sauce.formatters[parameter](post, file, ext); - if ((type == null)) { - missing.push(parameter); - return ''; - } - } - - if ((key === 'url') && !['%', 'semi'].includes(parameter)) { - if (/^javascript:/i.test(parts['url'])) { type = JSON.stringify(type); } - type = encodeURIComponent(type); - } - return type; - }); - } - - if (g.SITE.areMD5sDeferred?.(post.board) && missing.length && !missing.filter(x => !/^.?MD5$/.test(x)).length) { - a = Sauce.link.cloneNode(false); - a.dataset.skip = '1'; - return a; - } - - if (missing.length) { return null; } - - a = Sauce.link.cloneNode(false); - a.href = parts['url']; - a.textContent = parts['text']; - if (/^javascript:/i.test(parts['url'])) { a.removeAttribute('target'); } - return a; - }, - - node() { - if (this.isClone) { return; } - for (var file of this.files) { - Sauce.file(this, file); - } - }, - - file(post, file) { - let link, node; - const nodes = []; - const skipped = []; - for (link of Sauce.links) { - if (node = Sauce.createSauceLink(link, post, file)) { - nodes.push($$1.tn(' '), node); - if (node.dataset.skip) { skipped.push([link, node]); } - } - } - $$1.add(file.text, nodes); - - if (skipped.length) { - var observer = new MutationObserver(function() { - if (file.text.dataset.md5) { - for ([link, node] of skipped) { - var node2; - if (node2 = Sauce.createSauceLink(link, post, file)) { - $$1.replace(node, node2); - } - } - return observer.disconnect(); - } - }); - return observer.observe(file.text, {attributes: true}); - } - }, - - formatters: { - TURL(post, file) { return file.thumbURL; }, - URL(post, file) { return file.url; }, - IMG(post, file, ext) { if (['gif', 'jpg', 'jpeg', 'png'].includes(ext)) { return file.url; } else { return file.thumbURL; } }, - MD5(post, file) { return file.MD5; }, - sMD5(post, file) { return file.MD5?.replace(/[+/=]/g, c => ({'+': '-', '/': '_', '=': ''})[c]); }, - hMD5(post, file) { if (file.MD5) { return (atob(file.MD5).map((c) => `0${c.charCodeAt(0).toString(16)}`.slice(-2))).join(''); } }, - board(post) { return post.board.ID; }, - name(post, file) { return file.name; }, - '%'() { return '%'; }, - semi() { return ';'; } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var Sauce = { + init() { + let link; + if (!['index', 'thread'].includes(g.VIEW) || !Conf['Sauce']) { return; } + $$1.addClass(doc$1, 'show-sauce'); + + const links = []; + for (link of Conf['sauces'].split('\n')) { + var linkData; + if ((link[0] !== '#') && (linkData = this.parseLink(link))) { + links.push(linkData); + } + } + if (!links.length) { return; } + + this.links = links; + this.link = $$1.el('a', { + target: '_blank', + className: 'sauce' + } + ); + return Callbacks.Post.push({ + name: 'Sauce', + cb: this.node + }); + }, + + parseLink(link) { + if (!(link = link.trim())) { return null; } + const parts = dict(); + const iterable = link.split(/;(?=(?:text|boards|types|regexp|sandbox):?)/); + for (let i = 0; i < iterable.length; i++) { + var part = iterable[i]; + if (i === 0) { + parts['url'] = part; + } else { + var m = part.match(/^(\w*):?(.*)$/); + parts[m[1]] = m[2]; + } + } + if (!parts['text']) { parts['text'] = parts['url'].match(/(\w+)\.\w+\//)?.[1] || '?'; } + if ('boards' in parts) { + parts['boards'] = Filter.parseBoards(parts['boards']); + } + if ('regexp' in parts) { + try { + let regexp; + if (regexp = parts['regexp'].match(/^\/(.*)\/(\w*)$/)) { + parts['regexp'] = RegExp(regexp[1], regexp[2]); + } else { + parts['regexp'] = RegExp(parts['regexp']); + } + } catch (err) { + new Notice('warning', [ + $$1.tn("Invalid regexp for Sauce link:"), + $$1.el('br'), + $$1.tn(link), + $$1.el('br'), + $$1.tn(err.message) + ], 60); + return null; + } + } + return parts; + }, + + createSauceLink(link, post, file) { + let a, matches, needle; + const ext = file.url.match(/[^.]*$/)[0]; + const parts = dict(); + $$1.extend(parts, link); + + if (!!parts['boards'] && !parts['boards'][`${post.siteID}/${post.boardID}`] && !parts['boards'][`${post.siteID}/*`]) { return null; } + if (!!parts['types'] && (needle = ext, !parts['types'].split(',').includes(needle))) { return null; } + if (!!parts['regexp'] && (!(matches = file.name.match(parts['regexp'])))) { return null; } + + const missing = []; + for (var key of ['url', 'text']) { + parts[key] = parts[key].replace(/%(T?URL|IMG|[sh]?MD5|board|name|%|semi|\$\d+)/g, function(orig, parameter) { + let type; + if (parameter[0] === '$') { + if (!matches) { return orig; } + type = matches[parameter.slice(1)] || ''; + } else { + type = Sauce.formatters[parameter](post, file, ext); + if ((type == null)) { + missing.push(parameter); + return ''; + } + } + + if ((key === 'url') && !['%', 'semi'].includes(parameter)) { + if (/^javascript:/i.test(parts['url'])) { type = JSON.stringify(type); } + type = encodeURIComponent(type); + } + return type; + }); + } + + if (g.SITE.areMD5sDeferred?.(post.board) && missing.length && !missing.filter(x => !/^.?MD5$/.test(x)).length) { + a = Sauce.link.cloneNode(false); + a.dataset.skip = '1'; + return a; + } + + if (missing.length) { return null; } + + a = Sauce.link.cloneNode(false); + a.href = parts['url']; + a.textContent = parts['text']; + if (/^javascript:/i.test(parts['url'])) { a.removeAttribute('target'); } + return a; + }, + + node() { + if (this.isClone) { return; } + for (var file of this.files) { + Sauce.file(this, file); + } + }, + + file(post, file) { + let link, node; + const nodes = []; + const skipped = []; + for (link of Sauce.links) { + if (node = Sauce.createSauceLink(link, post, file)) { + nodes.push($$1.tn(' '), node); + if (node.dataset.skip) { skipped.push([link, node]); } + } + } + $$1.add(file.text, nodes); + + if (skipped.length) { + var observer = new MutationObserver(function() { + if (file.text.dataset.md5) { + for ([link, node] of skipped) { + var node2; + if (node2 = Sauce.createSauceLink(link, post, file)) { + $$1.replace(node, node2); + } + } + return observer.disconnect(); + } + }); + return observer.observe(file.text, {attributes: true}); + } + }, + + formatters: { + TURL(post, file) { return file.thumbURL; }, + URL(post, file) { return file.url; }, + IMG(post, file, ext) { if (['gif', 'jpg', 'jpeg', 'png'].includes(ext)) { return file.url; } else { return file.thumbURL; } }, + MD5(post, file) { return file.MD5; }, + sMD5(post, file) { return file.MD5?.replace(/[+/=]/g, c => ({'+': '-', '/': '_', '=': ''})[c]); }, + hMD5(post, file) { if (file.MD5) { return (atob(file.MD5).map((c) => `0${c.charCodeAt(0).toString(16)}`.slice(-2))).join(''); } }, + board(post) { return post.board.ID; }, + name(post, file) { return file.name; }, + '%'() { return '%'; }, + semi() { return ';'; } + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - - var Gallery = { - init() { - if (!(this.enabled = Conf['Gallery'] && ['index', 'thread'].includes(g.VIEW))) { return; } - - this.delay = Conf['Slide Delay']; - - const el = $$1.el('a', { - href: 'javascript:;', - title: 'Gallery', - textContent: '🖼︎', - }); - - $$1.on(el, 'click', this.cb.toggle); - - Header$1.addShortcut('gallery', el, 530); - - return Callbacks.Post.push({ - name: 'Gallery', - cb: this.node - }); - }, - - node() { - return (() => { - const result = []; - for (var file of this.files) { - if (file.thumb) { - if (Gallery.nodes) { - Gallery.generateThumb(this, file); - Gallery.nodes.total.textContent = Gallery.images.length; - } - - if (!Conf['Image Expansion'] && ((g.SITE.software !== 'tinyboard') || !Main$1.jsEnabled)) { - result.push($$1.on(file.thumbLink, 'click', Gallery.cb.image)); - } else { - result.push(undefined); - } - } - } - return result; - })(); - }, - - build(image) { - let dialog, thumb; - const {cb} = Gallery; - - if (Conf['Fullscreen Gallery']) { - $$1.one(d$1, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', () => $$1.on(d$1, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', cb.close)); - doc$1.mozRequestFullScreen?.(); - doc$1.webkitRequestFullScreen?.(Element.ALLOW_KEYBOARD_INPUT); - } - - Gallery.images = []; - const nodes = (Gallery.nodes = {}); - Gallery.fileIDs = dict(); - Gallery.slideshow = false; - - nodes.el = (dialog = $$1.el('div', - {id: 'a-gallery'})); - $$1.extend(dialog, {innerHTML: galleryPage }); - - const object = { - buttons: '.gal-buttons', - frame: '.gal-image', - name: '.gal-name', - count: '.count', - total: '.total', - sauce: '.gal-sauce', - thumbs: '.gal-thumbnails', - next: '.gal-image a', - current: '.gal-image img' - }; - for (var key in object) { var value = object[key]; nodes[key] = $$1(value, dialog); } - - const menuButton = $$1('.menu-button', dialog); - nodes.menu = new UI.Menu('gallery'); - - $$1.on(nodes.frame, 'click', cb.blank); - if (Conf['Mouse Wheel Volume']) { $$1.on(nodes.frame, 'wheel', Volume.wheel); } - $$1.on(nodes.next, 'click', cb.click); - $$1.on(nodes.name, 'click', ImageCommon.download); - - $$1.on($$1('.gal-prev', dialog), 'click', cb.prev); - $$1.on($$1('.gal-next', dialog), 'click', cb.next); - $$1.on($$1('.gal-start', dialog), 'click', cb.start); - $$1.on($$1('.gal-stop', dialog), 'click', cb.stop); - $$1.on($$1('.gal-close', dialog), 'click', cb.close); - - $$1.on(menuButton, 'click', function(e) { - return nodes.menu.toggle(e, this, g); - }); - - for (var entry of Gallery.menu.createSubEntries()) { - entry.order = 0; - nodes.menu.addEntry(entry); - } - - $$1.on(d$1, 'keydown', cb.keybinds); - if (Conf['Keybinds']) { $$1.off(d$1, 'keydown', Keybinds.keydown); } - - $$1.on(window, 'resize', Gallery.cb.setHeight); - - for (var postThumb of $$(g.SITE.selectors.file.thumb)) { - var post; - if (!(post = Get$1.postFromNode(postThumb))) { continue; } - for (var file of post.files) { - if (file.thumb) { - Gallery.generateThumb(post, file); - // If no image to open is given, pick image we have scrolled to. - if (!image && Gallery.fileIDs[`${post.fullID}.${file.index}`]) { - var candidate = file.thumbLink; - if ((Header$1.getTopOf(candidate) + candidate.getBoundingClientRect().height) >= 0) { - image = candidate; - } - } - } - } - } - $$1.addClass(doc$1, 'gallery-open'); - - $$1.add(d$1.body, dialog); - - nodes.thumbs.scrollTop = 0; - nodes.current.parentElement.scrollTop = 0; - - if (image) { thumb = $$1(`[href='${image.href}']`, nodes.thumbs); } - if (!thumb) { thumb = Gallery.images[Gallery.images.length-1]; } - if (thumb) { Gallery.open(thumb); } - - doc$1.style.overflow = 'hidden'; - return nodes.total.textContent = Gallery.images.length; - }, - - generateThumb(post, file) { - if (post.isClone || post.isHidden) { return; } - if (!file || !file.thumb || (!file.isImage && !file.isVideo && !Conf['PDF in Gallery'])) { return; } - if (Gallery.fileIDs[`${post.fullID}.${file.index}`]) { return; } - - Gallery.fileIDs[`${post.fullID}.${file.index}`] = true; - - const thumb = $$1.el('a', { - className: 'gal-thumb', - href: file.url, - target: '_blank', - title: file.name - } - ); - - thumb.dataset.id = Gallery.images.length; - thumb.dataset.post = post.fullID; - thumb.dataset.file = file.index; - - const thumbImg = file.thumb.cloneNode(false); - thumbImg.style.cssText = ''; - $$1.add(thumb, thumbImg); - - $$1.on(thumb, 'click', Gallery.cb.open); - - Gallery.images.push(thumb); - return $$1.add(Gallery.nodes.thumbs, thumb); - }, - - load(thumb, errorCB) { - const ext = thumb.href.match(/\w*$/); - const elType = $$1.getOwn({'webm': 'video', 'mp4': 'video', 'ogv': 'video', 'pdf': 'iframe'}, ext) || 'img'; - const file = $$1.el(elType); - $$1.extend(file.dataset, thumb.dataset); - $$1.on(file, 'error', errorCB); - file.src = thumb.href; - return file; - }, - - open(thumb) { - let el, file, post; - const {nodes} = Gallery; - const oldID = +nodes.current.dataset.id; - const newID = +thumb.dataset.id; - - // Highlight, center selected thumbnail - if (el = Gallery.images[oldID]) { $$1.rmClass(el, 'gal-highlight'); } - $$1.addClass(thumb, 'gal-highlight'); - nodes.thumbs.scrollTop = (thumb.offsetTop + (thumb.offsetHeight/2)) - (nodes.thumbs.clientHeight/2); - - // Load image or use preloaded image - if (Gallery.cache?.dataset.id === (''+newID)) { - file = Gallery.cache; - $$1.off(file, 'error', Gallery.cacheError); - $$1.on(file, 'error', Gallery.error); - } else { - file = Gallery.load(thumb, Gallery.error); - } - - // Replace old image with new one - $$1.off(nodes.current, 'error', Gallery.error); - ImageCommon.pause(nodes.current); - $$1.replace(nodes.current, file); - nodes.current = file; - - if (file.nodeName === 'VIDEO') { - file.loop = true; - Volume.setup(file); - if (Conf['Autoplay']) { file.play(); } - if (Conf['Show Controls']) { ImageCommon.addControls(file); } - } - - doc$1.classList.toggle('gal-pdf', file.nodeName === 'IFRAME'); - Gallery.cb.setHeight(); - nodes.count.textContent = +thumb.dataset.id + 1; - nodes.name.download = (nodes.name.textContent = thumb.title); - nodes.name.href = thumb.href; - nodes.frame.scrollTop = 0; - nodes.next.focus(); - - // Set sauce links - $$1.rmAll(nodes.sauce); - if (Conf['Sauce'] && Sauce.links && (post = g.posts.get(file.dataset.post))) { - const sauces = []; - for (var link of Sauce.links) { - var node; - if (node = Sauce.createSauceLink(link, post, post.files[+file.dataset.file])) { - sauces.push($$1.tn(' '), node); - } - } - $$1.add(nodes.sauce, sauces); - } - - // Continue slideshow if moving forward, stop otherwise - if (Gallery.slideshow && ((newID > oldID) || ((oldID === (Gallery.images.length-1)) && (newID === 0)))) { - Gallery.setupTimer(); - } else { - Gallery.cb.stop(); - } - - // Scroll to post - if (Conf['Scroll to Post'] && (post = g.posts.get(file.dataset.post))) { - Header$1.scrollTo(post.nodes.root); - } - - // Preload next image - if (isNaN(oldID) || (newID === ((oldID + 1) % Gallery.images.length))) { - return Gallery.cache = Gallery.load(Gallery.images[(newID + 1) % Gallery.images.length], Gallery.cacheError); - } - }, - - error() { - if (this.error?.code === MediaError.MEDIA_ERR_DECODE) { - return new Notice('error', 'Corrupt or unplayable video', 30); - } - if (ImageCommon.isFromArchive(this)) { return; } - const post = g.posts.get(this.dataset.post); - const file = post.files[+this.dataset.file]; - return ImageCommon.error(this, post, file, null, url => { - if (!url) { return; } - Gallery.images[+this.dataset.id].href = url; - if (Gallery.nodes.current === this) { return this.src = url; } - }); - }, - - cacheError() { - return delete Gallery.cache; - }, - - cleanupTimer() { - clearTimeout(Gallery.timeoutID); - const {current} = Gallery.nodes; - $$1.off(current, 'canplaythrough load', Gallery.startTimer); - return $$1.off(current, 'ended', Gallery.cb.next); - }, - - startTimer() { - return Gallery.timeoutID = setTimeout(Gallery.checkTimer, Gallery.delay * SECOND); - }, - - setupTimer() { - Gallery.cleanupTimer(); - const {current} = Gallery.nodes; - const isVideo = current.nodeName === 'VIDEO'; - if (isVideo) { current.play(); } - if ((isVideo ? current.readyState >= 4 : current.complete) || (current.nodeName === 'IFRAME')) { - return Gallery.startTimer(); - } else { - return $$1.on(current, (isVideo ? 'canplaythrough' : 'load'), Gallery.startTimer); - } - }, - - checkTimer() { - const {current} = Gallery.nodes; - if ((current.nodeName === 'VIDEO') && !current.paused) { - $$1.on(current, 'ended', Gallery.cb.next); - return current.loop = false; - } else { - return Gallery.cb.next(); - } - }, - - cb: { - keybinds(e) { - let key; - if (!(key = Keybinds.keyCode(e))) { return; } - - const cb = (() => { switch (key) { - case Conf['Close']: case Conf['Open Gallery']: - return Gallery.cb.close; - case Conf['Next Gallery Image']: - return Gallery.cb.next; - case Conf['Advance Gallery']: - return Gallery.cb.advance; - case Conf['Previous Gallery Image']: - return Gallery.cb.prev; - case Conf['Pause']: - return Gallery.cb.pause; - case Conf['Slideshow']: - return Gallery.cb.toggleSlideshow; - case Conf['Rotate image anticlockwise']: - return Gallery.cb.rotateLeft; - case Conf['Rotate image clockwise']: - return Gallery.cb.rotateRight; - case Conf['Download Gallery Image']: - return Gallery.cb.download; - } })(); - - if (!cb) { return; } - e.stopPropagation(); - e.preventDefault(); - return cb(); - }, - - open(e) { - if (e) { e.preventDefault(); } - if (this) { return Gallery.open(this); } - }, - - image(e) { - e.preventDefault(); - e.stopPropagation(); - return Gallery.build(this); - }, - - prev() { - return Gallery.cb.open.call( - Gallery.images[+Gallery.nodes.current.dataset.id - 1] || Gallery.images[Gallery.images.length - 1] - ); - }, - next() { - return Gallery.cb.open.call( - Gallery.images[+Gallery.nodes.current.dataset.id + 1] || Gallery.images[0] - ); - }, - - click(e) { - if (ImageCommon.onControls(e)) { return; } - e.preventDefault(); - return Gallery.cb.advance(); - }, - - advance() { if (!Conf['Autoplay'] && Gallery.nodes.current.paused) { return Gallery.nodes.current.play(); } else { return Gallery.cb.next(); } }, - toggle() { return (Gallery.nodes ? Gallery.cb.close : Gallery.build)(); }, - blank(e) { if (e.target === this) { return Gallery.cb.close(); } }, - toggleSlideshow() { return Gallery.cb[Gallery.slideshow ? 'stop' : 'start'](); }, - - download() { - const name = $$1('.gal-name'); - return name.click(); - }, - - pause() { - Gallery.cb.stop(); - const {current} = Gallery.nodes; - if (current.nodeName === 'VIDEO') { return current[current.paused ? 'play' : 'pause'](); } - }, - - start() { - $$1.addClass(Gallery.nodes.buttons, 'gal-playing'); - Gallery.slideshow = true; - return Gallery.setupTimer(); - }, - - stop() { - if (!Gallery.slideshow) { return; } - Gallery.cleanupTimer(); - const {current} = Gallery.nodes; - if (current.nodeName === 'VIDEO') { current.loop = true; } - $$1.rmClass(Gallery.nodes.buttons, 'gal-playing'); - return Gallery.slideshow = false; - }, - - rotateLeft() { return Gallery.cb.rotate(270); }, - rotateRight() { return Gallery.cb.rotate(90); }, - - rotate: debounce(100, function(delta) { - const {current} = Gallery.nodes; - if (current.nodeName === 'IFRAME') { return; } - current.dataRotate = ((current.dataRotate || 0) + delta) % 360; - current.style.transform = `rotate(${current.dataRotate}deg)`; - return Gallery.cb.setHeight(); - }), - - close() { - $$1.off(Gallery.nodes.current, 'error', Gallery.error); - ImageCommon.pause(Gallery.nodes.current); - $$1.rm(Gallery.nodes.el); - $$1.rmClass(doc$1, 'gallery-open'); - if (Conf['Fullscreen Gallery']) { - $$1.off(d$1, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', Gallery.cb.close); - d$1.mozCancelFullScreen?.(); - d$1.webkitExitFullscreen?.(); - } - delete Gallery.nodes; - delete Gallery.fileIDs; - doc$1.style.overflow = ''; - - $$1.off(d$1, 'keydown', Gallery.cb.keybinds); - if (Conf['Keybinds']) { $$1.on(d$1, 'keydown', Keybinds.keydown); } - $$1.off(window, 'resize', Gallery.cb.setHeight); - return clearTimeout(Gallery.timeoutID); - }, - - setFitness() { - return (this.checked ? $$1.addClass : $$1.rmClass)(doc$1, `gal-${this.name.toLowerCase().replace(/\s+/g, '-')}`); - }, - - setHeight: debounce(100, function () { - let dim, margin, minHeight; - const {current, frame} = Gallery.nodes; - const {style} = current; - - if (Conf['Stretch to Fit'] && (dim = g.posts.get(current.dataset.post)?.files[+current.dataset.file].dimensions)) { - const [width, height] = dim.split('x'); - let containerWidth = frame.clientWidth; - let containerHeight = doc$1.clientHeight - 25; - if (((current.dataRotate || 0) % 180) === 90) { - [containerWidth, containerHeight] = [containerHeight, containerWidth]; - } - minHeight = Math.min(containerHeight, (height / width) * containerWidth); - style.minHeight = minHeight + 'px'; - style.minWidth = ((width / height) * minHeight) + 'px'; - } else { - style.minHeight = (style.minWidth = ''); - } - - if (((current.dataRotate || 0) % 180) === 90) { - style.maxWidth = Conf['Fit Height'] ? `${doc$1.clientHeight - 25}px` : 'none'; - style.maxHeight = Conf['Fit Width'] ? `${frame.clientWidth}px` : 'none'; - margin = (current.clientWidth - current.clientHeight)/2; - return style.margin = `${margin}px ${-margin}px`; - } else { - return style.maxWidth = (style.maxHeight = (style.margin = '')); - } - }), - - setDelay() { return Gallery.delay = +this.value; } - }, - - menu: { - init() { - if (!Gallery.enabled) { return; } - - const el = $$1.el('span', { - textContent: 'Gallery', - className: 'gallery-link' - } - ); - - return Header$1.menu.addEntry({ - el, - order: 105, - subEntries: Gallery.menu.createSubEntries() - }); - }, - - createSubEntry(name) { - const label = UI.checkbox(name, name); - const input = label.firstElementChild; - if (['Hide Thumbnails', 'Fit Width', 'Fit Height'].includes(name)) { $$1.on(input, 'change', Gallery.cb.setFitness); } - $$1.event('change', null, input); - $$1.on(input, 'change', $$1.cb.checked); - if (['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit'].includes(name)) { $$1.on(input, 'change', Gallery.cb.setHeight); } - return {el: label}; - }, - - createSubEntries() { - const subEntries = (['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit', 'Scroll to Post'].map((item) => Gallery.menu.createSubEntry(item))); - - const delayLabel = $$1.el('label', {innerHTML: 'Slide Delay: '}); - const delayInput = delayLabel.firstElementChild; - delayInput.value = Gallery.delay; - $$1.on(delayInput, 'change', Gallery.cb.setDelay); - $$1.on(delayInput, 'change', $$1.cb.value); - subEntries.push({el: delayLabel}); - - return subEntries; - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + + var Gallery = { + init() { + if (!(this.enabled = Conf['Gallery'] && ['index', 'thread'].includes(g.VIEW))) { return; } + + this.delay = Conf['Slide Delay']; + + const el = $$1.el('a', { + href: 'javascript:;', + title: 'Gallery', + textContent: '🖼︎', + }); + + $$1.on(el, 'click', this.cb.toggle); + + Header$1.addShortcut('gallery', el, 530); + + return Callbacks.Post.push({ + name: 'Gallery', + cb: this.node + }); + }, + + node() { + return (() => { + const result = []; + for (var file of this.files) { + if (file.thumb) { + if (Gallery.nodes) { + Gallery.generateThumb(this, file); + Gallery.nodes.total.textContent = Gallery.images.length; + } + + if (!Conf['Image Expansion'] && ((g.SITE.software !== 'tinyboard') || !Main$1.jsEnabled)) { + result.push($$1.on(file.thumbLink, 'click', Gallery.cb.image)); + } else { + result.push(undefined); + } + } + } + return result; + })(); + }, + + build(image) { + let dialog, thumb; + const {cb} = Gallery; + + if (Conf['Fullscreen Gallery']) { + $$1.one(d$1, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', () => $$1.on(d$1, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', cb.close)); + doc$1.mozRequestFullScreen?.(); + doc$1.webkitRequestFullScreen?.(Element.ALLOW_KEYBOARD_INPUT); + } + + Gallery.images = []; + const nodes = (Gallery.nodes = {}); + Gallery.fileIDs = dict(); + Gallery.slideshow = false; + + nodes.el = (dialog = $$1.el('div', + {id: 'a-gallery'})); + $$1.extend(dialog, {innerHTML: galleryPage }); + + const object = { + buttons: '.gal-buttons', + frame: '.gal-image', + name: '.gal-name', + count: '.count', + total: '.total', + sauce: '.gal-sauce', + thumbs: '.gal-thumbnails', + next: '.gal-image a', + current: '.gal-image img' + }; + for (var key in object) { var value = object[key]; nodes[key] = $$1(value, dialog); } + + const menuButton = $$1('.menu-button', dialog); + nodes.menu = new UI.Menu('gallery'); + + $$1.on(nodes.frame, 'click', cb.blank); + if (Conf['Mouse Wheel Volume']) { $$1.on(nodes.frame, 'wheel', Volume.wheel); } + $$1.on(nodes.next, 'click', cb.click); + $$1.on(nodes.name, 'click', ImageCommon.download); + + $$1.on($$1('.gal-prev', dialog), 'click', cb.prev); + $$1.on($$1('.gal-next', dialog), 'click', cb.next); + $$1.on($$1('.gal-start', dialog), 'click', cb.start); + $$1.on($$1('.gal-stop', dialog), 'click', cb.stop); + $$1.on($$1('.gal-close', dialog), 'click', cb.close); + + $$1.on(menuButton, 'click', function(e) { + return nodes.menu.toggle(e, this, g); + }); + + for (var entry of Gallery.menu.createSubEntries()) { + entry.order = 0; + nodes.menu.addEntry(entry); + } + + $$1.on(d$1, 'keydown', cb.keybinds); + if (Conf['Keybinds']) { $$1.off(d$1, 'keydown', Keybinds.keydown); } + + $$1.on(window, 'resize', Gallery.cb.setHeight); + + for (var postThumb of $$(g.SITE.selectors.file.thumb)) { + var post; + if (!(post = Get$1.postFromNode(postThumb))) { continue; } + for (var file of post.files) { + if (file.thumb) { + Gallery.generateThumb(post, file); + // If no image to open is given, pick image we have scrolled to. + if (!image && Gallery.fileIDs[`${post.fullID}.${file.index}`]) { + var candidate = file.thumbLink; + if ((Header$1.getTopOf(candidate) + candidate.getBoundingClientRect().height) >= 0) { + image = candidate; + } + } + } + } + } + $$1.addClass(doc$1, 'gallery-open'); + + $$1.add(d$1.body, dialog); + + nodes.thumbs.scrollTop = 0; + nodes.current.parentElement.scrollTop = 0; + + if (image) { thumb = $$1(`[href='${image.href}']`, nodes.thumbs); } + if (!thumb) { thumb = Gallery.images[Gallery.images.length-1]; } + if (thumb) { Gallery.open(thumb); } + + doc$1.style.overflow = 'hidden'; + return nodes.total.textContent = Gallery.images.length; + }, + + generateThumb(post, file) { + if (post.isClone || post.isHidden) { return; } + if (!file || !file.thumb || (!file.isImage && !file.isVideo && !Conf['PDF in Gallery'])) { return; } + if (Gallery.fileIDs[`${post.fullID}.${file.index}`]) { return; } + + Gallery.fileIDs[`${post.fullID}.${file.index}`] = true; + + const thumb = $$1.el('a', { + className: 'gal-thumb', + href: file.url, + target: '_blank', + title: file.name + } + ); + + thumb.dataset.id = Gallery.images.length; + thumb.dataset.post = post.fullID; + thumb.dataset.file = file.index; + + const thumbImg = file.thumb.cloneNode(false); + thumbImg.style.cssText = ''; + $$1.add(thumb, thumbImg); + + $$1.on(thumb, 'click', Gallery.cb.open); + + Gallery.images.push(thumb); + return $$1.add(Gallery.nodes.thumbs, thumb); + }, + + load(thumb, errorCB) { + const ext = thumb.href.match(/\w*$/); + const elType = $$1.getOwn({'webm': 'video', 'mp4': 'video', 'ogv': 'video', 'pdf': 'iframe'}, ext) || 'img'; + const file = $$1.el(elType); + $$1.extend(file.dataset, thumb.dataset); + $$1.on(file, 'error', errorCB); + file.src = thumb.href; + return file; + }, + + open(thumb) { + let el, file, post; + const {nodes} = Gallery; + const oldID = +nodes.current.dataset.id; + const newID = +thumb.dataset.id; + + // Highlight, center selected thumbnail + if (el = Gallery.images[oldID]) { $$1.rmClass(el, 'gal-highlight'); } + $$1.addClass(thumb, 'gal-highlight'); + nodes.thumbs.scrollTop = (thumb.offsetTop + (thumb.offsetHeight/2)) - (nodes.thumbs.clientHeight/2); + + // Load image or use preloaded image + if (Gallery.cache?.dataset.id === (''+newID)) { + file = Gallery.cache; + $$1.off(file, 'error', Gallery.cacheError); + $$1.on(file, 'error', Gallery.error); + } else { + file = Gallery.load(thumb, Gallery.error); + } + + // Replace old image with new one + $$1.off(nodes.current, 'error', Gallery.error); + ImageCommon.pause(nodes.current); + $$1.replace(nodes.current, file); + nodes.current = file; + + if (file.nodeName === 'VIDEO') { + file.loop = true; + Volume.setup(file); + if (Conf['Autoplay']) { file.play(); } + if (Conf['Show Controls']) { ImageCommon.addControls(file); } + } + + doc$1.classList.toggle('gal-pdf', file.nodeName === 'IFRAME'); + Gallery.cb.setHeight(); + nodes.count.textContent = +thumb.dataset.id + 1; + nodes.name.download = (nodes.name.textContent = thumb.title); + nodes.name.href = thumb.href; + nodes.frame.scrollTop = 0; + nodes.next.focus(); + + // Set sauce links + $$1.rmAll(nodes.sauce); + if (Conf['Sauce'] && Sauce.links && (post = g.posts.get(file.dataset.post))) { + const sauces = []; + for (var link of Sauce.links) { + var node; + if (node = Sauce.createSauceLink(link, post, post.files[+file.dataset.file])) { + sauces.push($$1.tn(' '), node); + } + } + $$1.add(nodes.sauce, sauces); + } + + // Continue slideshow if moving forward, stop otherwise + if (Gallery.slideshow && ((newID > oldID) || ((oldID === (Gallery.images.length-1)) && (newID === 0)))) { + Gallery.setupTimer(); + } else { + Gallery.cb.stop(); + } + + // Scroll to post + if (Conf['Scroll to Post'] && (post = g.posts.get(file.dataset.post))) { + Header$1.scrollTo(post.nodes.root); + } + + // Preload next image + if (isNaN(oldID) || (newID === ((oldID + 1) % Gallery.images.length))) { + return Gallery.cache = Gallery.load(Gallery.images[(newID + 1) % Gallery.images.length], Gallery.cacheError); + } + }, + + error() { + if (this.error?.code === MediaError.MEDIA_ERR_DECODE) { + return new Notice('error', 'Corrupt or unplayable video', 30); + } + if (ImageCommon.isFromArchive(this)) { return; } + const post = g.posts.get(this.dataset.post); + const file = post.files[+this.dataset.file]; + return ImageCommon.error(this, post, file, null, url => { + if (!url) { return; } + Gallery.images[+this.dataset.id].href = url; + if (Gallery.nodes.current === this) { return this.src = url; } + }); + }, + + cacheError() { + return delete Gallery.cache; + }, + + cleanupTimer() { + clearTimeout(Gallery.timeoutID); + const {current} = Gallery.nodes; + $$1.off(current, 'canplaythrough load', Gallery.startTimer); + return $$1.off(current, 'ended', Gallery.cb.next); + }, + + startTimer() { + return Gallery.timeoutID = setTimeout(Gallery.checkTimer, Gallery.delay * SECOND); + }, + + setupTimer() { + Gallery.cleanupTimer(); + const {current} = Gallery.nodes; + const isVideo = current.nodeName === 'VIDEO'; + if (isVideo) { current.play(); } + if ((isVideo ? current.readyState >= 4 : current.complete) || (current.nodeName === 'IFRAME')) { + return Gallery.startTimer(); + } else { + return $$1.on(current, (isVideo ? 'canplaythrough' : 'load'), Gallery.startTimer); + } + }, + + checkTimer() { + const {current} = Gallery.nodes; + if ((current.nodeName === 'VIDEO') && !current.paused) { + $$1.on(current, 'ended', Gallery.cb.next); + return current.loop = false; + } else { + return Gallery.cb.next(); + } + }, + + cb: { + keybinds(e) { + let key; + if (!(key = Keybinds.keyCode(e))) { return; } + + const cb = (() => { switch (key) { + case Conf['Close']: case Conf['Open Gallery']: + return Gallery.cb.close; + case Conf['Next Gallery Image']: + return Gallery.cb.next; + case Conf['Advance Gallery']: + return Gallery.cb.advance; + case Conf['Previous Gallery Image']: + return Gallery.cb.prev; + case Conf['Pause']: + return Gallery.cb.pause; + case Conf['Slideshow']: + return Gallery.cb.toggleSlideshow; + case Conf['Rotate image anticlockwise']: + return Gallery.cb.rotateLeft; + case Conf['Rotate image clockwise']: + return Gallery.cb.rotateRight; + case Conf['Download Gallery Image']: + return Gallery.cb.download; + } })(); + + if (!cb) { return; } + e.stopPropagation(); + e.preventDefault(); + return cb(); + }, + + open(e) { + if (e) { e.preventDefault(); } + if (this) { return Gallery.open(this); } + }, + + image(e) { + e.preventDefault(); + e.stopPropagation(); + return Gallery.build(this); + }, + + prev() { + return Gallery.cb.open.call( + Gallery.images[+Gallery.nodes.current.dataset.id - 1] || Gallery.images[Gallery.images.length - 1] + ); + }, + next() { + return Gallery.cb.open.call( + Gallery.images[+Gallery.nodes.current.dataset.id + 1] || Gallery.images[0] + ); + }, + + click(e) { + if (ImageCommon.onControls(e)) { return; } + e.preventDefault(); + return Gallery.cb.advance(); + }, + + advance() { if (!Conf['Autoplay'] && Gallery.nodes.current.paused) { return Gallery.nodes.current.play(); } else { return Gallery.cb.next(); } }, + toggle() { return (Gallery.nodes ? Gallery.cb.close : Gallery.build)(); }, + blank(e) { if (e.target === this) { return Gallery.cb.close(); } }, + toggleSlideshow() { return Gallery.cb[Gallery.slideshow ? 'stop' : 'start'](); }, + + download() { + const name = $$1('.gal-name'); + return name.click(); + }, + + pause() { + Gallery.cb.stop(); + const {current} = Gallery.nodes; + if (current.nodeName === 'VIDEO') { return current[current.paused ? 'play' : 'pause'](); } + }, + + start() { + $$1.addClass(Gallery.nodes.buttons, 'gal-playing'); + Gallery.slideshow = true; + return Gallery.setupTimer(); + }, + + stop() { + if (!Gallery.slideshow) { return; } + Gallery.cleanupTimer(); + const {current} = Gallery.nodes; + if (current.nodeName === 'VIDEO') { current.loop = true; } + $$1.rmClass(Gallery.nodes.buttons, 'gal-playing'); + return Gallery.slideshow = false; + }, + + rotateLeft() { return Gallery.cb.rotate(270); }, + rotateRight() { return Gallery.cb.rotate(90); }, + + rotate: debounce(100, function(delta) { + const {current} = Gallery.nodes; + if (current.nodeName === 'IFRAME') { return; } + current.dataRotate = ((current.dataRotate || 0) + delta) % 360; + current.style.transform = `rotate(${current.dataRotate}deg)`; + return Gallery.cb.setHeight(); + }), + + close() { + $$1.off(Gallery.nodes.current, 'error', Gallery.error); + ImageCommon.pause(Gallery.nodes.current); + $$1.rm(Gallery.nodes.el); + $$1.rmClass(doc$1, 'gallery-open'); + if (Conf['Fullscreen Gallery']) { + $$1.off(d$1, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', Gallery.cb.close); + d$1.mozCancelFullScreen?.(); + d$1.webkitExitFullscreen?.(); + } + delete Gallery.nodes; + delete Gallery.fileIDs; + doc$1.style.overflow = ''; + + $$1.off(d$1, 'keydown', Gallery.cb.keybinds); + if (Conf['Keybinds']) { $$1.on(d$1, 'keydown', Keybinds.keydown); } + $$1.off(window, 'resize', Gallery.cb.setHeight); + return clearTimeout(Gallery.timeoutID); + }, + + setFitness() { + return (this.checked ? $$1.addClass : $$1.rmClass)(doc$1, `gal-${this.name.toLowerCase().replace(/\s+/g, '-')}`); + }, + + setHeight: debounce(100, function () { + let dim, margin, minHeight; + const {current, frame} = Gallery.nodes; + const {style} = current; + + if (Conf['Stretch to Fit'] && (dim = g.posts.get(current.dataset.post)?.files[+current.dataset.file].dimensions)) { + const [width, height] = dim.split('x'); + let containerWidth = frame.clientWidth; + let containerHeight = doc$1.clientHeight - 25; + if (((current.dataRotate || 0) % 180) === 90) { + [containerWidth, containerHeight] = [containerHeight, containerWidth]; + } + minHeight = Math.min(containerHeight, (height / width) * containerWidth); + style.minHeight = minHeight + 'px'; + style.minWidth = ((width / height) * minHeight) + 'px'; + } else { + style.minHeight = (style.minWidth = ''); + } + + if (((current.dataRotate || 0) % 180) === 90) { + style.maxWidth = Conf['Fit Height'] ? `${doc$1.clientHeight - 25}px` : 'none'; + style.maxHeight = Conf['Fit Width'] ? `${frame.clientWidth}px` : 'none'; + margin = (current.clientWidth - current.clientHeight)/2; + return style.margin = `${margin}px ${-margin}px`; + } else { + return style.maxWidth = (style.maxHeight = (style.margin = '')); + } + }), + + setDelay() { return Gallery.delay = +this.value; } + }, + + menu: { + init() { + if (!Gallery.enabled) { return; } + + const el = $$1.el('span', { + textContent: 'Gallery', + className: 'gallery-link' + } + ); + + return Header$1.menu.addEntry({ + el, + order: 105, + subEntries: Gallery.menu.createSubEntries() + }); + }, + + createSubEntry(name) { + const label = UI.checkbox(name, name); + const input = label.firstElementChild; + if (['Hide Thumbnails', 'Fit Width', 'Fit Height'].includes(name)) { $$1.on(input, 'change', Gallery.cb.setFitness); } + $$1.event('change', null, input); + $$1.on(input, 'change', $$1.cb.checked); + if (['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit'].includes(name)) { $$1.on(input, 'change', Gallery.cb.setHeight); } + return {el: label}; + }, + + createSubEntries() { + const subEntries = (['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit', 'Scroll to Post'].map((item) => Gallery.menu.createSubEntry(item))); + + const delayLabel = $$1.el('label', {innerHTML: 'Slide Delay: '}); + const delayInput = delayLabel.firstElementChild; + delayInput.value = Gallery.delay; + $$1.on(delayInput, 'change', Gallery.cb.setDelay); + $$1.on(delayInput, 'change', $$1.cb.value); + subEntries.push({el: delayLabel}); + + return subEntries; + } + } }; - var EmbeddingPage = `
-
- - × -
-
+ var EmbeddingPage = `
+
+ + × +
+
`; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - - var Embedding = { - init() { - if (!['index', 'thread', 'archive'].includes(g.VIEW) || !Conf['Linkify'] || (!Conf['Embedding'] && !Conf['Link Title'] && !Conf['Cover Preview'])) { return; } - this.types = dict(); - for (var type of this.ordered_types) { this.types[type.key] = type; } - - if (Conf['Embedding'] && (g.VIEW !== 'archive')) { - this.dialog = UI.dialog('embedding', - { innerHTML: EmbeddingPage }); - this.media = $$1('#media-embed', this.dialog); - $$1.one(d$1, '4chanXInitFinished', this.ready); - $$1.on(d$1, 'IndexRefreshInternal', () => g.posts.forEach(function(post) { - for (post of [post, ...post.clones]) { - for (var embed of post.nodes.embedlinks) { - Embedding.cb.catalogRemove.call(embed); - } - } - })); - } - if (Conf['Link Title']) { - return $$1.on(d$1, '4chanXInitFinished PostsInserted', function() { - for (var key in Embedding.types) { - var service = Embedding.types[key]; - if (service.title?.batchSize) { - Embedding.flushTitles(service.title); - } - } - }); - } - }, - - events(post) { - let el, i, items; - if (g.VIEW === 'archive') { return; } - if (Conf['Embedding']) { - i = 0; - items = (post.nodes.embedlinks = $$('.embedder', post.nodes.comment)); - while ((el = items[i++])) { - $$1.on(el, 'click', Embedding.cb.click); - if ($$1.hasClass(el, 'embedded')) { Embedding.cb.toggle.call(el); } - } - } - if (Conf['Cover Preview']) { - i = 0; - items = $$('.linkify', post.nodes.comment); - while ((el = items[i++])) { - var data; - if (data = Embedding.services(el)) { - Embedding.preview(data); - } - } - return; - } - }, - - process(link, post) { - let data; - if (!Conf['Embedding'] && !Conf['Link Title'] && !Conf['Cover Preview']) { return; } - if ($$1.x('ancestor::pre', link)) { return; } - if (data = Embedding.services(link)) { - data.post = post; - if (Conf['Embedding'] && (g.VIEW !== 'archive')) { Embedding.embed(data); } - if (Conf['Link Title']) { Embedding.title(data); } - if (Conf['Cover Preview'] && (g.VIEW !== 'archive')) { return Embedding.preview(data); } - } - }, - - services(link) { - const {href} = link; - for (var type of Embedding.ordered_types) { - var match; - if (match = type.regExp.exec(href)) { - return {key: type.key, uid: match[1], options: match[2], link}; - } - } - }, - - embed(data) { - const {key, uid, options, link, post} = data; - const {href} = link; - - $$1.addClass(link, key.toLowerCase()); - - const embed = $$1.el('a', { - className: 'embedder', - href: 'javascript:;' - } - , - {innerHTML: '(unembed)'}); - - const object = {key, uid, options, href}; - for (var name in object) { var value = object[name]; embed.dataset[name] = value; } - - $$1.on(embed, 'click', Embedding.cb.click); - $$1.after(link, [$$1.tn(' '), embed]); - post.nodes.embedlinks.push(embed); - - if (Conf['Auto-embed'] && !Conf['Floating Embeds'] && !post.isFetchedQuote) { - if ($$1.hasClass(doc$1, 'catalog-mode')) { - return $$1.addClass(embed, 'embed-removed'); - } else { - return Embedding.cb.toggle.call(embed); - } - } - }, - - ready() { - if (!Main$1.isThisPageLegit()) { return; } - $$1.addClass(Embedding.dialog, 'empty'); - $$1.on($$1('.close', Embedding.dialog), 'click', Embedding.closeFloat); - $$1.on($$1('.move', Embedding.dialog), 'mousedown', Embedding.dragEmbed); - $$1.on($$1('.jump', Embedding.dialog), 'click', function() { - if (doc$1.contains(Embedding.lastEmbed)) { return Header$1.scrollTo(Embedding.lastEmbed); } - }); - return $$1.add(d$1.body, Embedding.dialog); - }, - - closeFloat() { - delete Embedding.lastEmbed; - $$1.addClass(Embedding.dialog, 'empty'); - return $$1.replace(Embedding.media.firstChild, $$1.el('div')); - }, - - dragEmbed() { - // only webkit can handle a blocking div - const {style} = Embedding.media; - if (Embedding.dragEmbed.mouseup) { - $$1.off(d$1, 'mouseup', Embedding.dragEmbed); - Embedding.dragEmbed.mouseup = false; - style.pointerEvents = ''; - return; - } - $$1.on(d$1, 'mouseup', Embedding.dragEmbed); - Embedding.dragEmbed.mouseup = true; - return style.pointerEvents = 'none'; - }, - - title(data) { - let service; - const {key, uid, options, link, post} = data; - if (!(service = Embedding.types[key].title)) { return; } - $$1.addClass(link, key.toLowerCase()); - if (service.batchSize) { - (service.queue || (service.queue = [])).push(data); - if (service.queue.length >= service.batchSize) { - return Embedding.flushTitles(service); - } - } else { - return CrossOrigin$1.cache(service.api(uid), (function() { return Embedding.cb.title(this, data); })); - } - }, - - flushTitles(service) { - let data; - const {queue} = service; - if (!queue?.length) { return; } - service.queue = []; - const cb = function() { - for (data of queue) { Embedding.cb.title(this, data); } - }; - return CrossOrigin$1.cache(service.api((() => { - const result = []; - for (data of queue) { result.push(data.uid); - } - return result; - })()), cb); - }, - - preview(data) { - let service; - const {key, uid, link} = data; - if (!(service = Embedding.types[key].preview)) { return; } - return $$1.on(link, 'mouseover', function(e) { - const src = service.url(uid); - const {height} = service; - const el = $$1.el('img', { - src, - id: 'ihover' - } - ); - $$1.add(Header$1.hover, el); - return UI.hover({ - root: link, - el, - latestEvent: e, - endEvents: 'mouseout click', - height - }); - }); - }, - - cb: { - click(e) { - e.preventDefault(); - if (!$$1.hasClass(this, 'embedded') && (Conf['Floating Embeds'] || $$1.hasClass(doc$1, 'catalog-mode'))) { - let div; - if (!(div = Embedding.media.firstChild)) { return; } - $$1.replace(div, Embedding.cb.embed(this)); - Embedding.lastEmbed = Get$1.postFromNode(this).nodes.root; - return $$1.rmClass(Embedding.dialog, 'empty'); - } else { - return Embedding.cb.toggle.call(this); - } - }, - - toggle() { - if ($$1.hasClass(this, "embedded")) { - $$1.rm(this.nextElementSibling); - } else { - $$1.after(this, Embedding.cb.embed(this)); - } - return $$1.toggleClass(this, 'embedded'); - }, - - embed(a) { - // We create an element to embed - let el, type; - const container = $$1.el('div', {className: 'media-embed'}); - $$1.add(container, (el = (type = Embedding.types[a.dataset.key]).el(a))); - - // Set style values. - el.style.cssText = (type.style != null) ? - type.style - : - 'border: none; width: 640px; height: 360px;'; - - return container; - }, - - catalogRemove() { - const isCatalog = $$1.hasClass(doc$1, 'catalog-mode'); - if ((isCatalog && $$1.hasClass(this, 'embedded')) || (!isCatalog && $$1.hasClass(this, 'embed-removed'))) { - Embedding.cb.toggle.call(this); - return $$1.toggleClass(this, 'embed-removed'); - } - }, - - title(req, data) { - let text; - const {key, uid, options, link, post} = data; - const service = Embedding.types[key].title; - - let {status} = req; - if ([200, 304].includes(status) && service.status) { - status = service.status(req.response)[0]; - } - - if (!status) { return; } - - text = `[${key}] ${(() => { switch (status) { - case 200: case 304: - text = service.text(req.response, uid); - if (typeof text === 'string') { - return text; - } else { - return text = link.textContent; - } - case 404: - return "Not Found"; - case 403: case 401: - return "Forbidden or Private"; - default: - return `${status}'d`; - } })() - }`; - - link.dataset.original = link.textContent; - link.textContent = text; - for (var post2 of post.clones) { - for (var link2 of $$('a.linkify', post2.nodes.comment)) { - if (link2.href === link.href) { - if (link2.dataset.original == null) { link2.dataset.original = link2.textContent; } - link2.textContent = text; - } - } - } - } - }, - - ordered_types: [{ - key: 'audio', - regExp: /^[^?#]+\.(?:mp3|m4a|oga|wav|flac)(?:[?#]|$)/i, - style: '', - el(a) { - return $$1.el('audio', { - controls: true, - preload: 'auto', - src: a.dataset.href - } - ); - } - } - , { - key: 'image', - regExp: /^[^?#]+\.(?:gif|png|jpg|jpeg|bmp|webp)(?::\w+)?(?:[?#]|$)/i, - style: '', - el(a) { - const hrefEsc = E(a.dataset.href); - return $$1.el('div', { innerHTML: ``}); - } - } - , { - key: 'video', - regExp: /^[^?#]+\.(?:og[gv]|webm|mp4)(?:[?#]|$)/i, - style: 'max-width: 80vw; max-height: 80vh;', - el(a) { - const el = $$1.el('video', { - hidden: true, - controls: true, - preload: 'auto', - src: a.dataset.href, - loop: ImageHost.test(a.dataset.href.split('/')[2]) - }); - $$1.on(el, 'loadedmetadata', function() { - if ((el.videoHeight === 0) && el.parentNode) { - return $$1.replace(el, Embedding.types.audio.el(a)); - } else { - return el.hidden = false; - } - }); - return el; - } - } - , { - key: 'PeerTube', - regExp: /^(\w+:\/\/[^\/]+\/videos\/watch\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12})(.*)/, - el(a) { - let start; - const options = (start = a.dataset.options.match(/[?&](start=\w+)/)) ? `?${start[1]}` : ''; - const el = $$1.el('iframe', - {src: a.dataset.uid.replace('/videos/watch/', '/videos/embed/') + options}); - el.setAttribute("allowfullscreen", "true"); - return el; - } - } - , { - key: 'BitChute', - regExp: /^\w+:\/\/(?:www\.)?bitchute\.com\/video\/([\w\-]+)/, - el(a) { - const el = $$1.el('iframe', - {src: `https://www.bitchute.com/embed/${a.dataset.uid}/`}); - el.setAttribute("allowfullscreen", "true"); - return el; - } - } - , { - key: 'Clyp', - regExp: /^\w+:\/\/(?:www\.)?clyp\.it\/(\w{8})/, - style: 'border: 0; width: 640px; height: 160px;', - el(a) { - return $$1.el('iframe', - {src: `https://clyp.it/${a.dataset.uid}/widget`}); - }, - title: { - api(uid) { return `https://api.clyp.it/oembed?url=https://clyp.it/${uid}`; }, - text(_) { return _.title; } - } - } - , { - key: 'Dailymotion', - regExp: /^\w+:\/\/(?:(?:www\.)?dailymotion\.com\/(?:embed\/)?video|dai\.ly)\/([A-Za-z0-9]+)[^?]*(.*)/, - el(a) { - let start; - const options = (start = a.dataset.options.match(/[?&](start=\d+)/)) ? `?${start[1]}` : ''; - const el = $$1.el('iframe', - {src: `//www.dailymotion.com/embed/video/${a.dataset.uid}${options}`}); - el.setAttribute("allowfullscreen", "true"); - return el; - }, - title: { - api(uid) { return `https://api.dailymotion.com/video/${uid}`; }, - text(_) { return _.title; } - }, - preview: { - url(uid) { return `https://www.dailymotion.com/thumbnail/video/${uid}`; }, - height: 240 - } - } - , { - key: 'Gfycat', - regExp: /^\w+:\/\/(?:www\.)?gfycat\.com\/(?:iframe\/)?(\w+)/, - el(a) { - const el = $$1.el('iframe', - {src: `//gfycat.com/ifr/${a.dataset.uid}`}); - el.setAttribute("allowfullscreen", "true"); - return el; - } - } - , { - key: 'Gist', - regExp: /^\w+:\/\/gist\.github\.com\/[\w\-]+\/(\w+)/, - style: '', - el: (function() { - let counter = 0; - return function(a) { - const el = $$1.el('pre', { - hidden: true, - id: `gist-embed-${counter++}` - } - ); - CrossOrigin$1.cache(`https://api.github.com/gists/${a.dataset.uid}`, function() { - el.textContent = Object.values(this.response.files)[0].content; - el.className = 'prettyprint'; - $$1.global(() => window.prettyPrint?.((function() {}), document.getElementById(document.currentScript.dataset.id).parentNode) - , {id: el.id}); - return el.hidden = false; - }); - return el; - }; - })(), - title: { - api(uid) { return `https://api.github.com/gists/${uid}`; }, - text({files}) { - for (var file in files) { if (files.hasOwnProperty(file)) { return file; } } - } - } - } - , { - key: 'InstallGentoo', - regExp: /^\w+:\/\/paste\.installgentoo\.com\/view\/(?:raw\/|download\/|embed\/)?(\w+)/, - el(a) { - return $$1.el('iframe', - {src: `https://paste.installgentoo.com/view/embed/${a.dataset.uid}`}); - } - } - , { - key: 'LiveLeak', - regExp: /^\w+:\/\/(?:\w+\.)?liveleak\.com\/.*\?.*[tif]=(\w+)/, - el(a) { - const el = $$1.el('iframe', - {src: `https://www.liveleak.com/e/${a.dataset.uid}`,}); - el.setAttribute("allowfullscreen", "true"); - return el; - } - } - , { - key: 'Loopvid', - regExp: /^\w+:\/\/(?:www\.)?loopvid.appspot.com\/#?((?:pf|kd|lv|gd|gh|db|dx|nn|cp|wu|ig|ky|mf|m2|pc|1c|pi|ni|wl|ko|mm|ic|gc)\/[\w\-\/]+(?:,[\w\-\/]+)*|fc\/\w+\/\d+|https?:\/\/.+)/, - style: 'max-width: 80vw; max-height: 80vh;', - el(a) { - const el = $$1.el('video', { - controls: true, - preload: 'auto', - loop: true - } - ); - if (/^http/.test(a.dataset.uid)) { - $$1.add(el, $$1.el('source', {src: a.dataset.uid})); - return el; - } - const [_, host, names] = a.dataset.uid.match(/(\w+)\/(.*)/); - const types = (() => { switch (host) { - case 'gd': case 'wu': case 'fc': return ['']; - case 'gc': return ['giant', 'fat', 'zippy']; - default: return ['.webm', '.mp4']; - } })(); - for (var name of names.split(',')) { - for (var type of types) { - var base = `${name}${type}`; - var urls = (() => { switch (host) { - // list from src/common.py at http://loopvid.appspot.com/source.html - case 'pf': return [`https://kastden.org/_loopvid_media/pf/${base}`, `https://web.archive.org/web/2/http://a.pomf.se/${base}`]; - case 'kd': return [`https://kastden.org/loopvid/${base}`]; - case 'lv': return [`https://lv.kastden.org/${base}`]; - case 'gd': return [`https://docs.google.com/uc?export=download&id=${base}`]; - case 'gh': return [`https://googledrive.com/host/${base}`]; - case 'db': return [`https://dl.dropboxusercontent.com/u/${base}`]; - case 'dx': return [`https://dl.dropboxusercontent.com/${base}`]; - case 'nn': return [`https://kastden.org/_loopvid_media/nn/${base}`]; - case 'cp': return [`https://copy.com/${base}`]; - case 'wu': return [`http://webmup.com/${base}/vid.webm`]; - case 'ig': return [`https://i.imgur.com/${base}`]; - case 'ky': return [`https://kastden.org/_loopvid_media/ky/${base}`]; - case 'mf': return [`https://kastden.org/_loopvid_media/mf/${base}`, `https://web.archive.org/web/2/https://d.maxfile.ro/${base}`]; - case 'm2': return [`https://kastden.org/_loopvid_media/m2/${base}`]; - case 'pc': return [`https://kastden.org/_loopvid_media/pc/${base}`, `https://web.archive.org/web/2/http://a.pomf.cat/${base}`]; - case '1c': return [`http://b.1339.cf/${base}`]; - case 'pi': return [`https://kastden.org/_loopvid_media/pi/${base}`, `https://web.archive.org/web/2/https://u.pomf.is/${base}`]; - case 'ni': return [`https://kastden.org/_loopvid_media/ni/${base}`, `https://web.archive.org/web/2/https://u.nya.is/${base}`]; - case 'wl': return [`http://webm.land/media/${base}`]; - case 'ko': return [`https://kordy.kastden.org/loopvid/${base}`]; - case 'mm': return [`https://kastden.org/_loopvid_media/mm/${base}`, `https://web.archive.org/web/2/https://my.mixtape.moe/${base}`]; - case 'ic': return [`https://media.8ch.net/file_store/${base}`]; - case 'fc': return [`//${ImageHost.host()}/${base}.webm`]; - case 'gc': return [`https://${type}.gfycat.com/${name}.webm`]; - } })(); - - for (var url of urls) { - $$1.add(el, $$1.el('source', {src: url})); - } - } - } - return el; - } - } - , { - key: 'Openings.moe', - regExp: /^\w+:\/\/openings.moe\/\?video=([^.&=]+)/, - style: 'width: 1280px; height: 720px; max-width: 80vw; max-height: 80vh;', - el(a) { - const el = $$1.el('iframe', - {src: `https://openings.moe/?video=${a.dataset.uid}`,}); - el.setAttribute("allowfullscreen", "true"); - return el; - } - } - , { - key: 'Pastebin', - regExp: /^\w+:\/\/(?:\w+\.)?pastebin\.com\/(?!u\/)(?:[\w.]+(?:\/|\?i\=))?(\w+)/, - el(a) { - return $$1.el('iframe', - {src: `//pastebin.com/embed_iframe.php?i=${a.dataset.uid}`}); - } - } - , { - key: 'SoundCloud', - regExp: /^\w+:\/\/(?:www\.)?(?:soundcloud\.com\/|snd\.sc\/)([\w\-\/]+)/, - style: 'border: 0; width: 500px; height: 400px;', - el(a) { - return $$1.el('iframe', - {src: `https://w.soundcloud.com/player/?visual=true&show_comments=false&url=https%3A%2F%2Fsoundcloud.com%2F${encodeURIComponent(a.dataset.uid)}`}); - }, - title: { - api(uid) { return `${location.protocol}//soundcloud.com/oembed?format=json&url=https%3A%2F%2Fsoundcloud.com%2F${encodeURIComponent(uid)}`; }, - text(_) { return _.title; } - } - } - , { - key: 'StrawPoll', - regExp: /^\w+:\/\/(?:www\.)?strawpoll\.me\/(?:embed_\d+\/)?(\d+(?:\/r)?)/, - style: 'border: 0; width: 600px; height: 406px;', - el(a) { - return $$1.el('iframe', - {src: `https://www.strawpoll.me/embed_1/${a.dataset.uid}`}); - } - } - , { - key: 'Streamable', - regExp: /^\w+:\/\/(?:www\.)?streamable\.com\/(\w+)/, - el(a) { - const el = $$1.el('iframe', - {src: `https://streamable.com/o/${a.dataset.uid}`}); - el.setAttribute("allowfullscreen", "true"); - return el; - }, - title: { - api(uid) { return `https://api.streamable.com/oembed?url=https://streamable.com/${uid}`; }, - text(_) { return _.title; } - } - } - , { - key: 'TwitchTV', - regExp: /^\w+:\/\/(?:www\.|secure\.|clips\.|m\.)?twitch\.tv\/(\w[^#\&\?]*)/, - el(a) { - let url; - let m = a.dataset.href.match(/^\w+:\/\/(?:(clips\.)|\w+\.)?twitch\.tv\/(?:\w+\/)?(clip\/)?(\w[^#\&\?]*)/); - if (m[1] || m[2]) { - url = `//clips.twitch.tv/embed?clip=${m[3]}&parent=${location.hostname}`; - } else { - let time; - m = a.dataset.uid.match(/(\w+)(?:\/(?:v\/)?(\d+))?/); - url = `//player.twitch.tv/?${m[2] ? `video=v${m[2]}` : `channel=${m[1]}`}&autoplay=false&parent=${location.hostname}`; - if (time = a.dataset.href.match(/\bt=(\w+)/)) { - url += `&time=${time[1]}`; - } - } - const el = $$1.el('iframe', - {src: url}); - el.setAttribute("allowfullscreen", "true"); - return el; - } - } - , { - key: 'Twitter', - regExp: /^\w+:\/\/(?:www\.|mobile\.)?twitter\.com\/(\w+\/status\/\d+)/, - style: 'border: none; width: 550px; height: 250px; overflow: hidden; resize: both;', - el(a) { - const el = $$1.el('iframe'); - $$1.on(el, 'load', function() { - return this.contentWindow.postMessage({element: 't', query: 'height'}, 'https://twitframe.com'); - }); - var onMessage = function(e) { - if ((e.source === el.contentWindow) && (e.origin === 'https://twitframe.com')) { - $$1.off(window, 'message', onMessage); - return (cont || el).style.height = `${+$$1.minmax(e.data.height, 250, 0.8 * doc$1.clientHeight)}px`; - } - }; - $$1.on(window, 'message', onMessage); - el.src = `https://twitframe.com/show?url=https://twitter.com/${a.dataset.uid}`; - if ($$1.engine === 'gecko') { - // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=680823 - el.style.cssText = 'border: none; width: 100%; height: 100%;'; - var cont = $$1.el('div'); - $$1.add(cont, el); - return cont; - } else { - return el; - } - } - } - , { - key: 'VidLii', - regExp: /^\w+:\/\/(?:www\.)?vidlii\.com\/watch\?v=(\w{11})/, - style: 'border: none; width: 640px; height: 392px;', - el(a) { - const el = $$1.el('iframe', - {src: `https://www.vidlii.com/embed?v=${a.dataset.uid}&a=0`}); - el.setAttribute("allowfullscreen", "true"); - return el; - } - } - , { - key: 'Vimeo', - regExp: /^\w+:\/\/(?:www\.)?vimeo\.com\/(\d+)/, - el(a) { - const el = $$1.el('iframe', - {src: `//player.vimeo.com/video/${a.dataset.uid}?wmode=opaque`}); - el.setAttribute("allowfullscreen", "true"); - return el; - }, - title: { - api(uid) { return `https://vimeo.com/api/oembed.json?url=https://vimeo.com/${uid}`; }, - text(_) { return _.title; } - } - } - , { - key: 'Vine', - regExp: /^\w+:\/\/(?:www\.)?vine\.co\/v\/(\w+)/, - style: 'border: none; width: 500px; height: 500px;', - el(a) { - return $$1.el('iframe', - {src: `https://vine.co/v/${a.dataset.uid}/card`}); - } - } - , { - key: 'Vocaroo', - regExp: /^\w+:\/\/(?:(?:www\.|old\.)?vocaroo\.com|voca\.ro)\/((?:i\/)?\w+)/, - style: '', - el(a) { - const el = $$1.el('iframe'); - el.width = 300; - el.height = 60; - el.setAttribute('frameborder', 0); - el.src = `https://vocaroo.com/embed/${a.dataset.uid.replace(/^i\//, '')}?autoplay=0`; - return el; - } - } - , { - key: 'YouTube', - regExp: /^\w+:\/\/(?:youtu.be\/|[\w.]*youtube[\w.]*\/.*(?:v=|\bembed\/|\bv\/|live\/))([\w\-]{11})(.*)/, - el(a) { - let start = a.dataset.options.match(/\b(?:star)?t\=(\w+)/); - if (start) { start = start[1]; } - if (start && !/^\d+$/.test(start)) { - start += ' 0h0m0s'; - start = (3600 * start.match(/(\d+)h/)[1]) + (60 * start.match(/(\d+)m/)[1]) + (1 * start.match(/(\d+)s/)[1]); - } - const el = $$1.el('iframe', - {src: `//www.youtube.com/embed/${a.dataset.uid}?rel=0&wmode=opaque${start ? '&start=' + start : ''}`}); - el.setAttribute("allowfullscreen", "true"); - return el; - }, - title: { - api(uid) { return `https://www.youtube.com/oembed?url=https%3A//www.youtube.com/watch%3Fv%3D${uid}&format=json`; }, - text(_) { return _.title; }, - status(_) { - if (_.error) { - const m = _.error.match(/^(\d*)\s*(.*)/); - return [+m[1], m[2]]; - } else { - return [200, 'OK']; - } - } - }, - preview: { - url(uid) { return `https://img.youtube.com/vi/${uid}/0.jpg`; }, - height: 360 - } - } - ] + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + + var Embedding = { + init() { + if (!['index', 'thread', 'archive'].includes(g.VIEW) || !Conf['Linkify'] || (!Conf['Embedding'] && !Conf['Link Title'] && !Conf['Cover Preview'])) { return; } + this.types = dict(); + for (var type of this.ordered_types) { this.types[type.key] = type; } + + if (Conf['Embedding'] && (g.VIEW !== 'archive')) { + this.dialog = UI.dialog('embedding', + { innerHTML: EmbeddingPage }); + this.media = $$1('#media-embed', this.dialog); + $$1.one(d$1, '4chanXInitFinished', this.ready); + $$1.on(d$1, 'IndexRefreshInternal', () => g.posts.forEach(function(post) { + for (post of [post, ...post.clones]) { + for (var embed of post.nodes.embedlinks) { + Embedding.cb.catalogRemove.call(embed); + } + } + })); + } + if (Conf['Link Title']) { + return $$1.on(d$1, '4chanXInitFinished PostsInserted', function() { + for (var key in Embedding.types) { + var service = Embedding.types[key]; + if (service.title?.batchSize) { + Embedding.flushTitles(service.title); + } + } + }); + } + }, + + events(post) { + let el, i, items; + if (g.VIEW === 'archive') { return; } + if (Conf['Embedding']) { + i = 0; + items = (post.nodes.embedlinks = $$('.embedder', post.nodes.comment)); + while ((el = items[i++])) { + $$1.on(el, 'click', Embedding.cb.click); + if ($$1.hasClass(el, 'embedded')) { Embedding.cb.toggle.call(el); } + } + } + if (Conf['Cover Preview']) { + i = 0; + items = $$('.linkify', post.nodes.comment); + while ((el = items[i++])) { + var data; + if (data = Embedding.services(el)) { + Embedding.preview(data); + } + } + return; + } + }, + + process(link, post) { + let data; + if (!Conf['Embedding'] && !Conf['Link Title'] && !Conf['Cover Preview']) { return; } + if ($$1.x('ancestor::pre', link)) { return; } + if (data = Embedding.services(link)) { + data.post = post; + if (Conf['Embedding'] && (g.VIEW !== 'archive')) { Embedding.embed(data); } + if (Conf['Link Title']) { Embedding.title(data); } + if (Conf['Cover Preview'] && (g.VIEW !== 'archive')) { return Embedding.preview(data); } + } + }, + + services(link) { + const {href} = link; + for (var type of Embedding.ordered_types) { + var match; + if (match = type.regExp.exec(href)) { + return {key: type.key, uid: match[1], options: match[2], link}; + } + } + }, + + embed(data) { + const {key, uid, options, link, post} = data; + const {href} = link; + + $$1.addClass(link, key.toLowerCase()); + + const embed = $$1.el('a', { + className: 'embedder', + href: 'javascript:;' + } + , + {innerHTML: '(unembed)'}); + + const object = {key, uid, options, href}; + for (var name in object) { var value = object[name]; embed.dataset[name] = value; } + + $$1.on(embed, 'click', Embedding.cb.click); + $$1.after(link, [$$1.tn(' '), embed]); + post.nodes.embedlinks.push(embed); + + if (Conf['Auto-embed'] && !Conf['Floating Embeds'] && !post.isFetchedQuote) { + if ($$1.hasClass(doc$1, 'catalog-mode')) { + return $$1.addClass(embed, 'embed-removed'); + } else { + return Embedding.cb.toggle.call(embed); + } + } + }, + + ready() { + if (!Main$1.isThisPageLegit()) { return; } + $$1.addClass(Embedding.dialog, 'empty'); + $$1.on($$1('.close', Embedding.dialog), 'click', Embedding.closeFloat); + $$1.on($$1('.move', Embedding.dialog), 'mousedown', Embedding.dragEmbed); + $$1.on($$1('.jump', Embedding.dialog), 'click', function() { + if (doc$1.contains(Embedding.lastEmbed)) { return Header$1.scrollTo(Embedding.lastEmbed); } + }); + return $$1.add(d$1.body, Embedding.dialog); + }, + + closeFloat() { + delete Embedding.lastEmbed; + $$1.addClass(Embedding.dialog, 'empty'); + return $$1.replace(Embedding.media.firstChild, $$1.el('div')); + }, + + dragEmbed() { + // only webkit can handle a blocking div + const {style} = Embedding.media; + if (Embedding.dragEmbed.mouseup) { + $$1.off(d$1, 'mouseup', Embedding.dragEmbed); + Embedding.dragEmbed.mouseup = false; + style.pointerEvents = ''; + return; + } + $$1.on(d$1, 'mouseup', Embedding.dragEmbed); + Embedding.dragEmbed.mouseup = true; + return style.pointerEvents = 'none'; + }, + + title(data) { + let service; + const {key, uid, options, link, post} = data; + if (!(service = Embedding.types[key].title)) { return; } + $$1.addClass(link, key.toLowerCase()); + if (service.batchSize) { + (service.queue || (service.queue = [])).push(data); + if (service.queue.length >= service.batchSize) { + return Embedding.flushTitles(service); + } + } else { + return CrossOrigin$1.cache(service.api(uid), (function() { return Embedding.cb.title(this, data); })); + } + }, + + flushTitles(service) { + let data; + const {queue} = service; + if (!queue?.length) { return; } + service.queue = []; + const cb = function() { + for (data of queue) { Embedding.cb.title(this, data); } + }; + return CrossOrigin$1.cache(service.api((() => { + const result = []; + for (data of queue) { result.push(data.uid); + } + return result; + })()), cb); + }, + + preview(data) { + let service; + const {key, uid, link} = data; + if (!(service = Embedding.types[key].preview)) { return; } + return $$1.on(link, 'mouseover', function(e) { + const src = service.url(uid); + const {height} = service; + const el = $$1.el('img', { + src, + id: 'ihover' + } + ); + $$1.add(Header$1.hover, el); + return UI.hover({ + root: link, + el, + latestEvent: e, + endEvents: 'mouseout click', + height + }); + }); + }, + + cb: { + click(e) { + e.preventDefault(); + if (!$$1.hasClass(this, 'embedded') && (Conf['Floating Embeds'] || $$1.hasClass(doc$1, 'catalog-mode'))) { + let div; + if (!(div = Embedding.media.firstChild)) { return; } + $$1.replace(div, Embedding.cb.embed(this)); + Embedding.lastEmbed = Get$1.postFromNode(this).nodes.root; + return $$1.rmClass(Embedding.dialog, 'empty'); + } else { + return Embedding.cb.toggle.call(this); + } + }, + + toggle() { + if ($$1.hasClass(this, "embedded")) { + $$1.rm(this.nextElementSibling); + } else { + $$1.after(this, Embedding.cb.embed(this)); + } + return $$1.toggleClass(this, 'embedded'); + }, + + embed(a) { + // We create an element to embed + let el, type; + const container = $$1.el('div', {className: 'media-embed'}); + $$1.add(container, (el = (type = Embedding.types[a.dataset.key]).el(a))); + + // Set style values. + el.style.cssText = (type.style != null) ? + type.style + : + 'border: none; width: 640px; height: 360px;'; + + return container; + }, + + catalogRemove() { + const isCatalog = $$1.hasClass(doc$1, 'catalog-mode'); + if ((isCatalog && $$1.hasClass(this, 'embedded')) || (!isCatalog && $$1.hasClass(this, 'embed-removed'))) { + Embedding.cb.toggle.call(this); + return $$1.toggleClass(this, 'embed-removed'); + } + }, + + title(req, data) { + let text; + const {key, uid, options, link, post} = data; + const service = Embedding.types[key].title; + + let {status} = req; + if ([200, 304].includes(status) && service.status) { + status = service.status(req.response)[0]; + } + + if (!status) { return; } + + text = `[${key}] ${(() => { switch (status) { + case 200: case 304: + text = service.text(req.response, uid); + if (typeof text === 'string') { + return text; + } else { + return text = link.textContent; + } + case 404: + return "Not Found"; + case 403: case 401: + return "Forbidden or Private"; + default: + return `${status}'d`; + } })() + }`; + + link.dataset.original = link.textContent; + link.textContent = text; + for (var post2 of post.clones) { + for (var link2 of $$('a.linkify', post2.nodes.comment)) { + if (link2.href === link.href) { + if (link2.dataset.original == null) { link2.dataset.original = link2.textContent; } + link2.textContent = text; + } + } + } + } + }, + + ordered_types: [{ + key: 'audio', + regExp: /^[^?#]+\.(?:mp3|m4a|oga|wav|flac)(?:[?#]|$)/i, + style: '', + el(a) { + return $$1.el('audio', { + controls: true, + preload: 'auto', + src: a.dataset.href + } + ); + } + } + , { + key: 'image', + regExp: /^[^?#]+\.(?:gif|png|jpg|jpeg|bmp|webp)(?::\w+)?(?:[?#]|$)/i, + style: '', + el(a) { + const hrefEsc = E(a.dataset.href); + return $$1.el('div', { innerHTML: ``}); + } + } + , { + key: 'video', + regExp: /^[^?#]+\.(?:og[gv]|webm|mp4)(?:[?#]|$)/i, + style: 'max-width: 80vw; max-height: 80vh;', + el(a) { + const el = $$1.el('video', { + hidden: true, + controls: true, + preload: 'auto', + src: a.dataset.href, + loop: ImageHost.test(a.dataset.href.split('/')[2]) + }); + $$1.on(el, 'loadedmetadata', function() { + if ((el.videoHeight === 0) && el.parentNode) { + return $$1.replace(el, Embedding.types.audio.el(a)); + } else { + return el.hidden = false; + } + }); + return el; + } + } + , { + key: 'PeerTube', + regExp: /^(\w+:\/\/[^\/]+\/videos\/watch\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12})(.*)/, + el(a) { + let start; + const options = (start = a.dataset.options.match(/[?&](start=\w+)/)) ? `?${start[1]}` : ''; + const el = $$1.el('iframe', + {src: a.dataset.uid.replace('/videos/watch/', '/videos/embed/') + options}); + el.setAttribute("allowfullscreen", "true"); + return el; + } + } + , { + key: 'BitChute', + regExp: /^\w+:\/\/(?:www\.)?bitchute\.com\/video\/([\w\-]+)/, + el(a) { + const el = $$1.el('iframe', + {src: `https://www.bitchute.com/embed/${a.dataset.uid}/`}); + el.setAttribute("allowfullscreen", "true"); + return el; + } + } + , { + key: 'Clyp', + regExp: /^\w+:\/\/(?:www\.)?clyp\.it\/(\w{8})/, + style: 'border: 0; width: 640px; height: 160px;', + el(a) { + return $$1.el('iframe', + {src: `https://clyp.it/${a.dataset.uid}/widget`}); + }, + title: { + api(uid) { return `https://api.clyp.it/oembed?url=https://clyp.it/${uid}`; }, + text(_) { return _.title; } + } + } + , { + key: 'Dailymotion', + regExp: /^\w+:\/\/(?:(?:www\.)?dailymotion\.com\/(?:embed\/)?video|dai\.ly)\/([A-Za-z0-9]+)[^?]*(.*)/, + el(a) { + let start; + const options = (start = a.dataset.options.match(/[?&](start=\d+)/)) ? `?${start[1]}` : ''; + const el = $$1.el('iframe', + {src: `//www.dailymotion.com/embed/video/${a.dataset.uid}${options}`}); + el.setAttribute("allowfullscreen", "true"); + return el; + }, + title: { + api(uid) { return `https://api.dailymotion.com/video/${uid}`; }, + text(_) { return _.title; } + }, + preview: { + url(uid) { return `https://www.dailymotion.com/thumbnail/video/${uid}`; }, + height: 240 + } + } + , { + key: 'Gfycat', + regExp: /^\w+:\/\/(?:www\.)?gfycat\.com\/(?:iframe\/)?(\w+)/, + el(a) { + const el = $$1.el('iframe', + {src: `//gfycat.com/ifr/${a.dataset.uid}`}); + el.setAttribute("allowfullscreen", "true"); + return el; + } + } + , { + key: 'Gist', + regExp: /^\w+:\/\/gist\.github\.com\/[\w\-]+\/(\w+)/, + style: '', + el: (function() { + let counter = 0; + return function(a) { + const el = $$1.el('pre', { + hidden: true, + id: `gist-embed-${counter++}` + } + ); + CrossOrigin$1.cache(`https://api.github.com/gists/${a.dataset.uid}`, function() { + el.textContent = Object.values(this.response.files)[0].content; + el.className = 'prettyprint'; + $$1.global(() => window.prettyPrint?.((function() {}), document.getElementById(document.currentScript.dataset.id).parentNode) + , {id: el.id}); + return el.hidden = false; + }); + return el; + }; + })(), + title: { + api(uid) { return `https://api.github.com/gists/${uid}`; }, + text({files}) { + for (var file in files) { if (files.hasOwnProperty(file)) { return file; } } + } + } + } + , { + key: 'InstallGentoo', + regExp: /^\w+:\/\/paste\.installgentoo\.com\/view\/(?:raw\/|download\/|embed\/)?(\w+)/, + el(a) { + return $$1.el('iframe', + {src: `https://paste.installgentoo.com/view/embed/${a.dataset.uid}`}); + } + } + , { + key: 'LiveLeak', + regExp: /^\w+:\/\/(?:\w+\.)?liveleak\.com\/.*\?.*[tif]=(\w+)/, + el(a) { + const el = $$1.el('iframe', + {src: `https://www.liveleak.com/e/${a.dataset.uid}`,}); + el.setAttribute("allowfullscreen", "true"); + return el; + } + } + , { + key: 'Loopvid', + regExp: /^\w+:\/\/(?:www\.)?loopvid.appspot.com\/#?((?:pf|kd|lv|gd|gh|db|dx|nn|cp|wu|ig|ky|mf|m2|pc|1c|pi|ni|wl|ko|mm|ic|gc)\/[\w\-\/]+(?:,[\w\-\/]+)*|fc\/\w+\/\d+|https?:\/\/.+)/, + style: 'max-width: 80vw; max-height: 80vh;', + el(a) { + const el = $$1.el('video', { + controls: true, + preload: 'auto', + loop: true + } + ); + if (/^http/.test(a.dataset.uid)) { + $$1.add(el, $$1.el('source', {src: a.dataset.uid})); + return el; + } + const [_, host, names] = a.dataset.uid.match(/(\w+)\/(.*)/); + const types = (() => { switch (host) { + case 'gd': case 'wu': case 'fc': return ['']; + case 'gc': return ['giant', 'fat', 'zippy']; + default: return ['.webm', '.mp4']; + } })(); + for (var name of names.split(',')) { + for (var type of types) { + var base = `${name}${type}`; + var urls = (() => { switch (host) { + // list from src/common.py at http://loopvid.appspot.com/source.html + case 'pf': return [`https://kastden.org/_loopvid_media/pf/${base}`, `https://web.archive.org/web/2/http://a.pomf.se/${base}`]; + case 'kd': return [`https://kastden.org/loopvid/${base}`]; + case 'lv': return [`https://lv.kastden.org/${base}`]; + case 'gd': return [`https://docs.google.com/uc?export=download&id=${base}`]; + case 'gh': return [`https://googledrive.com/host/${base}`]; + case 'db': return [`https://dl.dropboxusercontent.com/u/${base}`]; + case 'dx': return [`https://dl.dropboxusercontent.com/${base}`]; + case 'nn': return [`https://kastden.org/_loopvid_media/nn/${base}`]; + case 'cp': return [`https://copy.com/${base}`]; + case 'wu': return [`http://webmup.com/${base}/vid.webm`]; + case 'ig': return [`https://i.imgur.com/${base}`]; + case 'ky': return [`https://kastden.org/_loopvid_media/ky/${base}`]; + case 'mf': return [`https://kastden.org/_loopvid_media/mf/${base}`, `https://web.archive.org/web/2/https://d.maxfile.ro/${base}`]; + case 'm2': return [`https://kastden.org/_loopvid_media/m2/${base}`]; + case 'pc': return [`https://kastden.org/_loopvid_media/pc/${base}`, `https://web.archive.org/web/2/http://a.pomf.cat/${base}`]; + case '1c': return [`http://b.1339.cf/${base}`]; + case 'pi': return [`https://kastden.org/_loopvid_media/pi/${base}`, `https://web.archive.org/web/2/https://u.pomf.is/${base}`]; + case 'ni': return [`https://kastden.org/_loopvid_media/ni/${base}`, `https://web.archive.org/web/2/https://u.nya.is/${base}`]; + case 'wl': return [`http://webm.land/media/${base}`]; + case 'ko': return [`https://kordy.kastden.org/loopvid/${base}`]; + case 'mm': return [`https://kastden.org/_loopvid_media/mm/${base}`, `https://web.archive.org/web/2/https://my.mixtape.moe/${base}`]; + case 'ic': return [`https://media.8ch.net/file_store/${base}`]; + case 'fc': return [`//${ImageHost.host()}/${base}.webm`]; + case 'gc': return [`https://${type}.gfycat.com/${name}.webm`]; + } })(); + + for (var url of urls) { + $$1.add(el, $$1.el('source', {src: url})); + } + } + } + return el; + } + } + , { + key: 'Openings.moe', + regExp: /^\w+:\/\/openings.moe\/\?video=([^.&=]+)/, + style: 'width: 1280px; height: 720px; max-width: 80vw; max-height: 80vh;', + el(a) { + const el = $$1.el('iframe', + {src: `https://openings.moe/?video=${a.dataset.uid}`,}); + el.setAttribute("allowfullscreen", "true"); + return el; + } + } + , { + key: 'Pastebin', + regExp: /^\w+:\/\/(?:\w+\.)?pastebin\.com\/(?!u\/)(?:[\w.]+(?:\/|\?i\=))?(\w+)/, + el(a) { + return $$1.el('iframe', + {src: `//pastebin.com/embed_iframe.php?i=${a.dataset.uid}`}); + } + } + , { + key: 'SoundCloud', + regExp: /^\w+:\/\/(?:www\.)?(?:soundcloud\.com\/|snd\.sc\/)([\w\-\/]+)/, + style: 'border: 0; width: 500px; height: 400px;', + el(a) { + return $$1.el('iframe', + {src: `https://w.soundcloud.com/player/?visual=true&show_comments=false&url=https%3A%2F%2Fsoundcloud.com%2F${encodeURIComponent(a.dataset.uid)}`}); + }, + title: { + api(uid) { return `${location.protocol}//soundcloud.com/oembed?format=json&url=https%3A%2F%2Fsoundcloud.com%2F${encodeURIComponent(uid)}`; }, + text(_) { return _.title; } + } + } + , { + key: 'StrawPoll', + regExp: /^\w+:\/\/(?:www\.)?strawpoll\.me\/(?:embed_\d+\/)?(\d+(?:\/r)?)/, + style: 'border: 0; width: 600px; height: 406px;', + el(a) { + return $$1.el('iframe', + {src: `https://www.strawpoll.me/embed_1/${a.dataset.uid}`}); + } + } + , { + key: 'Streamable', + regExp: /^\w+:\/\/(?:www\.)?streamable\.com\/(\w+)/, + el(a) { + const el = $$1.el('iframe', + {src: `https://streamable.com/o/${a.dataset.uid}`}); + el.setAttribute("allowfullscreen", "true"); + return el; + }, + title: { + api(uid) { return `https://api.streamable.com/oembed?url=https://streamable.com/${uid}`; }, + text(_) { return _.title; } + } + } + , { + key: 'TwitchTV', + regExp: /^\w+:\/\/(?:www\.|secure\.|clips\.|m\.)?twitch\.tv\/(\w[^#\&\?]*)/, + el(a) { + let url; + let m = a.dataset.href.match(/^\w+:\/\/(?:(clips\.)|\w+\.)?twitch\.tv\/(?:\w+\/)?(clip\/)?(\w[^#\&\?]*)/); + if (m[1] || m[2]) { + url = `//clips.twitch.tv/embed?clip=${m[3]}&parent=${location.hostname}`; + } else { + let time; + m = a.dataset.uid.match(/(\w+)(?:\/(?:v\/)?(\d+))?/); + url = `//player.twitch.tv/?${m[2] ? `video=v${m[2]}` : `channel=${m[1]}`}&autoplay=false&parent=${location.hostname}`; + if (time = a.dataset.href.match(/\bt=(\w+)/)) { + url += `&time=${time[1]}`; + } + } + const el = $$1.el('iframe', + {src: url}); + el.setAttribute("allowfullscreen", "true"); + return el; + } + } + , { + key: 'Twitter', + regExp: /^\w+:\/\/(?:www\.|mobile\.)?twitter\.com\/(\w+\/status\/\d+)/, + style: 'border: none; width: 550px; height: 250px; overflow: hidden; resize: both;', + el(a) { + const el = $$1.el('iframe'); + $$1.on(el, 'load', function() { + return this.contentWindow.postMessage({element: 't', query: 'height'}, 'https://twitframe.com'); + }); + var onMessage = function(e) { + if ((e.source === el.contentWindow) && (e.origin === 'https://twitframe.com')) { + $$1.off(window, 'message', onMessage); + return (cont || el).style.height = `${+$$1.minmax(e.data.height, 250, 0.8 * doc$1.clientHeight)}px`; + } + }; + $$1.on(window, 'message', onMessage); + el.src = `https://twitframe.com/show?url=https://twitter.com/${a.dataset.uid}`; + if ($$1.engine === 'gecko') { + // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=680823 + el.style.cssText = 'border: none; width: 100%; height: 100%;'; + var cont = $$1.el('div'); + $$1.add(cont, el); + return cont; + } else { + return el; + } + } + } + , { + key: 'VidLii', + regExp: /^\w+:\/\/(?:www\.)?vidlii\.com\/watch\?v=(\w{11})/, + style: 'border: none; width: 640px; height: 392px;', + el(a) { + const el = $$1.el('iframe', + {src: `https://www.vidlii.com/embed?v=${a.dataset.uid}&a=0`}); + el.setAttribute("allowfullscreen", "true"); + return el; + } + } + , { + key: 'Vimeo', + regExp: /^\w+:\/\/(?:www\.)?vimeo\.com\/(\d+)/, + el(a) { + const el = $$1.el('iframe', + {src: `//player.vimeo.com/video/${a.dataset.uid}?wmode=opaque`}); + el.setAttribute("allowfullscreen", "true"); + return el; + }, + title: { + api(uid) { return `https://vimeo.com/api/oembed.json?url=https://vimeo.com/${uid}`; }, + text(_) { return _.title; } + } + } + , { + key: 'Vine', + regExp: /^\w+:\/\/(?:www\.)?vine\.co\/v\/(\w+)/, + style: 'border: none; width: 500px; height: 500px;', + el(a) { + return $$1.el('iframe', + {src: `https://vine.co/v/${a.dataset.uid}/card`}); + } + } + , { + key: 'Vocaroo', + regExp: /^\w+:\/\/(?:(?:www\.|old\.)?vocaroo\.com|voca\.ro)\/((?:i\/)?\w+)/, + style: '', + el(a) { + const el = $$1.el('iframe'); + el.width = 300; + el.height = 60; + el.setAttribute('frameborder', 0); + el.src = `https://vocaroo.com/embed/${a.dataset.uid.replace(/^i\//, '')}?autoplay=0`; + return el; + } + } + , { + key: 'YouTube', + regExp: /^\w+:\/\/(?:youtu.be\/|[\w.]*youtube[\w.]*\/.*(?:v=|\bembed\/|\bv\/|live\/))([\w\-]{11})(.*)/, + el(a) { + let start = a.dataset.options.match(/\b(?:star)?t\=(\w+)/); + if (start) { start = start[1]; } + if (start && !/^\d+$/.test(start)) { + start += ' 0h0m0s'; + start = (3600 * start.match(/(\d+)h/)[1]) + (60 * start.match(/(\d+)m/)[1]) + (1 * start.match(/(\d+)s/)[1]); + } + const el = $$1.el('iframe', + {src: `//www.youtube.com/embed/${a.dataset.uid}?rel=0&wmode=opaque${start ? '&start=' + start : ''}`}); + el.setAttribute("allowfullscreen", "true"); + return el; + }, + title: { + api(uid) { return `https://www.youtube.com/oembed?url=https%3A//www.youtube.com/watch%3Fv%3D${uid}&format=json`; }, + text(_) { return _.title; }, + status(_) { + if (_.error) { + const m = _.error.match(/^(\d*)\s*(.*)/); + return [+m[1], m[2]]; + } else { + return [200, 'OK']; + } + } + }, + preview: { + url(uid) { return `https://img.youtube.com/vi/${uid}/0.jpg`; }, + height: 360 + } + } + ] }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var Keybinds = { - init() { - if (!Conf['Keybinds']) { return; } - - for (var hotkey in Config.hotkeys) { - $$1.sync(hotkey, Keybinds.sync); - } - - var init = function() { - $$1.off(d$1, '4chanXInitFinished', init); - $$1.on(d$1, 'keydown', Keybinds.keydown); - for (var node of $$('[accesskey]')) { - node.removeAttribute('accesskey'); - } - }; - return $$1.on(d$1, '4chanXInitFinished', init); - }, - - sync(key, hotkey) { - return Conf[hotkey] = key; - }, - - keydown(e) { - let key, thread, threadRoot; - let catalog, notifications; - if (!(key = Keybinds.keyCode(e))) { return; } - const {target} = e; - if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) { - if (!/(Esc|Alt|Ctrl|Meta|Shift\+\w{2,})/.test(key) || !!/^Alt\+(\d|Up|Down|Left|Right)$/.test(key)) { return; } - } - if (['index', 'thread'].includes(g.VIEW)) { - threadRoot = Nav.getThread(); - thread = Get$1.threadFromRoot(threadRoot); - } - switch (key) { - // QR & Options - case Conf['Toggle board list']: - if (!Conf['Custom Board Navigation']) { return; } - Header$1.toggleBoardList(); - break; - case Conf['Toggle header']: - Header$1.toggleBarVisibility(); - break; - case Conf['Open empty QR']: - if (!QR.postingIsEnabled) { return; } - Keybinds.qr(); - break; - case Conf['Open QR']: - if (!QR.postingIsEnabled || !threadRoot) { return; } - Keybinds.qr(threadRoot); - break; - case Conf['Open settings']: - Settings.open(); - break; - case Conf['Close']: - if (Settings.dialog) { - Settings.close(); - } else if ((notifications = $$('.notification')).length) { - for (var notification of notifications) { - $$1('.close', notification).click(); - } - } else if (QR.nodes && !(QR.nodes.el.hidden || (window.getComputedStyle(QR.nodes.form).display === 'none'))) { - if (Conf['Persistent QR']) { - QR.hide(); - } else { - QR.close(); - } - } else if (Embedding.lastEmbed) { - Embedding.closeFloat(); - } else { - return; - } - break; - case Conf['Spoiler tags']: - if (target.nodeName !== 'TEXTAREA') { return; } - Keybinds.tags('spoiler', target); - break; - case Conf['Code tags']: - if (target.nodeName !== 'TEXTAREA') { return; } - Keybinds.tags('code', target); - break; - case Conf['Eqn tags']: - if (target.nodeName !== 'TEXTAREA') { return; } - Keybinds.tags('eqn', target); - break; - case Conf['Math tags']: - if (target.nodeName !== 'TEXTAREA') { return; } - Keybinds.tags('math', target); - break; - case Conf['SJIS tags']: - if (target.nodeName !== 'TEXTAREA') { return; } - Keybinds.tags('sjis', target); - break; - case Conf['Toggle sage']: - if (!QR.nodes || !!QR.nodes.el.hidden) { return; } - Keybinds.sage(); - break; - case Conf['Toggle Cooldown']: - if (!QR.nodes || !!QR.nodes.el.hidden || !$$1.hasClass(QR.nodes.fileSubmit, 'custom-cooldown')) { return; } - QR.toggleCustomCooldown(); - break; - case Conf['Post from URL']: - if (!QR.postingIsEnabled) { return; } - QR.handleUrl(''); - break; - case Conf['Add new post']: - if (!QR.postingIsEnabled) { return; } - QR.addPost(); - break; - case Conf['Submit QR']: - if (!QR.nodes || !!QR.nodes.el.hidden) { return; } - if (!QR.status()) { QR.submit(); } - break; - // Index/Thread related - case Conf['Update']: - switch (g.VIEW) { - case 'thread': - if (!ThreadUpdater.enabled) { return; } - ThreadUpdater.update(); - break; - case 'index': - if (!Index$1.enabled) { return; } - Index$1.update(); - break; - default: - return; - } - break; - case Conf['Watch']: - if (!ThreadWatcher$1.enabled || !thread) { return; } - ThreadWatcher$1.toggle(thread); - break; - case Conf['Update thread watcher']: - if (!ThreadWatcher$1.enabled) { return; } - ThreadWatcher$1.buttonFetchAll(); - break; - case Conf['Toggle thread watcher']: - if (!ThreadWatcher$1.enabled) { return; } - ThreadWatcher$1.toggleWatcher(); - break; - case Conf['Toggle threading']: - if (!QuoteThreading.ready) { return; } - QuoteThreading.toggleThreading(); - break; - case Conf['Mark thread read']: - if ((g.VIEW !== 'index') || !thread || !UnreadIndex.enabled) { return; } - UnreadIndex.markRead.call(threadRoot); - break; - // Images - case Conf['Expand image']: - if (!ImageExpand.enabled || !threadRoot) { return; } - var post = Get$1.postFromNode(Keybinds.post(threadRoot)); - if (post.file) { ImageExpand.toggle(post); } - break; - case Conf['Expand images']: - if (!ImageExpand.enabled) { return; } - ImageExpand.cb.toggleAll(); - break; - case Conf['Open Gallery']: - if (!Gallery.enabled) { return; } - Gallery.cb.toggle(); - break; - case Conf['fappeTyme']: - if (!FappeTyme.nodes?.fappe) { return; } - FappeTyme.toggle('fappe'); - break; - case Conf['werkTyme']: - if (!FappeTyme.nodes?.werk) { return; } - FappeTyme.toggle('werk'); - break; - // Board Navigation - case Conf['Front page']: - if (Index$1.enabled) { - Index$1.userPageNav(1); - } else { - location.href = `/${g.BOARD}/`; - } - break; - case Conf['Open front page']: - $$1.open(`${location.origin}/${g.BOARD}/`); - break; - case Conf['Next page']: - if ((g.VIEW !== 'index') || !!g.SITE.isOnePage?.(g.BOARD)) { return; } - if (Index$1.enabled) { - if (!['paged', 'infinite'].includes(Conf['Index Mode'])) { return; } - $$1('.next button', Index$1.pagelist).click(); - } else { - $$1(g.SITE.selectors.nav.next)?.click(); - } - break; - case Conf['Previous page']: - if ((g.VIEW !== 'index') || !!g.SITE.isOnePage?.(g.BOARD)) { return; } - if (Index$1.enabled) { - if (!['paged', 'infinite'].includes(Conf['Index Mode'])) { return; } - $$1('.prev button', Index$1.pagelist).click(); - } else { - $$1(g.SITE.selectors.nav.prev)?.click(); - } - break; - case Conf['Search form']: - if (g.VIEW !== 'index') { return; } - var searchInput = Index$1.enabled ? - Index$1.searchInput - : g.SITE.selectors.searchBox ? - $$1(g.SITE.selectors.searchBox) - : - undefined; - if (!searchInput) { return; } - Header$1.scrollToIfNeeded(searchInput); - searchInput.focus(); - break; - case Conf['Paged mode']: - if (!Index$1.enabledOn(g.BOARD)) { return; } - location.href = g.VIEW === 'index' ? '#paged' : `/${g.BOARD}/#paged`; - break; - case Conf['Infinite scrolling mode']: - if (!Index$1.enabledOn(g.BOARD)) { return; } - location.href = g.VIEW === 'index' ? '#infinite' : `/${g.BOARD}/#infinite`; - break; - case Conf['All pages mode']: - if (!Index$1.enabledOn(g.BOARD)) { return; } - location.href = g.VIEW === 'index' ? '#all-pages' : `/${g.BOARD}/#all-pages`; - break; - case Conf['Open catalog']: - if (!(catalog = CatalogLinks.catalog())) { return; } - location.href = catalog; - break; - case Conf['Cycle sort type']: - if (!Index$1.enabled) { return; } - Index$1.cycleSortType(); - break; - // Thread Navigation - case Conf['Next thread']: - if ((g.VIEW !== 'index') || !threadRoot) { return; } - Nav.scroll(+1); - break; - case Conf['Previous thread']: - if ((g.VIEW !== 'index') || !threadRoot) { return; } - Nav.scroll(-1); - break; - case Conf['Expand thread']: - if ((g.VIEW !== 'index') || !threadRoot) { return; } - ExpandThread.toggle(thread); - // Keep thread from moving off screen when contracted. - Header$1.scrollTo(threadRoot); - break; - case Conf['Open thread']: - if ((g.VIEW !== 'index') || !threadRoot) { return; } - Keybinds.open(thread); - break; - case Conf['Open thread tab']: - if ((g.VIEW !== 'index') || !threadRoot) { return; } - Keybinds.open(thread, true); - break; - // Reply Navigation - case Conf['Next reply']: - if (!threadRoot) { return; } - Keybinds.hl(+1, threadRoot); - break; - case Conf['Previous reply']: - if (!threadRoot) { return; } - Keybinds.hl(-1, threadRoot); - break; - case Conf['Deselect reply']: - if (!threadRoot) { return; } - Keybinds.hl(0, threadRoot); - break; - case Conf['Hide']: - if (!thread || !ThreadHiding.db) { return; } - Header$1.scrollTo(threadRoot); - ThreadHiding.toggle(thread); - break; - case Conf['Quick Filter MD5']: - if (!threadRoot) { return; } - post = Keybinds.post(threadRoot); - Keybinds.hl(+1, threadRoot); - Filter.quickFilterMD5.call(post, e); - break; - case Conf['Previous Post Quoting You']: - if (!threadRoot || !QuoteYou.db) { return; } - QuoteYou.cb.seek('preceding'); - break; - case Conf['Next Post Quoting You']: - if (!threadRoot || !QuoteYou.db) { return; } - QuoteYou.cb.seek('following'); - break; - default: - return; - } - e.preventDefault(); - return e.stopPropagation(); - }, - - keyCode(e) { - let key = (() => { let kc; - switch ((kc = e.keyCode)) { - case 8: // return - return ''; - case 13: - return 'Enter'; - case 27: - return 'Esc'; - case 32: - return 'Space'; - case 37: - return 'Left'; - case 38: - return 'Up'; - case 39: - return 'Right'; - case 40: - return 'Down'; - case 188: - return 'Comma'; - case 190: - return 'Period'; - case 191: - return 'Slash'; - case 59: case 186: - return 'Semicolon'; - default: - if ((48 <= kc && kc <= 57) || (65 <= kc && kc <= 90)) { // 0-9, A-Z - return String.fromCharCode(kc).toLowerCase(); - } else if (96 <= kc && kc <= 105) { // numpad 0-9 - return String.fromCharCode(kc - 48).toLowerCase(); - } else { - return null; - } - } })(); - if (key) { - if (e.altKey) { key = 'Alt+' + key; } - if (e.ctrlKey) { key = 'Ctrl+' + key; } - if (e.metaKey) { key = 'Meta+' + key; } - if (e.shiftKey) { key = 'Shift+' + key; } - } - return key; - }, - - post(thread) { - const s = g.SITE.selectors; - return ( - $$1(`${s.postContainer}${s.highlightable.reply}.${g.SITE.classes.highlight}`, thread) || - $$1(`${g.SITE.isOPContainerThread ? s.thread : s.postContainer}${s.highlightable.op}`, thread) - ); - }, - - qr(thread) { - QR.open(); - if (thread != null) { - QR.quote.call(Keybinds.post(thread)); - } - return QR.nodes.com.focus(); - }, - - tags(tag, ta) { - BoardConfig.ready(function() { - const {config} = g.BOARD; - const supported = (() => { switch (tag) { - case 'spoiler': return !!config.spoilers; - case 'code': return !!config.code_tags; - case 'math': case 'eqn': return !!config.math_tags; - case 'sjis': return !!config.sjis_tags; - } })(); - if (!supported) { return new Notice('warning', `[${tag}] tags are not supported on /${g.BOARD}/.`, 20); } - }); - - const { - value - } = ta; - const selStart = ta.selectionStart; - const selEnd = ta.selectionEnd; - - ta.value = - value.slice(0, selStart) + - `[${tag}]` + value.slice(selStart, selEnd) + `[/${tag}]` + - value.slice(selEnd); - - // Move the caret to the end of the selection. - const range = (`[${tag}]`).length + selEnd; - ta.setSelectionRange(range, range); - - // Fire the 'input' event - return $$1.event('input', null, ta); - }, - - sage() { - const isSage = /sage/i.test(QR.nodes.email.value); - return QR.nodes.email.value = isSage ? - "" - : "sage"; - }, - - open(thread, tab) { - if (g.VIEW !== 'index') { return; } - const url = Get$1.url('thread', thread); - if (tab) { - return $$1.open(url); - } else { - return location.href = url; - } - }, - - hl(delta, thread) { - const replySelector = `${g.SITE.selectors.postContainer}${g.SITE.selectors.highlightable.reply}`; - const {highlight} = g.SITE.classes; - - const postEl = $$1(`${replySelector}.${highlight}`, thread); - - if (!delta) { - if (postEl) { $$1.rmClass(postEl, highlight); } - return; - } - - if (postEl) { - const {height} = postEl.getBoundingClientRect(); - if ((Header$1.getTopOf(postEl) >= -height) && (Header$1.getBottomOf(postEl) >= -height)) { // We're at least partially visible - let next; - const {root} = Get$1.postFromNode(postEl).nodes; - const axis = delta === +1 ? - 'following' - : - 'preceding'; - if (!(next = $$1.x(`${axis}-sibling::${g.SITE.xpath.replyContainer}[not(@hidden) and not(child::div[@class='stub'])][1]`, root))) { return; } - if (!next.matches(replySelector)) { next = $$1(replySelector, next); } - Header$1.scrollToIfNeeded(next, delta === +1); - $$1.addClass(next, highlight); - $$1.rmClass(postEl, highlight); - return; - } - $$1.rmClass(postEl, highlight); - } - - const replies = $$(replySelector, thread); - if (delta === -1) { replies.reverse(); } - for (var reply of replies) { - if (((delta === +1) && (Header$1.getTopOf(reply) > 0)) || ((delta === -1) && (Header$1.getBottomOf(reply) > 0))) { - $$1.addClass(reply, highlight); - return; - } - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var Keybinds = { + init() { + if (!Conf['Keybinds']) { return; } + + for (var hotkey in Config.hotkeys) { + $$1.sync(hotkey, Keybinds.sync); + } + + var init = function() { + $$1.off(d$1, '4chanXInitFinished', init); + $$1.on(d$1, 'keydown', Keybinds.keydown); + for (var node of $$('[accesskey]')) { + node.removeAttribute('accesskey'); + } + }; + return $$1.on(d$1, '4chanXInitFinished', init); + }, + + sync(key, hotkey) { + return Conf[hotkey] = key; + }, + + keydown(e) { + let key, thread, threadRoot; + let catalog, notifications; + if (!(key = Keybinds.keyCode(e))) { return; } + const {target} = e; + if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) { + if (!/(Esc|Alt|Ctrl|Meta|Shift\+\w{2,})/.test(key) || !!/^Alt\+(\d|Up|Down|Left|Right)$/.test(key)) { return; } + } + if (['index', 'thread'].includes(g.VIEW)) { + threadRoot = Nav.getThread(); + thread = Get$1.threadFromRoot(threadRoot); + } + switch (key) { + // QR & Options + case Conf['Toggle board list']: + if (!Conf['Custom Board Navigation']) { return; } + Header$1.toggleBoardList(); + break; + case Conf['Toggle header']: + Header$1.toggleBarVisibility(); + break; + case Conf['Open empty QR']: + if (!QR.postingIsEnabled) { return; } + Keybinds.qr(); + break; + case Conf['Open QR']: + if (!QR.postingIsEnabled || !threadRoot) { return; } + Keybinds.qr(threadRoot); + break; + case Conf['Open settings']: + Settings.open(); + break; + case Conf['Close']: + if (Settings.dialog) { + Settings.close(); + } else if ((notifications = $$('.notification')).length) { + for (var notification of notifications) { + $$1('.close', notification).click(); + } + } else if (QR.nodes && !(QR.nodes.el.hidden || (window.getComputedStyle(QR.nodes.form).display === 'none'))) { + if (Conf['Persistent QR']) { + QR.hide(); + } else { + QR.close(); + } + } else if (Embedding.lastEmbed) { + Embedding.closeFloat(); + } else { + return; + } + break; + case Conf['Spoiler tags']: + if (target.nodeName !== 'TEXTAREA') { return; } + Keybinds.tags('spoiler', target); + break; + case Conf['Code tags']: + if (target.nodeName !== 'TEXTAREA') { return; } + Keybinds.tags('code', target); + break; + case Conf['Eqn tags']: + if (target.nodeName !== 'TEXTAREA') { return; } + Keybinds.tags('eqn', target); + break; + case Conf['Math tags']: + if (target.nodeName !== 'TEXTAREA') { return; } + Keybinds.tags('math', target); + break; + case Conf['SJIS tags']: + if (target.nodeName !== 'TEXTAREA') { return; } + Keybinds.tags('sjis', target); + break; + case Conf['Toggle sage']: + if (!QR.nodes || !!QR.nodes.el.hidden) { return; } + Keybinds.sage(); + break; + case Conf['Toggle Cooldown']: + if (!QR.nodes || !!QR.nodes.el.hidden || !$$1.hasClass(QR.nodes.fileSubmit, 'custom-cooldown')) { return; } + QR.toggleCustomCooldown(); + break; + case Conf['Post from URL']: + if (!QR.postingIsEnabled) { return; } + QR.handleUrl(''); + break; + case Conf['Add new post']: + if (!QR.postingIsEnabled) { return; } + QR.addPost(); + break; + case Conf['Submit QR']: + if (!QR.nodes || !!QR.nodes.el.hidden) { return; } + if (!QR.status()) { QR.submit(); } + break; + // Index/Thread related + case Conf['Update']: + switch (g.VIEW) { + case 'thread': + if (!ThreadUpdater.enabled) { return; } + ThreadUpdater.update(); + break; + case 'index': + if (!Index$1.enabled) { return; } + Index$1.update(); + break; + default: + return; + } + break; + case Conf['Watch']: + if (!ThreadWatcher$1.enabled || !thread) { return; } + ThreadWatcher$1.toggle(thread); + break; + case Conf['Update thread watcher']: + if (!ThreadWatcher$1.enabled) { return; } + ThreadWatcher$1.buttonFetchAll(); + break; + case Conf['Toggle thread watcher']: + if (!ThreadWatcher$1.enabled) { return; } + ThreadWatcher$1.toggleWatcher(); + break; + case Conf['Toggle threading']: + if (!QuoteThreading.ready) { return; } + QuoteThreading.toggleThreading(); + break; + case Conf['Mark thread read']: + if ((g.VIEW !== 'index') || !thread || !UnreadIndex.enabled) { return; } + UnreadIndex.markRead.call(threadRoot); + break; + // Images + case Conf['Expand image']: + if (!ImageExpand.enabled || !threadRoot) { return; } + var post = Get$1.postFromNode(Keybinds.post(threadRoot)); + if (post.file) { ImageExpand.toggle(post); } + break; + case Conf['Expand images']: + if (!ImageExpand.enabled) { return; } + ImageExpand.cb.toggleAll(); + break; + case Conf['Open Gallery']: + if (!Gallery.enabled) { return; } + Gallery.cb.toggle(); + break; + case Conf['fappeTyme']: + if (!FappeTyme.nodes?.fappe) { return; } + FappeTyme.toggle('fappe'); + break; + case Conf['werkTyme']: + if (!FappeTyme.nodes?.werk) { return; } + FappeTyme.toggle('werk'); + break; + // Board Navigation + case Conf['Front page']: + if (Index$1.enabled) { + Index$1.userPageNav(1); + } else { + location.href = `/${g.BOARD}/`; + } + break; + case Conf['Open front page']: + $$1.open(`${location.origin}/${g.BOARD}/`); + break; + case Conf['Next page']: + if ((g.VIEW !== 'index') || !!g.SITE.isOnePage?.(g.BOARD)) { return; } + if (Index$1.enabled) { + if (!['paged', 'infinite'].includes(Conf['Index Mode'])) { return; } + $$1('.next button', Index$1.pagelist).click(); + } else { + $$1(g.SITE.selectors.nav.next)?.click(); + } + break; + case Conf['Previous page']: + if ((g.VIEW !== 'index') || !!g.SITE.isOnePage?.(g.BOARD)) { return; } + if (Index$1.enabled) { + if (!['paged', 'infinite'].includes(Conf['Index Mode'])) { return; } + $$1('.prev button', Index$1.pagelist).click(); + } else { + $$1(g.SITE.selectors.nav.prev)?.click(); + } + break; + case Conf['Search form']: + if (g.VIEW !== 'index') { return; } + var searchInput = Index$1.enabled ? + Index$1.searchInput + : g.SITE.selectors.searchBox ? + $$1(g.SITE.selectors.searchBox) + : + undefined; + if (!searchInput) { return; } + Header$1.scrollToIfNeeded(searchInput); + searchInput.focus(); + break; + case Conf['Paged mode']: + if (!Index$1.enabledOn(g.BOARD)) { return; } + location.href = g.VIEW === 'index' ? '#paged' : `/${g.BOARD}/#paged`; + break; + case Conf['Infinite scrolling mode']: + if (!Index$1.enabledOn(g.BOARD)) { return; } + location.href = g.VIEW === 'index' ? '#infinite' : `/${g.BOARD}/#infinite`; + break; + case Conf['All pages mode']: + if (!Index$1.enabledOn(g.BOARD)) { return; } + location.href = g.VIEW === 'index' ? '#all-pages' : `/${g.BOARD}/#all-pages`; + break; + case Conf['Open catalog']: + if (!(catalog = CatalogLinks.catalog())) { return; } + location.href = catalog; + break; + case Conf['Cycle sort type']: + if (!Index$1.enabled) { return; } + Index$1.cycleSortType(); + break; + // Thread Navigation + case Conf['Next thread']: + if ((g.VIEW !== 'index') || !threadRoot) { return; } + Nav.scroll(+1); + break; + case Conf['Previous thread']: + if ((g.VIEW !== 'index') || !threadRoot) { return; } + Nav.scroll(-1); + break; + case Conf['Expand thread']: + if ((g.VIEW !== 'index') || !threadRoot) { return; } + ExpandThread.toggle(thread); + // Keep thread from moving off screen when contracted. + Header$1.scrollTo(threadRoot); + break; + case Conf['Open thread']: + if ((g.VIEW !== 'index') || !threadRoot) { return; } + Keybinds.open(thread); + break; + case Conf['Open thread tab']: + if ((g.VIEW !== 'index') || !threadRoot) { return; } + Keybinds.open(thread, true); + break; + // Reply Navigation + case Conf['Next reply']: + if (!threadRoot) { return; } + Keybinds.hl(+1, threadRoot); + break; + case Conf['Previous reply']: + if (!threadRoot) { return; } + Keybinds.hl(-1, threadRoot); + break; + case Conf['Deselect reply']: + if (!threadRoot) { return; } + Keybinds.hl(0, threadRoot); + break; + case Conf['Hide']: + if (!thread || !ThreadHiding.db) { return; } + Header$1.scrollTo(threadRoot); + ThreadHiding.toggle(thread); + break; + case Conf['Quick Filter MD5']: + if (!threadRoot) { return; } + post = Keybinds.post(threadRoot); + Keybinds.hl(+1, threadRoot); + Filter.quickFilterMD5.call(post, e); + break; + case Conf['Previous Post Quoting You']: + if (!threadRoot || !QuoteYou.db) { return; } + QuoteYou.cb.seek('preceding'); + break; + case Conf['Next Post Quoting You']: + if (!threadRoot || !QuoteYou.db) { return; } + QuoteYou.cb.seek('following'); + break; + default: + return; + } + e.preventDefault(); + return e.stopPropagation(); + }, + + keyCode(e) { + let key = (() => { let kc; + switch ((kc = e.keyCode)) { + case 8: // return + return ''; + case 13: + return 'Enter'; + case 27: + return 'Esc'; + case 32: + return 'Space'; + case 37: + return 'Left'; + case 38: + return 'Up'; + case 39: + return 'Right'; + case 40: + return 'Down'; + case 188: + return 'Comma'; + case 190: + return 'Period'; + case 191: + return 'Slash'; + case 59: case 186: + return 'Semicolon'; + default: + if ((48 <= kc && kc <= 57) || (65 <= kc && kc <= 90)) { // 0-9, A-Z + return String.fromCharCode(kc).toLowerCase(); + } else if (96 <= kc && kc <= 105) { // numpad 0-9 + return String.fromCharCode(kc - 48).toLowerCase(); + } else { + return null; + } + } })(); + if (key) { + if (e.altKey) { key = 'Alt+' + key; } + if (e.ctrlKey) { key = 'Ctrl+' + key; } + if (e.metaKey) { key = 'Meta+' + key; } + if (e.shiftKey) { key = 'Shift+' + key; } + } + return key; + }, + + post(thread) { + const s = g.SITE.selectors; + return ( + $$1(`${s.postContainer}${s.highlightable.reply}.${g.SITE.classes.highlight}`, thread) || + $$1(`${g.SITE.isOPContainerThread ? s.thread : s.postContainer}${s.highlightable.op}`, thread) + ); + }, + + qr(thread) { + QR.open(); + if (thread != null) { + QR.quote.call(Keybinds.post(thread)); + } + return QR.nodes.com.focus(); + }, + + tags(tag, ta) { + BoardConfig.ready(function() { + const {config} = g.BOARD; + const supported = (() => { switch (tag) { + case 'spoiler': return !!config.spoilers; + case 'code': return !!config.code_tags; + case 'math': case 'eqn': return !!config.math_tags; + case 'sjis': return !!config.sjis_tags; + } })(); + if (!supported) { return new Notice('warning', `[${tag}] tags are not supported on /${g.BOARD}/.`, 20); } + }); + + const { + value + } = ta; + const selStart = ta.selectionStart; + const selEnd = ta.selectionEnd; + + ta.value = + value.slice(0, selStart) + + `[${tag}]` + value.slice(selStart, selEnd) + `[/${tag}]` + + value.slice(selEnd); + + // Move the caret to the end of the selection. + const range = (`[${tag}]`).length + selEnd; + ta.setSelectionRange(range, range); + + // Fire the 'input' event + return $$1.event('input', null, ta); + }, + + sage() { + const isSage = /sage/i.test(QR.nodes.email.value); + return QR.nodes.email.value = isSage ? + "" + : "sage"; + }, + + open(thread, tab) { + if (g.VIEW !== 'index') { return; } + const url = Get$1.url('thread', thread); + if (tab) { + return $$1.open(url); + } else { + return location.href = url; + } + }, + + hl(delta, thread) { + const replySelector = `${g.SITE.selectors.postContainer}${g.SITE.selectors.highlightable.reply}`; + const {highlight} = g.SITE.classes; + + const postEl = $$1(`${replySelector}.${highlight}`, thread); + + if (!delta) { + if (postEl) { $$1.rmClass(postEl, highlight); } + return; + } + + if (postEl) { + const {height} = postEl.getBoundingClientRect(); + if ((Header$1.getTopOf(postEl) >= -height) && (Header$1.getBottomOf(postEl) >= -height)) { // We're at least partially visible + let next; + const {root} = Get$1.postFromNode(postEl).nodes; + const axis = delta === +1 ? + 'following' + : + 'preceding'; + if (!(next = $$1.x(`${axis}-sibling::${g.SITE.xpath.replyContainer}[not(@hidden) and not(child::div[@class='stub'])][1]`, root))) { return; } + if (!next.matches(replySelector)) { next = $$1(replySelector, next); } + Header$1.scrollToIfNeeded(next, delta === +1); + $$1.addClass(next, highlight); + $$1.rmClass(postEl, highlight); + return; + } + $$1.rmClass(postEl, highlight); + } + + const replies = $$(replySelector, thread); + if (delta === -1) { replies.reverse(); } + for (var reply of replies) { + if (((delta === +1) && (Header$1.getTopOf(reply) > 0)) || ((delta === -1) && (Header$1.getBottomOf(reply) > 0))) { + $$1.addClass(reply, highlight); + return; + } + } + } }; - const Captcha = { - Cache: { - init() { - $$1.on(d$1, 'SaveCaptcha', e => { - return this.saveAPI(e.detail); - }); - return $$1.on(d$1, 'NoCaptcha', e => { - return this.noCaptcha(e.detail); - }); - }, - - captchas: [], - - getCount() { - return this.captchas.length; - }, - - neededRaw() { - return !( - this.haveCookie() || this.captchas.length || QR.req || this.submitCB - ) && ( - (QR.posts.length > 1) || Conf['Auto-load captcha'] || !QR.posts[0].isOnlyQuotes() || QR.posts[0].file - ); - }, - - needed() { - return this.neededRaw() && $$1.event('LoadCaptcha'); - }, - - prerequest() { - if (!Conf['Prerequest Captcha']) { return; } - // Post count temporarily off by 1 when called from QR.post.rm, QR.close, or QR.submit - return $$1.queueTask(() => { - if ( - !this.prerequested && - this.neededRaw() && - !$$1.event('LoadCaptcha') && - !QR.captcha.occupied() && - (QR.cooldown.seconds <= 60) && - (QR.selected === QR.posts[QR.posts.length - 1]) && - !QR.selected.isOnlyQuotes() - ) { - const isReply = (QR.selected.thread !== 'new'); - if (!$$1.event('RequestCaptcha', { isReply })) { - this.prerequested = true; - this.submitCB = captcha => { - if (captcha) { return this.save(captcha); } - }; - return this.updateCount(); - } - } - }); - }, - - haveCookie() { - return /\b_ct=/.test(d$1.cookie) && (QR.posts[0].thread !== 'new'); - }, - - getOne() { - let captcha; - delete this.prerequested; - this.clear(); - if (captcha = this.captchas.shift()) { - this.count(); - return captcha; - } else { - return null; - } - }, - - request(isReply) { - if (!this.submitCB) { - if ($$1.event('RequestCaptcha', { isReply })) { return; } - } - return cb => { - this.submitCB = cb; - return this.updateCount(); - }; - }, - - abort() { - if (this.submitCB) { - delete this.submitCB; - $$1.event('AbortCaptcha'); - return this.updateCount(); - } - }, - - saveAPI(captcha) { - let cb; - if (cb = this.submitCB) { - delete this.submitCB; - cb(captcha); - return this.updateCount(); - } else { - return this.save(captcha); - } - }, - - noCaptcha(detail) { - let cb; - if (cb = this.submitCB) { - if (!this.haveCookie() || detail?.error) { - QR.error(detail?.error || 'Failed to retrieve captcha.'); - QR.captcha.setup(d$1.activeElement === QR.nodes.status); - } - delete this.submitCB; - cb(); - return this.updateCount(); - } - }, - - save(captcha) { - let cb; - if (cb = this.submitCB) { - this.abort(); - cb(captcha); - return; - } - this.captchas.push(captcha); - this.captchas.sort((a, b) => a.timeout - b.timeout); - return this.count(); - }, - - clear() { - if (this.captchas.length) { - let i; - const now = Date.now(); - for (i = 0; i < this.captchas.length; i++) { - var captcha = this.captchas[i]; - if (captcha.timeout > now) { break; } - } - if (i) { - this.captchas = this.captchas.slice(i); - return this.count(); - } - } - }, - - count() { - clearTimeout(this.timer); - if (this.captchas.length) { - this.timer = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now()); - } - return this.updateCount(); - }, - - updateCount() { - return $$1.event('CaptchaCount', this.captchas.length); - } - }, Replace: CaptchaReplace, t: CaptchaT, v2: { - lifetime: 2 * MINUTE, - - init() { - if (d$1.cookie.indexOf('pass_enabled=1') >= 0) { return; } - if (!(this.isEnabled = !!$$1('#g-recaptcha, #captcha-forced-noscript') || !$$1.id('postForm'))) { return; } - - if (this.noscript = Conf['Force Noscript Captcha'] || !Main$1.jsEnabled) { - $$1.addClass(QR.nodes.el, 'noscript-captcha'); - } - - Captcha.cache.init(); - $$1.on(d$1, 'CaptchaCount', this.count.bind(this)); - - const root = $$1.el('div', { className: 'captcha-root' }); - $$1.extend(root, { - innerHTML: - '
' - } - ); - const counter = $$1('.captcha-counter > a', root); - this.nodes = { root, counter }; - this.count(); - $$1.addClass(QR.nodes.el, 'has-captcha', 'captcha-v2'); - $$1.after(QR.nodes.com.parentNode, root); - - $$1.on(counter, 'click', this.toggle.bind(this)); - $$1.on(counter, 'keydown', e => { - if (Keybinds.keyCode(e) !== 'Space') { return; } - this.toggle(); - e.preventDefault(); - return e.stopPropagation(); - }); - return $$1.on(window, 'captcha:success', () => { - // XXX Greasemonkey 1.x workaround to gain access to GM_* functions. - return $$1.queueTask(() => this.save(false)); - }); - }, - - timeouts: {}, - prevNeeded: 0, - - noscriptURL() { - let lang; - let url = `https://www.google.com/recaptcha/api/fallback?k=${meta.recaptchaKey}`; - if (lang = Conf['captchaLanguage'].trim()) { - url += `&hl=${encodeURIComponent(lang)}`; - } - return url; - }, - - moreNeeded() { - // Post count temporarily off by 1 when called from QR.post.rm, QR.close, or QR.submit - return $$1.queueTask(() => { - const needed = Captcha.cache.needed(); - if (needed && !this.prevNeeded) { - this.setup(QR.cooldown.auto && (d$1.activeElement === QR.nodes.status)); - } - return this.prevNeeded = needed; - }); - }, - - toggle() { - if (this.nodes.container && !this.timeouts.destroy) { - return this.destroy(); - } else { - return this.setup(true, true); - } - }, - - setup(focus, force) { - if (!this.isEnabled || (!Captcha.cache.needed() && !force)) { return; } - - if (focus) { - $$1.addClass(QR.nodes.el, 'focus'); - this.nodes.counter.focus(); - } - - if (this.timeouts.destroy) { - clearTimeout(this.timeouts.destroy); - delete this.timeouts.destroy; - return this.reload(); - } - - if (this.nodes.container) { - // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1226835 - $$1.queueTask(() => { - let iframe; - if (this.nodes.container && (d$1.activeElement === this.nodes.counter) && (iframe = $$1('iframe[src^="https://www.google.com/recaptcha/"]', this.nodes.container))) { - iframe.focus(); - return QR.focus(); - } - }); // Event handler not fired in Firefox - return; - } - - this.nodes.container = $$1.el('div', { className: 'captcha-container' }); - $$1.prepend(this.nodes.root, this.nodes.container); - new MutationObserver(this.afterSetup.bind(this)).observe(this.nodes.container, { - childList: true, - subtree: true - } - ); - - if (this.noscript) { - return this.setupNoscript(); - } else { - return this.setupJS(); - } - }, - - setupNoscript() { - const iframe = $$1.el('iframe', { - id: 'qr-captcha-iframe', - scrolling: 'no', - src: this.noscriptURL() - } - ); - const div = $$1.el('div'); - const textarea = $$1.el('textarea'); - $$1.add(div, textarea); - return $$1.add(this.nodes.container, [iframe, div]); - }, - - setupJS() { - return $$1.global(function () { - const render = function () { - const { classList } = document.documentElement; - const container = document.querySelector('#qr .captcha-container'); - return container.dataset.widgetID = window.grecaptcha.render(container, { - sitekey: meta.recaptchaKey, - theme: classList.contains('tomorrow') || classList.contains('spooky') || classList.contains('dark-captcha') ? 'dark' : 'light', - callback(response) { - return window.dispatchEvent(new CustomEvent('captcha:success', { detail: response })); - } - } - ); - }; - if (window.grecaptcha) { - return render(); - } else { - const cbNative = window.onRecaptchaLoaded; - window.onRecaptchaLoaded = function () { - render(); - return cbNative(); - }; - if (!document.head.querySelector('script[src^="https://www.google.com/recaptcha/api.js"]')) { - const script = document.createElement('script'); - script.src = 'https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoaded&render=explicit'; - return document.head.appendChild(script); - } - } - }); - }, - - afterSetup(mutations) { - for (var mutation of mutations) { - for (var node of mutation.addedNodes) { - var iframe, textarea; - if (iframe = $$1.x('./descendant-or-self::iframe[starts-with(@src, "https://www.google.com/recaptcha/")]', node)) { this.setupIFrame(iframe); } - if (textarea = $$1.x('./descendant-or-self::textarea', node)) { this.setupTextArea(textarea); } - } - } - }, - - setupIFrame(iframe) { - let needle; - if (!doc.contains(iframe)) { return; } - Captcha.replace.iframe(iframe); - $$1.addClass(QR.nodes.el, 'captcha-open'); - this.fixQRPosition(); - $$1.on(iframe, 'load', this.fixQRPosition); - if (d$1.activeElement === this.nodes.counter) { iframe.focus(); } - // XXX Make sure scroll on space prevention (see src/css/style.css) doesn't cause scrolling of div - if (['blink', 'edge'].includes($$1.engine) && (needle = iframe.parentNode, $$('#qr .captcha-container > div > div:first-of-type').includes(needle))) { - return $$1.on(iframe.parentNode, 'scroll', function () { return this.scrollTop = 0; }); - } - }, - - fixQRPosition() { - if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) { - QR.nodes.el.style.top = ''; - return QR.nodes.el.style.bottom = '0px'; - } - }, - - setupTextArea(textarea) { - return $$1.one(textarea, 'input', () => this.save(true)); - }, - - destroy() { - if (!this.isEnabled) { return; } - delete this.timeouts.destroy; - $$1.rmClass(QR.nodes.el, 'captcha-open'); - if (this.nodes.container) { - $$1.global(function () { - const container = document.querySelector('#qr .captcha-container'); - return window.grecaptcha.reset(container.dataset.widgetID); - }); - $$1.rm(this.nodes.container); - return delete this.nodes.container; - } - }, - - getOne(isReply) { - return Captcha.cache.getOne(isReply); - }, - - save(pasted, token) { - Captcha.cache.save({ - response: token || $$1('textarea', this.nodes.container).value, - timeout: Date.now() + this.lifetime - }); - - const focus = (d$1.activeElement?.nodeName === 'IFRAME') && /https?:\/\/www\.google\.com\/recaptcha\//.test(d$1.activeElement.src); - if (Captcha.cache.needed()) { - if (focus) { - if (QR.cooldown.auto || Conf['Post on Captcha Completion']) { - this.nodes.counter.focus(); - } else { - QR.nodes.status.focus(); - } - } - this.reload(); - } else { - if (pasted) { - this.destroy(); - } else { - if (this.timeouts.destroy == null) { this.timeouts.destroy = setTimeout(this.destroy.bind(this), 3 * SECOND); } - } - if (focus) { QR.nodes.status.focus(); } - } - - if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) { return QR.submit(); } - }, - - count() { - const count = Captcha.cache.getCount(); - const loading = Captcha.cache.submitCB ? '...' : ''; - this.nodes.counter.textContent = `Captchas: ${count}${loading}`; - return this.moreNeeded(); - }, - - reload() { - if ($$1('iframe[src^="https://www.google.com/recaptcha/api/fallback?"]', this.nodes.container)) { - this.destroy(); - return this.setup(false, true); - } else { - return $$1.global(function () { - const container = document.querySelector('#qr .captcha-container'); - return window.grecaptcha.reset(container.dataset.widgetID); - }); - } - }, - - occupied() { - return !!this.nodes.container && !this.timeouts.destroy; - } - } + const Captcha = { + Cache: { + init() { + $$1.on(d$1, 'SaveCaptcha', e => { + return this.saveAPI(e.detail); + }); + return $$1.on(d$1, 'NoCaptcha', e => { + return this.noCaptcha(e.detail); + }); + }, + + captchas: [], + + getCount() { + return this.captchas.length; + }, + + neededRaw() { + return !( + this.haveCookie() || this.captchas.length || QR.req || this.submitCB + ) && ( + (QR.posts.length > 1) || Conf['Auto-load captcha'] || !QR.posts[0].isOnlyQuotes() || QR.posts[0].file + ); + }, + + needed() { + return this.neededRaw() && $$1.event('LoadCaptcha'); + }, + + prerequest() { + if (!Conf['Prerequest Captcha']) { return; } + // Post count temporarily off by 1 when called from QR.post.rm, QR.close, or QR.submit + return $$1.queueTask(() => { + if ( + !this.prerequested && + this.neededRaw() && + !$$1.event('LoadCaptcha') && + !QR.captcha.occupied() && + (QR.cooldown.seconds <= 60) && + (QR.selected === QR.posts[QR.posts.length - 1]) && + !QR.selected.isOnlyQuotes() + ) { + const isReply = (QR.selected.thread !== 'new'); + if (!$$1.event('RequestCaptcha', { isReply })) { + this.prerequested = true; + this.submitCB = captcha => { + if (captcha) { return this.save(captcha); } + }; + return this.updateCount(); + } + } + }); + }, + + haveCookie() { + return /\b_ct=/.test(d$1.cookie) && (QR.posts[0].thread !== 'new'); + }, + + getOne() { + let captcha; + delete this.prerequested; + this.clear(); + if (captcha = this.captchas.shift()) { + this.count(); + return captcha; + } else { + return null; + } + }, + + request(isReply) { + if (!this.submitCB) { + if ($$1.event('RequestCaptcha', { isReply })) { return; } + } + return cb => { + this.submitCB = cb; + return this.updateCount(); + }; + }, + + abort() { + if (this.submitCB) { + delete this.submitCB; + $$1.event('AbortCaptcha'); + return this.updateCount(); + } + }, + + saveAPI(captcha) { + let cb; + if (cb = this.submitCB) { + delete this.submitCB; + cb(captcha); + return this.updateCount(); + } else { + return this.save(captcha); + } + }, + + noCaptcha(detail) { + let cb; + if (cb = this.submitCB) { + if (!this.haveCookie() || detail?.error) { + QR.error(detail?.error || 'Failed to retrieve captcha.'); + QR.captcha.setup(d$1.activeElement === QR.nodes.status); + } + delete this.submitCB; + cb(); + return this.updateCount(); + } + }, + + save(captcha) { + let cb; + if (cb = this.submitCB) { + this.abort(); + cb(captcha); + return; + } + this.captchas.push(captcha); + this.captchas.sort((a, b) => a.timeout - b.timeout); + return this.count(); + }, + + clear() { + if (this.captchas.length) { + let i; + const now = Date.now(); + for (i = 0; i < this.captchas.length; i++) { + var captcha = this.captchas[i]; + if (captcha.timeout > now) { break; } + } + if (i) { + this.captchas = this.captchas.slice(i); + return this.count(); + } + } + }, + + count() { + clearTimeout(this.timer); + if (this.captchas.length) { + this.timer = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now()); + } + return this.updateCount(); + }, + + updateCount() { + return $$1.event('CaptchaCount', this.captchas.length); + } + }, Replace: CaptchaReplace, t: CaptchaT, v2: { + lifetime: 2 * MINUTE, + + init() { + if (d$1.cookie.indexOf('pass_enabled=1') >= 0) { return; } + if (!(this.isEnabled = !!$$1('#g-recaptcha, #captcha-forced-noscript') || !$$1.id('postForm'))) { return; } + + if (this.noscript = Conf['Force Noscript Captcha'] || !Main$1.jsEnabled) { + $$1.addClass(QR.nodes.el, 'noscript-captcha'); + } + + Captcha.cache.init(); + $$1.on(d$1, 'CaptchaCount', this.count.bind(this)); + + const root = $$1.el('div', { className: 'captcha-root' }); + $$1.extend(root, { + innerHTML: + '
' + } + ); + const counter = $$1('.captcha-counter > a', root); + this.nodes = { root, counter }; + this.count(); + $$1.addClass(QR.nodes.el, 'has-captcha', 'captcha-v2'); + $$1.after(QR.nodes.com.parentNode, root); + + $$1.on(counter, 'click', this.toggle.bind(this)); + $$1.on(counter, 'keydown', e => { + if (Keybinds.keyCode(e) !== 'Space') { return; } + this.toggle(); + e.preventDefault(); + return e.stopPropagation(); + }); + return $$1.on(window, 'captcha:success', () => { + // XXX Greasemonkey 1.x workaround to gain access to GM_* functions. + return $$1.queueTask(() => this.save(false)); + }); + }, + + timeouts: {}, + prevNeeded: 0, + + noscriptURL() { + let lang; + let url = `https://www.google.com/recaptcha/api/fallback?k=${meta.recaptchaKey}`; + if (lang = Conf['captchaLanguage'].trim()) { + url += `&hl=${encodeURIComponent(lang)}`; + } + return url; + }, + + moreNeeded() { + // Post count temporarily off by 1 when called from QR.post.rm, QR.close, or QR.submit + return $$1.queueTask(() => { + const needed = Captcha.cache.needed(); + if (needed && !this.prevNeeded) { + this.setup(QR.cooldown.auto && (d$1.activeElement === QR.nodes.status)); + } + return this.prevNeeded = needed; + }); + }, + + toggle() { + if (this.nodes.container && !this.timeouts.destroy) { + return this.destroy(); + } else { + return this.setup(true, true); + } + }, + + setup(focus, force) { + if (!this.isEnabled || (!Captcha.cache.needed() && !force)) { return; } + + if (focus) { + $$1.addClass(QR.nodes.el, 'focus'); + this.nodes.counter.focus(); + } + + if (this.timeouts.destroy) { + clearTimeout(this.timeouts.destroy); + delete this.timeouts.destroy; + return this.reload(); + } + + if (this.nodes.container) { + // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1226835 + $$1.queueTask(() => { + let iframe; + if (this.nodes.container && (d$1.activeElement === this.nodes.counter) && (iframe = $$1('iframe[src^="https://www.google.com/recaptcha/"]', this.nodes.container))) { + iframe.focus(); + return QR.focus(); + } + }); // Event handler not fired in Firefox + return; + } + + this.nodes.container = $$1.el('div', { className: 'captcha-container' }); + $$1.prepend(this.nodes.root, this.nodes.container); + new MutationObserver(this.afterSetup.bind(this)).observe(this.nodes.container, { + childList: true, + subtree: true + } + ); + + if (this.noscript) { + return this.setupNoscript(); + } else { + return this.setupJS(); + } + }, + + setupNoscript() { + const iframe = $$1.el('iframe', { + id: 'qr-captcha-iframe', + scrolling: 'no', + src: this.noscriptURL() + } + ); + const div = $$1.el('div'); + const textarea = $$1.el('textarea'); + $$1.add(div, textarea); + return $$1.add(this.nodes.container, [iframe, div]); + }, + + setupJS() { + return $$1.global(function () { + const render = function () { + const { classList } = document.documentElement; + const container = document.querySelector('#qr .captcha-container'); + return container.dataset.widgetID = window.grecaptcha.render(container, { + sitekey: meta.recaptchaKey, + theme: classList.contains('tomorrow') || classList.contains('spooky') || classList.contains('dark-captcha') ? 'dark' : 'light', + callback(response) { + return window.dispatchEvent(new CustomEvent('captcha:success', { detail: response })); + } + } + ); + }; + if (window.grecaptcha) { + return render(); + } else { + const cbNative = window.onRecaptchaLoaded; + window.onRecaptchaLoaded = function () { + render(); + return cbNative(); + }; + if (!document.head.querySelector('script[src^="https://www.google.com/recaptcha/api.js"]')) { + const script = document.createElement('script'); + script.src = 'https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoaded&render=explicit'; + return document.head.appendChild(script); + } + } + }); + }, + + afterSetup(mutations) { + for (var mutation of mutations) { + for (var node of mutation.addedNodes) { + var iframe, textarea; + if (iframe = $$1.x('./descendant-or-self::iframe[starts-with(@src, "https://www.google.com/recaptcha/")]', node)) { this.setupIFrame(iframe); } + if (textarea = $$1.x('./descendant-or-self::textarea', node)) { this.setupTextArea(textarea); } + } + } + }, + + setupIFrame(iframe) { + let needle; + if (!doc.contains(iframe)) { return; } + Captcha.replace.iframe(iframe); + $$1.addClass(QR.nodes.el, 'captcha-open'); + this.fixQRPosition(); + $$1.on(iframe, 'load', this.fixQRPosition); + if (d$1.activeElement === this.nodes.counter) { iframe.focus(); } + // XXX Make sure scroll on space prevention (see src/css/style.css) doesn't cause scrolling of div + if (['blink', 'edge'].includes($$1.engine) && (needle = iframe.parentNode, $$('#qr .captcha-container > div > div:first-of-type').includes(needle))) { + return $$1.on(iframe.parentNode, 'scroll', function () { return this.scrollTop = 0; }); + } + }, + + fixQRPosition() { + if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) { + QR.nodes.el.style.top = ''; + return QR.nodes.el.style.bottom = '0px'; + } + }, + + setupTextArea(textarea) { + return $$1.one(textarea, 'input', () => this.save(true)); + }, + + destroy() { + if (!this.isEnabled) { return; } + delete this.timeouts.destroy; + $$1.rmClass(QR.nodes.el, 'captcha-open'); + if (this.nodes.container) { + $$1.global(function () { + const container = document.querySelector('#qr .captcha-container'); + return window.grecaptcha.reset(container.dataset.widgetID); + }); + $$1.rm(this.nodes.container); + return delete this.nodes.container; + } + }, + + getOne(isReply) { + return Captcha.cache.getOne(isReply); + }, + + save(pasted, token) { + Captcha.cache.save({ + response: token || $$1('textarea', this.nodes.container).value, + timeout: Date.now() + this.lifetime + }); + + const focus = (d$1.activeElement?.nodeName === 'IFRAME') && /https?:\/\/www\.google\.com\/recaptcha\//.test(d$1.activeElement.src); + if (Captcha.cache.needed()) { + if (focus) { + if (QR.cooldown.auto || Conf['Post on Captcha Completion']) { + this.nodes.counter.focus(); + } else { + QR.nodes.status.focus(); + } + } + this.reload(); + } else { + if (pasted) { + this.destroy(); + } else { + if (this.timeouts.destroy == null) { this.timeouts.destroy = setTimeout(this.destroy.bind(this), 3 * SECOND); } + } + if (focus) { QR.nodes.status.focus(); } + } + + if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) { return QR.submit(); } + }, + + count() { + const count = Captcha.cache.getCount(); + const loading = Captcha.cache.submitCB ? '...' : ''; + this.nodes.counter.textContent = `Captchas: ${count}${loading}`; + return this.moreNeeded(); + }, + + reload() { + if ($$1('iframe[src^="https://www.google.com/recaptcha/api/fallback?"]', this.nodes.container)) { + this.destroy(); + return this.setup(false, true); + } else { + return $$1.global(function () { + const container = document.querySelector('#qr .captcha-container'); + return window.grecaptcha.reset(container.dataset.widgetID); + }); + } + }, + + occupied() { + return !!this.nodes.container && !this.timeouts.destroy; + } + } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS202: Simplify dynamic range loops - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - - var QR = { - mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'], - - validExtension: /\.(jpe?g|png|gif|pdf|swf|webm)$/i, - - typeFromExtension: { - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'png': 'image/png', - 'gif': 'image/gif', - 'pdf': 'application/pdf', - 'swf': 'application/vnd.adobe.flash.movie', - 'webm': 'video/webm' - }, - - extensionFromType: { - 'image/jpeg': 'jpg', - 'image/png': 'png', - 'image/gif': 'gif', - 'application/pdf': 'pdf', - 'application/vnd.adobe.flash.movie': 'swf', - 'application/x-shockwave-flash': 'swf', - 'video/webm': 'webm' - }, - - init() { - let sc; - if (!Conf['Quick Reply']) { return; } - - this.posts = []; - - $$1.on(d$1, '4chanXInitFinished', () => BoardConfig.ready(QR.initReady)); - - Callbacks.Post.push({ - name: 'Quick Reply', - cb: this.node - }); - - this.shortcut = (sc = $$1.el('a', { - className: 'disabled', - textContent: '↩', - title: 'Quick Reply', - href: 'javascript:;' - } - )); - $$1.on(sc, 'click', function() { - if (!QR.postingIsEnabled) { return; } - if (Conf['Persistent QR'] || !QR.nodes || QR.nodes.el.hidden) { - QR.open(); - return QR.nodes.com.focus(); - } else { - return QR.close(); - } - }); - - return Header$1.addShortcut('qr', sc, 540); - }, - - initReady() { - let origToggle; - const captchaVersion = $$1('#g-recaptcha, #captcha-forced-noscript') ? 'v2' : 't'; - QR.captcha = Captcha[captchaVersion]; - QR.postingIsEnabled = true; - - const {config} = g.BOARD; - const prop = (key, def) => +(config[key] ?? def); - - QR.min_width = prop('min_image_width', 1); - QR.min_height = prop('min_image_height', 1); - QR.max_width = (QR.max_height = 10000); - - QR.max_size = prop('max_filesize', 4194304); - QR.max_size_video = prop('max_webm_filesize', QR.max_size); - QR.max_comment = prop('max_comment_chars', 2000); - - QR.max_width_video = (QR.max_height_video = 2048); - QR.max_duration_video = prop('max_webm_duration', 120); - - QR.forcedAnon = !!config.forced_anon; - QR.spoiler = !!config.spoilers; - - if (origToggle = $$1.id('togglePostFormLink')) { - const link = $$1.el('h1', - {className: "qr-link-container"}); - $$1.extend(link, { - innerHTML: - `${g.VIEW === "thread" ? "Reply to Thread" : "Start a Thread"}` - }); - - QR.link = link.firstElementChild; - $$1.on(link.firstChild, 'click', function() { - QR.open(); - return QR.nodes.com.focus(); - }); - - $$1.before(origToggle, link); - origToggle.firstElementChild.textContent = 'Original Form'; - } - - if (g.VIEW === 'thread') { - let navLinksBot; - const linkBot = $$1.el('div', - {className: "brackets-wrap qr-link-container-bottom"}); - $$1.extend(linkBot, {innerHTML: 'Reply to Thread'}); - - $$1.on(linkBot.firstElementChild, 'click', function() { - QR.open(); - return QR.nodes.com.focus(); - }); - - if (navLinksBot = $$1('.navLinksBot')) { $$1.prepend(navLinksBot, linkBot); } - } - - $$1.on(d$1, 'QRGetFile', QR.getFile); - $$1.on(d$1, 'QRDrawFile', QR.drawFile); - $$1.on(d$1, 'QRSetFile', QR.setFile); - - $$1.on(d$1, 'paste', QR.paste); - $$1.on(d$1, 'dragover', QR.dragOver); - $$1.on(d$1, 'drop', QR.dropFile); - $$1.on(d$1, 'dragstart dragend', QR.drag); - - $$1.on(d$1, 'IndexRefreshInternal', QR.generatePostableThreadsList); - $$1.on(d$1, 'ThreadUpdate', QR.statusCheck); - - if (!Conf['Persistent QR']) { return; } - QR.open(); - if (Conf['Auto Hide QR']) { return QR.hide(); } - }, - - statusCheck() { - if (!QR.nodes) { return; } - const {thread} = QR.posts[0]; - if ((thread !== 'new') && g.threads.get(`${g.BOARD}.${thread}`).isDead) { - return QR.abort(); - } else { - return QR.status(); - } - }, - - node() { - $$1.on(this.nodes.quote, 'click', QR.quote); - if (this.isFetchedQuote) { return QR.generatePostableThreadsList(); } - }, - - open() { - if (QR.nodes) { - if (QR.nodes.el.hidden) { QR.captcha.setup(); } - QR.nodes.el.hidden = false; - QR.unhide(); - } else { - try { - QR.dialog(); - } catch (err) { - delete QR.nodes; - Main$1.handleErrors({ - message: 'Quick Reply dialog creation crashed.', - error: err - }); - return; - } - } - return $$1.rmClass(QR.shortcut, 'disabled'); - }, - - close() { - if (QR.req) { - QR.abort(); - return; - } - QR.nodes.el.hidden = true; - QR.cleanNotifications(); - QR.blur(); - $$1.rmClass(QR.nodes.el, 'dump'); - $$1.addClass(QR.shortcut, 'disabled'); - new QR.post(true); - for (var post of QR.posts.splice(0, QR.posts.length - 1)) { - post.delete(); - } - QR.cooldown.auto = false; - QR.status(); - return QR.captcha.destroy(); - }, - - focus() { - return $$1.queueTask(function() { - if (!QR.inBubble()) { - QR.hasFocus = d$1.activeElement && QR.nodes.el.contains(d$1.activeElement); - return QR.nodes.el.classList.toggle('focus', QR.hasFocus); - } - }); - }, - - inBubble() { - const bubbles = $$('iframe[src^="https://www.google.com/recaptcha/api2/frame"]'); - return bubbles.includes(d$1.activeElement) || bubbles.some(el => (getComputedStyle(el).visibility !== 'hidden') && (el.getBoundingClientRect().bottom > 0)); - }, - - hide() { - QR.blur(); - $$1.addClass(QR.nodes.el, 'autohide'); - return QR.nodes.autohide.checked = true; - }, - - unhide() { - $$1.rmClass(QR.nodes.el, 'autohide'); - return QR.nodes.autohide.checked = false; - }, - - toggleHide() { - if (this.checked) { - return QR.hide(); - } else { - return QR.unhide(); - } - }, - - blur() { - if (QR.nodes.el.contains(d$1.activeElement)) { return d$1.activeElement.blur(); } - }, - - toggleSJIS(e) { - e.preventDefault(); - Conf['sjisPreview'] = !Conf['sjisPreview']; - $$1.set('sjisPreview', Conf['sjisPreview']); - return QR.nodes.el.classList.toggle('sjis-preview', Conf['sjisPreview']); - }, - - texPreviewShow() { - if ($$1.hasClass(QR.nodes.el, 'tex-preview')) { return QR.texPreviewHide(); } - $$1.addClass(QR.nodes.el, 'tex-preview'); - QR.nodes.texPreview.textContent = QR.nodes.com.value; - return $$1.event('mathjax', null, QR.nodes.texPreview); - }, - - texPreviewHide() { - return $$1.rmClass(QR.nodes.el, 'tex-preview'); - }, - - addPost() { - const wasOpen = (QR.nodes && !QR.nodes.el.hidden); - QR.open(); - if (wasOpen) { - $$1.addClass(QR.nodes.el, 'dump'); - new QR.post(true); - } - return QR.nodes.com.focus(); - }, - - setCustomCooldown(enabled) { - Conf['customCooldownEnabled'] = enabled; - QR.cooldown.customCooldown = enabled; - return QR.nodes.customCooldown.classList.toggle('disabled', !enabled); - }, - - toggleCustomCooldown() { - const enabled = $$1.hasClass(QR.nodes.customCooldown, 'disabled'); - QR.setCustomCooldown(enabled); - return $$1.set('customCooldownEnabled', enabled); - }, - - error(err, focusOverride) { - let el; - QR.open(); - if (typeof err === 'string') { - el = $$1.tn(err); - } else { - el = err; - el.removeAttribute('style'); - } - const notice = new Notice('warning', el); - QR.notifications.push(notice); - if (!Header$1.areNotificationsEnabled) { - if (d$1.hidden && !QR.cooldown.auto) { return alert(el.textContent); } - } else if (d$1.hidden || !(focusOverride || d$1.hasFocus())) { - const notif = new Notification(el.textContent, { - body: el.textContent, - icon: Favicon.logo - } - ); - notif.onclick = () => window.focus(); - if ($$1.engine !== 'gecko') { - // Firefox automatically closes notifications - // so we can't control the onclose properly. - notif.onclose = () => notice.close(); - return notif.onshow = () => setTimeout(function() { - notif.onclose = null; - return notif.close(); - } - , 7 * SECOND); - } - } - }, - - connectionError() { - return $$1.el('span', - { innerHTML: - 'Connection error while posting. ' + - '[More info]' - } - ); - }, - - notifications: [], - - cleanNotifications() { - for (var notification of QR.notifications) { - notification.close(); - } - return QR.notifications = []; - }, - - status() { - let disabled, value; - if (!QR.nodes) { return; } - const {thread} = QR.posts[0]; - if ((thread !== 'new') && g.threads.get(`${g.BOARD}.${thread}`).isDead) { - value = 'Dead'; - disabled = true; - QR.cooldown.auto = false; - } - - value = QR.req ? - QR.req.progress - : - QR.cooldown.seconds || value; - - const {status} = QR.nodes; - status.value = !value ? - 'Submit' - : QR.cooldown.auto ? - `Auto ${value}` - : - value; - return status.disabled = disabled || false; - }, - - openPost() { - QR.open(); - if (QR.selected.isLocked) { - const index = QR.posts.indexOf(QR.selected); - (QR.posts[index+1] || new QR.post()).select(); - $$1.addClass(QR.nodes.el, 'dump'); - return QR.cooldown.auto = true; - } - }, - - quote(e) { - let range; - e?.preventDefault(); - if (!QR.postingIsEnabled) { return; } - const sel = d$1.getSelection(); - const post = Get$1.postFromNode(this); - const {root} = post.nodes; - const postRange = new Range(); - postRange.selectNode(root); - let text = post.board.ID === g.BOARD.ID ? `>>${post}\n` : `>>>/${post.board}/${post}\n`; - for (let i = 0, end = sel.rangeCount, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { - try { - var insideCode, node; - range = sel.getRangeAt(i); - // Trim range to be fully inside post - if (range.compareBoundaryPoints(Range.START_TO_START, postRange) < 0) { - range.setStartBefore(root); - } - if (range.compareBoundaryPoints(Range.END_TO_END, postRange) > 0) { - range.setEndAfter(root); - } - - if (!range.toString().trim()) { continue; } - - var frag = range.cloneContents(); - var ancestor = range.commonAncestorContainer; - // Quoting the insides of a spoiler/code tag. - if ($$1.x('ancestor-or-self::*[self::s or contains(@class,"removed-spoiler")]', ancestor)) { - $$1.prepend(frag, $$1.tn('[spoiler]')); - $$1.add(frag, $$1.tn('[/spoiler]')); - } - if (insideCode = $$1.x('ancestor-or-self::pre[contains(@class,"prettyprint")]', ancestor)) { - $$1.prepend(frag, $$1.tn('[code]')); - $$1.add(frag, $$1.tn('[/code]')); - } - for (node of $$((insideCode ? 'br' : '.prettyprint br'), frag)) { - $$1.replace(node, $$1.tn('\n')); - } - for (node of $$('br', frag)) { - if (node !== frag.lastChild) { $$1.replace(node, $$1.tn('\n>')); } - } - g.SITE.insertTags?.(frag); - for (node of $$('.linkify[data-original]', frag)) { - $$1.replace(node, $$1.tn(node.dataset.original)); - } - for (node of $$('.embedder', frag)) { - if (node.previousSibling?.nodeValue === ' ') { $$1.rm(node.previousSibling); } - $$1.rm(node); - } - text += `>${frag.textContent.trim()}\n`; - } catch (error) { } - } - - QR.openPost(); - const {com, thread} = QR.nodes; - if (!com.value) { thread.value = Get$1.threadFromNode(this); } - - const wasOnlyQuotes = QR.selected.isOnlyQuotes(); - - const caretPos = com.selectionStart; - // Replace selection for text. - com.value = com.value.slice(0, caretPos) + text + com.value.slice(com.selectionEnd); - // Move the caret to the end of the new quote. - range = caretPos + text.length; - com.setSelectionRange(range, range); - com.focus(); - - // This allows us to determine if any text other than quotes has been typed. - if (wasOnlyQuotes) { QR.selected.quotedText = com.value; } - - QR.selected.save(com); - return QR.selected.save(thread); - }, - - characterCount() { - const counter = QR.nodes.charCount; - const count = QR.nodes.com.value.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '_').length; - counter.textContent = count; - counter.hidden = count < (QR.max_comment/2); - return (count > QR.max_comment ? $$1.addClass : $$1.rmClass)(counter, 'warning'); - }, - - getFile() { - return $$1.event('QRFile', QR.selected?.file); - }, - - drawFile(e) { - const file = QR.selected?.file; - if (!file || !/^(image|video)\//.test(file.type)) { return; } - const isVideo = /^video\//.test(file); - const el = $$1.el((isVideo ? 'video' : 'img')); - $$1.on(el, 'error', () => QR.openError()); - $$1.on(el, (isVideo ? 'loadeddata' : 'load'), function() { - e.target.getContext('2d').drawImage(el, 0, 0); - URL.revokeObjectURL(el.src); - return $$1.event('QRImageDrawn', null, e.target); - }); - return el.src = URL.createObjectURL(file); - }, - - openError() { - const div = $$1.el('div'); - $$1.extend(div, { - innerHTML: - 'Could not open file. [More info]' - }); - return QR.error(div); - }, - - setFile(e) { - const {file, name, source} = e.detail; - if (name != null) { file.name = name; } - if (source != null) { file.source = source; } - QR.open(); - return QR.handleFiles([file]); - }, - - drag(e) { - // Let it drag anything from the page. - const toggle = e.type === 'dragstart' ? $$1.off : $$1.on; - toggle(d$1, 'dragover', QR.dragOver); - return toggle(d$1, 'drop', QR.dropFile); - }, - - dragOver(e) { - e.preventDefault(); - return e.dataTransfer.dropEffect = 'copy'; - }, // cursor feedback - - dropFile(e) { - // Let it only handle files from the desktop. - if (!e.dataTransfer.files.length) { return; } - e.preventDefault(); - QR.open(); - return QR.handleFiles(e.dataTransfer.files); - }, - - paste(e) { - if (!e.clipboardData.items) { return; } - let file = null; - let score = -1; - for (var item of e.clipboardData.items) { - var file2; - if ((item.kind === 'file') && (file2 = item.getAsFile())) { - var score2 = (2*(file2.size <= QR.max_size)) + (file2.type === 'image/png'); - if (score2 > score) { - file = file2; - score = score2; - } - } - } - if (file) { - const {type} = file; - const blob = new Blob([file], {type}); - blob.name = `${Conf['pastedname']}.${$$1.getOwn(QR.extensionFromType, type) || 'jpg'}`; - QR.open(); - QR.handleFiles([blob]); - $$1.addClass(QR.nodes.el, 'dump'); - } - }, - - pasteFF() { - const {pasteArea} = QR.nodes; - if (!pasteArea.childNodes.length) { return; } - const images = $$('img', pasteArea); - $$1.rmAll(pasteArea); - for (var img of images) { - var m; - var {src} = img; - if (m = src.match(/data:(image\/(\w+));base64,(.+)/)) { - var bstr = atob(m[3]); - var arr = new Uint8Array(bstr.length); - for (var i = 0, end = bstr.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { - arr[i] = bstr.charCodeAt(i); - } - var blob = new Blob([arr], {type: m[1]}); - blob.name = `${Conf['pastedname']}.${m[2]}`; - QR.handleFiles([blob]); - } else if (/^https?:\/\//.test(src)) { - QR.handleUrl(src); - } - } - }, - - handleUrl(urlDefault) { - QR.open(); - QR.selected.preventAutoPost(); - return CrossOrigin$1.permission(function() { - const url = prompt('Enter a URL:', urlDefault); - if (url === null) { return; } - QR.nodes.fileButton.focus(); - return CrossOrigin$1.file(url, function(blob) { - if (blob && !/^text\//.test(blob.type)) { - return QR.handleFiles([blob]); - } else { - return QR.error("Can't load file."); - } - }); - }); - }, - - handleFiles(files) { - if (this !== QR) { // file input - files = [...this.files]; - this.value = null; - } - if (!files.length) { return; } - QR.cleanNotifications(); - for (var file of files) { - QR.handleFile(file, files.length); - } - if (files.length !== 1) { $$1.addClass(QR.nodes.el, 'dump'); } - if ((d$1.activeElement === QR.nodes.fileButton) && $$1.hasClass(QR.nodes.fileSubmit, 'has-file')) { - return QR.nodes.filename.focus(); - } - }, - - handleFile(file, nfiles) { - let post; - const isText = /^text\//.test(file.type); - if (nfiles === 1) { - post = QR.selected; - } else { - post = QR.posts[QR.posts.length - 1]; - if (isText ? post.com || post.pasting : post.file) { - post = new QR.post(); - } - } - return post[isText ? 'pasteText' : 'setFile'](file); - }, - - openFileInput() { - if (QR.nodes.fileButton.disabled) { return; } - QR.nodes.fileInput.click(); - return QR.nodes.fileButton.focus(); - }, - - generatePostableThreadsList() { - if (!QR.nodes) { return; } - const list = QR.nodes.thread; - const options = [list.firstElementChild]; - for (var thread of g.BOARD.threads.keys) { - options.push($$1.el('option', { - value: thread, - textContent: `Thread ${thread}` - } - ) - ); - } - const val = list.value; - $$1.rmAll(list); - $$1.add(list, options); - list.value = val; - if (list.value === val) { return; } - // Fix the value if the option disappeared. - list.value = g.VIEW === 'thread' ? - g.THREADID - : - 'new'; - return (g.VIEW === 'thread' ? $$1.addClass : $$1.rmClass)(QR.nodes.el, 'reply-to-thread'); - }, - - dialog() { - let dialog, event, nodes; - let name; - QR.nodes = (nodes = { - el: (dialog = UI.dialog('qr', - { innerHTML: QuickReplyPage })) - }); - - const setNode = (name, query) => nodes[name] = $$1(query, dialog); - - setNode('move', '.move'); - setNode('autohide', '#autohide'); - setNode('close', '.close'); - setNode('thread', 'select'); - setNode('form', 'form'); - setNode('sjisToggle', '#sjis-toggle'); - setNode('texButton', '#tex-preview-button'); - setNode('name', '[data-name=name]'); - setNode('email', '[data-name=email]'); - setNode('sub', '[data-name=sub]'); - setNode('com', '[data-name=com]'); - setNode('charCount', '#char-count'); - setNode('texPreview', '#tex-preview'); - setNode('dumpList', '#dump-list'); - setNode('addPost', '#add-post'); - setNode('oekaki', '.oekaki'); - setNode('drawButton', '#qr-draw-button'); - setNode('fileSubmit', '#file-n-submit'); - setNode('fileButton', '#qr-file-button'); - setNode('noFile', '#qr-no-file'); - setNode('filename', '#qr-filename'); - setNode('spoiler', '#qr-file-spoiler'); - setNode('oekakiButton', '#qr-oekaki-button'); - setNode('fileRM', '#qr-filerm'); - setNode('urlButton', '#url-button'); - setNode('pasteArea', '#paste-area'); - setNode('customCooldown', '#custom-cooldown-button'); - setNode('dumpButton', '#dump-button'); - setNode('status', '[type=submit]'); - setNode('flashTag', '[name=filetag]'); - setNode('fileInput', '[type=file]'); - - const {config} = g.BOARD; - const {classList} = QR.nodes.el; - classList.toggle('forced-anon', QR.forcedAnon); - classList.toggle('has-spoiler', QR.spoiler); - classList.toggle('has-sjis', !!config.sjis_tags); - classList.toggle('has-math', !!config.math_tags); - classList.toggle('sjis-preview', !!config.sjis_tags && Conf['sjisPreview']); - classList.toggle('show-new-thread-option', Conf['Show New Thread Option in Threads']); - - if (parseInt(Conf['customCooldown'], 10) > 0) { - $$1.addClass(QR.nodes.fileSubmit, 'custom-cooldown'); - $$1.get('customCooldownEnabled', Conf['customCooldownEnabled'], function({customCooldownEnabled}) { - QR.setCustomCooldown(customCooldownEnabled); - return $$1.sync('customCooldownEnabled', QR.setCustomCooldown); - }); - } - - QR.flagsInput(); - - $$1.on(nodes.autohide, 'change', QR.toggleHide); - $$1.on(nodes.close, 'click', QR.close); - $$1.on(nodes.status, 'click', QR.submit); - $$1.on(nodes.form, 'submit', QR.submit); - $$1.on(nodes.sjisToggle, 'click', QR.toggleSJIS); - $$1.on(nodes.texButton, 'mousedown', QR.texPreviewShow); - $$1.on(nodes.texButton, 'mouseup', QR.texPreviewHide); - $$1.on(nodes.addPost, 'click', () => new QR.post(true)); - $$1.on(nodes.drawButton, 'click', QR.oekaki.draw); - $$1.on(nodes.fileButton, 'click', QR.openFileInput); - $$1.on(nodes.noFile, 'click', QR.openFileInput); - $$1.on(nodes.filename, 'focus', function() { return $$1.addClass(this.parentNode, 'focus'); }); - $$1.on(nodes.filename, 'blur', function() { return $$1.rmClass(this.parentNode, 'focus'); }); - $$1.on(nodes.spoiler, 'change', () => QR.selected.nodes.spoiler.click()); - $$1.on(nodes.oekakiButton, 'click', QR.oekaki.button); - $$1.on(nodes.fileRM, 'click', () => QR.selected.rmFile()); - $$1.on(nodes.urlButton, 'click', () => QR.handleUrl('')); - $$1.on(nodes.customCooldown, 'click', QR.toggleCustomCooldown); - $$1.on(nodes.dumpButton, 'click', () => nodes.el.classList.toggle('dump')); - $$1.on(nodes.fileInput, 'change', QR.handleFiles); - - window.addEventListener('focus', QR.focus, true); - window.addEventListener('blur', QR.focus, true); - // We don't receive blur events from captcha iframe. - $$1.on(d$1, 'click', QR.focus); - - // XXX Workaround for image pasting in Firefox, obsolete as of v50. - // https://bugzilla.mozilla.org/show_bug.cgi?id=906420 - if (($$1.engine === 'gecko') && !window.DataTransferItemList) { - nodes.pasteArea.hidden = false; - } - new MutationObserver(QR.pasteFF).observe(nodes.pasteArea, {childList: true}); - - // save selected post's data - const items = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']; - let i = 0; - const save = function() { return QR.selected.save(this); }; - while ((name = items[i++])) { - var node; - if (!(node = nodes[name])) { continue; } - event = node.nodeName === 'SELECT' ? 'change' : 'input'; - $$1.on(nodes[name], event, save); - } - - // XXX Blink and WebKit treat width and height of