From 144143e2b0079e42777ab7b27836bfcd148f3d4f Mon Sep 17 00:00:00 2001 From: Tuxedo Takodachi Date: Sat, 22 Jul 2023 17:54:11 +0200 Subject: [PATCH] Fix inlining/previewing of archive links like quote links. #5 --- CHANGELOG.md | 4 + builds/4chan-XT-noupdate.user.js | 46067 ++++++++++----------- builds/4chan-XT-noupdate.user.min.js | 144 +- builds/4chan-XT-noupdate.user.min.js.map | 2 +- builds/crx/manifest.json | 2 +- builds/crx/script.js | 199 +- src/Quotelinks/QuotePreview.js | 2 +- src/classes/Fetcher.js | 84 +- version.json | 4 +- 9 files changed, 23246 insertions(+), 23262 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d2ea4fd1..8a11fd75a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ 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.2 (2023-07-22) + +- Fix inlining/previewing of archive links like quote links. [#5](https://github.com/TuxedoTako/4chan-xt/issues/5) + ### 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. diff --git a/builds/4chan-XT-noupdate.user.js b/builds/4chan-XT-noupdate.user.js index 7ff3526cb..bff90a419 100644 --- a/builds/4chan-XT-noupdate.user.js +++ b/builds/4chan-XT-noupdate.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name 4chan XT -// @version XT 2.1.1 +// @version XT 2.1.2 // @minGMVer 1.14 // @minFFVer 74 // @namespace 4chan-XT @@ -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.1.1", - "date": "2023-07-16T08:49:02.722Z" + var version = { + "version": "XT 2.1.2", + "date": "2023-07-22T15:46:24.103Z" }; var meta = { @@ -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; - }; - - 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 + /* + * 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 - * 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 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 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 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 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 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 - * 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); - } - }); - } + 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); + } + }); + } }; const Audio = { @@ -4281,4593 +4281,2663 @@ 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); - } - } - 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'); - } - } - }; - - var NavLinksPage = `Index -Catalog -Archive -Bottom - - -× - - - - - - - - -`; - - 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; - } - }; - 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); - } - } - }; - /* * This file has the code for the jsx to { innerHTML: "safe string" } * @@ -8923,6 +6993,1929 @@ https://*.hcaptcha.com return { innerHTML, [isEscaped]: true }; } + /* + * 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 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 = comment.map((text, i) => { + if ((i % 2) === 1) { + var tag = Fetcher.archiveTags[text.replace(/\ .*\]/, ']')]; + return (typeof tag === 'function') ? tag(text) : tag; + } else { + var greentext = text[0] === '>'; + text = text + .replace(/(\[\/?[a-z]+):lit(\])/g, '$1$2') + .split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g) + .map((text2, j) => ((j % 2) ? `${E(text2)}`: E(text2))) + .join(''); + return {innerHTML: (greentext ? `${text}` : text)}; + } + }); + comment = { innerHTML: E.cat(comment), [isEscaped]: true }; + + 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 + * 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$1, '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 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; + } + }; + 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); + } + } + }; + // \u00A0 is non breaking space const separator = '\u00A0|\u00A0'; const settingsHtml = h("div", { id: "fourchanx-settings", class: "dialog" }, @@ -8947,507 +8940,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 +9485,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 +12797,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 +13135,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 +13576,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(?:$|[?#])/, @@ -14341,829 +14334,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 + } + ); + } }; /* @@ -15287,9 +15280,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").\ ` })); } @@ -15876,24 +15869,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}"]\ `); } } @@ -16268,5803 +16261,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 \n\n'}),s=Ve("#archive-report-enabled",r),l=Ve("#archive-report-reason",r),d=Ve("#archive-report-submit",r);if(Ve.on(s,"change",(function(){return l.disabled=!this.checked})),o&&a?(r.hidden=!Ve('[value="31"]',a).checked,Ve.on(a,"change",(function(e){return r.hidden="31"!==e.target.value,ye.fit("body")})),Ve.after(a,r),ye.fit("body"),Ve.one(o,"submit",(function(e){if(!r.hidden&&s.checked)return e.preventDefault(),ye.archiveSubmit(t,l.value,(e=>(this.action="#archiveresults="+encodeURIComponent(JSON.stringify(e)), this.submit())))}))):i&&(r.hidden=/Report submitted!/.test(i.textContent),Ve.on(s,"change",(function(){return d.hidden=!this.checked})),Ve.after(i,r),Ve.on(d,"click",(()=>ye.archiveSubmit(t,l.value,ye.archiveResults)))),e=location.hash.match(/^#archiveresults=(.*)$/))try{return ye.archiveResults(JSON.parse(decodeURIComponent(e[1])))}catch(e){}},archiveSubmit(e,t,o){const a=Ve.formData({board:n.BOARD.ID,num:ye.postID,reason:t}),i=[];for(var[r,s]of e)!function(t,n){Ve.ajax(n,{onloadend(){if(i.push([t,this.response||{error:""}]),i.length===e.length)return o(i)},form:a})}(r,s)},archiveResults(e){const t=Ve.id("archive-report");for(var[n,o]of e){var i=Ve.el("h3",{className:"archive-report-response"});"success"in o?(Ve.addClass(i,"archive-report-success"),i.textContent=`${n}: ${o.success}`):(Ve.addClass(i,"archive-report-error"),i.textContent=`${n}: ${o.error||"Error reporting post."}`),t?Ve.before(t,i):Ve.add(a.body,i)}}};const ke={init(){ @@ -392,19 +392,19 @@ isAuxiliaryPage:e=>!["boards.4chan.org","boards.4channel.org"].includes(e.hostna e.fileLimit=/\bimagelimit *= *1\b/.test(o),e.ipCount=(t=o.match(/\bunique_ips *= *(\d+)\b/))?+t[1]:void 0,"f"===n.BOARD.ID&&e.OP.file){const{file:t}=e.OP;return Ve.ajax(this.urls.threadJSON({boardID:"f",threadID:e.ID}),{timeout:A,onloadend(){if(this.response)return t.text.dataset.md5=t.MD5=this.response.posts[0].md5}})}},parseNodes(e,t){if("f"===e.boardID)return(()=>{const e=[];for(var n of["Sticky","Closed"]){var o;(o=Ve(`img[alt=${n}]`,t.info))&&e.push(Ve.addClass(o,`${n.toLowerCase()}Icon`,"retina"))}return e})()},parseDate:e=>new Date(1e3*e.dataset.utc),parseFile(e,t){let n;const{text:o,link:a,thumb:i}=t;if(!(n=a.nextSibling?.textContent.match(/\(([\d.]+ [KMG]?B).*\)/)))return!1;if(Ve.extend(t,{name:o.title||a.title||a.textContent,size:n[1],dimensions:n[0].match(/\d+x\d+/)?.[0],tag:n[0].match(/,[^,]*, ([a-z]+)\)/i)?.[1],MD5:o.dataset.md5}),i&&(Ve.extend(t,{thumbURL:i.src,MD5:i.dataset.md5,isSpoiler:Ve.hasClass(i.parentNode,"imgspoiler")}),t.isSpoiler)){let n ;t.thumbURL=(n=a.href.match(/\d+(?=\.\w+$)/))?`${location.protocol}//${O.thumbHost()}/${e.board}/${n[0]}s.jpg`:void 0}return!0},cleanComment(e){let t;if(t=Ve(".abbr",e)){for(var n of u(".abbr + br, .exif",e))Ve.rm(n);for(let e=0;e<2;e++){var o;(o=t.previousSibling)&&"BR"===o.nodeName&&Ve.rm(o)}return Ve.rm(t)}},cleanCommentDisplay(e){let t;return(t=Ve("b",e))&&/^Rolled /.test(t.textContent)&&Ve.rm(t),Ve.rm(Ve(".fortune",e))},insertTags(e){let t;for(t of u("s, .removed-spoiler",e))Ve.replace(t,[Ve.tn("[spoiler]"),...t.childNodes,Ve.tn("[/spoiler]")]);for(t of u(".prettyprint",e))Ve.replace(t,[Ve.tn("[code]"),...t.childNodes,Ve.tn("[/code]")])},hasCORS:e=>e.split("/").slice(0,3).join("/")===location.protocol+"//a.4cdn.org",sfwBoards:e=>Y.sfwBoards(e),uidColor(e){let t=0,n=0;for(;n<8;)t=(t<<5)-t+e.charCodeAt(n++);return t>>8&16777215},isLinkified:e=>O.test(e.hostname),testNativeExtension:()=>Ve.global((function(){if(window.Parser?.postMenuIcon)return this.enabled="true"})), transformBoardList(){let e;const t=[],n=()=>Ve.el("span",{className:"spacer"}),o=Ve.X(".//a|.//text()[not(ancestor::a)]",Ve(Ie.selectors.boardList));let a=0;for(;e=o.snapshotItem(a++);)switch(e.nodeName){case"#text":for(var i of e.nodeValue){var r=Ve.el("span",{textContent:i});" "===i&&(r.className="space"),"]"===i&&t.push(n()),t.push(r),"["===i&&t.push(n())}break;case"A":var s=e.cloneNode(!0);t.push(s)}return t},Build:{staticPath:"//s.4cdn.org/image/",gifIcon:window.devicePixelRatio>=2?"@2x.gif":".gif",spoilerRange:Object.create(null),shortFilename(e){const t=e.match(/\.?[^\.]*$/)[0];return e.length-t.length>30?`${e.match(/(?:[\uD800-\uDBFF][\uDC00-\uDFFF]|[^]){0,25}/)[0]}(...)${t}`:e},spoilerThumb(e){let t;return(t=this.spoilerRange[e])?`${this.staticPath}spoiler-${e}${Math.floor(1+t*Math.random())}.png`:`${this.staticPath}spoiler.png`},sameThread:(e,t)=>"thread"===n.VIEW&&n.BOARD.ID===e&&n.THREADID===+t, -threadURL:(e,t)=>e!==n.BOARD.ID?`//${Y.domain(e)}/${e}/thread/${t}`:"thread"!==n.VIEW||+t!==n.THREADID?`/${e}/thread/${t}`:"",postURL(e,t,n){return`${this.threadURL(e,t)}#p${n}`},parseJSON(e,{siteID:t,boardID:n}){const o={ID:e.no,postID:e.no,threadID:e.resto||e.no,boardID:n,siteID:t,isReply:!!e.resto,isSticky:!!e.sticky,isClosed:!!e.closed,isArchived:!!e.archived,fileDeleted:!!e.filedeleted,filesDeleted:e.filedeleted?[0]:[]};for(var a in o.info={subject:Ve.unescape(e.sub),email:Ve.unescape(e.email),name:Ve.unescape(e.name)||"",tripcode:e.trip,pass:null!=e.since4pass?`${e.since4pass}`:void 0,uniqueID:e.id,flagCode:e.country,flagCodeTroll:e.board_flag,flag:Ve.unescape(e.country_name||e.flag_name),dateUTC:e.time,dateText:e.now,commentHTML:{innerHTML:e.com||"",[ce]:!0}},e.capcode&&(o.info.capcode=e.capcode.replace(/_highlight$/,"").replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase())),o.capcodeHighlight=/_highlight$/.test(e.capcode),delete o.info.uniqueID),o.files=[], +threadURL:(e,t)=>e!==n.BOARD.ID?`//${Y.domain(e)}/${e}/thread/${t}`:"thread"!==n.VIEW||+t!==n.THREADID?`/${e}/thread/${t}`:"",postURL(e,t,n){return`${this.threadURL(e,t)}#p${n}`},parseJSON(e,{siteID:t,boardID:n}){const o={ID:e.no,postID:e.no,threadID:e.resto||e.no,boardID:n,siteID:t,isReply:!!e.resto,isSticky:!!e.sticky,isClosed:!!e.closed,isArchived:!!e.archived,fileDeleted:!!e.filedeleted,filesDeleted:e.filedeleted?[0]:[]};for(var a in o.info={subject:Ve.unescape(e.sub),email:Ve.unescape(e.email),name:Ve.unescape(e.name)||"",tripcode:e.trip,pass:null!=e.since4pass?`${e.since4pass}`:void 0,uniqueID:e.id,flagCode:e.country,flagCodeTroll:e.board_flag,flag:Ve.unescape(e.country_name||e.flag_name),dateUTC:e.time,dateText:e.now,commentHTML:{innerHTML:e.com||"",[ie]:!0}},e.capcode&&(o.info.capcode=e.capcode.replace(/_highlight$/,"").replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase())),o.capcodeHighlight=/_highlight$/.test(e.capcode),delete o.info.uniqueID),o.files=[], e.ext&&(o.file=this.parseJSONFile(e,{siteID:t,boardID:n}),o.files.push(o.file)),o.extra=m(),e)"x"===a[0]&&(o.extra[a]=e[a]);return o},parseJSONFile(e,{siteID:t,boardID:o}){const a=n.sites[t],i="yotsuba"===a.software&&"f"===o?`${encodeURIComponent(e.filename)}${e.ext}`:`${e.tim}${e.ext}`,r={name:Ve.unescape(e.filename)+e.ext,url:a.urls.file({siteID:t,boardID:o},i),height:e.h,width:e.w,MD5:e.md5,size:Ve.bytesToString(e.fsize),thumbURL:a.urls.thumb({siteID:t,boardID:o},`${e.tim}s.jpg`),theight:e.tn_h,twidth:e.tn_w,isSpoiler:!!e.spoiler,tag:e.tag,hasDownscale:!!e.m_img};return null==e.h||/\.pdf$/.test(r.url)||(r.dimensions=`${r.width}x${r.height}`),r},parseComment:e=>(e=e.replace(//gi,"\n").replace(/\n\n]*>/g,""),Ve.unescape(e)),parseCommentDisplay(e){if(!t["Remove Spoilers"]&&!t["Reveal Spoilers"]){let t;for(;(t=e.replace(/(?:(?!<\/?s>).)*<\/s>/g,"[spoiler]"))!==e;)e=t} -return e=e.replace(/^Rolled [^<]*<\/b>/i,"").replace(/>"):"",pe("div",{id:`p${t}`,class:`post ${P}${e.capcodeHighlight?" highlightPost":""}`},e.isReply?pe(ue,null,D,B):pe(ue,null,B,D),pe("blockquote",{class:"postMessage",id:`m${t}`},v))),N=Ve.el("div",{className:`postContainer ${P}Container`,id:`pc${t}`});for(var L of(Ve.extend(N,M),u(".quotelink",N))){var F,O=L.getAttribute("href");if("#"===O[0])this.sameThread(a,o)||(L.href=this.threadURL(a,o)+O);else(F=L.href.match(Ie.regexp.quotelink))&&this.sameThread(F[1],F[2])&&(L.href=O.match(/(#[^#]*)?$/)[0]||"#")}return N},summaryText(e,t,n){let o="";return e&&(o+=`${e} `),o+=`${t} post${t>1?"s":""}`,+n&&(o+=` and ${n} image repl${n>1?"ies":"y"}`),o+` ${"-"===e?"shown":"omitted"}.`},summary(e,t,n,o){return Ve.el("a",{className:"summary", +return e=e.replace(/^Rolled [^<]*<\/b>/i,"").replace(/>"):"",le("div",{id:`p${t}`,class:`post ${P}${e.capcodeHighlight?" highlightPost":""}`},e.isReply?le(se,null,D,B):le(se,null,B,D),le("blockquote",{class:"postMessage",id:`m${t}`},v))),N=Ve.el("div",{className:`postContainer ${P}Container`,id:`pc${t}`});for(var L of(Ve.extend(N,M),u(".quotelink",N))){var F,O=L.getAttribute("href");if("#"===O[0])this.sameThread(a,o)||(L.href=this.threadURL(a,o)+O);else(F=L.href.match(Ie.regexp.quotelink))&&this.sameThread(F[1],F[2])&&(L.href=O.match(/(#[^#]*)?$/)[0]||"#")}return N},summaryText(e,t,n){let o="";return e&&(o+=`${e} `),o+=`${t} post${t>1?"s":""}`,+n&&(o+=` and ${n} image repl${n>1?"ies":"y"}`),o+` ${"-"===e?"shown":"omitted"}.`},summary(e,t,n,o){return Ve.el("a",{className:"summary", textContent:this.summaryText("",n,o),href:`/${e}/thread/${t}`})},thread(e,t,n){let o;if((o=e.nodes.root)?Ve.rmAll(o):e.nodes.root=o=Ve.el("div",{className:"thread",id:`t${t.no}`}),this.hat&&Ve.add(o,this.hat.cloneNode(!1)),Ve.add(o,e.OP.nodes.root),t.omitted_posts||!n&&t.replies){const[a,i]=n?[t.omitted_posts,t.images-t.last_replies.filter((e=>!!e.ext)).length]:[t.replies,t.images],r=this.summary(e.board.ID,t.no,a,i);Ve.add(o,r)}return o},catalogThread(e,n,o){let a,i,r;const{staticPath:s,gifIcon:l}=this,{tn_w:d,tn_h:c}=n;if(n.spoiler&&!t["Reveal Spoiler Thumbnails"]){let t;r=`${s}spoiler`,(t=this.spoilerRange[e.board])&&(r+=`-${e.board}`+Math.floor(1+t*Math.random())),r+=".png",i="spoiler-file",a="--tn-w: 100; --tn-h: 100;"}else if(n.filedeleted)r=`${s}filedeleted-res${l}`,i="deleted-file";else if(e.OP.file){r=e.OP.file.thumbURL;const t=250/Math.max(d,c);a=`--tn-w: ${d*t}; --tn-h: ${c*t};`}else r=`${s}nofile.png`,i="no-file" -;const h=n.replies+1,p=n.images+!!n.ext,g=Ve.el("div",function(e,t,n,o,a,i,r,s,l){return pe(ue,null,pe("a",{class:"catalog-link",href:`/${e.board}/thread/${e.ID}`},pe("img",n?{src:t,class:`catalog-thumb ${n}`}:{src:t,class:"catalog-thumb","data-width":o.tn_w,"data-height":o.tn_h})),pe("div",{class:"catalog-stats"},pe("span",{title:"Posts / Files / Page"},pe("span",{class:"post-count"+(o.bumplimit?" warning":"")},a)," / ",pe("span",{class:"file-count"+(o.imagelimit?" warning":"")},i)," / ",pe("span",{class:"page-count"},r)),pe("span",{class:"catalog-icons"},e.isSticky?pe("img",{src:`${s}sticky${l}`,class:"stickyIcon",title:"Sticky"}):"",e.isClosed?pe("img",{src:`${s}closed${l}`,class:"closedIcon",title:"Closed"}):"")))}(e,r,i,n,h,p,o,s,l));for(var f of(Ve.before(e.OP.nodes.info,[...g.childNodes]),u("br",e.OP.nodes.comment)))f.previousSibling&&"BR"===f.previousSibling.nodeName&&Ve.addClass(f,"extra-linebreak");const m=Ve.el("div",{className:"thread catalog-thread",id:`t${e}`}) -;return e.OP.highlights&&Ve.addClass(m,...e.OP.highlights),e.OP.file||Ve.addClass(m,"noFile"),m.style.cssText=a||"",m},catalogReply(e,t){let n="";t.com&&(n=this.parseCommentDisplay(t.com).replace(/>>\d+/g,"").trim().replace(/\n+/g," // ")),t.ext&&(n||(n=`${Ve.unescape(t.filename)}${t.ext}`)),t.com&&(n||(n=Ve.unescape(t.com.replace(//gi," // ")))),n||(n=" "),n.length>73&&(n=`${n.slice(0,70)}...`);const o=this.postURL(e.board.ID,e.ID,t.no);return Ve.el("div",{className:"catalog-reply"},pe(ue,null,pe("span",null,pe("time",{"data-utc":1e3*t.time,"data-abbrev":"1"},"..."),": "),pe("a",{class:"catalog-reply-excerpt",href:o},n),pe("a",{class:"catalog-reply-preview",href:o},"...")))}}},Ce={tinyboard:ve,yotsuba:Ie};var De={init(){if(["index","thread","archive"].includes(n.VIEW)&&t["File Info Formatting"])return l.Post.push({name:"File Info Formatting",cb:this.node})},node(){if(!this.file)return;if(this.isClone){let e -;for(e of u(".file-info .download-button",this.file.text))Ve.on(e,"click",j.download);for(e of u(".file-info .quick-filter-md5",this.file.text))Ve.on(e,"click",We.quickFilterMD5);return}const e=Ve.el("span",{className:"fileText-original"});Ve.prepend(this.file.link.parentNode,e),Ve.add(e,[this.file.link.previousSibling,this.file.link,this.file.link.nextSibling]);const n=Ve.el("span",{className:"file-info"});return De.format(t.fileInfo,this,n),Ve.prepend(this.file.text,n)},format(e,t,n){let a;const i=[];for(a of(e.replace(/%(.)|[^%]+/g,(function(e,n){return i.push(Ve.hasOwn(De.formatters,n)?De.formatters[n].call(t):{innerHTML:o(e)}),""})),Ve.extend(n,{innerHTML:o.cat(i)}),u(".download-button",n)))Ve.on(a,"click",j.download);for(a of u(".quick-filter-md5",n))Ve.on(a,"click",We.quickFilterMD5)},formatters:{t(){return{innerHTML:o(this.file.url.match(/[^/]*$/)[0]),[ce]:!0}},T(){return pe("a",{href:this.file.url,target:"_blank"},De.formatters.t.call(this))},l(){return pe("a",{ -href:this.file.url,target:"_blank"},De.formatters.n.call(this))},L(){return pe("a",{href:this.file.url,target:"_blank"},De.formatters.N.call(this))},n(){const e=this.file.name,t=Ce.yotsuba.Build.shortFilename(this.file.name,this.isReply);return e===t?{innerHTML:o(e),[ce]:!0}:pe("span",{class:"fnswitch"},pe("span",{class:"fntrunc"},t),pe("span",{class:"fnfull"},e))},N(){return{innerHTML:o(this.file.name),[ce]:!0}},d(){return pe("a",{href:this.file.url,download:this.file.name,class:"download-button"},"📥︎")},f:()=>({innerHTML:'',[ce]:!0}),p(){return{innerHTML:this.file.isSpoiler?"Spoiler, ":"",[ce]:!0}},s(){return{innerHTML:o(this.file.size),[ce]:!0}},B(){return{innerHTML:Math.round(this.file.sizeInBytes)+" Bytes",[ce]:!0}},K(){return{innerHTML:Math.round(this.file.sizeInBytes/1024)+" KB",[ce]:!0}},M(){return{innerHTML:Math.round(this.file.sizeInBytes/1048576*100)/100+" MB",[ce]:!0}},r(){return{innerHTML:o(this.file.dimensions||"PDF"), -[ce]:!0}},g(){return{innerHTML:this.file.tag?", "+o(this.file.tag):"",[ce]:!0}},"%":()=>({innerHTML:"%",[ce]:!0})}},Ee={init(){["index","thread","archive"].includes(n.VIEW)&&t["Time Formatting"]&&l.Post.push({name:"Time Formatting",cb:this.node})},node(){if(!this.info.date||this.isClone)return;const{textContent:e}=this.nodes.date;this.nodes.date.textContent=e.match(/^\s*/)[0]+Ee.format(t.time,this.info.date)+e.match(/\s*$/)[0]},format:(e,t)=>e.replace(/%(.)/g,(function(e,n){return Ve.hasOwn(Ee.formatters,n)?Ee.formatters[n].call(t):e})),zeroPad:e=>e<10?`0${e}`:e,formatterCache:new Map,formatters:{a(){let e=Ee.formatterCache.get("a");return e||(e=Intl.DateTimeFormat(t.timeLocale||void 0,{weekday:"short"}),Ee.formatterCache.set("a",e)),e.format(this)},A(){let e=Ee.formatterCache.get("A");return e||(e=Intl.DateTimeFormat(t.timeLocale||void 0,{weekday:"long"}),Ee.formatterCache.set("A",e)),e.format(this)},b(){let e=Ee.formatterCache.get("b") +;const h=n.replies+1,p=n.images+!!n.ext,g=Ve.el("div",function(e,t,n,o,a,i,r,s,l){return le(se,null,le("a",{class:"catalog-link",href:`/${e.board}/thread/${e.ID}`},le("img",n?{src:t,class:`catalog-thumb ${n}`}:{src:t,class:"catalog-thumb","data-width":o.tn_w,"data-height":o.tn_h})),le("div",{class:"catalog-stats"},le("span",{title:"Posts / Files / Page"},le("span",{class:"post-count"+(o.bumplimit?" warning":"")},a)," / ",le("span",{class:"file-count"+(o.imagelimit?" warning":"")},i)," / ",le("span",{class:"page-count"},r)),le("span",{class:"catalog-icons"},e.isSticky?le("img",{src:`${s}sticky${l}`,class:"stickyIcon",title:"Sticky"}):"",e.isClosed?le("img",{src:`${s}closed${l}`,class:"closedIcon",title:"Closed"}):"")))}(e,r,i,n,h,p,o,s,l));for(var f of(Ve.before(e.OP.nodes.info,[...g.childNodes]),u("br",e.OP.nodes.comment)))f.previousSibling&&"BR"===f.previousSibling.nodeName&&Ve.addClass(f,"extra-linebreak");const m=Ve.el("div",{className:"thread catalog-thread",id:`t${e}`}) +;return e.OP.highlights&&Ve.addClass(m,...e.OP.highlights),e.OP.file||Ve.addClass(m,"noFile"),m.style.cssText=a||"",m},catalogReply(e,t){let n="";t.com&&(n=this.parseCommentDisplay(t.com).replace(/>>\d+/g,"").trim().replace(/\n+/g," // ")),t.ext&&(n||(n=`${Ve.unescape(t.filename)}${t.ext}`)),t.com&&(n||(n=Ve.unescape(t.com.replace(//gi," // ")))),n||(n=" "),n.length>73&&(n=`${n.slice(0,70)}...`);const o=this.postURL(e.board.ID,e.ID,t.no);return Ve.el("div",{className:"catalog-reply"},le(se,null,le("span",null,le("time",{"data-utc":1e3*t.time,"data-abbrev":"1"},"..."),": "),le("a",{class:"catalog-reply-excerpt",href:o},n),le("a",{class:"catalog-reply-preview",href:o},"...")))}}},Ce={tinyboard:ve,yotsuba:Ie};var De={init(){if(["index","thread","archive"].includes(n.VIEW)&&t["File Info Formatting"])return l.Post.push({name:"File Info Formatting",cb:this.node})},node(){if(!this.file)return;if(this.isClone){let e +;for(e of u(".file-info .download-button",this.file.text))Ve.on(e,"click",j.download);for(e of u(".file-info .quick-filter-md5",this.file.text))Ve.on(e,"click",We.quickFilterMD5);return}const e=Ve.el("span",{className:"fileText-original"});Ve.prepend(this.file.link.parentNode,e),Ve.add(e,[this.file.link.previousSibling,this.file.link,this.file.link.nextSibling]);const n=Ve.el("span",{className:"file-info"});return De.format(t.fileInfo,this,n),Ve.prepend(this.file.text,n)},format(e,t,n){let a;const i=[];for(a of(e.replace(/%(.)|[^%]+/g,(function(e,n){return i.push(Ve.hasOwn(De.formatters,n)?De.formatters[n].call(t):{innerHTML:o(e)}),""})),Ve.extend(n,{innerHTML:o.cat(i)}),u(".download-button",n)))Ve.on(a,"click",j.download);for(a of u(".quick-filter-md5",n))Ve.on(a,"click",We.quickFilterMD5)},formatters:{t(){return{innerHTML:o(this.file.url.match(/[^/]*$/)[0]),[ie]:!0}},T(){return le("a",{href:this.file.url,target:"_blank"},De.formatters.t.call(this))},l(){return le("a",{ +href:this.file.url,target:"_blank"},De.formatters.n.call(this))},L(){return le("a",{href:this.file.url,target:"_blank"},De.formatters.N.call(this))},n(){const e=this.file.name,t=Ce.yotsuba.Build.shortFilename(this.file.name,this.isReply);return e===t?{innerHTML:o(e),[ie]:!0}:le("span",{class:"fnswitch"},le("span",{class:"fntrunc"},t),le("span",{class:"fnfull"},e))},N(){return{innerHTML:o(this.file.name),[ie]:!0}},d(){return le("a",{href:this.file.url,download:this.file.name,class:"download-button"},"📥︎")},f:()=>({innerHTML:'',[ie]:!0}),p(){return{innerHTML:this.file.isSpoiler?"Spoiler, ":"",[ie]:!0}},s(){return{innerHTML:o(this.file.size),[ie]:!0}},B(){return{innerHTML:Math.round(this.file.sizeInBytes)+" Bytes",[ie]:!0}},K(){return{innerHTML:Math.round(this.file.sizeInBytes/1024)+" KB",[ie]:!0}},M(){return{innerHTML:Math.round(this.file.sizeInBytes/1048576*100)/100+" MB",[ie]:!0}},r(){return{innerHTML:o(this.file.dimensions||"PDF"), +[ie]:!0}},g(){return{innerHTML:this.file.tag?", "+o(this.file.tag):"",[ie]:!0}},"%":()=>({innerHTML:"%",[ie]:!0})}},Ee={init(){["index","thread","archive"].includes(n.VIEW)&&t["Time Formatting"]&&l.Post.push({name:"Time Formatting",cb:this.node})},node(){if(!this.info.date||this.isClone)return;const{textContent:e}=this.nodes.date;this.nodes.date.textContent=e.match(/^\s*/)[0]+Ee.format(t.time,this.info.date)+e.match(/\s*$/)[0]},format:(e,t)=>e.replace(/%(.)/g,(function(e,n){return Ve.hasOwn(Ee.formatters,n)?Ee.formatters[n].call(t):e})),zeroPad:e=>e<10?`0${e}`:e,formatterCache:new Map,formatters:{a(){let e=Ee.formatterCache.get("a");return e||(e=Intl.DateTimeFormat(t.timeLocale||void 0,{weekday:"short"}),Ee.formatterCache.set("a",e)),e.format(this)},A(){let e=Ee.formatterCache.get("A");return e||(e=Intl.DateTimeFormat(t.timeLocale||void 0,{weekday:"long"}),Ee.formatterCache.set("A",e)),e.format(this)},b(){let e=Ee.formatterCache.get("b") ;return e||(e=Intl.DateTimeFormat(t.timeLocale||void 0,{month:"short"}),Ee.formatterCache.set("b",e)),e.format(this)},B(){let e=Ee.formatterCache.get("B");return e||(e=Intl.DateTimeFormat(t.timeLocale||void 0,{month:"long"}),Ee.formatterCache.set("B",e)),e.format(this)},d(){return Ee.zeroPad(this.getDate())},e(){return this.getDate()},H(){return Ee.zeroPad(this.getHours())},I(){return Ee.zeroPad(this.getHours()%12||12)},k(){return this.getHours()},l(){return this.getHours()%12||12},m(){return Ee.zeroPad(this.getMonth()+1)},M(){return Ee.zeroPad(this.getMinutes())},p(){let e=Ee.formatterCache.get("p");e||(e=Intl.DateTimeFormat(t.timeLocale||void 0,{hour:"numeric",hour12:!0}),Ee.formatterCache.set("p",e));return e.formatToParts(this).find((e=>"dayPeriod"===e.type)).value},P(){return Ee.formatters.p.call(this).toLowerCase()},S(){return Ee.zeroPad(this.getSeconds())},y(){return this.getFullYear().toString().slice(2)},Y(){return this.getFullYear()},"%":()=>"%"}},Se={init(){ if("thread"!==n.VIEW||!t["Reply Pruning"])return;this.container=Ve.frag(),this.summary=Ve.el("span",{hidden:!0,className:"summary"}),this.summary.style.cursor="pointer",Ve.on(this.summary,"click",(()=>(this.inputs.enabled.checked=!this.inputs.enabled.checked,Ve.event("change",null,this.inputs.enabled))));const e=L.checkbox("Prune Replies","Show Last",t["Prune All Threads"]),a=Ve.el("span",{title:"Maximum number of replies to show."},{innerHTML:' '});return Ve.prepend(a,e),this.inputs={enabled:e.firstElementChild,replies:a.lastElementChild},this.setEnabled.call(this.inputs.enabled),Ve.on(this.inputs.enabled,"change",this.setEnabled),Ve.on(this.inputs.replies,"change",Ve.cb.value),Ke.menu.addEntry({el:a,order:190}),l.Thread.push({name:"Reply Pruning",cb:this.node})},position:0,hidden:0,hiddenFiles:0,total:0,totalFiles:0,setEnabled(){const e=Te.input ;return this.checked&&e?.checked&&(e.checked=!1,Ve.event("change",null,e)),Se.active=this.checked},showIfHidden(e){if(Se.container&&Ve(`#${e}`,Se.container))return Se.inputs.enabled.checked=!1,Ve.event("change",null,Se.inputs.enabled)},node(){let e;return Se.thread=this,this.isSticky&&(Se.active=Se.inputs.enabled.checked=!0,Te.input&&(t["Thread Quotes"]=Te.input.checked=!1)),this.posts.forEach((function(e){if(e.isReply&&(Se.total++,e.file))return Se.totalFiles++})),Se.active&&/^#p\d+$/.test(location.hash)&&1<=(e=this.posts.keys.indexOf(location.hash.slice(2)))&&e<1+Math.max(Se.total-+t["Max Replies"],0)&&(Se.active=Se.inputs.enabled.checked=!1),Ve.after(this.OP.nodes.root,Se.summary),Ve.on(Se.inputs.enabled,"change",Se.update),Ve.on(Se.inputs.replies,"change",Se.update),Ve.on(a,"ThreadUpdate",Se.updateCount),Ve.on(a,"ThreadUpdate",Se.update),Se.update()},updateCount(e){if(!e.detail[404])for(var t of e.detail.newPosts)Se.total++,n.posts.get(t).file&&Se.totalFiles++},update(){let e,o,i @@ -427,7 +427,7 @@ Ve.event("ThreadUpdate",{404:!0,threadID:Re.thread.fullID})),error:e=>(304===e.s !a.hidden&&a.hasFocus()||(t["Beep Quoting You"]&&ee.postsQuotingYou?.size>n?(Re.playBeep(),t.Beep&&Re.playBeep()):t.Beep&&ee.posts?.size>0&&0===e&&Re.playBeep());const o=t["Auto Scroll"]&&Re.scrollBG()&&Re.root.getBoundingClientRect().bottom-i.clientHeight<25;let r=null;for(s of p)Te.insert(s)||(r||(r=s.nodes.root),Ve.add(Re.root,s.nodes.root));Ve.event("PostsInserted",null,Re.root),o&&(t["Bottom Scroll"]?window.scrollTo(0,a.body.clientHeight):r&&Ke.scrollTo(r))}else Re.set("status","");return null!=d.unique_ips&&(r=Ve.id("unique-ips"))&&(r.textContent=d.unique_ips,r.previousSibling.textContent=r.previousSibling.textContent.replace(/\b(?:is|are)\b/,1===d.unique_ips?"is":"are"),r.nextSibling.textContent=r.nextSibling.textContent.replace(/\bposters?\b/,1===d.unique_ips?"poster":"posters")),Ve.event("ThreadUpdate",{404:!1,threadID:c.fullID,newPosts:m,deletedPosts:v,deletedFiles:w,postCount:d.replies+1,fileCount:d.images+!!d.fsize,ipCount:d.unique_ips})}},Be={dialog:void 0,init(){ const o=Ve.el("a",{className:"settings-link",textContent:"🔧︎",title:`${e.name} Settings`,href:"javascript:;"});Ve.on(o,"click",Be.open),Ke.addShortcut("settings",o,820);const i=this.addSection;if(i("Main",this.main),i("Filter",this.filter),i("Sauce",this.sauce),i("Advanced",this.advanced),i("Keybinds",this.keybinds),Ve.on(a,"AddSettingsSection",Be.addSection),Ve.on(a,"OpenSettings",(e=>Be.open(e.detail))),"yotsuba"===n.SITE.software&&t["Disable Native Extension"])return Ve.hasStorage?Ve.global((function(){try{const e=JSON.parse(localStorage.getItem("4chan-settings"))||{};if(e.disableAll)return;return e.disableAll=!0,localStorage.setItem("4chan-settings",JSON.stringify(e))}catch(e){return Object.defineProperty(window,"Config",{value:{disableAll:!0}})}})):Ve.global((()=>Object.defineProperty(window,"Config",{value:{disableAll:!0}})))},open(e){let t,n;if(Be.dialog)return;Ve.event("CloseMenu"),Be.dialog=t=Ve.el("div",{id:"overlay"},fe),Ve.on(Ve(".export",t),"click",Be.export), Ve.on(Ve(".import",t),"click",Be.import),Ve.on(Ve(".reset",t),"click",Be.reset),Ve.on(Ve("input",t),"change",Be.onImport);const o=[];for(var i of Be.sections){var r=Ve.el("a",{className:`tab-${i.hyphenatedTitle}`,textContent:i.title,href:"javascript:;"});Ve.on(r,"click",Be.openSection.bind(i)),o.push(r,Ve.tn(" | ")),i.title===e&&(n=r)}return o.pop(),Ve.add(Ve(".sections-list",t),o),"none"!==e&&(n||o[0]).click(),Ve.on(Ve(".close",t),"click",Be.close),Ve.on(window,"beforeunload",Be.close),Ve.on(t,"click",Be.close),Ve.on(t.firstElementChild,"click",(e=>e.stopPropagation())),Ve.add(a.body,t),Ve.event("OpenSettings",null,t)},close(){if(Be.dialog)return a.activeElement?.blur(),Ve.rm(Be.dialog),delete Be.dialog},sections:[],addSection(e,t){"string"!=typeof e&&({title:e,open:t}=e.detail);const n=e.toLowerCase().replace(/\s+/g,"-");return Be.sections.push({title:e,hyphenatedTitle:n,open:t})},openSection(){let e;(e=Ve(".tab-selected",Be.dialog))&&Ve.rmClass(e,"tab-selected"), -Ve.addClass(Ve(`.tab-${this.hyphenatedTitle}`,Be.dialog),"tab-selected");const t=Ve("section",Be.dialog);return Ve.rmAll(t),t.className=`section-${this.hyphenatedTitle}`,this.open(t,n),t.scrollTop=0,Ve.event("OpenSettings",null,t)},warnings:{localStorage(t){if(Ve.cantSync){const n=Ve.cantSet?"save your settings":"synchronize settings between tabs";return t(Ve.el("li",{textContent:`${e.name} needs local storage to ${n}.\nEnable it on boards.${location.hostname.split(".")[1]}.org in your browser's privacy settings (may be listed as part of "local data" or "cookies").`}))}},ads:e=>Ve.onExists(i,".adg-rects > .desktop",(t=>Ve.onExists(t,"iframe",(function(){const t=Ze.to("thread",{boardID:"qa",threadID:362590});return e(Ve.el("li",pe(ue,null,"To protect yourself from ",pe("a",{href:t,target:"_blank"},"malicious ads"),", you should ",pe("a",{href:"https://github.com/gorhill/uBlock#ublock-origin",target:"_blank"},"block ads")," on 4chan.")))}))))},main(o){let a;const i=Ve.el("fieldset",{ +Ve.addClass(Ve(`.tab-${this.hyphenatedTitle}`,Be.dialog),"tab-selected");const t=Ve("section",Be.dialog);return Ve.rmAll(t),t.className=`section-${this.hyphenatedTitle}`,this.open(t,n),t.scrollTop=0,Ve.event("OpenSettings",null,t)},warnings:{localStorage(t){if(Ve.cantSync){const n=Ve.cantSet?"save your settings":"synchronize settings between tabs";return t(Ve.el("li",{textContent:`${e.name} needs local storage to ${n}.\nEnable it on boards.${location.hostname.split(".")[1]}.org in your browser's privacy settings (may be listed as part of "local data" or "cookies").`}))}},ads:e=>Ve.onExists(i,".adg-rects > .desktop",(t=>Ve.onExists(t,"iframe",(function(){const t=Ze.to("thread",{boardID:"qa",threadID:362590});return e(Ve.el("li",le(se,null,"To protect yourself from ",le("a",{href:t,target:"_blank"},"malicious ads"),", you should ",le("a",{href:"https://github.com/gorhill/uBlock#ublock-origin",target:"_blank"},"block ads")," on 4chan.")))}))))},main(o){let a;const i=Ve.el("fieldset",{ hidden:!0},{innerHTML:"Warnings
    "}),r=function(e){return Ve.add(Ve("ul",i),e),i.hidden=!1};for(a in Be.warnings){(0,Be.warnings[a])(r)}Ve.add(o,i);const s=m(),l=m(),d=function(e,n){const o=[e];return(()=>{const e=[];for(a in n){var i=n[a];if(i instanceof Array){var r=i[1],d=Ve.el("div",{innerHTML:`: ${r}`});d.dataset.name=a;var c=Ve("input",d);Ve.on(c,"change",Ve.cb.checked),Ve.on(c,"change",(function(){return this.parentNode.parentNode.dataset.checked=this.checked})),s[a]=t[a],l[a]=c;var h=i[2]||0;if(o.length<=h){var u=Ve.el("div",{className:"suboption-list"});Ve.add(o[o.length-1].lastElementChild,u),o[h]=u}else o.length>h+1&&o.splice(h+1,o.length-(h+1));e.push(Ve.add(o[h],d))}}return e})()};for(var h in c.main){var u=c.main[h],p=Ve.el("fieldset",{innerHTML:`${h}`});d(p,u),"Posting and Captchas"===h&&Ve.add(p,Ve.el("p",{ innerHTML:'For more info on captcha options and issues, see the captcha FAQ.'})),Ve.add(o,p)}d(Ve('div[data-name="JSON Index"] > .suboption-list',o),c.Index),"gecko"!==Ve.engine&&(Ve('div[data-name="Remember QR Size"]',o).hidden=!0),(Ve.perProtocolSettings||"https:"!==location.protocol)&&(Ve('div[data-name="Redirect to HTTPS"]',o).hidden=!0),"crx"!==x&&(Ve('div[data-name="Work around CORB Bug"]',o).hidden=!0),Ve.get(s,(function(e){for(a in e){var t=e[a];l[a].checked=t,l[a].parentNode.parentNode.dataset.checked=t}}));const g=Ve.el("div",{innerHTML:': Clear manually-hidden threads and posts on all boards. Reload the page to apply.'}),f=Ve("button",g);return Ve.get({hiddenThreads:m(),hiddenPosts:m()},(function({hiddenThreads:e,hiddenPosts:t}){let n,o,a,i,r=0;for(o in e)if(a=e[o],"boards"!==o)for(o in a.boards)n=a.boards[o],r+=Object.keys(n).length;for(o in e.boards)n=e.boards[o], r+=Object.keys(n).length;for(o in t)if(a=t[o],"boards"!==o)for(o in a.boards)for(o in n=a.boards[o],n)i=n[o],r+=Object.keys(i).length;for(o in t.boards)for(o in n=t.boards[o],n)i=n[o],r+=Object.keys(i).length;return f.textContent=`Hidden: ${r}`})),Ve.on(f,"click",(function(){return this.textContent="Hidden: 0",Ve.get("hiddenThreads",m(),(function({hiddenThreads:e}){if(Ve.hasStorage&&"yotsuba"===n.SITE.software){let t;for(t in e["4chan.org"]?.boards)localStorage.removeItem(`4chan-hide-t-${t}`);for(t in e.boards)localStorage.removeItem(`4chan-hide-t-${t}`)}return Ve.delete(["hiddenThreads","hiddenPosts"])}))})),Ve.after(Ve('input[name="Stubs"]',o).parentNode.parentNode,g)},export(){const e=m();return Ve.extend(e,t),Ve.get(e,(function(e){return delete e.boardConfig,Be.downloadExport({version:n.VERSION,date:Date.now(),Conf:e})}))},downloadExport(t){const o=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),a=URL.createObjectURL(o),i=Ve.el("a",{ @@ -490,10 +490,10 @@ let t,n=e.dataset.href.match(/^\w+:\/\/(?:(clips\.)|\w+\.)?twitch\.tv\/(?:\w+\/) el:e=>Ve.el("iframe",{src:`https://vine.co/v/${e.dataset.uid}/card`})},{key:"Vocaroo",regExp:/^\w+:\/\/(?:(?:www\.|old\.)?vocaroo\.com|voca\.ro)\/((?:i\/)?\w+)/,style:"",el(e){const t=Ve.el("iframe");return t.width=300,t.height=60,t.setAttribute("frameborder",0),t.src=`https://vocaroo.com/embed/${e.dataset.uid.replace(/^i\//,"")}?autoplay=0`,t}},{key:"YouTube",regExp:/^\w+:\/\/(?:youtu.be\/|[\w.]*youtube[\w.]*\/.*(?:v=|\bembed\/|\bv\/|live\/))([\w\-]{11})(.*)/,el(e){let t=e.dataset.options.match(/\b(?:star)?t\=(\w+)/);t&&(t=t[1]),t&&!/^\d+$/.test(t)&&(t+=" 0h0m0s",t=3600*t.match(/(\d+)h/)[1]+60*t.match(/(\d+)m/)[1]+1*t.match(/(\d+)s/)[1]);const n=Ve.el("iframe",{src:`//www.youtube.com/embed/${e.dataset.uid}?rel=0&wmode=opaque${t?"&start="+t:""}`});return n.setAttribute("allowfullscreen","true"),n},title:{api:e=>`https://www.youtube.com/oembed?url=https%3A//www.youtube.com/watch%3Fv%3D${e}&format=json`,text:e=>e.title,status(e){if(e.error){const t=e.error.match(/^(\d*)\s*(.*)/) ;return[+t[1],t[2]]}return[200,"OK"]}},preview:{url:e=>`https://img.youtube.com/vi/${e}/0.jpg`,height:360}}]},Fe={init(){if(t.Keybinds){for(var e in c.hotkeys)Ve.sync(e,Fe.sync);var n=function(){for(var e of(Ve.off(a,"4chanXInitFinished",n),Ve.on(a,"keydown",Fe.keydown),u("[accesskey]")))e.removeAttribute("accesskey")};return Ve.on(a,"4chanXInitFinished",n)}},sync:(e,n)=>t[n]=e,keydown(e){let o,a,i,r,s;if(!(o=Fe.keyCode(e)))return;const{target:l}=e;if(!["INPUT","TEXTAREA"].includes(l.nodeName)||/(Esc|Alt|Ctrl|Meta|Shift\+\w{2,})/.test(o)&&!/^Alt\+(\d|Up|Down|Left|Right)$/.test(o)){switch(["index","thread"].includes(n.VIEW)&&(i=F.getThread(),a=ze.threadFromRoot(i)),o){case t["Toggle board list"]:if(!t["Custom Board Navigation"])return;Ke.toggleBoardList();break;case t["Toggle header"]:Ke.toggleBarVisibility();break;case t["Open empty QR"]:if(!$e.postingIsEnabled)return;Fe.qr();break;case t["Open QR"]:if(!$e.postingIsEnabled||!i)return;Fe.qr(i);break;case t["Open settings"]:Be.open() ;break;case t.Close:if(Be.dialog)Be.close();else if((s=u(".notification")).length)for(var d of s)Ve(".close",d).click();else if($e.nodes&&!$e.nodes.el.hidden&&"none"!==window.getComputedStyle($e.nodes.form).display)t["Persistent QR"]?$e.hide():$e.close();else{if(!Le.lastEmbed)return;Le.closeFloat()}break;case t["Spoiler tags"]:if("TEXTAREA"!==l.nodeName)return;Fe.tags("spoiler",l);break;case t["Code tags"]:if("TEXTAREA"!==l.nodeName)return;Fe.tags("code",l);break;case t["Eqn tags"]:if("TEXTAREA"!==l.nodeName)return;Fe.tags("eqn",l);break;case t["Math tags"]:if("TEXTAREA"!==l.nodeName)return;Fe.tags("math",l);break;case t["SJIS tags"]:if("TEXTAREA"!==l.nodeName)return;Fe.tags("sjis",l);break;case t["Toggle sage"]:if(!$e.nodes||$e.nodes.el.hidden)return;Fe.sage();break;case t["Toggle Cooldown"]:if(!$e.nodes||$e.nodes.el.hidden||!Ve.hasClass($e.nodes.fileSubmit,"custom-cooldown"))return;$e.toggleCustomCooldown();break;case t["Post from URL"]:if(!$e.postingIsEnabled)return;$e.handleUrl("") -;break;case t["Add new post"]:if(!$e.postingIsEnabled)return;$e.addPost();break;case t["Submit QR"]:if(!$e.nodes||$e.nodes.el.hidden)return;$e.status()||$e.submit();break;case t.Update:switch(n.VIEW){case"thread":if(!Re.enabled)return;Re.update();break;case"index":if(!le.enabled)return;le.update();break;default:return}break;case t.Watch:if(!ae.enabled||!a)return;ae.toggle(a);break;case t["Update thread watcher"]:if(!ae.enabled)return;ae.buttonFetchAll();break;case t["Toggle thread watcher"]:if(!ae.enabled)return;ae.toggleWatcher();break;case t["Toggle threading"]:if(!Te.ready)return;Te.toggleThreading();break;case t["Mark thread read"]:if("index"!==n.VIEW||!a||!ne.enabled)return;ne.markRead.call(i);break;case t["Expand image"]:if(!U.enabled||!i)return;var c=ze.postFromNode(Fe.post(i));c.file&&U.toggle(c);break;case t["Expand images"]:if(!U.enabled)return;U.cb.toggleAll();break;case t["Open Gallery"]:if(!Ne.enabled)return;Ne.cb.toggle();break;case t.fappeTyme:if(!Pe.nodes?.fappe)return -;Pe.toggle("fappe");break;case t.werkTyme:if(!Pe.nodes?.werk)return;Pe.toggle("werk");break;case t["Front page"]:le.enabled?le.userPageNav(1):location.href=`/${n.BOARD}/`;break;case t["Open front page"]:Ve.open(`${location.origin}/${n.BOARD}/`);break;case t["Next page"]:if("index"!==n.VIEW||n.SITE.isOnePage?.(n.BOARD))return;if(le.enabled){if(!["paged","infinite"].includes(t["Index Mode"]))return;Ve(".next button",le.pagelist).click()}else Ve(n.SITE.selectors.nav.next)?.click();break;case t["Previous page"]:if("index"!==n.VIEW||n.SITE.isOnePage?.(n.BOARD))return;if(le.enabled){if(!["paged","infinite"].includes(t["Index Mode"]))return;Ve(".prev button",le.pagelist).click()}else Ve(n.SITE.selectors.nav.prev)?.click();break;case t["Search form"]:if("index"!==n.VIEW)return;var h=le.enabled?le.searchInput:n.SITE.selectors.searchBox?Ve(n.SITE.selectors.searchBox):void 0;if(!h)return;Ke.scrollToIfNeeded(h),h.focus();break;case t["Paged mode"]:if(!le.enabledOn(n.BOARD))return -;location.href="index"===n.VIEW?"#paged":`/${n.BOARD}/#paged`;break;case t["Infinite scrolling mode"]:if(!le.enabledOn(n.BOARD))return;location.href="index"===n.VIEW?"#infinite":`/${n.BOARD}/#infinite`;break;case t["All pages mode"]:if(!le.enabledOn(n.BOARD))return;location.href="index"===n.VIEW?"#all-pages":`/${n.BOARD}/#all-pages`;break;case t["Open catalog"]:if(!(r=Ye.catalog()))return;location.href=r;break;case t["Cycle sort type"]:if(!le.enabled)return;le.cycleSortType();break;case t["Next thread"]:if("index"!==n.VIEW||!i)return;F.scroll(1);break;case t["Previous thread"]:if("index"!==n.VIEW||!i)return;F.scroll(-1);break;case t["Expand thread"]:if("index"!==n.VIEW||!i)return;te.toggle(a),Ke.scrollTo(i);break;case t["Open thread"]:if("index"!==n.VIEW||!i)return;Fe.open(a);break;case t["Open thread tab"]:if("index"!==n.VIEW||!i)return;Fe.open(a,!0);break;case t["Next reply"]:if(!i)return;Fe.hl(1,i);break;case t["Previous reply"]:if(!i)return;Fe.hl(-1,i);break -;case t["Deselect reply"]:if(!i)return;Fe.hl(0,i);break;case t.Hide:if(!a||!de.db)return;Ke.scrollTo(i),de.toggle(a);break;case t["Quick Filter MD5"]:if(!i)return;c=Fe.post(i),Fe.hl(1,i),We.quickFilterMD5.call(c,e);break;case t["Previous Post Quoting You"]:if(!i||!_.db)return;_.cb.seek("preceding");break;case t["Next Post Quoting You"]:if(!i||!_.db)return;_.cb.seek("following");break;default:return}return e.preventDefault(),e.stopPropagation()}},keyCode(e){let t=(()=>{let t;switch(t=e.keyCode){case 8: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:return 48<=t&&t<=57||65<=t&&t<=90?String.fromCharCode(t).toLowerCase():96<=t&&t<=105?String.fromCharCode(t-48).toLowerCase():null}})();return t&&(e.altKey&&(t="Alt+"+t),e.ctrlKey&&(t="Ctrl+"+t),e.metaKey&&(t="Meta+"+t), +;break;case t["Add new post"]:if(!$e.postingIsEnabled)return;$e.addPost();break;case t["Submit QR"]:if(!$e.nodes||$e.nodes.el.hidden)return;$e.status()||$e.submit();break;case t.Update:switch(n.VIEW){case"thread":if(!Re.enabled)return;Re.update();break;case"index":if(!ue.enabled)return;ue.update();break;default:return}break;case t.Watch:if(!ae.enabled||!a)return;ae.toggle(a);break;case t["Update thread watcher"]:if(!ae.enabled)return;ae.buttonFetchAll();break;case t["Toggle thread watcher"]:if(!ae.enabled)return;ae.toggleWatcher();break;case t["Toggle threading"]:if(!Te.ready)return;Te.toggleThreading();break;case t["Mark thread read"]:if("index"!==n.VIEW||!a||!ne.enabled)return;ne.markRead.call(i);break;case t["Expand image"]:if(!U.enabled||!i)return;var c=ze.postFromNode(Fe.post(i));c.file&&U.toggle(c);break;case t["Expand images"]:if(!U.enabled)return;U.cb.toggleAll();break;case t["Open Gallery"]:if(!Ne.enabled)return;Ne.cb.toggle();break;case t.fappeTyme:if(!Pe.nodes?.fappe)return +;Pe.toggle("fappe");break;case t.werkTyme:if(!Pe.nodes?.werk)return;Pe.toggle("werk");break;case t["Front page"]:ue.enabled?ue.userPageNav(1):location.href=`/${n.BOARD}/`;break;case t["Open front page"]:Ve.open(`${location.origin}/${n.BOARD}/`);break;case t["Next page"]:if("index"!==n.VIEW||n.SITE.isOnePage?.(n.BOARD))return;if(ue.enabled){if(!["paged","infinite"].includes(t["Index Mode"]))return;Ve(".next button",ue.pagelist).click()}else Ve(n.SITE.selectors.nav.next)?.click();break;case t["Previous page"]:if("index"!==n.VIEW||n.SITE.isOnePage?.(n.BOARD))return;if(ue.enabled){if(!["paged","infinite"].includes(t["Index Mode"]))return;Ve(".prev button",ue.pagelist).click()}else Ve(n.SITE.selectors.nav.prev)?.click();break;case t["Search form"]:if("index"!==n.VIEW)return;var h=ue.enabled?ue.searchInput:n.SITE.selectors.searchBox?Ve(n.SITE.selectors.searchBox):void 0;if(!h)return;Ke.scrollToIfNeeded(h),h.focus();break;case t["Paged mode"]:if(!ue.enabledOn(n.BOARD))return +;location.href="index"===n.VIEW?"#paged":`/${n.BOARD}/#paged`;break;case t["Infinite scrolling mode"]:if(!ue.enabledOn(n.BOARD))return;location.href="index"===n.VIEW?"#infinite":`/${n.BOARD}/#infinite`;break;case t["All pages mode"]:if(!ue.enabledOn(n.BOARD))return;location.href="index"===n.VIEW?"#all-pages":`/${n.BOARD}/#all-pages`;break;case t["Open catalog"]:if(!(r=Ye.catalog()))return;location.href=r;break;case t["Cycle sort type"]:if(!ue.enabled)return;ue.cycleSortType();break;case t["Next thread"]:if("index"!==n.VIEW||!i)return;F.scroll(1);break;case t["Previous thread"]:if("index"!==n.VIEW||!i)return;F.scroll(-1);break;case t["Expand thread"]:if("index"!==n.VIEW||!i)return;te.toggle(a),Ke.scrollTo(i);break;case t["Open thread"]:if("index"!==n.VIEW||!i)return;Fe.open(a);break;case t["Open thread tab"]:if("index"!==n.VIEW||!i)return;Fe.open(a,!0);break;case t["Next reply"]:if(!i)return;Fe.hl(1,i);break;case t["Previous reply"]:if(!i)return;Fe.hl(-1,i);break +;case t["Deselect reply"]:if(!i)return;Fe.hl(0,i);break;case t.Hide:if(!a||!pe.db)return;Ke.scrollTo(i),pe.toggle(a);break;case t["Quick Filter MD5"]:if(!i)return;c=Fe.post(i),Fe.hl(1,i),We.quickFilterMD5.call(c,e);break;case t["Previous Post Quoting You"]:if(!i||!_.db)return;_.cb.seek("preceding");break;case t["Next Post Quoting You"]:if(!i||!_.db)return;_.cb.seek("following");break;default:return}return e.preventDefault(),e.stopPropagation()}},keyCode(e){let t=(()=>{let t;switch(t=e.keyCode){case 8: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:return 48<=t&&t<=57||65<=t&&t<=90?String.fromCharCode(t).toLowerCase():96<=t&&t<=105?String.fromCharCode(t-48).toLowerCase():null}})();return t&&(e.altKey&&(t="Alt+"+t),e.ctrlKey&&(t="Ctrl+"+t),e.metaKey&&(t="Meta+"+t), e.shiftKey&&(t="Shift+"+t)),t},post(e){const t=n.SITE.selectors;return Ve(`${t.postContainer}${t.highlightable.reply}.${n.SITE.classes.highlight}`,e)||Ve(`${n.SITE.isOPContainerThread?t.thread:t.postContainer}${t.highlightable.op}`,e)},qr:e=>($e.open(),null!=e&&$e.quote.call(Fe.post(e)),$e.nodes.com.focus()),tags(e,t){Y.ready((function(){const{config:t}=n.BOARD;if(!(()=>{switch(e){case"spoiler":return!!t.spoilers;case"code":return!!t.code_tags;case"math":case"eqn":return!!t.math_tags;case"sjis":return!!t.sjis_tags}})())return new Xe("warning",`[${e}] tags are not supported on /${n.BOARD}/.`,20)}));const{value:o}=t,a=t.selectionStart,i=t.selectionEnd;t.value=o.slice(0,a)+`[${e}]`+o.slice(a,i)+`[/${e}]`+o.slice(i);const r=`[${e}]`.length+i;return t.setSelectionRange(r,r),Ve.event("input",null,t)},sage(){const e=/sage/i.test($e.nodes.email.value);return $e.nodes.email.value=e?"":"sage"},open(e,t){if("index"!==n.VIEW)return;const o=ze.url("thread",e);return t?Ve.open(o):location.href=o}, hl(e,t){const o=`${n.SITE.selectors.postContainer}${n.SITE.selectors.highlightable.reply}`,{highlight:a}=n.SITE.classes,i=Ve(`${o}.${a}`,t);if(!e)return void(i&&Ve.rmClass(i,a));if(i){const{height:t}=i.getBoundingClientRect();if(Ke.getTopOf(i)>=-t&&Ke.getBottomOf(i)>=-t){let t;const{root:r}=ze.postFromNode(i).nodes,s=1===e?"following":"preceding";if(!(t=Ve.x(`${s}-sibling::${n.SITE.xpath.replyContainer}[not(@hidden) and not(child::div[@class='stub'])][1]`,r)))return;return t.matches(o)||(t=Ve(o,t)),Ke.scrollToIfNeeded(t,1===e),Ve.addClass(t,a),void Ve.rmClass(i,a)}Ve.rmClass(i,a)}const r=u(o,t);for(var s of(-1===e&&r.reverse(),r))if(1===e&&Ke.getTopOf(s)>0||-1===e&&Ke.getBottomOf(s)>0)return void Ve.addClass(s,a)}};const Oe={Cache:{init(){return Ve.on(a,"SaveCaptcha",(e=>this.saveAPI(e.detail))),Ve.on(a,"NoCaptcha",(e=>this.noCaptcha(e.detail)))},captchas:[],getCount(){return this.captchas.length},neededRaw(){ return!(this.haveCookie()||this.captchas.length||$e.req||this.submitCB)&&($e.posts.length>1||t["Auto-load captcha"]||!$e.posts[0].isOnlyQuotes()||$e.posts[0].file)},needed(){return this.neededRaw()&&Ve.event("LoadCaptcha")},prerequest(){if(t["Prerequest Captcha"])return Ve.queueTask((()=>{if(!this.prerequested&&this.neededRaw()&&!Ve.event("LoadCaptcha")&&!$e.captcha.occupied()&&$e.cooldown.seconds<=60&&$e.selected===$e.posts[$e.posts.length-1]&&!$e.selected.isOnlyQuotes()){const e="new"!==$e.selected.thread;if(!Ve.event("RequestCaptcha",{isReply:e}))return this.prerequested=!0,this.submitCB=e=>{if(e)return this.save(e)},this.updateCount()}}))},haveCookie:()=>/\b_ct=/.test(a.cookie)&&"new"!==$e.posts[0].thread,getOne(){let e;return delete this.prerequested,this.clear(),(e=this.captchas.shift())?(this.count(),e):null},request(e){if(this.submitCB||!Ve.event("RequestCaptcha",{isReply:e}))return e=>(this.submitCB=e,this.updateCount())},abort(){if(this.submitCB)return delete this.submitCB, @@ -570,17 +570,17 @@ const{OP:t}=e,n=`/${decodeURIComponent(e.board.ID)}/ - `+(t.info.subject?.trim() e.clones))o.push(...n.nodes[t]||[])};if(a.forEach((function(e){if(e.quotes.includes(i))return r(e,"quotelinks")})),t["Quote Backlinks"])for(var s of e.quotes){var l;(l=a.get(s))&&r(l,"backlinks")}return o.filter((function(t){const{boardID:n,postID:o}=Qe.postDataFromLink(t);return n===e.board.ID&&o===e.ID}))}},ze=Qe,We={filters:new Map,init(){if(["index","thread","catalog"].includes(n.VIEW)&&t.Filter&&("catalog"!==n.VIEW||t["Filter in Native Catalog"])){for(var e in t["Filtered Backlinks"]||Ve.addClass(i,"hide-backlinks"),c.filter)for(var o of t[e].split("\n")){let n,i,l,c,m;if("#"===o[0])continue;if(!(l=o.match(/\/(.*)\/(\w*)/)))continue;var a=o.replace(l[0],""),r=this.parseBoards(a.match(/(?:^|;)\s*boards:([^;]+)/)?.[1]),s=this.parseBoards(a.match(/(?:^|;)\s*exclude:([^;]+)/)?.[1]);if(i=["uniqueID","MD5"].includes(e))l=l[1];else try{l=RegExp(l[1],l[2])}catch(t){new Xe("warning",[Ve.tn(`Invalid ${e} filter:`),Ve.el("br"),Ve.tn(o),Ve.el("br"),Ve.tn(t.message)],60);continue} var d=a.match(/(?:^|;)\s*op:(no|only)/)?.[1]||"",h=Ve.getOwn({no:1,only:2},d)||0,u=a.match(/(?:^|;)\s*file:(no|only)/)?.[1]||"";h|=Ve.getOwn({no:4,only:8},u)||0;var p=(()=>{switch(a.match(/(?:^|;)\s*stub:(yes|no)/)?.[1]){case"yes":return!0;case"no":return!1;default:return t.Stubs}})(),g=/(?:^|;)\s*notify/.test(a);(n=/(?:^|;)\s*highlight/.test(a))&&(n=a.match(/(?:^|;)\s*highlight:([\w-]+)/)?.[1]||"filter-highlight",c=a.match(/(?:^|;)\s*top:(yes|no)/)?.[1]||"yes",c="yes"===c),"general"===e&&(m=(m=a.match(/(?:^|;)\s*type:([^;]*)/))?m[1].split(","):["subject","name","filename","comment"]);const b={isstring:i,regexp:l,boards:r,excludes:s,mask:h,hide:!(n||g),stub:p,hl:n,top:c,noti:g};if("general"===e)for(var f of m)this.filters.get(f)?.push(b)??this.filters.set(f,[b]);else this.filters.get(e)?.push(b)??this.filters.set(e,[b])}if(this.filters.size){for(const e of["MD5","uniqueID"]){const t=this.filters.get(e);if(!t)continue;const n=new Map ;for(const e of t)n.get(e.regexp)?.push(e)??n.set(e.regexp,[e]);this.filters.set(e,n)}return"catalog"===n.VIEW?We.catalog():l.Post.push({name:"Filter",cb:this.node})}}},parseBoards(e){let t;if(!e)return!1;if(t=We.parseBoardsMemo[e])return t;t=m();let o="";for(var a of e.split(","))for(var i in a.includes(":")&&([o,a]=a.split(":").slice(-2)),n.sites){var r=n.sites[i];if(i.slice(0,o.length)===o)if(["nsfw","sfw"].includes(a))for(var s of r.sfwBoards?.("sfw"===a)||[])t[`${i}/${s}`]=!0;else t[`${i}/${encodeURIComponent(a)}`]=!0}return We.parseBoardsMemo[e]=t,t},parseBoardsMemo:m(),test(e,t=!0){if(e.filterResults)return e.filterResults;let n,o=!1,a=!0,i=!1,r=!1;_.isYou(e)&&(t=!1);let s=e.isReply?2:1;s|=e.file?4:8;const l=`${e.siteID}/${e.boardID}`,d=`${e.siteID}/*`;for(const c of We.filters.keys())for(const h of We.values(c,e)){const e=We.filters.get(c),u=Array.isArray(e)?e:e.get(h) -;if(u)for(const e of u)e.boards&&!e.boards[l]&&!e.boards[d]||e.excludes&&(e.excludes[l]||e.excludes[d])||e.mask&s||!(e.isstring?e.regexp===h:e.regexp.test(h))||(e.hide?t&&(o=!0,a&&({stub:a}=e)):(n&&n.includes(e.hl)||(n||(n=[])).push(e.hl),i||({top:i}=e),e.noti&&(r=!0)))}return o?{hide:o,stub:a}:{hl:n,top:i,noti:r}},node(){if(this.isClone)return;const{hide:e,stub:t,hl:o,top:a,noti:i}=We.test(this,!this.isFetchedQuote&&(this.isReply||"index"===n.VIEW));return e?this.isReply?W.hide(this,t):de.hide(this.thread,t):o&&(this.highlights=o,Ve.addClass(this.nodes.root,...o)),i&&ee.posts&&this.ID>ee.lastReadPost&&!_.isYou(this)?ee.openNotification(this," triggered a notification filter"):void 0},catalog(){let e;if(e=n.SITE.urls.catalogJSON?.(n.BOARD))return We.catalogData=m(),Ve.ajax(e,{onloadend:We.catalogParse}),l.CatalogThreadNative.push({name:"Filter",cb:this.catalogNode})},catalogParse(){if([200,404].includes(this.status)){ +;if(u)for(const e of u)e.boards&&!e.boards[l]&&!e.boards[d]||e.excludes&&(e.excludes[l]||e.excludes[d])||e.mask&s||!(e.isstring?e.regexp===h:e.regexp.test(h))||(e.hide?t&&(o=!0,a&&({stub:a}=e)):(n&&n.includes(e.hl)||(n||(n=[])).push(e.hl),i||({top:i}=e),e.noti&&(r=!0)))}return o?{hide:o,stub:a}:{hl:n,top:i,noti:r}},node(){if(this.isClone)return;const{hide:e,stub:t,hl:o,top:a,noti:i}=We.test(this,!this.isFetchedQuote&&(this.isReply||"index"===n.VIEW));return e?this.isReply?W.hide(this,t):pe.hide(this.thread,t):o&&(this.highlights=o,Ve.addClass(this.nodes.root,...o)),i&&ee.posts&&this.ID>ee.lastReadPost&&!_.isYou(this)?ee.openNotification(this," triggered a notification filter"):void 0},catalog(){let e;if(e=n.SITE.urls.catalogJSON?.(n.BOARD))return We.catalogData=m(),Ve.ajax(e,{onloadend:We.catalogParse}),l.CatalogThreadNative.push({name:"Filter",cb:this.catalogNode})},catalogParse(){if([200,404].includes(this.status)){ for(var e of this.response)for(var t of e.threads)We.catalogData[t.no]=t;n.BOARD.threads.forEach((function(e){if(e.catalogViewNative)return We.catalogNode.call(e.catalogViewNative)}))}else new Xe("warning","Failed to fetch catalog JSON data. "+(this.status?`Error ${this.statusText} (${this.status})`:"Connection Error"),1)},catalogNode(){if(this.boardID!==n.BOARD.ID||!We.catalogData[this.ID])return;if(_.db?.get({siteID:n.SITE.ID,boardID:this.boardID,threadID:this.ID,postID:this.ID}))return;const{hide:e,hl:t,top:o}=We.test(n.SITE.Build.parseJSON(We.catalogData[this.ID],this));return e?this.nodes.root.hidden=!0:(t&&(this.highlights=t,Ve.addClass(this.nodes.root,...t)),o?(Ve.prepend(this.nodes.root.parentNode,this.nodes.root),n.SITE.catalogPin?.(this.nodes.root)):void 0)},isHidden:e=>!!We.test(e).hide,valueF:{postID:e=>[`${e.ID}`],name:e=>void 0===e.info.name?[]:[e.info.name],uniqueID:e=>[e.info.uniqueID||""],tripcode:e=>void 0===e.info.tripcode?[]:[e.info.tripcode], capcode:e=>void 0===e.info.capcode?[]:[e.info.capcode],pass:e=>[e.info.pass],email:e=>[e.info.email],subject:e=>[e.info.subject||(e.isReply?void 0:"")],comment:e=>(null==e.info.comment&&(e.info.comment=n.sites[e.siteID]?.Build?.parseComment?.(e.info.commentHTML.innerHTML)),[e.info.comment]),flag:e=>void 0===e.info.flag?[]:[e.info.flag],filename:e=>e.files.map((e=>e.name)),dimensions:e=>e.files.map((e=>e.dimensions)),filesize:e=>e.files.map((e=>e.size)),MD5:e=>e.files.map((e=>e.MD5))},values:(e,t)=>Ve.hasOwn(We.valueF,e)?We.valueF[e](t).filter((e=>null!=e)):[e.split("+").map((function(e){let n;return(n=Ve.getOwn(We.valueF,e))?n(t).map((e=>e||"")).join("\n"):""})).join("\n")],addFilter(e,n,o){if(Ve.hasOwn(c.filter,e))return Ve.get(e,t[e],(function(t){let a=t[e];return a=a?`${a}\n${n}`:n,Ve.set(e,a,o)}))},removeFilters:(e,n,o)=>Ve.get(e,t[e],(function(t){let a=t[e];const i=(Array.isArray(n)?n:[...n.values()].flat()).map(We.escape).join("|") -;return a=a.replace(RegExp(`(?:$\n|^)(?:${i})$`,"mg"),""),Ve.set(e,a,o)})),showFilters(e){Be.open("Filter");const t=Ve(".section-container"),n=Ve("select[name=filter]",t);return n.value=e,Be.selectFilter.call(n),Ve.onExists(t,"textarea",(function(e){const t=e.textLength;return e.setSelectionRange(t,t),e.focus()}))},quickFilterMD5(){const e=ze.postFromNode(this),o=e.files.filter((e=>e.MD5));if(!o.length)return;const a=o.map((e=>`/${e.MD5}/`)).join("\n");We.addFilter("MD5",a);const i=e.origin||e;if(i.isReply?W.hide(i):"index"===n.VIEW&&de.hide(i.thread),!t["MD5 Quick Filter Notifications"])return void(e.nodes.post.getBoundingClientRect().height&&new Xe("info","MD5 filtered.",2));let{notice:r}=We.quickFilterMD5;if(r)return r.filters.push(a),r.posts.push(i),Ve("span",r.el).textContent=`${r.filters.length} MD5s filtered.`;{const e=Ve.el("div",{innerHTML:'MD5 filtered. [show] [undo]'}) -;r=We.quickFilterMD5.notice=new Xe("info",e,void 0,(()=>delete We.quickFilterMD5.notice)),r.filters=[a],r.posts=[i];const t=u("a",e);return Ve.on(t[0],"click",We.quickFilterCB.show.bind(r)),Ve.on(t[1],"click",We.quickFilterCB.undo.bind(r))}},quickFilterCB:{show(){return We.showFilters("MD5"),this.close()},undo(){for(var e of(We.removeFilters("MD5",this.filters),this.posts))e.isReply?W.show(e):"index"===n.VIEW&&de.show(e.thread);return this.close()}},escape:e=>e.replace(new RegExp("/|\\\\|\\^|\\$|\\n|\\.|\\(|\\)|\\{|\\}|\\[|\\]|\\?|\\*|\\+|\\|","g"),(function(e){return"\n"===e?"\\n":"\\"===e?"\\\\":`\\${e}`})),menu:{init(){if(!["index","thread"].includes(n.VIEW)||!t.Menu||!t.Filter)return;const e={el:Ve.el("div",{textContent:"Filter"}),order:50,open:e=>(We.menu.post=e,!0),subEntries:[]} +;return a=a.replace(RegExp(`(?:$\n|^)(?:${i})$`,"mg"),""),Ve.set(e,a,o)})),showFilters(e){Be.open("Filter");const t=Ve(".section-container"),n=Ve("select[name=filter]",t);return n.value=e,Be.selectFilter.call(n),Ve.onExists(t,"textarea",(function(e){const t=e.textLength;return e.setSelectionRange(t,t),e.focus()}))},quickFilterMD5(){const e=ze.postFromNode(this),o=e.files.filter((e=>e.MD5));if(!o.length)return;const a=o.map((e=>`/${e.MD5}/`)).join("\n");We.addFilter("MD5",a);const i=e.origin||e;if(i.isReply?W.hide(i):"index"===n.VIEW&&pe.hide(i.thread),!t["MD5 Quick Filter Notifications"])return void(e.nodes.post.getBoundingClientRect().height&&new Xe("info","MD5 filtered.",2));let{notice:r}=We.quickFilterMD5;if(r)return r.filters.push(a),r.posts.push(i),Ve("span",r.el).textContent=`${r.filters.length} MD5s filtered.`;{const e=Ve.el("div",{innerHTML:'MD5 filtered. [show] [undo]'}) +;r=We.quickFilterMD5.notice=new Xe("info",e,void 0,(()=>delete We.quickFilterMD5.notice)),r.filters=[a],r.posts=[i];const t=u("a",e);return Ve.on(t[0],"click",We.quickFilterCB.show.bind(r)),Ve.on(t[1],"click",We.quickFilterCB.undo.bind(r))}},quickFilterCB:{show(){return We.showFilters("MD5"),this.close()},undo(){for(var e of(We.removeFilters("MD5",this.filters),this.posts))e.isReply?W.show(e):"index"===n.VIEW&&pe.show(e.thread);return this.close()}},escape:e=>e.replace(new RegExp("/|\\\\|\\^|\\$|\\n|\\.|\\(|\\)|\\{|\\}|\\[|\\]|\\?|\\*|\\+|\\|","g"),(function(e){return"\n"===e?"\\n":"\\"===e?"\\\\":`\\${e}`})),menu:{init(){if(!["index","thread"].includes(n.VIEW)||!t.Menu||!t.Filter)return;const e={el:Ve.el("div",{textContent:"Filter"}),order:50,open:e=>(We.menu.post=e,!0),subEntries:[]} ;for(var o of[["Name","name"],["Unique ID","uniqueID"],["Tripcode","tripcode"],["Capcode","capcode"],["Pass Date","pass"],["Email","email"],["Subject","subject"],["Comment","comment"],["Flag","flag"],["Filename","filename"],["Image dimensions","dimensions"],["Filesize","filesize"],["Image MD5","MD5"]])e.subEntries.push(We.menu.createSubEntry(o[0],o[1]));return Q.menu.addEntry(e)},createSubEntry(e,t){const n=Ve.el("a",{href:"javascript:;",textContent:e});return n.dataset.type=t,Ve.on(n,"click",We.menu.makeFilter),{el:n,open:e=>We.values(t,e).length}},makeFilter(){const{type:e}=this.dataset,t=We.values(e,We.menu.post).map((function(t){const n=["uniqueID","MD5"].includes(e)?t:We.escape(t);return["uniqueID","MD5"].includes(e)?`/${n}/`:`/^${n}$/`})).join("\n");return We.addFilter(e,t,(()=>We.showFilters(e)))}}},Ge={defaultProperties:{"4chan.org":{software:"yotsuba"},"4channel.org":{canonical:"4chan.org"},"4cdn.org":{canonical:"4chan.org"},"notso.smuglo.li":{canonical:"smuglo.li"}, "smugloli.net":{canonical:"smuglo.li"},"smug.nepu.moe":{canonical:"smuglo.li"}},init(e){Ve.extend(t.siteProperties,Ge.defaultProperties);let o=Ge.resolve();return o&&Ve.hasOwn(Ce,t.siteProperties[o].software)&&(this.set(o),e()),Ve.onExists(i,"body",(()=>{for(var a in Ce){var i;if(i=Ce[a].detect?.()){i.software=a,o=location.hostname.replace(/^www\./,"");var r=t.siteProperties[o]||(t.siteProperties[o]=m()),s=0;for(var l in i)r[l]!==i[l]&&(r[l]=i[l],s++);return s&&Ve.set("siteProperties",t.siteProperties),void(n.SITE||(this.set(o),e()))}}}))},resolve(e=location){let{hostname:n}=e;for(;n&&!Ve.hasOwn(t.siteProperties,n);)n=n.replace(/^[^.]*\.?/,"");if(n){let e;(e=t.siteProperties[n].canonical)&&(n=e)}return n},parseURL(e){const t=Ge.resolve(e);return Ot.parseURL(n.sites[t],e)},set(e){for(var o in t.siteProperties){var a,i=t.siteProperties[o];if(!i.canonical){var{software:r}=i;r&&Ve.hasOwn(Ce,r)&&(n.sites[o]=a=Object.create(Ce[r]),Ve.extend(a,{ID:o,siteID:o,properties:i,software:r}))}} return n.SITE=n.sites[e]}},Ye={init(){if("yotsuba"===n.SITE.software&&(t["External Catalog"]||t["JSON Index"])&&(!t["JSON Index"]||"index"!==n.VIEW)){const o=(()=>{switch(n.VIEW){case"thread":case"archive":return".navLinks.desktop > a";case"catalog":return".navLinks > :first-child > a";case"index":return"#ctrl-top > a, .cataloglink > a"}})();Ve.ready((function(){for(var a of u(o)){var i;switch(a.pathname.replace(/\/+/g,"/")){case`/${n.BOARD}/`:t["JSON Index"]&&(a.textContent="Index"),a.href=Ye.index();break;case`/${n.BOARD}/catalog`:a.href=Ye.catalog()}if("catalog"===n.VIEW&&(i=Ye.catalog())!==n.SITE.urls.catalog?.(n.BOARD)){var r=a.parentNode.cloneNode(!0),s=r.firstElementChild;s.href=i,s.textContent=s.hostname===location.hostname?`${e.name} Catalog`:"External Catalog",Ve.after(a.parentNode,[Ve.tn(" "),r])}}}))}if("yotsuba"===n.SITE.software&&t["JSON Index"]&&t[`Use ${e.name} Catalog`]&&l.Post.push({name:"Catalog Link Rewrite",cb:this.node}),this.enabled=t["Catalog Links"]){let e ;Ye.el=e=L.checkbox("Header catalog links","Catalog Links"),e.id="toggleCatalog";const t=Ve("input",e);return Ve.on(t,"change",this.toggle),Ve.sync("Header catalog links",Ye.set),Ke.menu.addEntry({el:e,order:95})}},node(){for(var e of u("a",this.nodes.comment)){var t;(t=e.href.match(/^https?:\/\/(boards\.4chan(?:nel)?\.org\/[^\/]+)\/catalog(#s=.*)?/))&&(e.href=`//${t[1]}/${t[2]||"#catalog"}`)}},toggle(){return Ve.event("CloseMenu"),Ve.set("Header catalog links",this.checked),Ye.set(this.checked)},set:e=>(t["Header catalog links"]=e,Ye.setLinks(Ke.boardList),Ye.setLinks(Ke.bottomBoardList),Ye.el.title=`Turn catalog links ${e?"off":"on"}.`,Ve("input",Ye.el).checked=e),setLinks(e){if(!(Ye.enabled??t["Catalog Links"])||!e)return;const n=/(?:index)?(?:\.\w+)?$/;for(var o of u("a:not([data-only])",e)){var{siteID:a,boardID:i}=o.dataset;if(!a||!i){var r;if(({siteID:a,boardID:i,VIEW:r}=Ge.parseURL(o)), !a||!i||!["index","catalog"].includes(r)||!o.dataset.indexOptions&&o.href.replace(n,"")!==(ze.url(r,{siteID:a,boardID:i})||"").replace(n,""))continue;Ve.extend(o.dataset,{siteID:a,boardID:i})}var s={siteID:a,boardID:i},l=t["Header catalog links"]?Ye.catalog(s):ze.url("index",s);l&&(o.href=l,o.dataset.indexOptions&&l.split("#")[0]===ze.url("index",s)&&(o.href+=(o.hash?"/":"#")+o.dataset.indexOptions))}},externalParse(){for(var e of(Ye.externalList=m(),t.externalCatalogURLs.split("\n")))if("#"!==e[0]){var n=e.split(";")[0],o=We.parseBoards(e.match(/;boards:([^;]+)/)?.[1]||"*"),a=We.parseBoards(e.match(/;exclude:([^;]+)/)?.[1])||m();for(var i in o)a[i]||a[i.split("/")[0]+"/*"]||(Ye.externalList[i]=n)}},external({siteID:e,boardID:t}){Ye.externalList||Ye.externalParse();const n=Ye.externalList[`${e}/${t}`]||Ye.externalList[`${e}/*`];return n?n.replace(/%board/g,t):void 0},jsonIndex:(e,t)=>n.SITE.ID===e.siteID&&n.BOARD.ID===e.boardID&&"index"===n.VIEW?t:ze.url("index",e)+t, -catalog(o=n.BOARD){let a,i;return t["External Catalog"]&&(a=Ye.external(o))?a:le.enabledOn(o)&&t[`Use ${e.name} Catalog`]?Ye.jsonIndex(o,"#catalog"):(i=ze.url("catalog",o))?i:Ye.external(o)},index:(e=n.BOARD)=>le.enabledOn(e)?Ye.jsonIndex(e,"#index"):ze.url("index",e)},Je={init(){Ve.onExists(i,"body",(()=>{if(Ot.isThisPageLegit())return Ve.add(this.bar,[this.noticesRoot,this.toggle]),Ve.prepend(a.body,this.bar),Ve.add(a.body,Je.hover),this.setBarPosition(t["Bottom Header"])})),this.menu=new L.Menu("header");const e=Ve.el("span",{className:"menu-button"});Ve.extend(e,{innerHTML:""});const o=L.checkbox,r=o("Fixed Header","Fixed Header"),s=o("Header auto-hide","Auto-hide header"),l=o("Header auto-hide on scroll","Auto-hide header on scroll"),d=o("Bottom Header","Bottom header"),c=o("Centered links","Centered links"),h=o("Custom Board Navigation","Custom board navigation"),p=o("Bottom Board List","Hide bottom board list"),g=o("Shortcut Icons","Shortcut Icons"),f=Ve.el("a",{ +catalog(o=n.BOARD){let a,i;return t["External Catalog"]&&(a=Ye.external(o))?a:ue.enabledOn(o)&&t[`Use ${e.name} Catalog`]?Ye.jsonIndex(o,"#catalog"):(i=ze.url("catalog",o))?i:Ye.external(o)},index:(e=n.BOARD)=>ue.enabledOn(e)?Ye.jsonIndex(e,"#index"):ze.url("index",e)},Je={init(){Ve.onExists(i,"body",(()=>{if(Ot.isThisPageLegit())return Ve.add(this.bar,[this.noticesRoot,this.toggle]),Ve.prepend(a.body,this.bar),Ve.add(a.body,Je.hover),this.setBarPosition(t["Bottom Header"])})),this.menu=new L.Menu("header");const e=Ve.el("span",{className:"menu-button"});Ve.extend(e,{innerHTML:""});const o=L.checkbox,r=o("Fixed Header","Fixed Header"),s=o("Header auto-hide","Auto-hide header"),l=o("Header auto-hide on scroll","Auto-hide header on scroll"),d=o("Bottom Header","Bottom header"),c=o("Centered links","Centered links"),h=o("Custom Board Navigation","Custom board navigation"),p=o("Bottom Board List","Hide bottom board list"),g=o("Shortcut Icons","Shortcut Icons"),f=Ve.el("a",{ textContent:"Edit custom board navigation",href:"javascript:;"});if(this.barFixedToggler=r.firstElementChild,this.scrollHeaderToggler=l.firstElementChild,this.barPositionToggler=d.firstElementChild,this.linkJustifyToggler=c.firstElementChild,this.headerToggler=s.firstElementChild,this.footerToggler=p.firstElementChild,this.shortcutToggler=g.firstElementChild,this.customNavToggler=h.firstElementChild,Ve.on(e,"click",this.menuToggle),Ve.on(this.headerToggler,"change",this.toggleBarVisibility),Ve.on(this.barFixedToggler,"change",this.toggleBarFixed),Ve.on(this.barPositionToggler,"change",this.toggleBarPosition),Ve.on(this.scrollHeaderToggler,"change",this.toggleHideBarOnScroll),Ve.on(this.linkJustifyToggler,"change",this.toggleLinkJustify),Ve.on(this.footerToggler,"change",this.toggleFooterVisibility),Ve.on(this.shortcutToggler,"change",this.toggleShortcutIcons),Ve.on(this.customNavToggler,"change",this.toggleCustomNav),Ve.on(f,"click",this.editCustomNav), this.setBarFixed(t["Fixed Header"]),this.setHideBarOnScroll(t["Header auto-hide on scroll"]),this.setBarVisibility(t["Header auto-hide"]),this.setLinkJustify(t["Centered links"]),this.setShortcutIcons(t["Shortcut Icons"]),this.setFooterVisibility(t["Bottom Board List"]),Ve.sync("Fixed Header",this.setBarFixed),Ve.sync("Header auto-hide on scroll",this.setHideBarOnScroll),Ve.sync("Bottom Header",this.setBarPosition),Ve.sync("Shortcut Icons",this.setShortcutIcons),Ve.sync("Header auto-hide",this.setBarVisibility),Ve.sync("Centered links",this.setLinkJustify),Ve.sync("Bottom Board List",this.setFooterVisibility),this.addShortcut("menu",e,900),this.menu.addEntry({el:Ve.el("span",{textContent:"Header"}),order:107,subEntries:[{el:r},{el:s},{el:l},{el:d},{el:c},{el:p},{el:g},{el:h},{el:f}]}),Ve.on(window,"load popstate",Je.hashScroll),Ve.on(a,"CreateNotification",this.createNotification),this.setBoardList(),Ve.onExists(i,`${n.SITE.selectors.boardList} + *`,Je.generateFullBoardList), Ot.ready((function(){let e;if("yotsuba"===n.SITE.software&&!(e=Ve.id("boardNavDesktopFoot"))){let t;if(!(t=Ve.id("absbot")))return;e=Ve.id("boardNavDesktop").cloneNode(!0),e.id="boardNavDesktopFoot",Ve("#navtopright",e).id="navbotright",Ve("#settingsWindowLink",e).id="settingsWindowLinkBot",Ve.before(t,e),Ve.global((()=>window.cloneTopNav=function(){}))}if(Je.bottomBoardList=Ve(n.SITE.selectors.boardListBottom)){for(var t of u("a",Je.bottomBoardList))t.hostname===location.hostname&&t.pathname.split("/")[1]===n.BOARD.ID&&(t.className="current");return Ye.setLinks(Je.bottomBoardList)}})),"yotsuba"===n.SITE.software&&("catalog"===n.VIEW||!t["Disable Native Extension"])){const e=Ve.el("a",{href:"javascript:;"});"catalog"===n.VIEW?(e.title=e.textContent="Catalog Settings",e.textContent="🕮︎"):(e.title=e.textContent="4chan Settings",e.className="native-settings"),Ve.on(e,"click",(()=>Ve.id("settingsWindowLink").click())),this.addShortcut("native",e,810)} @@ -637,9 +637,9 @@ St.update(),St.fetchPage(),Ve.on(a,"PostsInserted",(()=>Ve.queueTask(St.onPostsI },fetchPage(){if(St.pageCountEl)return clearTimeout(St.timeout),St.thread.isDead?(St.pageCountEl.textContent="Dead",void Ve.addClass(St.pageCountEl,"warning")):(St.timeout=setTimeout(St.fetchPage,12e4),Ve.whenModified(n.SITE.urls.threadsListJSON(St.thread),"ThreadStats",St.onThreadsLoad))},onThreadsLoad(){if(200===this.status){let e,t;if(St.showPurgePos){let n=1;for(e of this.response)for(t of e.threads)t.no=n-this.response[0].threads.length),St.lastPageUpdate=new Date(t.last_modified*b),void St.retry();o++}}}else if(304===this.status)return St.retry()},retry(){ if(!(!St.showPage||"1"===St.pageCountEl.textContent||n.SITE.threadModTimeIgnoresSage||St.thread.posts.get(St.thread.lastPost).info.date<=St.lastPageUpdate))return clearTimeout(St.timeout),St.timeout=setTimeout(St.fetchPage,5e3)}};const Tt={init(){if("yotsuba"===n.SITE.software&&t["Pass Link"])return Ot.ready(this.ready)},ready(){let e;if(!(e=Ve.id("styleSelector")))return;const t=Ve.el("span",{className:"brackets-wrap pass-link-container"});return Ve.extend(t,{innerHTML:'4chan Pass'}),Ve.on(t.firstElementChild,"click",(()=>window.open(`//sys.${location.hostname.split(".")[1]}.org/auth`,Date.now(),"width=500,height=280,toolbar=0"))),Ve.before(e.previousSibling,[t,Ve.tn("  ")])}};var Rt={init(){if(["index","thread"].includes(n.VIEW)&&t["Quote Inlining"])return t["Comment Expansion"]&&X.callbacks.push(this.node),l.Post.push({name:"Quote Inlining",cb:this.node})},node(){const{process:e}=Rt,{isClone:t}=this ;for(var n of this.nodes.quotelinks.concat([...this.nodes.backlinks],this.nodes.archivelinks))e(n,t)},process:(e,n)=>(t["Quote Hash Navigation"]&&(n||Ve.after(e,Rt.qiQuote(e,Ve.hasClass(e,"filtered")))),Ve.on(e,"click",Rt.toggle)),qiQuote(e,t){let n="hashlink";return t&&(n+=" filtered"),Ve.el("a",{className:n,textContent:"#",href:e.href})},toggle(e){if(Ve.modifiedClick(e))return;const{boardID:o,threadID:a,postID:r}=ze.postDataFromLink(this);if(t["Inline Cross-thread Quotes Only"]&&"thread"===n.VIEW&&n.posts.get(`${o}.${r}`)?.nodes.root.offsetParent)return;if(Ve.hasClass(i,"catalog-mode"))return;e.preventDefault();const s=ze.postFromNode(this),{context:l}=s;if(Ve.hasClass(this,"inlined"))Rt.rm(this,o,a,r,l);else{if(Ve.x(`ancestor::div[@data-full-i-d='${o}.${r}']`,this))return;Rt.add(this,o,a,r,l,s)}return this.classList.toggle("inlined")},findRoot:(e,t)=>t?Ve.x('ancestor::*[parent::*[contains(@class,"post")]][1]',e):Ve.x("ancestor-or-self::*[parent::blockquote][1]",e),add(e,o,a,i,r,s){ -let l;const d=Ve.hasClass(e,"backlink"),c=Ve.el("div",{className:"inline"});c.dataset.fullID=`${o}.${i}`;const h=Rt.findRoot(e,d);Ve.after(h,c);const u=Ve.x('ancestor::*[contains(@class,"postContainer")][1]',h);if(Ve.addClass(u,"hasInline"),new ie(o,a,i,c,s),(l=n.posts.get(`${o}.${i}`))&&r.thread===l.thread&&(d&&t["Forward Hiding"]&&(Ve.addClass(l.nodes.root,"forwarded"),l.forwarded++||(l.forwarded=1)),ee.posts))return ee.readSinglePost(l)},rm(e,o,a,i,r){let s,l;const d=Ve.hasClass(e,"backlink");let c=Rt.findRoot(e,d);c=Ve.x(`following-sibling::div[@data-full-i-d='${o}.${i}'][1]`,c);const h=Ve.x('ancestor::*[contains(@class,"postContainer")][1]',c),{parentNode:u}=c;if(Ve.rm(c),Ve.event("PostsRemoved",null,u),Ve(".inline",h)||Ve.rmClass(h,"hasInline"),!(s=c.firstElementChild))return;const p=n.posts.get(`${o}.${i}`);for(p.rmClone(s.dataset.clone),t["Forward Hiding"]&&d&&r.thread===n.threads.get(`${o}.${a}`)&&!--p.forwarded&&(delete p.forwarded, +let l;const d=Ve.hasClass(e,"backlink"),c=Ve.el("div",{className:"inline"});c.dataset.fullID=`${o}.${i}`;const h=Rt.findRoot(e,d);Ve.after(h,c);const u=Ve.x('ancestor::*[contains(@class,"postContainer")][1]',h);if(Ve.addClass(u,"hasInline"),new de(o,a,i,c,s),(l=n.posts.get(`${o}.${i}`))&&r.thread===l.thread&&(d&&t["Forward Hiding"]&&(Ve.addClass(l.nodes.root,"forwarded"),l.forwarded++||(l.forwarded=1)),ee.posts))return ee.readSinglePost(l)},rm(e,o,a,i,r){let s,l;const d=Ve.hasClass(e,"backlink");let c=Rt.findRoot(e,d);c=Ve.x(`following-sibling::div[@data-full-i-d='${o}.${i}'][1]`,c);const h=Ve.x('ancestor::*[contains(@class,"postContainer")][1]',c),{parentNode:u}=c;if(Ve.rm(c),Ve.event("PostsRemoved",null,u),Ve(".inline",h)||Ve.rmClass(h,"hasInline"),!(s=c.firstElementChild))return;const p=n.posts.get(`${o}.${i}`);for(p.rmClone(s.dataset.clone),t["Forward Hiding"]&&d&&r.thread===n.threads.get(`${o}.${a}`)&&!--p.forwarded&&(delete p.forwarded, Ve.rmClass(p.nodes.root,"forwarded"));l=Ve(".inlined",s);)({boardID:o,threadID:a,postID:i}=ze.postDataFromLink(l)),Rt.rm(l,o,a,i,r),Ve.rmClass(l,"inlined")}},Bt={containers:m(),init(){if(["index","thread"].includes(n.VIEW)&&t["Quote Backlinks"])return(this.bottomBacklinks=t["Bottom Backlinks"])&&Ve.addClass(i,"bottom-backlinks"),l.Post.push({name:"Quote Backlinking Part 1",cb:this.firstNode}),l.Post.push({name:"Quote Backlinking Part 2",cb:this.secondNode})},firstNode(){if(this.isClone||!this.quotes.length||this.isRebuilt)return;const e=t["Mark Quotes of You"]&&_.isYou(this),o=Ve.el("a",{href:n.SITE.Build.postURL(this.board.ID,this.thread.ID,this.ID),className:this.isHidden?"filtered backlink":"backlink",textContent:t.backlink.replace(/%(?:id|%)/g,(e=>({"%id":this.ID,"%%":"%"}[e])))});for(var a of(e&&Ve.add(o,_.mark.cloneNode(!0)),this.quotes)){var i,r=[Bt.getContainer(a)];if((i=n.posts.get(a))&&i.nodes.backlinkContainer)for(var s of i.clones)r.push(s.nodes.backlinkContainer) -;for(var l of r){var d=o.cloneNode(!0),c=l.firstChild?[Ve.tn(" "),d]:[d];if(t["Quote Previewing"]&&Ve.on(d,"mouseover",re.mouseover),t["Quote Inlining"]&&(Ve.on(d,"click",Rt.toggle),t["Quote Hash Navigation"])){var h=Rt.qiQuote(d,Ve.hasClass(d,"filtered"));c.push(h)}Ve.add(l,c)}}},secondNode(){if(this.isClone&&(this.origin.isReply||t["OP Backlinks"]))return void(this.nodes.backlinkContainer=Ve(".container",this.nodes.post));if(!this.isReply&&!t["OP Backlinks"])return;const e=Bt.getContainer(this.fullID);return this.nodes.backlinkContainer=e,Bt.bottomBacklinks?Ve.add(this.nodes.post,e):Ve.add(this.nodes.info,e)},getContainer(e){return this.containers[e]||(this.containers[e]=Ve.el("span",{className:"container"}))}},Pt={init(){if(["index","thread"].includes(n.VIEW)&&t["Mark Cross-thread Quotes"])return t["Comment Expansion"]&&X.callbacks.push(this.node),this.mark=Ve.el("span",{textContent:" (Cross-thread)",className:"qmark-ct"}),l.Post.push({name:"Mark Cross-thread Quotes",cb:this.node}) +;for(var l of r){var d=o.cloneNode(!0),c=l.firstChild?[Ve.tn(" "),d]:[d];if(t["Quote Previewing"]&&Ve.on(d,"mouseover",ce.mouseover),t["Quote Inlining"]&&(Ve.on(d,"click",Rt.toggle),t["Quote Hash Navigation"])){var h=Rt.qiQuote(d,Ve.hasClass(d,"filtered"));c.push(h)}Ve.add(l,c)}}},secondNode(){if(this.isClone&&(this.origin.isReply||t["OP Backlinks"]))return void(this.nodes.backlinkContainer=Ve(".container",this.nodes.post));if(!this.isReply&&!t["OP Backlinks"])return;const e=Bt.getContainer(this.fullID);return this.nodes.backlinkContainer=e,Bt.bottomBacklinks?Ve.add(this.nodes.post,e):Ve.add(this.nodes.info,e)},getContainer(e){return this.containers[e]||(this.containers[e]=Ve.el("span",{className:"container"}))}},Pt={init(){if(["index","thread"].includes(n.VIEW)&&t["Mark Cross-thread Quotes"])return t["Comment Expansion"]&&X.callbacks.push(this.node),this.mark=Ve.el("span",{textContent:" (Cross-thread)",className:"qmark-ct"}),l.Post.push({name:"Mark Cross-thread Quotes",cb:this.node}) },node(){if(this.isClone&&this.thread===this.context.thread)return;const{board:e,thread:t}=this.context;for(var n of this.nodes.quotelinks){var{boardID:o,threadID:a}=ze.postDataFromLink(n);a&&(this.isClone&&Ve.rm(Ve(".qmark-ct",n)),o===e.ID&&a!==t.ID&&Ve.add(n,Pt.mark.cloneNode(!0)))}}},Mt={init(){if(["index","thread"].includes(n.VIEW)&&t["Mark OP Quotes"])return t["Comment Expansion"]&&X.callbacks.push(this.node),this.mark=Ve.el("span",{textContent:" (OP)",className:"qmark-op"}),l.Post.push({name:"Mark OP Quotes",cb:this.node})},node(){let e,t,n;if(this.isClone&&this.thread===this.context.thread)return;if(!(n=this.quotes).length)return;const{quotelinks:o}=this.nodes;if(this.isClone&&n.includes(this.thread.fullID))for(e=0;t=o[e++];)Ve.rm(Ve(".qmark-op",t));const{fullID:a}=this.context.thread;if(n.includes(a))for(e=0;t=o[e++];){var{boardID:i,postID:r}=ze.postDataFromLink(t);`${i}.${r}`===a&&Ve.add(t,Mt.mark.cloneNode(!0))}}};var Nt={init(){ if(["index","thread"].includes(n.VIEW)&&t["Resurrect Quotes"])return Ve.addClass(i,"resurrect-quotes"),t["Comment Expansion"]&&X.callbacks.push(this.node),l.Post.push({name:"Resurrect Quotes",cb:this.node})},node(){if(this.isClone)this.nodes.archivelinks=u("a.linkify.quotelink",this.nodes.comment);else{for(var e of u("a.linkify",this.nodes.comment))Nt.parseArchivelink.call(this,e);for(var t of u(".deadlink",this.nodes.comment))Nt.parseDeadlink.call(this,t)}},parseArchivelink(e){let t;if(!(t=e.pathname.match(/^\/([^/]+)\/thread\/S?(\d+)\/?$/)))return;if(["boards.4chan.org","boards.4channel.org"].includes(e.hostname))return;const n=t[1],o=t[2],a=e.hash.match(/^#[pq]?(\d+)$|$/)[1]||o;return Ze.to("post",{boardID:n,postID:a})?(Ve.addClass(e,"quotelink"),Ve.extend(e.dataset,{boardID:n,threadID:o,postID:a}),this.nodes.archivelinks.push(e)):void 0},parseDeadlink(e){let t,o,a,i;if(Ve.hasClass(e.parentNode,"prettyprint"))return void Nt.fixDeadlink(e);const r=e.textContent ;if(!(i=r.match(/\d+$/)?.[0]))return;if("0"===i[0])return void Nt.fixDeadlink(e);const s=(o=r.match(/^>>>\/([a-z\d]+)/))?o[1]:this.board.ID,l=`${s}.${i}`;if(a=n.posts.get(l))a.isDead?(t=Ve.el("a",{href:n.SITE.Build.postURL(s,a.thread.ID,i),className:"quotelink deadlink",textContent:r}),Ve.add(t,H.deadMark.cloneNode(!0)),Ve.extend(t.dataset,{boardID:s,threadID:a.thread.ID,postID:i})):t=Ve.el("a",{href:n.SITE.Build.postURL(s,a.thread.ID,i),className:"quotelink",textContent:r});else{const e=Ze.to("thread",{boardID:s,threadID:0,postID:i}),n=Ze.to("post",{boardID:s,postID:i});(e||n)&&(t=Ve.el("a",{href:e||"javascript:;",className:"deadlink",textContent:r}),Ve.add(t,H.deadMark.cloneNode(!0)),n&&(Ve.addClass(t,"quotelink"),Ve.extend(t.dataset,{boardID:s,postID:i})))}if(this.quotes.includes(l)||this.quotes.push(l),t)return Ve.replace(e,t),Ve.hasClass(t,"quotelink")?this.nodes.quotelinks.push(t):void 0;Ve.add(e,H.deadMark.cloneNode(!0))},fixDeadlink(e){let t @@ -652,15 +652,15 @@ message:`"${e}" initialization crashed.`,error:t})}return Ve.ready(Ft.initReady) if(a.body.clientHeight>i.clientHeight&&window.innerWidth===i.clientWidth!==t["Autohiding Scrollbar"])return t["Autohiding Scrollbar"]=!t["Autohiding Scrollbar"],Ve.set("Autohiding Scrollbar",t["Autohiding Scrollbar"]),Ve.toggleClass(i,"autohiding-scrollbar")})),Ve.addStyle(me.sub(me.boards),"fourchanx-css"),Ft.bgColorStyle=Ve.el("style",{id:"fourchanx-bgcolor-css"});let o=!1;return Ve.on(a,"mousedown",(()=>o=!1)),Ve.on(a,"keydown",(function(e){if(9===e.keyCode)return o=!0})),window.addEventListener("focus",(()=>i.classList.toggle("keyboard-focus",o)),!0),Ft.setClass()},setClass(){let e,t,o;const r=["yotsuba","yotsuba-b","futaba","burichan","photon","tomorrow","spooky"];if("yotsuba"===n.SITE.software&&"catalog"===n.VIEW&&(e=Ve.id("base-css"))&&(t=e.href.match(/catalog_(\w+)/)?.[1].replace("_new","").replace(/_+/g,"-"),r.includes(t)))return void Ve.addClass(i,t);t=e=o=null;const s=function(){if("yotsuba"===n.SITE.software){for(var s of(Ve.rmClass(i,t),t=null,o))if(s.href===e?.href){ t=s.title.toLowerCase().replace("new","").trim().replace(/\s+/g,"-"),"_special"===t&&(t=s.href.match(/[a-z]*(?=[^/]*$)/)[0]),r.includes(t)||(t=null);break}if(t)return Ve.addClass(i,t),void Ve.rm(Ft.bgColorStyle)}const l=n.SITE.bgColoredEl();l.style.position="absolute",l.style.visibility="hidden",Ve.add(a.body,l);let d=window.getComputedStyle(l).backgroundColor;Ve.rm(l);const c=d.match(/[\d.]+/g);if(!/^rgb\(/.test(d)){const e=window.getComputedStyle(a.body);d=`${e.backgroundColor} ${e.backgroundImage} ${e.backgroundRepeat} ${e.backgroundPosition}`}let h=`.dialog, .suboption-list > div:last-of-type, :root.catalog-hover-expand .catalog-container:hover > .post {\n background: ${d};\n}\n.unread-mark-read {\n background-color: rgba(${c.slice(0,3).join(", ")}, ${.5*(c[3]||1)});\n}` ;return Ve.luma(c)<100&&(h+=".watch-thread-link {\n background-image: url(\"data:image/svg+xml,\");\n}"),Ft.bgColorStyle.textContent=h,Ve.after(Ve.id("fourchanx-css"),Ft.bgColorStyle)};if(Ve.onExists(a.head,n.SITE.selectors.styleSheet,(function(t){return e=t,"yotsuba"===n.SITE.software&&(o=u('link[rel="alternate stylesheet"]',a.head)),new MutationObserver(s).observe(e,{attributes:!0,attributeFilter:["href"]}),Ve.on(e,"load",s),s()})),!e){for(var l of u('link[rel="stylesheet"]',a.head))Ve.on(l,"load",s);return s()}},initReady(){if(!n.SITE.is404?.()){if(n.SITE.isIncomplete?.()){const e=Ve.el("div",{ -innerHTML:'The page didn't load completely.
    Some features may not work unless you reload.'});Ve.on(Ve("a",e),"click",(()=>location.reload())),new Xe("warning",e)}return"catalog"===n.VIEW?Ft.initCatalog():le.enabled?(Ft.expectInitFinished=!0,Ve.event("4chanXInitFinished")):n.SITE.awaitBoard?n.SITE.awaitBoard(Ft.initThread):Ft.initThread()}"thread"===n.VIEW&&ae.set404(n.BOARD.ID,n.THREADID,(function(){if(t["404 Redirect"])return Ze.navigate("thread",{boardID:n.BOARD.ID,threadID:n.THREADID,postID:+location.hash.match(/\d+/)},`/${n.BOARD}/`)}))},initThread(){let e;const t=n.SITE.selectors;if(e=Ve(t.boardFor?.[n.VIEW]||t.board)){const o=[],a=[],i=[];try{n.SITE.preParsingFixes?.(e)}catch(e){}return Ft.addThreadsObserver=new MutationObserver(Ft.addThreads),Ft.addPostsObserver=new MutationObserver(Ft.addPosts),Ft.addThreadsObserver.observe(e,{childList:!0}),Ft.parseThreads(u(t.thread,e),o,a,i),i.length&&Ft.handleErrors(i), +innerHTML:'The page didn't load completely.
    Some features may not work unless you reload.'});Ve.on(Ve("a",e),"click",(()=>location.reload())),new Xe("warning",e)}return"catalog"===n.VIEW?Ft.initCatalog():ue.enabled?(Ft.expectInitFinished=!0,Ve.event("4chanXInitFinished")):n.SITE.awaitBoard?n.SITE.awaitBoard(Ft.initThread):Ft.initThread()}"thread"===n.VIEW&&ae.set404(n.BOARD.ID,n.THREADID,(function(){if(t["404 Redirect"])return Ze.navigate("thread",{boardID:n.BOARD.ID,threadID:n.THREADID,postID:+location.hash.match(/\d+/)},`/${n.BOARD}/`)}))},initThread(){let e;const t=n.SITE.selectors;if(e=Ve(t.boardFor?.[n.VIEW]||t.board)){const o=[],a=[],i=[];try{n.SITE.preParsingFixes?.(e)}catch(e){}return Ft.addThreadsObserver=new MutationObserver(Ft.addThreads),Ft.addPostsObserver=new MutationObserver(Ft.addPosts),Ft.addThreadsObserver.observe(e,{childList:!0}),Ft.parseThreads(u(t.thread,e),o,a,i),i.length&&Ft.handleErrors(i), "thread"===n.VIEW&&(n.threadArchived&&(o[0].isArchived=!0,o[0].kill()),n.SITE.parseThreadMetadata?.(o[0])),Ft.callbackNodes("Thread",o),Ft.callbackNodesDB("Post",a,(function(){for(var e of a)Te.insert(e);return Ft.expectInitFinished=!0,Ve.event("4chanXInitFinished")}))}return Ft.expectInitFinished=!0,Ve.event("4chanXInitFinished")},parseThreads(e,t,o,a){for(var i of e){var r=(()=>{let e;return(e=i.dataset.board)?(e=encodeURIComponent(e),n.boards[e]||new J(e)):n.BOARD})(),s=+i.id.match(/\d*$/)[0];if(!s||r.threads.get(s)?.nodes.root)return;var l=new I(s,r);l.nodes.root=i,t.push(l);var d=u(n.SITE.selectors.postContainer,i);n.SITE.isOPContainerThread&&d.unshift(i),Ft.parsePosts(d,l,o,a),Ft.addPostsObserver.observe(i,{childList:!0})}},parsePosts(e,t,o,a){for(var i of e)if((!i.dataset.fullID||!n.posts.get(i.dataset.fullID))&&Ve(n.SITE.selectors.comment,i))try{o.push(new H(i,t,t.board))}catch(e){a.push({message:`Parsing of Post No.${i.id.match(/\d+/)} failed. Post will be skipped.`,error:e, html:i.outerHTML})}},addThreads(e){const t=[];for(var o of e)for(var a of o.addedNodes)a.nodeType===Node.ELEMENT_NODE&&a.matches(n.SITE.selectors.thread)&&t.push(a);if(!t.length)return;const i=[],r=[],s=[];return Ft.parseThreads(t,i,r,s),s.length&&Ft.handleErrors(s),Ft.callbackNodes("Thread",i),Ft.callbackNodesDB("Post",r,(()=>Ve.event("PostsInserted",null,e[0].target)))},addPosts(e){let t;const o=[],a=[],r=[],s=[];for(var l of e){t=ze.threadFromRoot(l.target);var d=[];for(var c of l.addedNodes)c.nodeType===Node.ELEMENT_NODE&&(c.matches(n.SITE.selectors.postContainer)||(c=Ve(n.SITE.selectors.postContainer,c)))&&d.push(c);var h=r.length;Ft.parsePosts(d,t,r,s),r.length>h&&!o.includes(t)&&o.push(t);var u=!1;for(var p of l.removedNodes)if(ze.postFromRoot(p)?.nodes.root===p&&!i.contains(p)){u=!0;break}u&&!a.includes(t)&&a.push(t)}return s.length&&Ft.handleErrors(s),Ft.callbackNodesDB("Post",r,(function(){for(t of o)Ve.event("PostsInserted",null,t.nodes.root) ;for(t of a)Ve.event("PostsRemoved",null,t.nodes.root)}))},initCatalog(){let e;const t=n.SITE.selectors.catalog;if(t&&(e=Ve(t.board))){const n=[],o=[];Ft.addCatalogThreadsObserver=new MutationObserver(Ft.addCatalogThreads),Ft.addCatalogThreadsObserver.observe(e,{childList:!0}),Ft.parseCatalogThreads(u(t.thread,e),n,o),o.length&&Ft.handleErrors(o),Ft.callbackNodes("CatalogThreadNative",n)}return Ft.expectInitFinished=!0,Ve.event("4chanXInitFinished")},parseCatalogThreads(e,t,n){for(var o of e)try{var a=new et(o);a.thread.catalogViewNative?.nodes.root!==o&&(a.thread.catalogViewNative=a,t.push(a))}catch(e){n.push({message:`Parsing of Catalog Thread No.${(o.dataset.id||o.id).match(/\d+/)} failed. Thread will be skipped.`,error:e,html:o.outerHTML})}},addCatalogThreads(e){const t=[];for(var o of e)for(var a of o.addedNodes)a.nodeType===Node.ELEMENT_NODE&&a.matches(n.SITE.selectors.catalog.thread)&&t.push(a);if(!t.length)return;const i=[],r=[];return Ft.parseCatalogThreads(t,i,r), r.length&&Ft.handleErrors(r),Ft.callbackNodes("CatalogThreadNative",i)},callbackNodes(e,t){let n,o=0;const a=l[e];for(;n=t[o++];)a.execute(n)},callbackNodesDB(e,t,n){let o=0;const a=l[e],i=function(){let e;return!!(e=t[o])&&(a.execute(e),++o%250)};var r=function(){for(;i(););if(t[o])return setTimeout(r,0);n&&n()};return r()},handleErrors(r){let s;if(a.body&&Ve.hasClass(a.body,"fourchan_x")&&!Ve.hasClass(i,"tainted")&&(new Xe("error","Error: Multiple copies of 4chan X are enabled."),Ve.addClass(i,"tainted")),n.SITE.testNativeExtension&&!Ve.hasClass(i,"tainted")){const{enabled:a}=n.SITE.testNativeExtension();if(a&&(Ve.addClass(i,"tainted"),t["Disable Native Extension"]&&!Ft.isFirstRun)){const t=Ve.el("div",{innerHTML:'Failed to disable the native extension. You may need to block it.'});new Xe("error",t)}}if(r instanceof Array?1===r.length&&(s=r[0]):s=r,s)return void new Xe("error",Ft.parseError(s,Ft.reportLink([s])),15) ;const l=Ve.el("div",{innerHTML:`${r.length} errors occurred.${Ft.reportLink(r).innerHTML} [show]`});Ve.on(l.lastElementChild,"click",(function(){return[this.textContent,d.hidden]="show"===this.textContent?["hide",!1]:["show",!0]}));var d=Ve.el("div",{hidden:!0});for(s of r)Ve.add(d,Ft.parseError(s));return new Xe("error",[l,d],30)},parseError(t,a){r.error(t.message,t.error.stack);const i=Ve.el("div",{innerHTML:o(t.message)+(a?a.innerHTML:"")}),s=Ve.el("div",{textContent:`${t.error.name||"Error"}: ${t.error.message||"see console for details"}`}),l=t.error.stack?.match(/\d+(?=:\d+\)?$)/gm)?.join().replace(/^/," at ")||"";return[i,s,Ve.el("div",{textContent:`(${e.name} ${e.fork} v${n.VERSION} ${x} on ${Ve.engine}${l})`})]},reportLink(t){let o;const a=t[0];let i=a.message;t.length>1&&(i+=` (+${t.length-1} other errors)`);let r="";const s=function(t){ if(encodeURIComponent(i+r+t+"\n").length<=e.newIssueMaxLength-e.newIssue.replace(/%(title|details)/,"").length)return r+=t+"\n"};s(`[Please describe the steps needed to reproduce this error.]\n\nScript: ${e.name} ${e.fork} v${n.VERSION} ${x}\nURL: ${location.href}\nUser agent: ${navigator.userAgent}`),"userscript"===x&&(o="undefined"!=typeof GM&&null!==GM?GM.info:"undefined"!=typeof GM_info&&null!==GM_info?GM_info:void 0)&&s(`Userscript manager: ${o.scriptHandler} ${o.version}`),s("\n"+a.error),a.error.stack&&s(a.error.stack.replace(a.error.toString(),"").trim()),a.html&&s("\n`"+a.html+"`"),r=r.replace(/file:\/{3}.+\//g,"");return{innerHTML:` [report]`}}, -isThisPageLegit:()=>("thisPageIsLegit"in Ft||(Ft.thisPageIsLegit=n.SITE.isThisPageLegit?n.SITE.isThisPageLegit():!/^[45]\d\d\b/.test(document.title)&&!/\.(?:json|rss)$/.test(location.pathname)),Ft.thisPageIsLegit),ready:e=>Ve.ready((function(){if(Ft.isThisPageLegit())return e()})),mounted:e=>Ft.isMounted?e():Ft.mountedCBs.push(e),mountedCBs:[],features:[["Polyfill",Lt],["Board Configuration",Y],["Normalize URL",wt],["Delay Redirect on Post",K],["Captcha Configuration",p],["Image Host Rewriting",O],["Redirect",Ze],["Header",Ke],["Catalog Links",Ye],["Settings",Be],["Index Generator",le],["Disable Autoplay",ut],["Announcement Hiding",kt],["Fourchan thingies",ft],["Tinyboard Glue",Dt],["Color User IDs",mt],["Highlight by User ID",bt],["Count Posts by ID",At],["Custom CSS",Ae],["Thread Links",Ct],["Linkify",rt],["Reveal Spoilers",It],["Resurrect Quotes",Nt],["Filter",We],["Thread Hiding Buttons",de],["Reply Hiding Buttons",W],["Recursive",z],["Strike-through Quotes",{init(){ +isThisPageLegit:()=>("thisPageIsLegit"in Ft||(Ft.thisPageIsLegit=n.SITE.isThisPageLegit?n.SITE.isThisPageLegit():!/^[45]\d\d\b/.test(document.title)&&!/\.(?:json|rss)$/.test(location.pathname)),Ft.thisPageIsLegit),ready:e=>Ve.ready((function(){if(Ft.isThisPageLegit())return e()})),mounted:e=>Ft.isMounted?e():Ft.mountedCBs.push(e),mountedCBs:[],features:[["Polyfill",Lt],["Board Configuration",Y],["Normalize URL",wt],["Delay Redirect on Post",K],["Captcha Configuration",p],["Image Host Rewriting",O],["Redirect",Ze],["Header",Ke],["Catalog Links",Ye],["Settings",Be],["Index Generator",ue],["Disable Autoplay",ut],["Announcement Hiding",kt],["Fourchan thingies",ft],["Tinyboard Glue",Dt],["Color User IDs",mt],["Highlight by User ID",bt],["Count Posts by ID",At],["Custom CSS",Ae],["Thread Links",Ct],["Linkify",rt],["Reveal Spoilers",It],["Resurrect Quotes",Nt],["Filter",We],["Thread Hiding Buttons",pe],["Reply Hiding Buttons",W],["Recursive",z],["Strike-through Quotes",{init(){ if(["index","thread"].includes(n.VIEW)&&(t["Reply Hiding Buttons"]||t.Menu&&t["Reply Hiding Link"]||t.Filter))return l.Post.push({name:"Strike-through Quotes",cb:this.node})},node(){if(!this.isClone)for(var e of this.nodes.quotelinks){var{boardID:t,postID:o}=ze.postDataFromLink(e);n.posts.get(`${t}.${o}`)?.isHidden&&Ve.addClass(e,"filtered")}} -}],["Quick Reply Personas",$e.persona],["Quick Reply",$e],["Cooldown",$e.cooldown],["Post Jumper",xt],["Pass Link",Tt],["Menu",Q],["Index Generator (Menu)",le.menu],["Report Link",ht],["Copy Text Link",lt],["Thread Hiding (Menu)",de.menu],["Reply Hiding (Menu)",W.menu],["Delete Link",dt],["Filter (Menu)",We.menu],["Edit Link",$e.oekaki.menu],["Download Link",ct],["Archive Link",st],["Quote Inlining",Rt],["Quote Previewing",re],["Quote Backlinks",Bt],["Mark Quotes of You",_],["Mark OP Quotes",Mt],["Mark Cross-thread Quotes",Pt],["Anonymize",tt],["Time Formatting",Ee],["Relative Post Dates",G],["File Info Formatting",De],["Fappe Tyme",Pe],["Gallery",Ne],["Gallery (menu)",Ne.menu],["Sauce",Me],["Image Expansion",U],["Image Expansion (Menu)",U.menu],["Reveal Spoiler Thumbnails",it],["Image Loading",ot],["Image Hover",nt],["Volume Control",$],["WEBM Metadata",at],["Comment Expansion",X],["Thread Expansion",te],["Favicon",h],["Unread",ee],["Unread Line in Index",ne],["Quote Threading",Te],["Thread Stats",St],["Thread Updater",Re],["Thread Watcher",ae],["Thread Watcher (Menu)",ae.menu],["Mark New IPs",Et],["Index Navigation",F],["Keybinds",Fe],["Banner",pt],["Announcements",yt],["Flash Features",gt],["Reply Pruning",Se],["Mod Contact Links",vt]] +}],["Quick Reply Personas",$e.persona],["Quick Reply",$e],["Cooldown",$e.cooldown],["Post Jumper",xt],["Pass Link",Tt],["Menu",Q],["Index Generator (Menu)",ue.menu],["Report Link",ht],["Copy Text Link",lt],["Thread Hiding (Menu)",pe.menu],["Reply Hiding (Menu)",W.menu],["Delete Link",dt],["Filter (Menu)",We.menu],["Edit Link",$e.oekaki.menu],["Download Link",ct],["Archive Link",st],["Quote Inlining",Rt],["Quote Previewing",ce],["Quote Backlinks",Bt],["Mark Quotes of You",_],["Mark OP Quotes",Mt],["Mark Cross-thread Quotes",Pt],["Anonymize",tt],["Time Formatting",Ee],["Relative Post Dates",G],["File Info Formatting",De],["Fappe Tyme",Pe],["Gallery",Ne],["Gallery (menu)",Ne.menu],["Sauce",Me],["Image Expansion",U],["Image Expansion (Menu)",U.menu],["Reveal Spoiler Thumbnails",it],["Image Loading",ot],["Image Hover",nt],["Volume Control",$],["WEBM Metadata",at],["Comment Expansion",X],["Thread Expansion",te],["Favicon",h],["Unread",ee],["Unread Line in Index",ne],["Quote Threading",Te],["Thread Stats",St],["Thread Updater",Re],["Thread Watcher",ae],["Thread Watcher (Menu)",ae.menu],["Mark New IPs",Et],["Index Navigation",F],["Keybinds",Fe],["Banner",pt],["Announcements",yt],["Flash Features",gt],["Reply Pruning",Se],["Mod Contact Links",vt]] },Ot=Ft;Ve.ready((()=>Ft.init()))}(); //# sourceMappingURL=4chan-XT-noupdate.user.min.js.map diff --git a/builds/4chan-XT-noupdate.user.min.js.map b/builds/4chan-XT-noupdate.user.min.js.map index 5a6cd2475..6d28ad4bb 100644 --- a/builds/4chan-XT-noupdate.user.min.js.map +++ b/builds/4chan-XT-noupdate.user.min.js.map @@ -1 +1 @@ -{"version":3,"file":"4chan-XT-noupdate.user.min.js","sources":["../../../../src/globals/globals.ts","../src/classes/Callbacks.js","../src/config/Config.js","../src/Monitoring/Favicon.js","../src/Monitoring/Favicon/ferongr.unreadDead.png","../src/Monitoring/Favicon/ferongr.unreadDeadY.png","../src/Monitoring/Favicon/ferongr.unreadSFW.png","../src/Monitoring/Favicon/ferongr.unreadSFWY.png","../src/Monitoring/Favicon/ferongr.unreadNSFW.png","../src/Monitoring/Favicon/ferongr.unreadNSFWY.png","../src/Monitoring/Favicon/xat-.unreadDead.png","../src/Monitoring/Favicon/xat-.unreadDeadY.png","../src/Monitoring/Favicon/xat-.unreadSFW.png","../src/Monitoring/Favicon/xat-.unreadSFWY.png","../src/Monitoring/Favicon/xat-.unreadNSFW.png","../src/Monitoring/Favicon/xat-.unreadNSFWY.png","../src/Monitoring/Favicon/Mayhem.unreadDead.png","../src/Monitoring/Favicon/Mayhem.unreadDeadY.png","../src/Monitoring/Favicon/Mayhem.unreadSFW.png","../src/Monitoring/Favicon/Mayhem.unreadSFWY.png","../src/Monitoring/Favicon/Mayhem.unreadNSFW.png","../src/Monitoring/Favicon/Mayhem.unreadNSFWY.png","../src/Monitoring/Favicon/4chanJS.unreadDead.png","../src/Monitoring/Favicon/4chanJS.unreadDeadY.png","../src/Monitoring/Favicon/4chanJS.unreadSFW.png","../src/Monitoring/Favicon/4chanJS.unreadSFWY.png","../src/Monitoring/Favicon/4chanJS.unreadNSFW.png","../src/Monitoring/Favicon/4chanJS.unreadNSFWY.png","../src/Monitoring/Favicon/Original.unreadDead.png","../src/Monitoring/Favicon/Original.unreadDeadY.png","../src/Monitoring/Favicon/Original.unreadSFW.png","../src/Monitoring/Favicon/Original.unreadSFWY.png","../src/Monitoring/Favicon/Original.unreadNSFW.png","../src/Monitoring/Favicon/Original.unreadNSFWY.png","../src/Monitoring/Favicon/Metro.unreadDead.png","../src/Monitoring/Favicon/Metro.unreadDeadY.png","../src/Monitoring/Favicon/Metro.unreadSFW.png","../src/Monitoring/Favicon/Metro.unreadSFWY.png","../src/Monitoring/Favicon/Metro.unreadNSFW.png","../src/Monitoring/Favicon/Metro.unreadNSFWY.png","../src/platform/$$.js","../src/Posting/Captcha.replace.js","../src/Posting/Captcha.t.js","../../../../src/platform/helpers.ts","../src/classes/DataBoard.js","../../../../src/classes/SimpleDict.ts","../src/classes/Thread.js","../src/classes/CatalogThread.js","../src/General/UI.js","../src/Miscellaneous/Nav.js","../src/Images/ImageHost.js","../src/Images/Volume.js","../src/Images/ImageCommon.js","../../../../src/Images/Audio.ts","../../../../src/Images/ImageExpand.ts","../../../../src/classes/Post.ts","../src/Menu/Menu.js","../src/Filtering/Recursive.js","../src/Filtering/PostHiding.js","../src/Miscellaneous/RelativeDates.js","../src/General/BoardConfig.js","../src/classes/Board.js","../src/Posting/PostRedirect.js","../src/Miscellaneous/ExpandComment.js","../src/Quotelinks/QuoteYou.js","../src/classes/RandomAccessList.js","../src/Monitoring/Unread.js","../src/Miscellaneous/ExpandThread.js","../src/Monitoring/UnreadIndex.js","../src/Monitoring/ThreadWatcher.js","../src/classes/Fetcher.js","../src/Quotelinks/QuotePreview.js","../src/General/Index.js","../src/Filtering/ThreadHiding.js","../../../../src/globals/jsx.ts","../../../../src/General/Settings/SettingsHtml.tsx","../../../../src/css/style.ts","../../../../src/css/CSS.ts","../src/css/linkify.audio.png","../src/css/linkify.bitchute.png","../src/css/linkify.clyp.png","../src/css/linkify.dailymotion.png","../src/css/linkify.gfycat.png","../src/css/linkify.gist.png","../src/css/linkify.image.png","../src/css/linkify.installgentoo.png","../src/css/linkify.liveleak.png","../src/css/linkify.pastebin.png","../src/css/linkify.peertube.png","../src/css/linkify.soundcloud.png","../src/css/linkify.streamable.png","../src/css/linkify.twitchtv.png","../src/css/linkify.twitter.png","../src/css/linkify.video.png","../src/css/linkify.vidlii.png","../src/css/linkify.vimeo.png","../src/css/linkify.vine.png","../src/css/linkify.vocaroo.png","../src/css/linkify.youtube.png","../src/Miscellaneous/CustomCSS.js","../src/site/SW.tinyboard.js","../../../../src/Miscellaneous/PassMessage/PassMessageHtml.tsx","../src/Miscellaneous/PassMessage.js","../src/Miscellaneous/Report.js","../src/Posting/PostSuccessful.js","../../../../src/site/SW.yotsuba.tsx","../../../../src/site/SW.yotsuba.Build/PostInfoHtml.tsx","../../../../src/site/SW.yotsuba.Build/FileHtml.tsx","../../../../src/site/SW.yotsuba.Build/CatalogThreadHtml.tsx","../src/site/SW.js","../../../../src/Miscellaneous/FileInfo.tsx","../../../../src/Miscellaneous/Time.ts","../src/Monitoring/ReplyPruning.js","../src/Quotelinks/QuoteThreading.js","../src/Monitoring/ThreadUpdater.js","../../../../src/General/Settings.tsx","../src/Images/FappeTyme.js","../src/Images/Sauce.js","../src/Images/Gallery.js","../src/Linkification/Embedding.js","../src/Miscellaneous/Keybinds.js","../src/Posting/Captcha.js","../src/Posting/QR.js","../src/platform/CrossOrigin.js","../src/platform/$.js","../src/General/Get.js","../../../../src/Filtering/Filter.ts","../src/site/Site.js","../src/Miscellaneous/CatalogLinks.js","../src/General/Header.js","../src/classes/Notice.js","../src/Archive/Redirect.js","../src/classes/CatalogThreadNative.js","../src/Filtering/Anonymize.js","../src/Images/ImageHover.js","../src/Images/ImageLoader.js","../src/Images/Metadata.js","../src/Images/RevealSpoilers.js","../src/Linkification/Linkify.js","../src/Menu/ArchiveLink.js","../src/Menu/CopyTextLink.js","../src/Menu/DeleteLink.js","../src/Menu/DownloadLink.js","../src/Menu/ReportLink.js","../src/Miscellaneous/AntiAutoplay.js","../src/Miscellaneous/Banner.js","../src/Miscellaneous/Flash.js","../src/Miscellaneous/Fourchan.js","../src/Miscellaneous/IDColor.js","../src/Miscellaneous/IDHighlight.js","../src/Miscellaneous/IDPostCount.js","../src/Miscellaneous/ModContact.js","../src/Miscellaneous/NormalizeURL.js","../src/Miscellaneous/PostJumper.js","../src/Miscellaneous/PSA.js","../src/Miscellaneous/PSAHiding.js","../src/Miscellaneous/RemoveSpoilers.js","../src/Miscellaneous/ThreadLinks.js","../src/Miscellaneous/Tinyboard.js","../src/Monitoring/MarkNewIPs.js","../src/Monitoring/ThreadStats.js","../src/Posting/PassLink.js","../src/Quotelinks/QuoteInline.js","../src/Quotelinks/QuoteBacklink.js","../src/Quotelinks/QuoteCT.js","../src/Quotelinks/QuoteOP.js","../src/Quotelinks/Quotify.js","../src/General/Polyfill.js","../src/main/Main.js","../src/Quotelinks/QuoteStrikeThrough.js"],"sourcesContent":[null,"import Main from \"../main/Main\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS206: Consider reworking classes to avoid initClass\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nexport default class Callbacks {\r\n static initClass() {\r\n this.Post = new Callbacks('Post');\r\n this.Thread = new Callbacks('Thread');\r\n this.CatalogThread = new Callbacks('Catalog Thread');\r\n this.CatalogThreadNative = new Callbacks('Catalog Thread');\r\n }\r\n\r\n constructor(type) {\r\n this.type = type;\r\n this.keys = [];\r\n }\r\n\r\n push({name, cb}) {\r\n if (!this[name]) { this.keys.push(name); }\r\n return this[name] = cb;\r\n }\r\n\r\n execute(node, keys=this.keys, force=false) {\r\n let errors;\r\n if (node.callbacksExecuted && !force) { return; }\r\n node.callbacksExecuted = true;\r\n for (var name of keys) {\r\n try {\r\n this[name]?.call(node);\r\n } catch (err) {\r\n if (!errors) { errors = []; }\r\n errors.push({\r\n message: ['\"', name, '\" crashed on node ', this.type, ' No.', node.ID, ' (', node.board, ').'].join(''),\r\n error: err,\r\n html: node.nodes?.root?.outerHTML\r\n });\r\n }\r\n }\r\n\r\n if (errors) { return Main.handleErrors(errors); }\r\n }\r\n}\r\nCallbacks.initClass();\r\n","import userCss from './user.css';\r\nimport banners from './banners.json';\r\nimport meta from '../../package.json';\r\n\r\nconst Config = {\r\n main: {\r\n 'Miscellaneous': {\r\n 'Redirect to HTTPS': [\r\n true,\r\n 'Redirect to the HTTPS version of 4chan.'\r\n ],\r\n 'JSON Index': [\r\n true,\r\n 'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.'\r\n ],\r\n [`Use ${meta.name} Catalog`]: [\r\n true,\r\n `Link to ${meta.name}'s catalog instead of the native 4chan one.`,\r\n 1\r\n ],\r\n 'Index Refresh Notifications': [\r\n false,\r\n 'Show a notice at the top of the page when the index is refreshed.',\r\n 1\r\n ],\r\n 'Follow Cursor': [\r\n true,\r\n 'Image Hover and Quote Preview move with the mouse cursor.'\r\n ],\r\n 'Open Threads in New Tab': [\r\n false,\r\n `Make links to threads in the index / ${meta.name} catalog open in a new tab.`\r\n ],\r\n 'External Catalog': [\r\n false,\r\n 'Link to external catalog instead of the internal one.'\r\n ],\r\n 'Catalog Links': [\r\n false,\r\n 'Add toggle link in header menu to turn Navigation links into links to each board\\'s catalog.'\r\n ],\r\n 'Announcement Hiding': [\r\n true,\r\n 'Add button to hide 4chan announcements.'\r\n ],\r\n 'Desktop Notifications': [\r\n true,\r\n `Enables desktop notifications across various ${meta.name} features.`\r\n ],\r\n '404 Redirect': [\r\n true,\r\n 'Redirect dead threads and images to the archives.'\r\n ],\r\n 'Archive Report': [\r\n true,\r\n 'Enable reporting posts to supported archives.'\r\n ],\r\n 'Exempt Archives from Encryption': [\r\n true,\r\n 'Permit loading content from, and warningless redirects to, HTTP-only archives from HTTPS pages.'\r\n ],\r\n 'Keybinds': [\r\n true,\r\n 'Bind actions to keyboard shortcuts.'\r\n ],\r\n 'Time Formatting': [\r\n true,\r\n 'Localize and format timestamps.'\r\n ],\r\n 'Relative Post Dates': [\r\n true,\r\n 'Display dates like \"3 minutes ago\". Tooltip shows the timestamp.'\r\n ],\r\n 'Relative Date Title': [\r\n true,\r\n 'Show Relative Post Date only when hovering over dates.',\r\n 1\r\n ],\r\n 'Comment Expansion': [\r\n true,\r\n 'Expand comments that are too long to display on the index. Not applicable with JSON Index.'\r\n ],\r\n 'File Info Formatting': [\r\n true,\r\n 'Reformat the file information.'\r\n ],\r\n 'Thread Expansion': [\r\n true,\r\n 'Add buttons to expand threads.'\r\n ],\r\n 'Index Navigation': [\r\n false,\r\n 'Add buttons to navigate between threads.'\r\n ],\r\n 'Reply Navigation': [\r\n false,\r\n 'Add buttons to navigate to top / bottom of thread.'\r\n ],\r\n 'Unique ID and Capcode Navigation': [\r\n false,\r\n 'Add buttons to navigate to posts having the same unique ID or capcode.'\r\n ],\r\n 'Custom Board Titles': [\r\n true,\r\n 'Allow editing of the board title and subtitle by ctrl/\\u2318+clicking them.'\r\n ],\r\n 'Persistent Custom Board Titles': [\r\n false,\r\n 'Force custom board titles to be persistent, even if the board titles are updated.',\r\n 1\r\n ],\r\n 'Show Updated Notifications': [\r\n true,\r\n `Show notifications when ${meta.name} is successfully updated.`\r\n ],\r\n 'Color User IDs': [\r\n true,\r\n 'Assign unique colors to user IDs on boards that use them'\r\n ],\r\n 'Count Posts by ID': [\r\n true,\r\n 'Display number of posts in the thread when hovering over an ID.'\r\n ],\r\n 'Remove Spoilers': [\r\n false,\r\n 'Remove all spoilers in text.'\r\n ],\r\n 'Reveal Spoilers': [\r\n false,\r\n 'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.'\r\n ],\r\n 'Normalize URL': [\r\n true,\r\n 'Rewrite the URL of the current page, removing slugs and excess slashes, and changing /res/ to /thread/.'\r\n ],\r\n 'Work around CORB Bug': [\r\n true,\r\n 'Leave this checked until your garbage browser is fixed.'\r\n ],\r\n 'Disable Autoplaying Sounds': [\r\n false,\r\n 'Prevent sounds on the page from autoplaying.'\r\n ],\r\n 'Disable Native Extension': [\r\n true,\r\n `${meta.name} is NOT designed to work with the native extension.`\r\n ],\r\n 'Enable Native Flash Embedding': [\r\n true,\r\n 'Activate the native extension\\'s Flash embedding if the native extension is disabled.'\r\n ]\r\n },\r\n\r\n 'Linkification': {\r\n 'Linkify': [\r\n true,\r\n 'Convert text into links where applicable.'\r\n ],\r\n 'Link Title': [\r\n true,\r\n 'Replace the link of a supported site with its actual title.',\r\n 1\r\n ],\r\n 'Cover Preview': [\r\n true,\r\n 'Show preview of supported links on hover.',\r\n 1\r\n ],\r\n 'Embedding': [\r\n true,\r\n 'Embed supported services. Note: Some services don\\'t work on HTTPS.',\r\n 1\r\n ],\r\n 'Auto-embed': [\r\n false,\r\n 'Auto-embed Linkify Embeds.',\r\n 2\r\n ],\r\n 'Floating Embeds': [\r\n false,\r\n 'Embed content in a frame that remains in place when the page is scrolled.',\r\n 2\r\n ]\r\n },\r\n\r\n 'Filtering': {\r\n 'Anonymize': [\r\n false,\r\n 'Make everyone Anonymous.'\r\n ],\r\n 'Filter': [\r\n true,\r\n 'Self-moderation placebo.'\r\n ],\r\n 'Filtered Backlinks': [\r\n false,\r\n 'When enabled, shows backlinks to filtered posts with a line-through decoration. Otherwise, hides the backlinks.',\r\n 1\r\n ],\r\n 'Filter in Native Catalog': [\r\n true,\r\n 'Apply 4chan X filters in native catalog.',\r\n 1\r\n ],\r\n 'MD5 Quick Filter Notifications': [\r\n true,\r\n 'Show notification when quick filtering MD5s using the button or keybind.',\r\n 1\r\n ],\r\n 'Recursive Hiding': [\r\n true,\r\n 'Hide replies of hidden posts, recursively.'\r\n ],\r\n 'Thread Hiding Buttons': [\r\n true,\r\n 'Add buttons to hide entire threads.'\r\n ],\r\n 'Reply Hiding Buttons': [\r\n true,\r\n 'Add buttons to hide single replies.'\r\n ],\r\n 'Stubs': [\r\n true,\r\n 'Show stubs of hidden threads / replies.'\r\n ]\r\n },\r\n\r\n 'Images and Videos': {\r\n 'Image Expansion': [\r\n true,\r\n 'Expand images / videos.'\r\n ],\r\n 'Image Hover': [\r\n true,\r\n 'Show full image / video on mouseover.'\r\n ],\r\n 'Image Hover in Catalog': [\r\n true,\r\n `Show full image / video on mouseover in ${meta.name} catalog.`\r\n ],\r\n 'Gallery': [\r\n true,\r\n 'Adds a simple and cute image gallery. Has more options in the gallery menu.'\r\n ],\r\n 'Fullscreen Gallery': [\r\n false,\r\n 'Open gallery in fullscreen mode.',\r\n 1\r\n ],\r\n 'PDF in Gallery': [\r\n false,\r\n 'Show PDF files in gallery.',\r\n 1\r\n ],\r\n 'Sauce': [\r\n true,\r\n 'Add sauce links to images.'\r\n ],\r\n 'WEBM Metadata': [\r\n true,\r\n 'Add link to fetch title metadata from webm videos.'\r\n ],\r\n 'Reveal Spoiler Thumbnails': [\r\n false,\r\n 'Replace spoiler thumbnails with the original image.'\r\n ],\r\n 'Replace GIF': [\r\n false,\r\n 'Replace gif thumbnails with the actual image.'\r\n ],\r\n 'Replace JPG': [\r\n false,\r\n 'Replace jpg thumbnails with the actual image.'\r\n ],\r\n 'Replace PNG': [\r\n false,\r\n 'Replace png thumbnails with the actual image.'\r\n ],\r\n 'Replace WEBM': [\r\n false,\r\n 'Replace webm, mp4, and ogv thumbnails with the actual video. Probably will degrade browser performance ;)'\r\n ],\r\n 'Image Prefetching': [\r\n true,\r\n 'Add a shortcut icon to the header to turn on image preloading.'\r\n ],\r\n 'Fappe Tyme': [\r\n true,\r\n 'Hide posts without images when header menu item is checked. *hint* *hint*'\r\n ],\r\n 'Werk Tyme': [\r\n true,\r\n 'Hide all post images when header menu item is checked.'\r\n ],\r\n 'Autoplay': [\r\n true,\r\n 'Videos begin playing immediately when opened.'\r\n ],\r\n 'Restart when Opened': [\r\n false,\r\n 'Restart GIFs and WebMs when you hover over or expand them.'\r\n ],\r\n 'Show Controls': [\r\n true,\r\n 'Show controls on videos expanded inline.'\r\n ],\r\n 'Click Passthrough': [\r\n false,\r\n 'Clicks on videos trigger your browser\\'s default behavior. Videos can be contracted with button / dragging to the left.',\r\n 1\r\n ],\r\n 'Allow Sound': [\r\n true,\r\n 'Open videos with the sound unmuted.'\r\n ],\r\n 'Mouse Wheel Volume': [\r\n true,\r\n 'Adjust volume of videos with the mouse wheel over the thumbnail/filename/gallery.'\r\n ],\r\n 'Loop in New Tab': [\r\n true,\r\n 'Loop videos opened in their own tabs.'\r\n ],\r\n 'Volume in New Tab': [\r\n true,\r\n `Apply ${meta.name} mute and volume settings to videos opened in their own tabs.`\r\n ],\r\n 'Enable sound posts': [\r\n true,\r\n 'Enable loading audio from [sound=] file names. This audio is fetched from third parties.'\r\n ],\r\n },\r\n\r\n 'Menu': {\r\n 'Menu': [\r\n true,\r\n 'Add a drop-down menu to posts.'\r\n ],\r\n 'Report Link': [\r\n true,\r\n 'Add a report link to the menu.',\r\n 1\r\n ],\r\n 'Copy Text Link': [\r\n true,\r\n 'Add a link to copy the post\\'s text.',\r\n 1\r\n ],\r\n 'Thread Hiding Link': [\r\n true,\r\n 'Add a link to hide entire threads.',\r\n 1\r\n ],\r\n 'Reply Hiding Link': [\r\n true,\r\n 'Add a link to hide single replies.',\r\n 1\r\n ],\r\n 'Delete Link': [\r\n true,\r\n 'Add post and image deletion links to the menu.',\r\n 1\r\n ],\r\n 'Archive Link': [\r\n true,\r\n 'Add an archive link to the menu.',\r\n 1\r\n ],\r\n 'Edit Link': [\r\n true,\r\n 'Add a link to edit the image in Tegaki, /i/\\'s painting program. Requires Quick Reply.',\r\n 1\r\n ],\r\n 'Download Link': [\r\n false,\r\n 'Add a download with original filename link to the menu.',\r\n 1\r\n ]\r\n },\r\n\r\n 'Monitoring': {\r\n 'Thread Updater': [\r\n true,\r\n 'Fetch and insert new replies. Has more options in the header menu and the \"Advanced\" tab.'\r\n ],\r\n 'Unread Count': [\r\n true,\r\n 'Show the unread posts count in the tab title.'\r\n ],\r\n 'Quoted Title': [\r\n false,\r\n 'Change the page title to reflect you\\'ve been quoted.',\r\n 1\r\n ],\r\n 'Hide Unread Count at (0)': [\r\n false,\r\n 'Hide the unread posts count in the tab title when it reaches 0.',\r\n 1\r\n ],\r\n 'Unread Favicon': [\r\n true,\r\n 'Show a different favicon when there are unread posts.'\r\n ],\r\n 'Unread Line': [\r\n true,\r\n 'Show a line to distinguish read posts from unread ones.'\r\n ],\r\n 'Remember Last Read Post': [\r\n true,\r\n 'Remember how far you\\'ve read after you close the thread.'\r\n ],\r\n 'Scroll to Last Read Post': [\r\n true,\r\n 'Scroll back to the last read post when reopening a thread.',\r\n 1\r\n ],\r\n 'Unread Line in Index': [\r\n false,\r\n 'Show a line between read and unread posts in threads in the index.',\r\n 1\r\n ],\r\n 'Remove Thread Excerpt': [\r\n false,\r\n 'Replace the excerpt of the thread in the tab title with the board title.'\r\n ],\r\n 'Thread Stats': [\r\n true,\r\n 'Display reply and image count.'\r\n ],\r\n 'IP Count in Stats': [\r\n true,\r\n 'Display the unique IP count in the thread stats.',\r\n 1\r\n ],\r\n 'Page Count in Stats': [\r\n true,\r\n 'Display the page count in the thread stats.',\r\n 1\r\n ],\r\n 'Updater and Stats in Header': [\r\n true,\r\n 'Places the thread updater and thread stats in the header instead of floating them.'\r\n ],\r\n 'Thread Watcher': [\r\n true,\r\n 'Bookmark threads. Has more options in the thread watcher menu.'\r\n ],\r\n 'Fixed Thread Watcher': [\r\n true,\r\n 'Makes the thread watcher scroll with the page.',\r\n 1\r\n ],\r\n 'Persistent Thread Watcher': [\r\n false,\r\n 'The thread watcher will be visible when the page is loaded.',\r\n 1\r\n ],\r\n 'Mark New IPs': [\r\n false,\r\n 'Label each post from a new IP with the thread\\'s current IP count.'\r\n ],\r\n 'Reply Pruning': [\r\n true,\r\n 'Add option in header menu to hide old replies in long threads. Activated by default in stickies.'\r\n ],\r\n 'Prune All Threads': [\r\n false,\r\n 'Activate Reply Pruning by default in all threads.',\r\n 1\r\n ]\r\n },\r\n\r\n 'Posting and Captchas': {\r\n 'Quick Reply': [\r\n true,\r\n 'All-in-one form to reply, create threads, automate dumping and more.'\r\n ],\r\n 'Persistent QR': [\r\n false,\r\n 'The Quick reply won\\'t disappear after posting.',\r\n 1\r\n ],\r\n 'Auto Hide QR': [\r\n true,\r\n 'Automatically hide the quick reply when posting.',\r\n 2\r\n ],\r\n 'Open Post in New Tab': [\r\n true,\r\n 'Open new threads in a new tab, and open replies in a new tab if you\\'re not already in the thread.',\r\n 1\r\n ],\r\n 'Remember QR Size': [\r\n false,\r\n 'Remember the size of the Quick reply.',\r\n 1\r\n ],\r\n 'Remember Spoiler': [\r\n false,\r\n 'Remember the spoiler state, instead of resetting after posting.',\r\n 1\r\n ],\r\n 'Randomize Filename': [\r\n false,\r\n 'Set the filename to a random timestamp within the past year. Disabled on /f/.',\r\n 1\r\n ],\r\n 'Show New Thread Option in Threads': [\r\n true,\r\n 'Show the option to post a new / different thread from inside a thread.',\r\n 1\r\n ],\r\n 'Show Upload Progress': [\r\n true,\r\n 'Track progress of file uploads as percentage in submit button.',\r\n 1\r\n ],\r\n 'Cooldown': [\r\n true,\r\n 'Indicate the remaining time before posting again.',\r\n 1\r\n ],\r\n 'Posting Success Notifications': [\r\n true,\r\n 'Show notifications on successful post creation or file uploading.',\r\n 1\r\n ],\r\n 'Auto-load captcha': [\r\n false,\r\n 'Automatically load the captcha in the QR even if your post is empty.',\r\n 1\r\n ],\r\n 'Post on Captcha Completion': [\r\n false,\r\n 'Submit the post immediately when the captcha is completed.',\r\n 1\r\n ],\r\n 'Force Noscript Captcha': [\r\n false,\r\n 'Use the non-Javascript fallback captcha even if Javascript is enabled.'\r\n ],\r\n 'Pass Link': [\r\n false,\r\n 'Add a 4chan Pass login link to the bottom of the page.'\r\n ]\r\n },\r\n\r\n 'Quote Links': {\r\n 'Quote Backlinks': [\r\n true,\r\n 'Add quote backlinks.'\r\n ],\r\n 'OP Backlinks': [\r\n true,\r\n 'Add backlinks to the OP.',\r\n 1\r\n ],\r\n 'Bottom Backlinks': [\r\n false,\r\n 'Place backlinks at the bottom of posts.',\r\n 1\r\n ],\r\n 'Quote Inlining': [\r\n true,\r\n 'Inline quoted post on click.'\r\n ],\r\n 'Inline Cross-thread Quotes Only': [\r\n false,\r\n 'Don\\'t inline quote links when the posts are visible in the thread.',\r\n 1\r\n ],\r\n 'Quote Hash Navigation': [\r\n false,\r\n 'Include an extra link after quotes for autoscrolling to quoted posts.',\r\n 1\r\n ],\r\n 'Forward Hiding': [\r\n true,\r\n 'Hide original posts of inlined backlinks.',\r\n 1\r\n ],\r\n 'Quote Previewing': [\r\n true,\r\n 'Show quoted post on hover.'\r\n ],\r\n 'Quote Highlighting': [\r\n true,\r\n 'Highlight the previewed post.',\r\n 1\r\n ],\r\n 'Resurrect Quotes': [\r\n true,\r\n 'Link dead quotes to the archives, and support inlining/previewing of archive links like quote links.'\r\n ],\r\n 'Remember Your Posts': [\r\n true,\r\n 'Remember your posting history.'\r\n ],\r\n 'Mark Quotes of You': [\r\n true,\r\n 'Add \\'(You)\\' to quotes linking to your posts.',\r\n 1\r\n ],\r\n 'Highlight Posts Quoting You': [\r\n true,\r\n 'Highlights any posts that contain a quote to your post.',\r\n 1\r\n ],\r\n 'Highlight Own Posts': [\r\n true,\r\n 'Highlights own posts.',\r\n 1\r\n ],\r\n 'Mark OP Quotes': [\r\n true,\r\n 'Add \\'(OP)\\' to OP quotes.'\r\n ],\r\n 'Mark Cross-thread Quotes': [\r\n true,\r\n 'Add \\'(Cross-thread)\\' to cross-threads quotes.'\r\n ],\r\n 'Quote Threading': [\r\n true,\r\n 'Add option in header menu to thread conversations.'\r\n ]\r\n }\r\n },\r\n\r\n imageExpansion: {\r\n 'Fit width': [\r\n true,\r\n ''\r\n ],\r\n 'Fit height': [\r\n false,\r\n ''\r\n ],\r\n 'Scroll into view': [\r\n true,\r\n 'Scroll down when expanding images to bring the full image into view.'\r\n ],\r\n 'Expand spoilers': [\r\n true,\r\n 'Expand all images along with spoilers.'\r\n ],\r\n 'Expand videos': [\r\n true,\r\n 'Expand all images also expands videos.'\r\n ],\r\n 'Expand from here': [\r\n false,\r\n 'Expand all images only from current position to thread end.'\r\n ],\r\n 'Expand thread only': [\r\n false,\r\n 'In index, expand all images only within the current thread.'\r\n ],\r\n 'Advance on contract': [\r\n false,\r\n 'Advance to next post when contracting an expanded image.'\r\n ]\r\n },\r\n\r\n gallery: {\r\n 'Hide Thumbnails': [\r\n false\r\n ],\r\n 'Fit Width': [ // 'Fit width' (lowercase W) belongs to Image Expansion. Engine limitations, heh.\r\n true\r\n ],\r\n 'Fit Height': [\r\n true\r\n ],\r\n 'Stretch to Fit': [\r\n false\r\n ],\r\n 'Scroll to Post': [\r\n true\r\n ],\r\n 'Slide Delay': [\r\n 6.0\r\n ]\r\n },\r\n\r\n 'Default Volume': 1.0,\r\n\r\n threadWatcher: {\r\n 'Current Board': [\r\n false,\r\n 'Only show watched threads from the current board.'\r\n ],\r\n 'Auto Update Thread Watcher': [\r\n true,\r\n 'Periodically check status of watched threads.'\r\n ],\r\n 'Auto Watch': [\r\n true,\r\n 'Automatically watch threads you start.'\r\n ],\r\n 'Auto Watch Reply': [\r\n true,\r\n 'Automatically watch threads you reply to.'\r\n ],\r\n 'Auto Prune': [\r\n false,\r\n 'Automatically remove dead threads.'\r\n ],\r\n 'Show Page': [\r\n true,\r\n 'Show what page watched threads are on.'\r\n ],\r\n 'Show Unread Count': [\r\n true,\r\n 'Show number of unread posts in watched threads.'\r\n ],\r\n 'Show Site Prefix': [\r\n true,\r\n 'When multiple sites are shown in the thread watcher, add a prefix to board names to distinguish them.'\r\n ],\r\n 'Require OP Quote Link': [\r\n false,\r\n 'For purposes of thread watcher highlighting, only consider posts with a quote link to the OP as replies to the OP.'\r\n ]\r\n },\r\n\r\n filter: {\r\n general: '',\r\n\r\n postID: `\\\r\n# Highlight dubs on [s4s]:\r\n#/(\\\\d)\\\\1$/;highlight;top:no;boards:s4s\\\r\n`,\r\n\r\n name: `\\\r\n# Filter any namefags:\r\n#/^(?!Anonymous$)/\\\r\n`,\r\n\r\n uniqueID: `\\\r\n# Filter a specific ID:\r\n#/Txhvk1Tl/\\\r\n`,\r\n\r\n tripcode: `\\\r\n# Filter any tripfag\r\n#/^!/\\\r\n`,\r\n\r\n capcode: `\\\r\n# Set a custom class for mods:\r\n#/Mod$/;highlight:mod;op:yes\r\n# Set a custom class for admins:\r\n#/Admin$/;highlight:admin;op:yes\\\r\n`,\r\n\r\n pass: `\\\r\n# Filter anyone using since4pass:\r\n#/./\\\r\n`,\r\n\r\n email: '',\r\n\r\n subject: `\\\r\n# Filter Generals on /v/:\r\n#/general/i;boards:v;op:only\\\r\n`,\r\n\r\n comment: `\\\r\n# Filter Stallman copypasta on /g/:\r\n#/what you\\'re refer+ing to as linux/i;boards:g\r\n# Filter posts with 20 or more quote links:\r\n#/(?:>>\\\\d(?:(?!>>\\\\d)[^])*){20}/\r\n# Filter posts like T H I S / H / I / S:\r\n#/^>?\\\\s?\\\\w\\\\s?(\\\\w)\\\\s?(\\\\w)\\\\s?(\\\\w).*$[\\\\s>]+\\\\1[\\\\s>]+\\\\2[\\\\s>]+\\\\3/im\\\r\n`,\r\n\r\n flag: '',\r\n filename: '',\r\n dimensions: `\\\r\n# Highlight potential wallpapers:\r\n#/1920x1080/;op:yes;highlight;top:no;boards:w,wg\\\r\n`,\r\n\r\n filesize: '',\r\n\r\n MD5: ''\r\n },\r\n\r\n sauces: `\\\r\n# Known filename formats:\r\nhttps://www.pixiv.net/member_illust.php?mode=medium&illust_id=%$1;regexp:/^(\\\\d+)_p\\\\d+/\r\njavascript:void(open(\"https://www.deviantart.com/\"+%$1.replace(/_/g,\"-\")+\"/art/\"+parseInt(%$2,36)));regexp:/^\\\\w+_by_(\\\\w+)[_-]d([\\\\da-z]{6})\\\\b/\r\nhttps://imgur.com/%$1;regexp:/^(?![a-zA-Z][a-z]{6})(?![A-Z]{7})(?!\\\\d{7})([\\\\da-zA-Z]{7})(?: \\\\(\\\\d+\\\\))?\\\\.\\\\w+$/\r\nhttps://flickr.com/photo.gne?id=%$1;regexp:/^(\\\\d+)_[\\\\da-f]{10}(?:_\\\\w)*\\\\b/\r\nhttps://www.facebook.com/photo.php?fbid=%$1;regexp:/^\\\\d+_(\\\\d+)_\\\\d+_[no]\\\\b/\r\n\r\n# Reverse image search:\r\nhttps://www.google.com/searchbyimage?sbisrc=4chanx&image_url=%IMG&safe=off\r\nhttps://yandex.com/images/search?rpt=imageview&url=%IMG\r\n#//tineye.com/search?url=%IMG\r\n#//www.bing.com/images/search?q=imgurl:%IMG&view=detailv2&iss=sbi#enterInsights\r\n#https://lens.google.com/uploadbyurl?url=%IMG;text:lens\r\n\r\n# Specialized reverse image search:\r\n//iqdb.org/?url=%IMG\r\nhttps://trace.moe/?auto&url=%IMG;text:wait\r\n#//3d.iqdb.org/?url=%IMG\r\n#//saucenao.com/search.php?url=%IMG\r\n\r\n# \"View Same\" in archives:\r\nhttp://eye.swfchan.com/search/?q=%name;types:swf\r\n#https://desuarchive.org/_/search/image/%sMD5/\r\n#https://archive.4plebs.org/_/search/image/%sMD5/\r\n#https://boards.fireden.net/_/search/image/%sMD5/\r\n#https://foolz.fireden.net/_/search/image/%sMD5/\r\n\r\n# Other tools:\r\n#http://exif.regex.info/exif.cgi?imgurl=%URL\r\n#//imgops.com/start?url=%URL;types:gif,jpg,png\r\n#//www.gif-explode.com/%URL;types:gif\\\r\n`,\r\n\r\n FappeT: {\r\n werk: false\r\n },\r\n\r\n 'Custom CSS': true,\r\n\r\n Index: {\r\n 'Index Mode': 'paged',\r\n 'Previous Index Mode': 'paged',\r\n 'Index Size': 'small',\r\n 'Show Replies': [true, 'Show replies in the index, and also in the catalog if \"Catalog hover expand\" is checked.'],\r\n 'Catalog Hover Expand': [false, 'Expand the comment and show more details when you hover over a thread in the catalog.'],\r\n 'Catalog Hover Toggle': [true, 'Turn \"Catalog hover expand\" on and off by clicking in the catalog.'],\r\n 'Pin Watched Threads': [false, 'Move watched threads to the start of the index.'],\r\n 'Anchor Hidden Threads': [true, 'Move hidden threads to the end of the index.'],\r\n 'Refreshed Navigation': [false, 'Refresh index when navigating through pages.']\r\n },\r\n\r\n Header: {\r\n 'Fixed Header': true,\r\n 'Header auto-hide': false,\r\n 'Header auto-hide on scroll': false,\r\n 'Bottom Header': false,\r\n 'Centered links': false,\r\n 'Header catalog links': false,\r\n 'Bottom Board List': true,\r\n 'Shortcut Icons': true,\r\n 'Custom Board Navigation': true\r\n },\r\n\r\n archives: {\r\n archiveLists: 'https://4chenz.github.io/archives.json/archives.json',\r\n lastarchivecheck: 0,\r\n archiveAutoUpdate: true\r\n },\r\n\r\n externalCatalogURLs: `\\\r\n//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\\\r\n`,\r\n\r\n boardnav: `\\\r\n[ toggle-all ]\r\n[current-index-text:\"Index\"\r\ncurrent-catalog-text:\"Catalog\"\r\ncurrent-expired-text:\"Expired\"\r\ncurrent-archive-text:\"Archive\"]\r\n[external-text:\"FAQ\",\"${meta.name}\"]\\\r\n`,\r\n\r\n QR: {\r\n 'QR.personas': `\\\r\n#options:\"sage\";boards:jp;always\\\r\n`,\r\n sjisPreview: false\r\n },\r\n\r\n jsWhitelist: `\\\r\nhttp://s.4cdn.org\r\nhttps://s.4cdn.org\r\nhttp://www.google.com\r\nhttps://www.google.com\r\nhttps://www.gstatic.com\r\nhttp://cdn.mathjax.org\r\nhttps://cdn.mathjax.org\r\nhttps://cdnjs.cloudflare.com\r\nhttps://hcaptcha.com\r\nhttps://*.hcaptcha.com\r\n'self'\r\n'unsafe-inline'\r\n'unsafe-eval'\\\r\n`,\r\n\r\n captchaLanguage: '',\r\n\r\n time: '%m/%d/%y(%a)%H:%M:%S',\r\n timeLocale: '',\r\n\r\n backlink: '>>%id',\r\n\r\n pastedname: 'file',\r\n\r\n fileInfo: '%l %d (%p%s, %r%g)',\r\n\r\n favicon: 'ferongr',\r\n\r\n usercss: userCss,\r\n\r\n hotkeys: {\r\n // QR & Options\r\n 'Toggle board list': [\r\n 'Ctrl+b',\r\n 'Toggle the full board list.'\r\n ],\r\n 'Toggle header': [\r\n 'Shift+h',\r\n 'Toggle the auto-hide option of the header.'\r\n ],\r\n 'Open empty QR': [\r\n 'q',\r\n 'Open QR without post number inserted.'\r\n ],\r\n 'Open QR': [\r\n 'Shift+q',\r\n 'Open QR with post number inserted.'\r\n ],\r\n 'Open settings': [\r\n 'Alt+o',\r\n 'Open Settings.'\r\n ],\r\n 'Close': [\r\n 'Esc',\r\n 'Close dialogs or notifications.'\r\n ],\r\n 'Spoiler tags': [\r\n 'Ctrl+s',\r\n 'Insert spoiler tags.'\r\n ],\r\n 'Code tags': [\r\n 'Alt+c',\r\n 'Insert code tags.'\r\n ],\r\n 'Eqn tags': [\r\n 'Alt+e',\r\n 'Insert eqn tags.'\r\n ],\r\n 'Math tags': [\r\n 'Alt+m',\r\n 'Insert math tags.'\r\n ],\r\n 'SJIS tags': [\r\n 'Alt+a',\r\n 'Insert SJIS tags.'\r\n ],\r\n 'Toggle sage': [\r\n 'Alt+s',\r\n 'Toggle sage in options field.'\r\n ],\r\n 'Toggle Cooldown': [\r\n 'Alt+Comma',\r\n 'Toggle custom cooldown timer.'\r\n ],\r\n 'Post from URL': [\r\n 'Alt+l',\r\n 'Post from URL.'\r\n ],\r\n 'Add new post': [\r\n 'Alt+n',\r\n 'Add new post to the QR dump list.'\r\n ],\r\n 'Submit QR': [\r\n 'Ctrl+Enter',\r\n 'Submit post.'\r\n ],\r\n // Thread related\r\n 'Watch': [\r\n 'w',\r\n 'Watch thread.'\r\n ],\r\n 'Update': [\r\n 'r',\r\n 'Update the thread / refresh the index.'\r\n ],\r\n 'Update thread watcher': [\r\n 'Shift+r',\r\n 'Manually refresh thread watcher.'\r\n ],\r\n 'Toggle thread watcher': [\r\n 't',\r\n 'Toggle visibility of thread watcher.'\r\n ],\r\n 'Toggle threading': [\r\n 'Shift+t',\r\n 'Toggle threading.'\r\n ],\r\n 'Mark thread read': [\r\n 'Ctrl+0',\r\n 'Mark thread read from index (requires \"Unread Line in Index\").'\r\n ],\r\n // Images\r\n 'Expand image': [\r\n 'Shift+e',\r\n 'Expand selected image.'\r\n ],\r\n 'Expand images': [\r\n 'e',\r\n 'Expand all images.'\r\n ],\r\n 'Open Gallery': [\r\n 'g',\r\n 'Opens the gallery.'\r\n ],\r\n 'Next Gallery Image': [\r\n 'Right',\r\n 'Go to the next image in gallery mode.'\r\n ],\r\n 'Previous Gallery Image': [\r\n 'Left',\r\n 'Go to the previous image in gallery mode.'\r\n ],\r\n 'Advance Gallery': [\r\n 'Enter',\r\n 'Go to next image or, if Autoplay is off, play video.'\r\n ],\r\n 'Pause': [\r\n 'p',\r\n 'Pause/play videos in the gallery.'\r\n ],\r\n 'Slideshow': [\r\n 'Ctrl+Right',\r\n 'Toggle the gallery slideshow mode.'\r\n ],\r\n 'Rotate image clockwise': [\r\n 'Shift+Right',\r\n 'Rotate image clockwise in gallery.'\r\n ],\r\n 'Rotate image anticlockwise': [\r\n 'Shift+Left',\r\n 'Rotate image anticlockwise in gallery.'\r\n ],\r\n 'Download Gallery Image': [\r\n 'Shift+j',\r\n 'Download current image in gallery.'\r\n ],\r\n 'fappeTyme': [\r\n 'f',\r\n 'Toggle Fappe Tyme.'\r\n ],\r\n 'werkTyme': [\r\n 'Shift+w',\r\n 'Toggle Werk Tyme.'\r\n ],\r\n // Board Navigation\r\n 'Front page': [\r\n '1',\r\n 'Jump to front page.'\r\n ],\r\n 'Open front page': [\r\n 'Shift+1',\r\n 'Open front page in a new tab.'\r\n ],\r\n 'Next page': [\r\n 'Ctrl+Right',\r\n 'Jump to the next page.'\r\n ],\r\n 'Previous page': [\r\n 'Ctrl+Left',\r\n 'Jump to the previous page.'\r\n ],\r\n 'Paged mode': [\r\n 'Alt+1',\r\n 'Open the index in paged mode.'\r\n ],\r\n 'Infinite scrolling mode': [\r\n 'Alt+2',\r\n 'Open the index in infinite scrolling mode.'\r\n ],\r\n 'All pages mode': [\r\n 'Alt+3',\r\n 'Open the index in all threads mode.'\r\n ],\r\n 'Open catalog': [\r\n 'Shift+c',\r\n 'Open the catalog of the current board.'\r\n ],\r\n 'Search form': [\r\n 'Ctrl+Alt+s',\r\n 'Focus the search field on the board index.'\r\n ],\r\n 'Cycle sort type': [\r\n 'Alt+x',\r\n 'Cycle through index sort types.'\r\n ],\r\n // Thread Navigation\r\n 'Next thread': [\r\n 'Ctrl+Down',\r\n 'See next thread.'\r\n ],\r\n 'Previous thread': [\r\n 'Ctrl+Up',\r\n 'See previous thread.'\r\n ],\r\n 'Expand thread': [\r\n 'Ctrl+e',\r\n 'Expand thread.'\r\n ],\r\n 'Open thread': [\r\n 'o',\r\n 'Open thread in current tab.'\r\n ],\r\n 'Open thread tab': [\r\n 'Shift+o',\r\n 'Open thread in new tab.'\r\n ],\r\n // Reply Navigation\r\n 'Next reply': [\r\n 'j',\r\n 'Select next reply.'\r\n ],\r\n 'Previous reply': [\r\n 'k',\r\n 'Select previous reply.'\r\n ],\r\n 'Deselect reply': [\r\n 'Shift+d',\r\n 'Deselect reply.'\r\n ],\r\n 'Hide': [\r\n 'x',\r\n 'Hide thread.'\r\n ],\r\n 'Quick Filter MD5': [\r\n '5',\r\n 'Add the MD5 of the selected image to the filter list.'\r\n ],\r\n 'Previous Post Quoting You': [\r\n 'Alt+Up',\r\n 'Scroll to the previous post that quotes you.'\r\n ],\r\n 'Next Post Quoting You': [\r\n 'Alt+Down',\r\n 'Scroll to the next post that quotes you.'\r\n ]\r\n },\r\n\r\n updater: {\r\n checkbox: {\r\n 'Beep': [\r\n false,\r\n 'Beep on new post to completely read thread.'\r\n ],\r\n 'Beep Quoting You': [\r\n false,\r\n 'Beep on new post quoting you.'\r\n ],\r\n 'Auto Scroll': [\r\n false,\r\n 'Scroll updated posts into view. Only enabled at bottom of page.'\r\n ],\r\n 'Bottom Scroll': [\r\n false,\r\n 'Always scroll to the bottom, not the first new post. Useful for event threads.'\r\n ],\r\n 'Scroll BG': [\r\n false,\r\n 'Auto-scroll background tabs.'\r\n ],\r\n 'Auto Update': [\r\n true,\r\n 'Automatically fetch new posts.'\r\n ],\r\n 'Optional Increase': [\r\n false,\r\n 'Increase the intervals between updates on threads without new posts.'\r\n ]\r\n },\r\n 'Interval': 5\r\n },\r\n\r\n customCooldown: 0,\r\n customCooldownEnabled: true,\r\n\r\n 'Thread Quotes': false,\r\n\r\n 'Max Replies': 1000,\r\n\r\n 'Autohiding Scrollbar': false,\r\n\r\n position: {\r\n 'embedding.position': 'top: 50px; right: 0px;',\r\n 'thread-stats.position': 'bottom: 0px; right: 0px;',\r\n 'updater.position': 'bottom: 0px; left: 0px;',\r\n 'thread-watcher.position': 'top: 50px; left: 0px;',\r\n 'qr.position': 'top: 50px; right: 0px;'\r\n },\r\n\r\n fourchanImageHost: 'i.4cdn.org',\r\n\r\n hiddenPSAList: [{}],\r\n\r\n knownBanners: banners.join(','),\r\n\r\n passMessageClosed: false,\r\n\r\n 'Prerequest Captcha': false,\r\n\r\n 'PSAseen': [[]]\r\n};\r\nexport default Config;\r\n","import ferongr_unreadDead from './Favicon/ferongr.unreadDead.png';\r\nimport ferongr_unreadDeadY from './Favicon/ferongr.unreadDeadY.png';\r\nimport ferongr_unreadSFW from './Favicon/ferongr.unreadSFW.png';\r\nimport ferongr_unreadSFWY from './Favicon/ferongr.unreadSFWY.png';\r\nimport ferongr_unreadNSFW from './Favicon/ferongr.unreadNSFW.png';\r\nimport ferongr_unreadNSFWY from './Favicon/ferongr.unreadNSFWY.png';\r\nimport xat_unreadDead from './Favicon/xat-.unreadDead.png';\r\nimport xat_unreadDeadY from './Favicon/xat-.unreadDeadY.png';\r\nimport xat_unreadSFW from './Favicon/xat-.unreadSFW.png';\r\nimport xat_unreadSFWY from './Favicon/xat-.unreadSFWY.png';\r\nimport xat_unreadNSFW from './Favicon/xat-.unreadNSFW.png';\r\nimport xat_unreadNSFWY from './Favicon/xat-.unreadNSFWY.png';\r\nimport Mayhem_unreadDead from './Favicon/Mayhem.unreadDead.png';\r\nimport Mayhem_unreadDeadY from './Favicon/Mayhem.unreadDeadY.png';\r\nimport Mayhem_unreadSFW from './Favicon/Mayhem.unreadSFW.png';\r\nimport Mayhem_unreadSFWY from './Favicon/Mayhem.unreadSFWY.png';\r\nimport Mayhem_unreadNSFW from './Favicon/Mayhem.unreadNSFW.png';\r\nimport Mayhem_unreadNSFWY from './Favicon/Mayhem.unreadNSFWY.png';\r\nimport fourChanJS_unreadDead from './Favicon/4chanJS.unreadDead.png';\r\nimport fourChanJS_unreadDeadY from './Favicon/4chanJS.unreadDeadY.png';\r\nimport fourChanJS_unreadSFW from './Favicon/4chanJS.unreadSFW.png';\r\nimport fourChanJS_unreadSFWY from './Favicon/4chanJS.unreadSFWY.png';\r\nimport fourChanJS_unreadNSFW from './Favicon/4chanJS.unreadNSFW.png';\r\nimport fourChanJS_unreadNSFWY from './Favicon/4chanJS.unreadNSFWY.png';\r\nimport Original_unreadDead from './Favicon/Original.unreadDead.png';\r\nimport Original_unreadDeadY from './Favicon/Original.unreadDeadY.png';\r\nimport Original_unreadSFW from './Favicon/Original.unreadSFW.png';\r\nimport Original_unreadSFWY from './Favicon/Original.unreadSFWY.png';\r\nimport Original_unreadNSFW from './Favicon/Original.unreadNSFW.png';\r\nimport Original_unreadNSFWY from './Favicon/Original.unreadNSFWY.png';\r\nimport Metro_unreadDead from './Favicon/Metro.unreadDead.png';\r\nimport Metro_unreadDeadY from './Favicon/Metro.unreadDeadY.png';\r\nimport Metro_unreadSFW from './Favicon/Metro.unreadSFW.png';\r\nimport Metro_unreadSFWY from './Favicon/Metro.unreadSFWY.png';\r\nimport Metro_unreadNSFW from './Favicon/Metro.unreadNSFW.png';\r\nimport Metro_unreadNSFWY from './Favicon/Metro.unreadNSFWY.png';\r\nimport dead from './Favicon/dead.gif';\r\nimport empty from './Favicon/empty.gif';\r\nimport $ from '../platform/$';\r\nimport { Conf, d } from '../globals/globals';\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\n\r\nvar Favicon = {\r\n init() {\r\n return $.asap((() => d.head && (Favicon.el = $('link[rel=\"shortcut icon\"]', d.head))), Favicon.initAsap);\r\n },\r\n\r\n set(status) {\r\n Favicon.status = status;\r\n if (Favicon.el) {\r\n Favicon.el.href = Favicon[status];\r\n // `favicon.href = href` doesn't work on Firefox.\r\n return $.add(d.head, Favicon.el);\r\n }\r\n },\r\n\r\n initAsap() {\r\n Favicon.el.type = 'image/x-icon';\r\n const {href} = Favicon.el;\r\n Favicon.isSFW = /ws\\.ico$/.test(href);\r\n Favicon.default = href;\r\n Favicon.switch();\r\n if (Favicon.status) {\r\n return Favicon.set(Favicon.status);\r\n }\r\n },\r\n\r\n switch() {\r\n let items = {\r\n ferongr: [\r\n ferongr_unreadDead,\r\n ferongr_unreadDeadY,\r\n ferongr_unreadSFW,\r\n ferongr_unreadSFWY,\r\n ferongr_unreadNSFW,\r\n ferongr_unreadNSFWY,\r\n ],\r\n 'xat-': [\r\n xat_unreadDead,\r\n xat_unreadDeadY,\r\n xat_unreadSFW,\r\n xat_unreadSFWY,\r\n xat_unreadNSFW,\r\n xat_unreadNSFWY,\r\n ],\r\n Mayhem: [\r\n Mayhem_unreadDead,\r\n Mayhem_unreadDeadY,\r\n Mayhem_unreadSFW,\r\n Mayhem_unreadSFWY,\r\n Mayhem_unreadNSFW,\r\n Mayhem_unreadNSFWY,\r\n ],\r\n '4chanJS': [\r\n fourChanJS_unreadDead,\r\n fourChanJS_unreadDeadY,\r\n fourChanJS_unreadSFW,\r\n fourChanJS_unreadSFWY,\r\n fourChanJS_unreadNSFW,\r\n fourChanJS_unreadNSFWY,\r\n ],\r\n Original: [\r\n Original_unreadDead,\r\n Original_unreadDeadY,\r\n Original_unreadSFW,\r\n Original_unreadSFWY,\r\n Original_unreadNSFW,\r\n Original_unreadNSFWY,\r\n ],\r\n 'Metro': [\r\n Metro_unreadDead,\r\n Metro_unreadDeadY,\r\n Metro_unreadSFW,\r\n Metro_unreadSFWY,\r\n Metro_unreadNSFW,\r\n Metro_unreadNSFWY,\r\n ]\r\n };\r\n items = $.getOwn(items, Conf['favicon']);\r\n\r\n const f = Favicon;\r\n const t = 'data:image/png;base64,';\r\n let i = 0;\r\n while (items[i]) {\r\n items[i] = t + items[i++];\r\n }\r\n\r\n [f.unreadDead, f.unreadDeadY, f.unreadSFW, f.unreadSFWY, f.unreadNSFW, f.unreadNSFWY] = items;\r\n return f.update();\r\n },\r\n\r\n update() {\r\n if (this.isSFW) {\r\n this.unread = this.unreadSFW;\r\n return this.unreadY = this.unreadSFWY;\r\n } else {\r\n this.unread = this.unreadNSFW;\r\n return this.unreadY = this.unreadNSFWY;\r\n }\r\n },\r\n\r\n SFW: '//s.4cdn.org/image/favicon-ws.ico',\r\n NSFW: '//s.4cdn.org/image/favicon.ico',\r\n dead: `data:image/gif;base64,${dead}`,\r\n logo: `data:image/png;base64,${empty}`,\r\n};\r\nexport default Favicon;\r\n","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///9zBQC/AADpDAP/gID/q6voCwJJTwpOAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxUlEQVR42q1TOwrCQBB9s0FRtJI0WoqFtSLYegoP4gVSeJsUHsHSI3iFeIqRXXgwrhlXwYHHhLwPTB7B36abBCV+0pA4DUBQUNZYQptGtW3jtoKyxgoe0yrBCoyZfL/5ioQ3URZOXW9I341l3oo+NXEZiW4CEuIzvPECopED4OaZ3RNmeAm4u+a8Jr5f17VyVoL8fr8qcltzwlyyj2iqcgPOQ9ExkHAITgD75bYBe0A5S4H/P9htuWMF3QXoQpwaKeT+lnsC6JE5I6aq6fEAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8AcH4AtswA2PJ55fKi6fIA1/FtpPADAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxElEQVQ4y2NgoBq4/vE/HJOsBiRQUIfA2AzBqQYqUfn00/9FLz+BaQxDCKqBmX7jExijKEDSDJPHrnnbGQhGV4RmOFwdVkNwhQMheYwQxhaIi7b9Z9A3gWAQm2BUoQOgRhgA8o7j1ozLC4LCyAZcx6kZI5qg4kLKqggDFFWxJySsUQVzlb4pwgAJaTRvokcVNgOqOv8zcHBCsL07DgNg8YsczzA5MxtUL+DMD8g0slxI/H8GQ/P/DJKyeKIRpglXZsIiBwBhP5O+VbI/JgAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEX///8oeQBJ3ABV/wHM/7Lu/+ZU/gAqUP3dAAAAAXRSTlMAQObYZgAAAGJJREFUeF5Fi7ENg0AQBCfa/AFdDh2gdwPIogMK2E2+/xLslwOvdqRJhv+GQQPUCtJM7svankLrq/I+TY5e6Ueh1jyBMX7AFJi9vwfyVO4CbbO6jNYpp9GyVPbdkFhVgAQ2H0NOE5jk9DT8AAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAx0lEQVQ4y2NgoBYI+cfwH4ZJVgMS0KhEYGyG4FQDkzjzf9P/d/+fgWl0QwiqgSkI/c8IxsgKkDXD5LFq9rwDweiK0A2HqcNqCK5wICSPEcLYAtH+AMN/IXMIBrEJRie6OEgjDAC5x3FqxuUFNiEUA67j1IweTTBxBQ1puAG86jgSEraogskJWSBcwCGF5k30qMJmgMFEhv/MXBAs5oLDAFj8IsczTE7UEeECbhU8+QGZRpaTi2b4L2zF8J9TGk80wjThykzY5AAW/2O1C2mIbgAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAG1BMVEX+AACLkZFub2yfaF3zZGIAAAD/AAD/iYr/zs8IPcF6AAAABXRSTlMAeprJ7xzg6IEAAABZSURBVAjXY2DABKGBSkqioQwMrGmpxsZhaQEMDGFpIa5pqSCRtPDSNJBIaGh5eShQDYOye0V7iREKAyQFYoiCFAcyILQDGcGmEEZYkGoqiMHKysAQEICwGwAAjBmBqhYlagAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAPFBMVEUAAACEgoBva2ilamDxcG7IaWYgFBNOSEf//f0PDQwBAAA7LCwAAAD/AAD+hIX+m5z+zc5HAADPAAAGAADl032uAAAADHRSTlMAzNv0/vz+6v3+7ALrmfyXAAAAaUlEQVQY042PyxKAIAhFAc1eV7T6/3/N8VXOtAgWwBm4ANEPA8AswpySXHvvYZLlpBNrh9pDtcSqAQ1BUTVIjNUQY5icmwfglmXNgE0d6QBF9GigrU0A9LoM53U1kFzk6SBQuWfD/vHqDUCpBmVKTTM4AAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAIVBMVEUAAACRjop4dXVpZ2tdcI9dfKdisfMAAAAumMN9xv+s2/+PADT2AAAAB3RSTlMAepGdv83v3HIc4QAAAFxJREFUCNdjYMAE5YXKRuLlDAzsHe2uIRUdBQwMFR1l6R3tIJGOyukdIJHy8lkry4FqGEwzV62aFozMUAFJOQEZ4iDFhQwI7UBGaTiEUVFs3g5isLMzMBQUIOwGAJRlIu9hk08QAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEUAAACAgYVlc4ljsu4AAAAAAAAAAAAumMODyP6b1P6e1f/g8v89msgSIiwNFxwbPU3tQYj5AAAABnRSTlMAxej+9VTmD9ciAAAAZElEQVQI12NgwARpiUKKYmkMDGzlZUpK6eUJDAzp5clm5WUgkfKMtnKQSFpa54o0oBoGJYvZO88+gjJu7wMyhIBS2SCGGFDxaxADpP32NjAjSe0bSFd6epIaWISNjYEhJRVhNwAGlyJpYtcvcAAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAHlBMVEUfJSCRi5Frbm9dn19082KR/30AAABmzDOq/5vZ/9Gt/vt2AAAABnRSTlMAe5rJ7/4vxEp4AAAAWUlEQVQI12NgwARpiUpKYmkMDGzlZcbG6eUJDAzp5Slu5WUgkfLUsHKQSFpaRGsaUA2DsmvnjBAjFAZICsQQAylOZEBoBzKSzSCM9CS1MhCDjY2BISEBYTcAtgAcKSK2vuIAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAM1BMVEUAAACBj39tfm1qj2RepFlu2VQAAQAAAAAAAABmyzOX/oSr/pus/pzk/98PGgtatC4CBAI1ENblAAAACHRSTlMA09/p9v77ig0SBcQAAABnSURBVBjTjY9LDsAgCEQRsR2xWu9/2hK/adJFYQG8wABEPwyAYzNnSatjjPAiviWLhPCqI1R7HBrQdCmGBrEETTmnUAq/QMm5dODHyAQOXXR1zLUGsIEI7lonMGfeHQTq9xw4P159AIxSBSC53km7AAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABFklEQVR4AZ2R4WqEMBCEFy1yiJQQ14gcIhIuFBFR+qPQ93+v66QMksrlTwMfkZ2ZZbMKTgVqYIDl3YAbeCM31lJP/Zul4MAEPJjBQGNDLGsz8PQ6aqLAP5PTdd1WlmU09mSKtdTDRgrkzspJPKq6RxMahfj9yhOzQEZwZAwfzrk1ox3MXibIN8hO4MAjeV72CemJGWblnRsOYOdoGw0jebB20BPAwKzUQPlrFhrXFw1Wagu9yuzZwINzVAZCURRL+gRr7Wd8Vtqg4Th/lsUmewyk9WQ/A7NiwJz5VV/GmO+MNjMrFvh/NPDMigHTaeJN09a27ZHRJmalBg54CgfvAGYSLpoHjlmpuAwFdzDy7oGS/qIpM9UPFGg1b1kUlssAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABR0lEQVR4AYWSQWq0QBCFCw0SRIK0PQ4hiIhEZBhEySLyewUPEMgqR/JIXiDhzz7kKKYePIZajEzDRxfV9dWU3SO6IiVWUsVxT5R75Y4gTmwNnUh4kCulUiuV8sjChDjmKtaUcHgmHsnNrMPh0IVhiMIjKZGzNXDoyhMzF7C89z2KtFGD+FoNXEUKZdgpaPM8P++cDXTtBDca7EyQK8+bXTufYBccuvLAG26UnqN1LCgI4g/lm7zTgSux4vk0J8rnKw3+m1//pBPbBrVyGZVNmiAITviEtm3t+D+2QcJx7GUxlN4594K4ZY75Xzh0JVWqnad6TdP0H+LRNBjHcYNDV5xS32qwaC4my7Lwn6guu5QoomgbdFmWDYhnM8E8zxscuhLzPWtKA/dGqUizrityX9M0YX+DQ1ciXobnP6vgfmTOM7Znnk70B58pPaEvx+epAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA/ElEQVR4AZ3RUWqEMBSF4ftQZAhSREQJIiIXpQwi+tSldkFdWPsLhyEE0ocKH2Fyzg1mNJ4KAQ1arTUeeJMH6qwTUJmCHjMcC6KKtbSIylzdXpl18J/k4fdTpUFmPLOOa9bGe+P4+n5RYYfLXuiMsAlXofBxK2QXpvwN/jqg+AY91vR+pStk+apZe0fEhhMXDhUmWXEoO9WNmrWAzvRPq7jnB2jvUGfWTEgPcJzZFTbZk/0Tnh5QI+af6lVGvq/Do2atwVL4VJ+3QrZo1lr4Pw5wzVqDWaV7SUvHrZDNmrWAHq7g0rphkS3LXDMBVqFGhxGT1gGdDFnWaab6BRmXRvbxDmYiAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABQElEQVR4AY2SQUrEQBBFS9CMNFEkhAQdYmiCIUgcZlYGc4VsBcGVF/AuWXme4F7RtXiVWF9+Y9MYtOHRTdX/NZWaEj2RYpQTJeEdK4fKPuA7DjSGXiQkU0qlUqxySmFMEsYsNSU8zEmK4OwdEbmkKCclYoGmolfWCGyenh1O0EJE2gXNWpFC2S0IGrCQ29EbdPCPAmEHmXIxByf8hDAPD71yzAnXypatbSgoAN8Pyju5h4deMUrqJk1z+0uBN+/XX+gxfoFK2QafUJO2aRq//Q+/QIx2wr+Kwq0rusrP/QKf9MTCtbQLf9U1wNvYnz3qug45S68kSvVXgbPbx3nvYPXNOI7cRPWySukK+DcGCvA+urqZ3RmGAbmSXjFK5rpwW8nhWVJP04TYa9/3uO/goVciDiPlZhW8c8ZAHuRSeqIv32FK/GYGL8YAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA/ElEQVR4AZ3RUWqEMBSF4ftQZAihDCKKiAQJShERQx+6o662e2p/4TCEQF468BEm95yLovFr4PBEq9PjgTd5wBcZp6559AiIWDAq6KXV3aJMUMfDOsTf7Mf/XaFBAvYiE9W16b74/vl8UeBAlKOSmWAzUiXwcavMkrrFE9QXVJ+gx5q9XvUVivmqrr1jxIYLCacCs6y6S8psGNU1hw4Bu4JHuUB3pzJBHZcviLiKV9jkyO4vxHyBx1h+qlcY5b2Wj+raE0vlU33dKrNFXWsR/7EgqmtPBIXuIw+dt8osqGsOPaIGSeeGRbZiFtVxsAYeHSbMOgd0MhSzTp3mD4RaQX4aW3NMAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABP0lEQVR4AYWS0UqFQBCGhziImNRBRImDmUgiIaF0kWSP4AMEXXXTE/QiPpL3UdR19Crb/PAvLEtyFj5mmfn/cdxd0RUokbJXEsZYCZUd4D72NBG8wkKmlEqtVMoFhTFJmKuoKelBTVIkjbNE5IainJTIeZqaXjkg8fp+Z7GCjiLQbWgOihTKsCFowUZtoNef4HgDf4JMuTbe8n/Br8NDr5zxhBul52i3FBQE+xflmzzTA69ESmpPmubunwZfztc/6IncBrXSe7/QkK5tW3f8H7dBjHH8q6Kwt033V6Hb4JeeWPgsq42rugfYZ92psWscRwMPvZIo9bEGD2+F2YUnBizLwpeoXnYpbQM34kAB9peP58aueZ4NPPRKxPusaRoYG6UizbquyH1O04T4RA+8EvAwUr6sgjFnDuReLaUn+ANygUa7+9SCWgAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAD1BMVEUBAAAAAAD/AABnZ2f///8nFk05AAAAAXRSTlMAQObYZgAAAEFJREFUeNqNjgEKACAMAjvX/98cAkkxgmSgO8Bt/Ai4ApJ6KKhzF3OiEMDASrGB/QWgPEHsUpN+Ng9xAETMYhDrWmeHAMcmvycWAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAD1BMVEUAAAAAAAD/AABmZmYA/wBD99DBAAAAAXRSTlMAQObYZgAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAE9JREFUCNdljcsRACEIQ5MOiNKAdGAJ9N/Uiu7nsMzABHgB4B8ygFoZA2hhVWavhhGeURPJU9q45+17hGbfGxa82Ndex3hEM44SJGD2/b4AzDgGlHbl388AAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAD1BMVEUBAAAAAAAul8NnZ2f////82iC9AAAAAXRSTlMAQObYZgAAAEFJREFUeNqNjgEKACAMAjvX/98cAkkxgmSgO8Bt/Ai4ApJ6KKhzF3OiEMDASrGB/QWgPEHsUpN+Ng9xAETMYhDrWmeHAMcmvycWAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAD1BMVEUAAAAAAAAul8NnZ2f/AAD7B+mqAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEgAACxIB0t1+/AAAAE9JREFUCNdljcsRACEIQ5MOiNKAdGAJ9N/Uiu7nsMzABHgB4B8ygFoZA2hhVWavhhGeURPJU9q45+17hGbfGxa82Ndex3hEM44SJGD2/b4AzDgGlHbl388AAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAElBMVEUBAAAAAABmzDNlyjJnZ2f///+6o7dfAAAAAXRSTlMAQObYZgAAAERJREFUeF6NjkEKADEIA51o///lJZfQxUsHITogWi8AvwZJuxmYa25xDooBLEwOWFTYAsYVhdorLZt9Ng9xCUTCUCQ2H3F4ANrZ2WNiAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAD1BMVEUAAAAAAABmzDNmZmb/AAC8/wCMAAAAAXRSTlMAQObYZgAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAE9JREFUCNdljcsRACEIQ5MOiNKAdGAJ9N/Uiu7nsMzABHgB4B8ygFoZA2hhVWavhhGeURPJU9q45+17hGbfGxa82Ndex3hEM44SJGD2/b4AzDgGlHbl388AAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEX/////AAD///8AAABBZmS3AAAAAXRSTlMAQObYZgAAAExJREFUeF4tyrENgDAMAMFXKuQswQLBG3mOlBnFS1gwDfIYLpEivvjq2MlqjmYvYg5jWEzCwtDSQlwcXKCVLrpFbvLvvSf9uZJ2HusDtJAY7Tkn1oYAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAhElEQVR42q1RwQnAMAjMu5M4guAKXa4j5dUROo5tipSDcrFChUONd0di2m/hEGVOHDyIPufgwAFASDkpoSzmBrkJ2UMyR9LsJ3rvrqo3Rt1YMIMhhNnOxLMnoMFBxHyJAr2IOBFzA8U+6pLBdmEJTA0aMVjpDd6Loks0s5HZNwYx8tfZCZ0kll7ORffZAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEX///8ul8P///8AAACaqgkzAAAAAXRSTlMAQObYZgAAAExJREFUeF4tyrENgDAMAMFXKuQswQLBG3mOlBnFS1gwDfIYLpEivvjq2MlqjmYvYg5jWEzCwtDSQlwcXKCVLrpFbvLvvSf9uZJ2HusDtJAY7Tkn1oYAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAALVBMVEUAAAAAAAAAAAAAAAABBQcHFx4KISoNLToaVW4oKCgul8M4ODg7OzvBwcH///8uS/CdAAAAA3RSTlMAx9dmesIgAAAAV0lEQVR42m2NWw6AIBAD1eILZO5/XI0UAgm7H9tOsu0yGWAQSOoFijHOxOANGqm/LczpOaXs4gISrPZ+gc2+hO5w2xdwgOjBFUIF+sEJrhUl9JFr+badFwR+BfqlmGUJAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEX///9mzDP///8AAACT0n1lAAAAAXRSTlMAQObYZgAAAExJREFUeF4tyrENgDAMAMFXKuQswQLBG3mOlBnFS1gwDfIYLpEivvjq2MlqjmYvYg5jWEzCwtDSQlwcXKCVLrpFbvLvvSf9uZJ2HusDtJAY7Tkn1oYAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAALVBMVEUAAAAAAAAAAAAAAAAECAIQIAgWLAsePA8oKCg4ODg6dB07OztmzDPBwcH///+rsf3XAAAAA3RSTlMAx9dmesIgAAAAV0lEQVR42m2NWw6AIBAD1eIDhbn/cTVSCCTsfmw7ybbLZIBBIKkXKKU0E4M3aKT+tjCn5xiziwuIsNr7BTb7ErrDZV/AAaIHdwgV6AcnuFaU0Eeu5dt2XiUyBjCQ2bIrAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAC/AABrZQDiAAAAAXRSTlMAQObYZgAAABJJREFUCB1jZGBgrMNAQEEc4gCSfAX5bRw/NQAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJFBMVEUAAAAAAAAAAAC/AAD///8dAAApAABsAAAHAAA4AACQAAAsAABMCpCvAAAAA3RSTlMAPse+s4iwAAAAMklEQVQI12NggAFmY2MDECaNAQZCilAzVJyg5oS4GqAxUtygjIp2KGOKJ5SxepcB3BUAcdYRqxAtgFoAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAAA1/GhpCidAAAAAXRSTlMAQObYZgAAABJJREFUCB1jZGBgrMNAQEEc4gCSfAX5bRw/NQAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJFBMVEUAAAAAAAAAAAAA1/H///8AISUALzQAeokACAkAQEcAorYAMTcE9WFNAAAAA3RSTlMAPse+s4iwAAAAMklEQVQI12NggAFmY2MDECaNAQZCilAzVJyg5oS4GqAxUtygjIp2KGOKJ5SxepcB3BUAcdYRqxAtgFoAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAABV/wErM5hwAAAAAXRSTlMAQObYZgAAABJJREFUCB1jZGBgrMNAQEEc4gCSfAX5bRw/NQAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJFBMVEUAAAAAAAAAAABV/wH///8NKAASOAAwkQADCgAZTABAwQATOwC5e3VGAAAAA3RSTlMAPse+s4iwAAAAMklEQVQI12NggAFmY2MDECaNAQZCilAzVJyg5oS4GqAxUtygjIp2KGOKJ5SxepcB3BUAcdYRqxAtgFoAAAAASUVORK5CYII=';","import { d } from \"../globals/globals\";\r\n\r\nconst $$ = (selector, root = d.body) => Array.from(root.querySelectorAll(selector));\r\nexport default $$;\r\n","import { g, Conf, doc, d } from \"../globals/globals\";\r\nimport Main from \"../main/Main\";\r\nimport $ from \"../platform/$\";\r\nimport Captcha from \"./Captcha\";\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nconst CaptchaReplace = {\r\n init() {\r\n if ((g.SITE.software !== 'yotsuba') || (d.cookie.indexOf('pass_enabled=1') >= 0)) { return; }\r\n\r\n if (Conf['Force Noscript Captcha'] && Main.jsEnabled) {\r\n $.ready(Captcha.replace.noscript);\r\n return;\r\n }\r\n\r\n if (Conf['captchaLanguage'].trim()) {\r\n if (['boards.4chan.org', 'boards.4channel.org'].includes(location.hostname)) {\r\n return $.onExists(doc, '#captchaFormPart', node => $.onExists(node, 'iframe[src^=\"https://www.google.com/recaptcha/\"]', Captcha.replace.iframe));\r\n } else {\r\n return $.onExists(doc, 'iframe[src^=\"https://www.google.com/recaptcha/\"]', Captcha.replace.iframe);\r\n }\r\n }\r\n },\r\n\r\n noscript() {\r\n let noscript, original, toggle;\r\n if (!((original = $('#g-recaptcha')) && (noscript = $('noscript', original.parentNode)))) { return; }\r\n const span = $.el('span',\r\n {id: 'captcha-forced-noscript'});\r\n $.replace(noscript, span);\r\n $.rm(original);\r\n const insert = function() {\r\n span.innerHTML = noscript.textContent;\r\n return Captcha.replace.iframe($('iframe[src^=\"https://www.google.com/recaptcha/\"]', span));\r\n };\r\n if (toggle = $('#togglePostFormLink a, #form-link')) {\r\n return $.on(toggle, 'click', insert);\r\n } else {\r\n return insert();\r\n }\r\n },\r\n\r\n iframe(iframe) {\r\n let lang;\r\n if (lang = Conf['captchaLanguage'].trim()) {\r\n const src = /[?&]hl=/.test(iframe.src) ?\r\n iframe.src.replace(/([?&]hl=)[^&]*/, '$1' + encodeURIComponent(lang))\r\n :\r\n iframe.src + `&hl=${encodeURIComponent(lang)}`;\r\n if (iframe.src !== src) { iframe.src = src; }\r\n }\r\n }\r\n};\r\nexport default CaptchaReplace;\r\n","import { d, g } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport QR from \"./QR\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nconst CaptchaT = {\r\n init() {\r\n if (d.cookie.indexOf('pass_enabled=1') >= 0) { return; }\r\n if (!(this.isEnabled = !!$('#t-root') || !$.id('postForm'))) { return; }\r\n\r\n const root = $.el('div', {className: 'captcha-root'});\r\n this.nodes = {root};\r\n\r\n $.addClass(QR.nodes.el, 'has-captcha', 'captcha-t');\r\n return $.after(QR.nodes.com.parentNode, root);\r\n },\r\n\r\n moreNeeded() {\r\n },\r\n\r\n getThread() {\r\n let threadID;\r\n const boardID = g.BOARD.ID;\r\n if (QR.posts[0].thread === 'new') {\r\n threadID = '0';\r\n } else {\r\n threadID = '' + QR.posts[0].thread;\r\n }\r\n return {boardID, threadID};\r\n },\r\n\r\n setup(focus) {\r\n if (!this.isEnabled) { return; }\r\n\r\n if (!this.nodes.container) {\r\n this.nodes.container = $.el('div', {className: 'captcha-container'});\r\n $.prepend(this.nodes.root, this.nodes.container);\r\n CaptchaT.currentThread = CaptchaT.getThread();\r\n $.global(function() {\r\n const el = document.querySelector('#qr .captcha-container');\r\n window.TCaptcha.init(el, this.boardID, +this.threadID);\r\n return window.TCaptcha.setErrorCb(err => window.dispatchEvent(new CustomEvent('CreateNotification', {detail: {\r\n type: 'warning',\r\n content: '' + err\r\n }})\r\n ));\r\n }\r\n , CaptchaT.currentThread);\r\n }\r\n\r\n if (focus) {\r\n return $('#t-resp').focus();\r\n }\r\n },\r\n\r\n destroy() {\r\n if (!this.isEnabled || !this.nodes.container) { return; }\r\n $.global(() => window.TCaptcha.destroy());\r\n $.rm(this.nodes.container);\r\n return delete this.nodes.container;\r\n },\r\n\r\n updateThread() {\r\n if (!this.isEnabled) { return; }\r\n const {boardID, threadID} = (CaptchaT.currentThread || {});\r\n const newThread = CaptchaT.getThread();\r\n if ((newThread.boardID !== boardID) || (newThread.threadID !== threadID)) {\r\n CaptchaT.destroy();\r\n return CaptchaT.setup();\r\n }\r\n },\r\n\r\n getOne() {\r\n let el;\r\n let response = {};\r\n if (this.nodes.container) {\r\n for (var key of ['t-response', 't-challenge']) {\r\n response[key] = $(`[name='${key}']`, this.nodes.container).value;\r\n }\r\n }\r\n if (!response['t-response'] && !((el = $('#t-msg')) && /Verification not required/i.test(el.textContent))) {\r\n response = null;\r\n }\r\n return response;\r\n },\r\n\r\n setUsed() {\r\n if (!this.isEnabled) { return; }\r\n if (this.nodes.container) {\r\n return $.global(() => window.TCaptcha.clearChallenge());\r\n }\r\n },\r\n\r\n occupied() {\r\n return !!this.nodes.container;\r\n }\r\n};\r\nexport default CaptchaT;\r\n",null,"import { Conf, d, g } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport { dict, HOUR } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS104: Avoid inline assignments\r\n * DS206: Consider reworking classes to avoid initClass\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nexport default class DataBoard {\r\n static initClass() {\r\n this.keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles'];\r\n\r\n this.changes = [];\r\n }\r\n\r\n constructor(key, sync, dontClean) {\r\n this.onSync = this.onSync.bind(this);\r\n this.key = key;\r\n this.initData(Conf[this.key]);\r\n $.sync(this.key, this.onSync);\r\n if (!dontClean) { this.clean(); }\r\n if (!sync) { return; }\r\n // Chrome also fires the onChanged callback on the current tab,\r\n // so we only start syncing when we're ready.\r\n var init = () => {\r\n $.off(d, '4chanXInitFinished', init);\r\n return this.sync = sync;\r\n };\r\n $.on(d, '4chanXInitFinished', init);\r\n }\r\n\r\n initData(data) {\r\n let boards;\r\n this.data = data;\r\n if (this.data.boards) {\r\n let lastChecked;\r\n ({boards, lastChecked} = this.data);\r\n this.data['4chan.org'] = {boards, lastChecked};\r\n delete this.data.boards;\r\n delete this.data.lastChecked;\r\n }\r\n return this.data[g.SITE.ID] || (this.data[g.SITE.ID] = { boards: dict() });\r\n }\r\n\r\n save(change, cb) {\r\n change();\r\n DataBoard.changes.push(change);\r\n return $.get(this.key, { boards: dict() }, items => {\r\n if (!DataBoard.changes.length) { return; }\r\n const needSync = ((items[this.key].version || 0) > (this.data.version || 0));\r\n if (needSync) {\r\n this.initData(items[this.key]);\r\n for (change of DataBoard.changes) { change(); }\r\n }\r\n DataBoard.changes = [];\r\n this.data.version = (this.data.version || 0) + 1;\r\n return $.set(this.key, this.data, () => {\r\n if (needSync) { this.sync?.(); }\r\n return cb?.();\r\n });\r\n });\r\n }\r\n\r\n forceSync(cb) {\r\n return $.get(this.key, { boards: dict() }, items => {\r\n if ((items[this.key].version || 0) > (this.data.version || 0)) {\r\n this.initData(items[this.key]);\r\n for (var change of DataBoard.changes) { change(); }\r\n this.sync?.();\r\n }\r\n return cb?.();\r\n });\r\n }\r\n\r\n delete({siteID, boardID, threadID, postID}, cb) {\r\n if (!siteID) { siteID = g.SITE.ID; }\r\n if (!this.data[siteID]) { return; }\r\n return this.save(() => {\r\n if (postID) {\r\n if (!this.data[siteID].boards[boardID]?.[threadID]) { return; }\r\n delete this.data[siteID].boards[boardID][threadID][postID];\r\n return this.deleteIfEmpty({siteID, boardID, threadID});\r\n } else if (threadID) {\r\n if (!this.data[siteID].boards[boardID]) { return; }\r\n delete this.data[siteID].boards[boardID][threadID];\r\n return this.deleteIfEmpty({siteID, boardID});\r\n } else {\r\n return delete this.data[siteID].boards[boardID];\r\n }\r\n }\r\n , cb);\r\n }\r\n\r\n deleteIfEmpty({siteID, boardID, threadID}) {\r\n if (!this.data[siteID]) { return; }\r\n if (threadID) {\r\n if (!Object.keys(this.data[siteID].boards[boardID][threadID]).length) {\r\n delete this.data[siteID].boards[boardID][threadID];\r\n return this.deleteIfEmpty({siteID, boardID});\r\n }\r\n } else if (!Object.keys(this.data[siteID].boards[boardID]).length) {\r\n return delete this.data[siteID].boards[boardID];\r\n }\r\n }\r\n\r\n set(data, cb) {\r\n return this.save(() => {\r\n return this.setUnsafe(data);\r\n }\r\n , cb);\r\n }\r\n\r\n setUnsafe({siteID, boardID, threadID, postID, val}) {\r\n if (!siteID) { siteID = g.SITE.ID; }\r\n if (!this.data[siteID]) { this.data[siteID] = { boards: dict() }; }\r\n if (postID !== undefined) {\r\n let base;\r\n return (((base = this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict())))[threadID] || (base[threadID] = dict()))[postID] = val;\r\n } else if (threadID !== undefined) {\r\n return (this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict()))[threadID] = val;\r\n } else {\r\n return this.data[siteID].boards[boardID] = val;\r\n }\r\n }\r\n\r\n extend({siteID, boardID, threadID, postID, val}, cb) {\r\n return this.save(() => {\r\n const oldVal = this.get({ siteID, boardID, threadID, postID, defaultValue: dict() });\r\n for (var key in val) {\r\n var subVal = val[key];\r\n if (typeof subVal === 'undefined') {\r\n delete oldVal[key];\r\n } else {\r\n oldVal[key] = subVal;\r\n }\r\n }\r\n return this.setUnsafe({siteID, boardID, threadID, postID, val: oldVal});\r\n }\r\n , cb);\r\n }\r\n\r\n setLastChecked(key='lastChecked') {\r\n return this.save(() => {\r\n return this.data[key] = Date.now();\r\n });\r\n }\r\n\r\n get({siteID, boardID, threadID, postID, defaultValue}) {\r\n let board, val;\r\n if (!siteID) { siteID = g.SITE.ID; }\r\n if (board = this.data[siteID]?.boards[boardID]) {\r\n let thread;\r\n if (threadID == null) {\r\n if (postID != null) {\r\n for (thread = 0; thread < board.length; thread++) {\r\n var ID = board[thread];\r\n if (postID in thread) {\r\n val = thread[postID];\r\n break;\r\n }\r\n }\r\n } else {\r\n val = board;\r\n }\r\n } else if (thread = board[threadID]) {\r\n val = (postID != null) ?\r\n thread[postID]\r\n :\r\n thread;\r\n }\r\n }\r\n return val || defaultValue;\r\n }\r\n\r\n clean() {\r\n let boardID, middle;\r\n const siteID = g.SITE.ID;\r\n for (boardID in this.data[siteID].boards) {\r\n var val = this.data[siteID].boards[boardID];\r\n this.deleteIfEmpty({siteID, boardID});\r\n }\r\n const now = Date.now();\r\n if (now - (2 * HOUR) >= ((middle = this.data[siteID].lastChecked || 0)) || middle > now) {\r\n this.data[siteID].lastChecked = now;\r\n for (boardID in this.data[siteID].boards) {\r\n this.ajaxClean(boardID);\r\n }\r\n }\r\n }\r\n\r\n ajaxClean(boardID) {\r\n const that = this;\r\n const siteID = g.SITE.ID;\r\n const threadsList = g.SITE.urls.threadsListJSON?.({siteID, boardID});\r\n if (!threadsList) { return; }\r\n return $.cache(threadsList, function() {\r\n if (this.status !== 200) { return; }\r\n const archiveList = g.SITE.urls.archiveListJSON?.({siteID, boardID});\r\n if (!archiveList) { return that.ajaxCleanParse(boardID, this.response); }\r\n const response1 = this.response;\r\n return $.cache(archiveList, function() {\r\n if ((this.status !== 200) && (!!g.SITE.archivedBoardsKnown || (this.status !== 404))) { return; }\r\n return that.ajaxCleanParse(boardID, response1, this.response);\r\n });\r\n });\r\n }\r\n\r\n ajaxCleanParse(boardID, response1, response2) {\r\n let board, ID;\r\n const siteID = g.SITE.ID;\r\n if (!(board = this.data[siteID].boards[boardID])) { return; }\r\n const threads = dict();\r\n if (response1) {\r\n for (var page of response1) {\r\n for (var thread of page.threads) {\r\n ID = thread.no;\r\n if (ID in board) { threads[ID] = board[ID]; }\r\n }\r\n }\r\n }\r\n if (response2) {\r\n for (ID of response2) {\r\n if (ID in board) { threads[ID] = board[ID]; }\r\n }\r\n }\r\n this.data[siteID].boards[boardID] = threads;\r\n this.deleteIfEmpty({siteID, boardID});\r\n return $.set(this.key, this.data);\r\n }\r\n\r\n onSync(data) {\r\n if ((data.version || 0) <= (this.data.version || 0)) { return; }\r\n this.initData(data);\r\n return this.sync?.();\r\n }\r\n}\r\nDataBoard.initClass();\r\n",null,"import SimpleDict from \"./SimpleDict\";\r\nimport $ from \"../platform/$\";\r\nimport { g } from \"../globals/globals\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nexport default class Thread {\r\n toString() { return this.ID; }\r\n\r\n constructor(ID, board) {\r\n this.board = board;\r\n this.ID = +ID;\r\n this.threadID = this.ID;\r\n this.boardID = this.board.ID;\r\n this.siteID = g.SITE.ID;\r\n this.fullID = `${this.board}.${this.ID}`;\r\n this.posts = new SimpleDict();\r\n this.isDead = false;\r\n this.isHidden = false;\r\n this.isSticky = false;\r\n this.isClosed = false;\r\n this.isArchived = false;\r\n this.postLimit = false;\r\n this.fileLimit = false;\r\n this.lastPost = 0;\r\n this.ipCount = undefined;\r\n this.json = null;\r\n\r\n this.OP = null;\r\n this.catalogView = null;\r\n\r\n this.nodes =\r\n {root: null};\r\n\r\n this.board.threads.push(this.ID, this);\r\n g.threads.push(this.fullID, this);\r\n }\r\n\r\n setPage(pageNum) {\r\n let icon;\r\n const {info, reply} = this.OP.nodes;\r\n if (!(icon = $('.page-num', info))) {\r\n icon = $.el('span', {className: 'page-num'});\r\n $.replace(reply.parentNode.previousSibling, [$.tn(' '), icon, $.tn(' ')]);\r\n }\r\n icon.title = `This thread is on page ${pageNum} in the original index.`;\r\n icon.textContent = `[${pageNum}]`;\r\n if (this.catalogView) { return this.catalogView.nodes.pageCount.textContent = pageNum; }\r\n }\r\n\r\n setCount(type, count, reachedLimit) {\r\n if (!this.catalogView) { return; }\r\n const el = this.catalogView.nodes[`${type}Count`];\r\n el.textContent = count;\r\n return (reachedLimit ? $.addClass : $.rmClass)(el, 'warning');\r\n }\r\n\r\n setStatus(type, status) {\r\n const name = `is${type}`;\r\n if (this[name] === status) { return; }\r\n this[name] = status;\r\n if (!this.OP) { return; }\r\n this.setIcon('Sticky', this.isSticky);\r\n this.setIcon('Closed', this.isClosed && !this.isArchived);\r\n return this.setIcon('Archived', this.isArchived);\r\n }\r\n\r\n setIcon(type, status) {\r\n const typeLC = type.toLowerCase();\r\n let icon = $(`.${typeLC}Icon`, this.OP.nodes.info);\r\n if (!!icon === status) { return; }\r\n\r\n if (!status) {\r\n $.rm(icon.previousSibling);\r\n $.rm(icon);\r\n if (this.catalogView) { $.rm($(`.${typeLC}Icon`, this.catalogView.nodes.icons)); }\r\n return;\r\n }\r\n icon = $.el('img', {\r\n src: `${g.SITE.Build.staticPath}${typeLC}${g.SITE.Build.gifIcon}`,\r\n alt: type,\r\n title: type,\r\n className: `${typeLC}Icon retina`\r\n }\r\n );\r\n if (g.BOARD.ID === 'f') {\r\n icon.style.cssText = 'height: 18px; width: 18px;';\r\n }\r\n\r\n const root = (type !== 'Sticky') && this.isSticky ?\r\n $('.stickyIcon', this.OP.nodes.info)\r\n :\r\n $('.page-num', this.OP.nodes.info) || this.OP.nodes.quote;\r\n $.after(root, [$.tn(' '), icon]);\r\n\r\n if (!this.catalogView) { return; }\r\n return ((type === 'Sticky') && this.isClosed ? $.prepend : $.add)(this.catalogView.nodes.icons, icon.cloneNode());\r\n }\r\n\r\n kill() {\r\n return this.isDead = true;\r\n }\r\n\r\n collect() {\r\n let n = 0;\r\n this.posts.forEach(function(post) {\r\n if (post.clones.length) {\r\n return n++;\r\n } else {\r\n return post.collect();\r\n }\r\n });\r\n if (!n) {\r\n g.threads.rm(this.fullID);\r\n return this.board.threads.rm(this);\r\n }\r\n }\r\n}\r\n","import $ from \"../platform/$\";\r\n\r\nexport default class CatalogThread {\r\n toString() { return this.ID; }\r\n\r\n constructor(root, thread) {\r\n this.thread = thread;\r\n this.ID = this.thread.ID;\r\n this.board = this.thread.board;\r\n const {post} = this.thread.OP.nodes;\r\n this.nodes = {\r\n root,\r\n thumb: $('.catalog-thumb', post),\r\n icons: $('.catalog-icons', post),\r\n postCount: $('.post-count', post),\r\n fileCount: $('.file-count', post),\r\n pageCount: $('.page-count', post),\r\n replies: null\r\n };\r\n this.thread.catalogView = this;\r\n }\r\n}\r\n","import { Conf, d, doc } from \"../globals/globals\";\r\nimport Main from \"../main/Main\";\r\nimport $ from \"../platform/$\";\r\nimport $$ from \"../platform/$$\";\r\nimport Header from \"./Header\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS206: Consider reworking classes to avoid initClass\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nconst dialog = function(id, properties) {\r\n const el = $.el('div', {\r\n className: 'dialog',\r\n id\r\n }\r\n );\r\n $.extend(el, properties);\r\n el.style.cssText = Conf[`${id}.position`];\r\n\r\n const move = $('.move', el);\r\n $.on(move, 'touchstart mousedown', dragstart);\r\n for (var child of move.children) {\r\n if (!child.tagName) { continue; }\r\n $.on(child, 'touchstart mousedown', e => e.stopPropagation());\r\n }\r\n\r\n return el;\r\n};\r\n\r\nvar Menu = (function() {\r\n let currentMenu = undefined;\r\n let lastToggledButton = undefined;\r\n Menu = class Menu {\r\n static initClass() {\r\n currentMenu = null;\r\n lastToggledButton = null;\r\n }\r\n\r\n constructor(type) {\r\n // XXX AddMenuEntry event is deprecated\r\n this.setPosition = this.setPosition.bind(this);\r\n this.close = this.close.bind(this);\r\n this.keybinds = this.keybinds.bind(this);\r\n this.onFocus = this.onFocus.bind(this);\r\n this.addEntry = this.addEntry.bind(this);\r\n this.type = type;\r\n $.on(d, 'AddMenuEntry', ({detail}) => {\r\n if (detail.type !== this.type) { return; }\r\n delete detail.open;\r\n return this.addEntry(detail);\r\n });\r\n this.entries = [];\r\n }\r\n\r\n makeMenu() {\r\n const menu = $.el('div', {\r\n className: 'dialog',\r\n id: 'menu',\r\n tabIndex: 0\r\n }\r\n );\r\n menu.dataset.type = this.type;\r\n $.on(menu, 'click', e => e.stopPropagation());\r\n $.on(menu, 'keydown', this.keybinds);\r\n return menu;\r\n }\r\n\r\n toggle(e, button, data) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (currentMenu) {\r\n // Close if it's already opened.\r\n // Reopen if we clicked on another button.\r\n const previousButton = lastToggledButton;\r\n currentMenu.close();\r\n if (previousButton === button) { return; }\r\n }\r\n\r\n if (!this.entries.length) { return; }\r\n return this.open(button, data);\r\n }\r\n\r\n open(button, data) {\r\n let entry;\r\n const menu = (this.menu = this.makeMenu());\r\n currentMenu = this;\r\n lastToggledButton = button;\r\n\r\n this.entries.sort((first, second) => first.order - second.order);\r\n\r\n for (entry of this.entries) {\r\n this.insertEntry(entry, menu, data);\r\n }\r\n\r\n $.addClass(lastToggledButton, 'active');\r\n\r\n $.on(d, 'click CloseMenu', this.close);\r\n $.on(d, 'scroll', this.setPosition);\r\n $.on(window, 'resize', this.setPosition);\r\n $.after(button, menu);\r\n\r\n this.setPosition();\r\n\r\n entry = $('.entry', menu);\r\n // We've removed flexbox, so we don't use order anymore.\r\n // while prevEntry = @findNextEntry entry, -1\r\n // entry = prevEntry\r\n this.focus(entry);\r\n\r\n return menu.focus();\r\n }\r\n\r\n setPosition() {\r\n const mRect = this.menu.getBoundingClientRect();\r\n const bRect = lastToggledButton.getBoundingClientRect();\r\n const bTop = window.scrollY + bRect.top;\r\n const bLeft = window.scrollX + bRect.left;\r\n const cHeight = doc.clientHeight;\r\n const cWidth = doc.clientWidth;\r\n const [top, bottom] = (bRect.top + bRect.height + mRect.height) < cHeight ?\r\n [`${bRect.bottom}px`, '']\r\n :\r\n ['', `${cHeight - bRect.top}px`];\r\n const [left, right] = (bRect.left + mRect.width) < cWidth ?\r\n [`${bRect.left}px`, '']\r\n :\r\n ['', `${cWidth - bRect.right}px`];\r\n $.extend(this.menu.style, {top, right, bottom, left});\r\n return this.menu.classList.toggle('left', right);\r\n }\r\n\r\n insertEntry(entry, parent, data) {\r\n let submenu;\r\n if (typeof entry.open === 'function') {\r\n try {\r\n if (!entry.open(data)) { return; }\r\n } catch (err) {\r\n Main.handleErrors({\r\n message: `Error in building the ${this.type} menu.`,\r\n error: err\r\n });\r\n return;\r\n }\r\n }\r\n $.add(parent, entry.el);\r\n\r\n if (!entry.subEntries) { return; }\r\n if (submenu = $('.submenu', entry.el)) {\r\n // Reset sub menu, remove irrelevant entries.\r\n $.rm(submenu);\r\n }\r\n submenu = $.el('div',\r\n {className: 'dialog submenu'});\r\n for (var subEntry of entry.subEntries) {\r\n this.insertEntry(subEntry, submenu, data);\r\n }\r\n $.add(entry.el, submenu);\r\n }\r\n\r\n close() {\r\n $.rm(this.menu);\r\n delete this.menu;\r\n $.rmClass(lastToggledButton, 'active');\r\n currentMenu = null;\r\n lastToggledButton = null;\r\n $.off(d, 'click scroll CloseMenu', this.close);\r\n $.off(d, 'scroll', this.setPosition);\r\n return $.off(window, 'resize', this.setPosition);\r\n }\r\n\r\n findNextEntry(entry, direction) {\r\n const entries = [...entry.parentNode.children];\r\n entries.sort((first, second) => first.style.order - second.style.order);\r\n return entries[entries.indexOf(entry) + direction];\r\n }\r\n\r\n keybinds(e) {\r\n let subEntry;\r\n let next, submenu;\r\n let entry = $('.focused', this.menu);\r\n while ((subEntry = $('.focused', entry))) {\r\n entry = subEntry;\r\n }\r\n\r\n switch (e.keyCode) {\r\n case 27: // Esc\r\n lastToggledButton.focus();\r\n this.close();\r\n break;\r\n case 13: case 32: // Enter, Space\r\n entry.click();\r\n break;\r\n case 38: // Up\r\n if (next = this.findNextEntry(entry, -1)) {\r\n this.focus(next);\r\n }\r\n break;\r\n case 40: // Down\r\n if (next = this.findNextEntry(entry, +1)) {\r\n this.focus(next);\r\n }\r\n break;\r\n case 39: // Right\r\n if ((submenu = $('.submenu', entry)) && (next = submenu.firstElementChild)) {\r\n let nextPrev;\r\n while ((nextPrev = this.findNextEntry(next, -1))) {\r\n next = nextPrev;\r\n }\r\n this.focus(next);\r\n }\r\n break;\r\n case 37: // Left\r\n if (next = $.x('parent::*[contains(@class,\"submenu\")]/parent::*', entry)) {\r\n this.focus(next);\r\n }\r\n break;\r\n default:\r\n return;\r\n }\r\n\r\n e.preventDefault();\r\n return e.stopPropagation();\r\n }\r\n\r\n onFocus(e) {\r\n e.stopPropagation();\r\n return this.focus(e.target);\r\n }\r\n\r\n focus(entry) {\r\n let focused, submenu;\r\n while ((focused = $.x('parent::*/child::*[contains(@class,\"focused\")]', entry))) {\r\n $.rmClass(focused, 'focused');\r\n }\r\n for (focused of $$('.focused', entry)) {\r\n $.rmClass(focused, 'focused');\r\n }\r\n $.addClass(entry, 'focused');\r\n\r\n // Submenu positioning.\r\n if (!(submenu = $('.submenu', entry))) { return; }\r\n const sRect = submenu.getBoundingClientRect();\r\n const eRect = entry.getBoundingClientRect();\r\n const cHeight = doc.clientHeight;\r\n const cWidth = doc.clientWidth;\r\n const [top, bottom] = (eRect.top + sRect.height) < cHeight ?\r\n ['0px', 'auto']\r\n :\r\n ['auto', '0px'];\r\n const [left, right] = (eRect.right + sRect.width) < (cWidth - 150) ?\r\n ['100%', 'auto']\r\n :\r\n ['auto', '100%'];\r\n const {style} = submenu;\r\n style.top = top;\r\n style.bottom = bottom;\r\n style.left = left;\r\n return style.right = right;\r\n }\r\n\r\n addEntry(entry) {\r\n this.parseEntry(entry);\r\n return this.entries.push(entry);\r\n }\r\n\r\n parseEntry(entry) {\r\n const {el, subEntries} = entry;\r\n $.addClass(el, 'entry');\r\n $.on(el, 'focus mouseover', this.onFocus);\r\n el.style.order = entry.order || 100;\r\n if (!subEntries) { return; }\r\n $.addClass(el, 'has-submenu');\r\n for (var subEntry of subEntries) {\r\n this.parseEntry(subEntry);\r\n }\r\n }\r\n };\r\n Menu.initClass();\r\n return Menu;\r\n})();\r\n\r\nexport var dragstart = function (e) {\r\n let isTouching;\r\n if ((e.type === 'mousedown') && (e.button !== 0)) { return; } // not LMB\r\n // prevent text selection\r\n e.preventDefault();\r\n if (isTouching = e.type === 'touchstart') {\r\n e = e.changedTouches[e.changedTouches.length - 1];\r\n }\r\n // distance from pointer to el edge is constant; calculate it here.\r\n const el = $.x('ancestor::div[contains(@class,\"dialog\")][1]', this);\r\n const rect = el.getBoundingClientRect();\r\n const screenHeight = doc.clientHeight;\r\n const screenWidth = doc.clientWidth;\r\n const o = {\r\n id: el.id,\r\n style: el.style,\r\n dx: e.clientX - rect.left,\r\n dy: e.clientY - rect.top,\r\n height: screenHeight - rect.height,\r\n width: screenWidth - rect.width,\r\n screenHeight,\r\n screenWidth,\r\n isTouching\r\n };\r\n\r\n [o.topBorder, o.bottomBorder] = Conf['Header auto-hide'] || !Conf['Fixed Header'] ?\r\n [0, 0]\r\n : Conf['Bottom Header'] ?\r\n [0, Header.bar.getBoundingClientRect().height]\r\n :\r\n [Header.bar.getBoundingClientRect().height, 0];\r\n\r\n if (isTouching) {\r\n o.identifier = e.identifier;\r\n o.move = touchmove.bind(o);\r\n o.up = touchend.bind(o);\r\n $.on(d, 'touchmove', o.move);\r\n return $.on(d, 'touchend touchcancel', o.up);\r\n } else { // mousedown\r\n o.move = drag.bind(o);\r\n o.up = dragend.bind(o);\r\n $.on(d, 'mousemove', o.move);\r\n return $.on(d, 'mouseup', o.up);\r\n }\r\n};\r\n\r\nexport var touchmove = function (e) {\r\n for (var touch of e.changedTouches) {\r\n if (touch.identifier === this.identifier) {\r\n drag.call(this, touch);\r\n return;\r\n }\r\n }\r\n};\r\n\r\nexport var drag = function (e) {\r\n const {clientX, clientY} = e;\r\n\r\n let left = clientX - this.dx;\r\n left = left < 10 ?\r\n 0\r\n : (this.width - left) < 10 ?\r\n ''\r\n :\r\n ((left / this.screenWidth) * 100) + '%';\r\n\r\n let top = clientY - this.dy;\r\n top = top < (10 + this.topBorder) ?\r\n this.topBorder + 'px'\r\n : (this.height - top) < (10 + this.bottomBorder) ?\r\n ''\r\n :\r\n ((top / this.screenHeight) * 100) + '%';\r\n\r\n const right = left === '' ?\r\n 0\r\n :\r\n '';\r\n\r\n const bottom = top === '' ?\r\n this.bottomBorder + 'px'\r\n :\r\n '';\r\n\r\n const {style} = this;\r\n style.left = left;\r\n style.right = right;\r\n style.top = top;\r\n return style.bottom = bottom;\r\n};\r\n\r\nexport var touchend = function (e) {\r\n for (var touch of e.changedTouches) {\r\n if (touch.identifier === this.identifier) {\r\n dragend.call(this);\r\n return;\r\n }\r\n }\r\n};\r\n\r\nexport var dragend = function () {\r\n if (this.isTouching) {\r\n $.off(d, 'touchmove', this.move);\r\n $.off(d, 'touchend touchcancel', this.up);\r\n } else { // mouseup\r\n $.off(d, 'mousemove', this.move);\r\n $.off(d, 'mouseup', this.up);\r\n }\r\n return $.set(`${this.id}.position`, this.style.cssText);\r\n};\r\n\r\nconst hoverstart = function ({ root, el, latestEvent, endEvents, height, width, cb, noRemove }) {\r\n const rect = root.getBoundingClientRect();\r\n const o = {\r\n root,\r\n el,\r\n style: el.style,\r\n isImage: ['IMG', 'VIDEO'].includes(el.nodeName),\r\n cb,\r\n endEvents,\r\n latestEvent,\r\n clientHeight: doc.clientHeight,\r\n clientWidth: doc.clientWidth,\r\n height,\r\n width,\r\n noRemove,\r\n clientX: (rect.left + rect.right) / 2,\r\n clientY: (rect.top + rect.bottom) / 2\r\n };\r\n o.hover = hover.bind(o);\r\n o.hoverend = hoverend.bind(o);\r\n\r\n o.hover(o.latestEvent);\r\n new MutationObserver(function() {\r\n if (el.parentNode) { return o.hover(o.latestEvent); }\r\n }).observe(el, {childList: true});\r\n\r\n $.on(root, endEvents, o.hoverend);\r\n if ($.x('ancestor::div[contains(@class,\"inline\")][1]', root)) {\r\n $.on(d, 'keydown', o.hoverend);\r\n }\r\n $.on(root, 'mousemove', o.hover);\r\n\r\n // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955\r\n o.workaround = function(e) { if (!root.contains(e.target)) { return o.hoverend(e); } };\r\n return $.on(doc, 'mousemove', o.workaround);\r\n};\r\n\r\nhoverstart.padding = 25;\r\n\r\nexport var hover = function (e) {\r\n this.latestEvent = e;\r\n const height = (this.height || this.el.offsetHeight) + hoverstart.padding;\r\n const width = (this.width || this.el.offsetWidth);\r\n const {clientX, clientY} = Conf['Follow Cursor'] ? e : this;\r\n\r\n const top = this.isImage ?\r\n Math.max(0, (clientY * (this.clientHeight - height)) / this.clientHeight)\r\n :\r\n Math.max(0, Math.min(this.clientHeight - height, clientY - 120));\r\n\r\n let threshold = this.clientWidth / 2;\r\n if (!this.isImage) { threshold = Math.max(threshold, this.clientWidth - 400); }\r\n let marginX = (clientX <= threshold ? clientX : this.clientWidth - clientX) + 45;\r\n if (this.isImage) { marginX = Math.min(marginX, this.clientWidth - width); }\r\n marginX += 'px';\r\n const [left, right] = clientX <= threshold ? [marginX, ''] : ['', marginX];\r\n\r\n const {style} = this;\r\n style.top = top + 'px';\r\n style.left = left;\r\n return style.right = right;\r\n};\r\n\r\nexport var hoverend = function (e) {\r\n if (((e.type === 'keydown') && (e.keyCode !== 13)) || (e.target.nodeName === \"TEXTAREA\")) { return; }\r\n if (!this.noRemove) { $.rm(this.el); }\r\n $.off(this.root, this.endEvents, this.hoverend);\r\n $.off(d, 'keydown', this.hoverend);\r\n $.off(this.root, 'mousemove', this.hover);\r\n // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955\r\n $.off(doc, 'mousemove', this.workaround);\r\n if (this.cb) { return this.cb.call(this); }\r\n};\r\n\r\nexport const checkbox = function (name, text, checked) {\r\n if (checked == null) { checked = Conf[name]; }\r\n const label = $.el('label');\r\n const input = $.el('input', {type: 'checkbox', name, checked});\r\n $.add(label, [input, $.tn(` ${text}`)]);\r\n return label;\r\n};\r\n\r\nconst UI = {\r\n dialog,\r\n Menu,\r\n hover: hoverstart,\r\n checkbox\r\n};\r\nexport default UI;\r\n","import Get from \"../General/Get\";\r\nimport Header from \"../General/Header\";\r\nimport { g, Conf, d, doc } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport $$ from \"../platform/$$\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar Nav = {\r\n init() {\r\n switch (g.VIEW) {\r\n case 'index':\r\n if (!Conf['Index Navigation']) { return; }\r\n break;\r\n case 'thread':\r\n if (!Conf['Reply Navigation']) { return; }\r\n break;\r\n default:\r\n return;\r\n }\r\n\r\n const span = $.el('span',\r\n {id: 'navlinks'});\r\n const prev = $.el('a', {\r\n textContent: '▲',\r\n href: 'javascript:;'\r\n }\r\n );\r\n const next = $.el('a', {\r\n textContent: '▼',\r\n href: 'javascript:;'\r\n }\r\n );\r\n\r\n $.on(prev, 'click', this.prev);\r\n $.on(next, 'click', this.next);\r\n\r\n $.add(span, [prev, $.tn(' '), next]);\r\n var append = function() {\r\n $.off(d, '4chanXInitFinished', append);\r\n return $.add(d.body, span);\r\n };\r\n return $.on(d, '4chanXInitFinished', append);\r\n },\r\n\r\n prev() {\r\n if (g.VIEW === 'thread') {\r\n return window.scrollTo(0, 0);\r\n } else {\r\n return Nav.scroll(-1);\r\n }\r\n },\r\n\r\n next() {\r\n if (g.VIEW === 'thread') {\r\n return window.scrollTo(0, d.body.scrollHeight);\r\n } else {\r\n return Nav.scroll(+1);\r\n }\r\n },\r\n\r\n getThread() {\r\n if (g.VIEW === 'thread') { return g.threads.get(`${g.BOARD}.${g.THREADID}`).nodes.root; }\r\n if ($.hasClass(doc, 'catalog-mode')) { return; }\r\n for (var threadRoot of $$(g.SITE.selectors.thread)) {\r\n var thread = Get.threadFromRoot(threadRoot);\r\n if (thread.isHidden && !thread.stub) { continue; }\r\n if (Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height) { // not scrolled past\r\n return threadRoot;\r\n }\r\n }\r\n },\r\n\r\n scroll(delta) {\r\n let next;\r\n d.activeElement?.blur();\r\n let thread = Nav.getThread();\r\n if (!thread) { return; }\r\n const axis = delta === +1 ?\r\n 'following'\r\n :\r\n 'preceding';\r\n if (next = $.x(`${axis}-sibling::${g.SITE.xpath.thread}[not(@hidden)][1]`, thread)) {\r\n // Unless we're not at the beginning of the current thread,\r\n // and thus wanting to move to beginning,\r\n // or we're above the first thread and don't want to skip it.\r\n const top = Header.getTopOf(thread);\r\n if (((delta === +1) && (top < 5)) || ((delta === -1) && (top > -5))) { thread = next; }\r\n }\r\n // Add extra space to the end of the page if necessary so that all threads can be selected by keybinds.\r\n const extra = (Header.getTopOf(thread) + doc.clientHeight) - d.body.getBoundingClientRect().bottom;\r\n if (extra > 0) { d.body.style.marginBottom = `${extra}px`; }\r\n\r\n Header.scrollTo(thread);\r\n\r\n if ((extra > 0) && !Nav.haveExtra) {\r\n Nav.haveExtra = true;\r\n return $.on(d, 'scroll', Nav.removeExtra);\r\n }\r\n },\r\n\r\n removeExtra() {\r\n const extra = doc.clientHeight - d.body.getBoundingClientRect().bottom;\r\n if (extra > 0) {\r\n return d.body.style.marginBottom = `${extra}px`;\r\n } else {\r\n d.body.style.marginBottom = '';\r\n delete Nav.haveExtra;\r\n return $.off(d, 'scroll', Nav.removeExtra);\r\n }\r\n }\r\n};\r\nexport default Nav;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport { Conf, g } from \"../globals/globals\";\r\nimport $$ from \"../platform/$$\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar ImageHost = {\r\n init() {\r\n if ((!(this.useFaster = /\\S/.test(Conf['fourchanImageHost']))) || (g.SITE.software !== 'yotsuba') || !['index', 'thread'].includes(g.VIEW)) { return; }\r\n return Callbacks.Post.push({\r\n name: 'Image Host Rewriting',\r\n cb: this.node\r\n });\r\n },\r\n\r\n suggestions: ['i.4cdn.org', 'is2.4chan.org'],\r\n\r\n host() {\r\n return Conf['fourchanImageHost'].trim() || 'i.4cdn.org';\r\n },\r\n flashHost() {\r\n return 'i.4cdn.org';\r\n },\r\n thumbHost() {\r\n return 'i.4cdn.org';\r\n },\r\n test(hostname) {\r\n return (hostname === 'i.4cdn.org') || ImageHost.regex.test(hostname);\r\n },\r\n\r\n regex: /^is\\d*\\.4chan(?:nel)?\\.org$/,\r\n\r\n node() {\r\n if (this.isClone) { return; }\r\n const host = ImageHost.host();\r\n if (this.file && ImageHost.test(this.file.url.split('/')[2]) && !/\\.swf$/.test(this.file.url)) {\r\n this.file.link.hostname = host;\r\n if (this.file.thumbLink) { this.file.thumbLink.hostname = host; }\r\n this.file.url = this.file.link.href;\r\n }\r\n return ImageHost.fixLinks($$('a', this.nodes.comment));\r\n },\r\n\r\n fixLinks(links) {\r\n for (var link of links) {\r\n if (ImageHost.test(link.hostname) && !/\\.swf$/.test(link.pathname)) {\r\n var host = ImageHost.host();\r\n if (link.hostname !== host) { link.hostname = host; }\r\n }\r\n }\r\n }\r\n};\r\nexport default ImageHost;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport Config from \"../config/Config\";\r\nimport Header from \"../General/Header\";\r\nimport UI from \"../General/UI\";\r\nimport { g, Conf, E } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar Volume = {\r\n init() {\r\n if (!['index', 'thread'].includes(g.VIEW) ||\r\n (!Conf['Image Expansion'] && !Conf['Image Hover'] && !Conf['Image Hover in Catalog'] && !Conf['Gallery'])) { return; }\r\n\r\n $.sync('Allow Sound', function(x) {\r\n Conf['Allow Sound'] = x;\r\n if (Volume.inputs) Volume.inputs.unmute.checked = x;\r\n });\r\n\r\n $.sync('Default Volume', function(x) {\r\n Conf['Default Volume'] = x;\r\n if (Volume.inputs) Volume.inputs.volume.value = x;\r\n });\r\n\r\n if (Conf['Mouse Wheel Volume']) {\r\n Callbacks.Post.push({\r\n name: 'Mouse Wheel Volume',\r\n cb: this.node\r\n });\r\n }\r\n\r\n if (g.SITE.noAudio?.(g.BOARD)) { return; }\r\n\r\n if (Conf['Mouse Wheel Volume']) {\r\n Callbacks.CatalogThread.push({\r\n name: 'Mouse Wheel Volume',\r\n cb: this.catalogNode\r\n });\r\n }\r\n\r\n const unmuteEntry = UI.checkbox('Allow Sound', 'Allow Sound');\r\n unmuteEntry.title = Config.main['Images and Videos']['Allow Sound'][1];\r\n\r\n const volumeEntry = $.el('label',\r\n {title: 'Default volume for videos.'});\r\n $.extend(volumeEntry,\r\n {innerHTML: \" Volume\"});\r\n\r\n this.inputs = {\r\n unmute: unmuteEntry.firstElementChild,\r\n volume: volumeEntry.firstElementChild\r\n };\r\n\r\n $.on(this.inputs.unmute, 'change', $.cb.checked);\r\n $.on(this.inputs.volume, 'change', $.cb.value);\r\n\r\n Header.menu.addEntry({el: unmuteEntry, order: 200});\r\n return Header.menu.addEntry({el: volumeEntry, order: 201});\r\n },\r\n\r\n setup(video) {\r\n video.muted = !Conf['Allow Sound'];\r\n video.volume = Conf['Default Volume'];\r\n return $.on(video, 'volumechange', Volume.change);\r\n },\r\n\r\n change() {\r\n const {muted, volume} = this;\r\n const items = {\r\n 'Allow Sound': !muted,\r\n 'Default Volume': volume\r\n };\r\n for (var key in items) {\r\n var val = items[key];\r\n if (Conf[key] === val) {\r\n delete items[key];\r\n }\r\n }\r\n $.set(items);\r\n $.extend(Conf, items);\r\n if (Volume.inputs) {\r\n Volume.inputs.unmute.checked = !muted;\r\n return Volume.inputs.volume.value = volume;\r\n }\r\n },\r\n\r\n node() {\r\n if (g.SITE.noAudio?.(this.board)) { return; }\r\n for (var file of this.files) {\r\n if (file.isVideo) {\r\n if (file.thumb) { $.on(file.thumb, 'wheel', Volume.wheel.bind(Header.hover)); }\r\n $.on(($('.file-info', file.text) || file.link), 'wheel', Volume.wheel.bind(file.thumbLink));\r\n }\r\n }\r\n },\r\n\r\n catalogNode() {\r\n const file = this.thread.OP.files[0];\r\n if (!file?.isVideo) { return; }\r\n return $.on(this.nodes.thumb, 'wheel', Volume.wheel.bind(Header.hover));\r\n },\r\n\r\n wheel(e) {\r\n let el;\r\n if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) { return; }\r\n if (!(el = $('video:not([data-md5])', this))) { return; }\r\n if (el.muted || !$.hasAudio(el)) { return; }\r\n let volume = el.volume + 0.1;\r\n if (e.deltaY < 0) { volume *= 1.1; }\r\n if (e.deltaY > 0) { volume /= 1.1; }\r\n el.volume = $.minmax(volume - 0.1, 0, 1);\r\n return e.preventDefault();\r\n }\r\n};\r\nexport default Volume;\r\n","import Redirect from \"../Archive/Redirect\";\r\nimport Notice from \"../classes/Notice\";\r\nimport { g, Conf, d } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport CrossOrigin from \"../platform/CrossOrigin\";\r\nimport ImageHost from \"./ImageHost\";\r\nimport Volume from \"./Volume\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS104: Avoid inline assignments\r\n * DS204: Change includes calls to have a more natural evaluation order\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar ImageCommon = {\r\n // Pause and mute video in preparation for removing the element from the document.\r\n pause(video) {\r\n if (video.nodeName !== 'VIDEO') { return; }\r\n video.pause();\r\n $.off(video, 'volumechange', Volume.change);\r\n return video.muted = true;\r\n },\r\n\r\n rewind(el) {\r\n if (el.nodeName === 'VIDEO') {\r\n if (el.readyState >= el.HAVE_METADATA) { return el.currentTime = 0; }\r\n } else if (/\\.gif$/.test(el.src)) {\r\n return $.queueTask(() => el.src = el.src);\r\n }\r\n },\r\n\r\n pushCache(el) {\r\n ImageCommon.cache = el;\r\n return $.on(el, 'error', ImageCommon.cacheError);\r\n },\r\n\r\n popCache() {\r\n const el = ImageCommon.cache;\r\n $.off(el, 'error', ImageCommon.cacheError);\r\n delete ImageCommon.cache;\r\n return el;\r\n },\r\n\r\n cacheError() {\r\n if (ImageCommon.cache === this) { return delete ImageCommon.cache; }\r\n },\r\n\r\n decodeError(file, fileObj) {\r\n let message;\r\n if (file.error?.code !== MediaError.MEDIA_ERR_DECODE) { return false; }\r\n if (!(message = $('.warning', fileObj.thumb.parentNode))) {\r\n message = $.el('div', {className: 'warning'});\r\n $.after(fileObj.thumb, message);\r\n }\r\n message.textContent = 'Error: Corrupt or unplayable video';\r\n return true;\r\n },\r\n\r\n isFromArchive(file) {\r\n return (g.SITE.software === 'yotsuba') && !ImageHost.test(file.src.split('/')[2]);\r\n },\r\n\r\n error(file, post, fileObj, delay, cb) {\r\n let timeoutID;\r\n const src = fileObj.url.split('/');\r\n let url = null;\r\n if ((g.SITE.software === 'yotsuba') && Conf['404 Redirect']) {\r\n url = Redirect.to('file', {\r\n boardID: post.board.ID,\r\n filename: src[src.length - 1]\r\n });\r\n }\r\n if (!url || !Redirect.securityCheck(url)) { url = null; }\r\n\r\n if ((post.isDead || fileObj.isDead) && !ImageCommon.isFromArchive(file)) { return cb(url); }\r\n\r\n if (delay != null) { timeoutID = setTimeout((() => cb(url)), delay); }\r\n if (post.isDead || fileObj.isDead) { return; }\r\n const redirect = function() {\r\n if (!ImageCommon.isFromArchive(file)) {\r\n if (delay != null) { clearTimeout(timeoutID); }\r\n return cb(url);\r\n }\r\n };\r\n\r\n const threadJSON = g.SITE.urls.threadJSON?.(post);\r\n if (!threadJSON) { return; }\r\n var parseJSON = function(isArchiveURL) {\r\n let needle, postObj;\r\n if (this.status === 404) {\r\n let archivedThreadJSON;\r\n if (!isArchiveURL && (archivedThreadJSON = g.SITE.urls.archivedThreadJSON?.(post))) {\r\n $.ajax(archivedThreadJSON, {onloadend() { return parseJSON.call(this, true); }});\r\n } else {\r\n post.kill(!post.isClone, fileObj.index);\r\n }\r\n }\r\n if (this.status !== 200) { return redirect(); }\r\n for (postObj of this.response.posts) {\r\n if (postObj.no === post.ID) { break; }\r\n }\r\n if (postObj.no !== post.ID) {\r\n post.kill();\r\n return redirect();\r\n } else if ((needle = fileObj.docIndex, g.SITE.Build.parseJSON(postObj, post.board).filesDeleted.includes(needle))) {\r\n post.kill(true);\r\n return redirect();\r\n } else {\r\n return url = fileObj.url;\r\n }\r\n };\r\n return $.ajax(threadJSON, {onloadend() { return parseJSON.call(this); }});\r\n },\r\n\r\n // Add controls, but not until the mouse is moved over the video.\r\n addControls(video) {\r\n var handler = function() {\r\n $.off(video, 'mouseover', handler);\r\n // Hacky workaround for Firefox forever-loading bug for very short videos\r\n const t = new Date().getTime();\r\n return $.asap((() => ($.engine !== 'gecko') || ((video.readyState >= 3) && (video.currentTime <= Math.max(0.1, (video.duration - 0.5)))) || (new Date().getTime() >= (t + 1000))), () => video.controls = true);\r\n };\r\n return $.on(video, 'mouseover', handler);\r\n },\r\n\r\n // XXX Estimate whether clicks are on the video controls and should be ignored.\r\n onControls(e) {\r\n return (Conf['Show Controls'] && Conf['Click Passthrough'] && (e.target.nodeName === 'VIDEO')) ||\r\n (e.target.controls && ((e.target.getBoundingClientRect().bottom - e.clientY) < 35));\r\n },\r\n\r\n download(e) {\r\n if (this.protocol === 'blob:') { return true; }\r\n e.preventDefault();\r\n const {href, download} = this;\r\n return CrossOrigin.file(href, function(blob) {\r\n if (blob) {\r\n const a = $.el('a', {\r\n href: URL.createObjectURL(blob),\r\n download,\r\n hidden: true\r\n }\r\n );\r\n $.add(d.body, a);\r\n a.click();\r\n return $.rm(a);\r\n } else {\r\n return new Notice('warning', `Could not download ${href}`, 20);\r\n }\r\n });\r\n }\r\n};\r\nexport default ImageCommon;\r\n",null,null,null,"import Callbacks from \"../classes/Callbacks\";\r\nimport UI from \"../General/UI\";\r\nimport { g, Conf } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar Menu = {\r\n init() {\r\n if (!['index', 'thread'].includes(g.VIEW) || !Conf['Menu']) { return; }\r\n\r\n this.button = $.el('a', {\r\n className: 'menu-button',\r\n href: 'javascript:;'\r\n }\r\n );\r\n\r\n $.extend(this.button, {textContent: \"🞃\"});\r\n\r\n this.menu = new UI.Menu('post');\r\n Callbacks.Post.push({\r\n name: 'Menu',\r\n cb: this.node\r\n });\r\n\r\n return Callbacks.CatalogThread.push({\r\n name: 'Menu',\r\n cb: this.catalogNode\r\n });\r\n },\r\n\r\n node() {\r\n if (this.isClone) {\r\n const button = $('.menu-button', this.nodes.info);\r\n $.rmClass(button, 'active');\r\n $.rm($('.dialog', this.nodes.info));\r\n Menu.makeButton(this, button);\r\n return;\r\n }\r\n return $.add(this.nodes.info, Menu.makeButton(this));\r\n },\r\n\r\n catalogNode() {\r\n return $.after(this.nodes.icons, Menu.makeButton(this.thread.OP));\r\n },\r\n\r\n makeButton(post, button) {\r\n if (!button) { button = Menu.button.cloneNode(true); }\r\n $.on(button, 'click', function(e) {\r\n return Menu.menu.toggle(e, this, post);\r\n });\r\n return button;\r\n }\r\n};\r\nexport default Menu;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport { g } from \"../globals/globals\";\r\nimport { dict } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar Recursive = {\r\n recursives: dict(),\r\n init() {\r\n if (!['index', 'thread'].includes(g.VIEW)) { return; }\r\n return Callbacks.Post.push({\r\n name: 'Recursive',\r\n cb: this.node\r\n });\r\n },\r\n\r\n node() {\r\n if (this.isClone || this.isFetchedQuote) { return; }\r\n for (var quote of this.quotes) {\r\n var obj;\r\n if ((obj = Recursive.recursives[quote])) {\r\n for (var i = 0; i < obj.recursives.length; i++) {\r\n var recursive = obj.recursives[i];\r\n recursive(this, ...obj.args[i]);\r\n }\r\n }\r\n }\r\n },\r\n\r\n add(recursive, post, ...args) {\r\n const obj = Recursive.recursives[post.fullID] || (Recursive.recursives[post.fullID] = {\r\n recursives: [],\r\n args: []\r\n });\r\n obj.recursives.push(recursive);\r\n return obj.args.push(args);\r\n },\r\n\r\n rm(recursive, post) {\r\n let obj;\r\n if (!(obj = Recursive.recursives[post.fullID])) { return; }\r\n for (let i = 0; i < obj.recursives.length; i++) {\r\n var rec = obj.recursives[i];\r\n if (rec === recursive) {\r\n obj.recursives.splice(i, 1);\r\n obj.args.splice(i, 1);\r\n }\r\n }\r\n },\r\n\r\n apply(recursive, post, ...args) {\r\n const {fullID} = post;\r\n return g.posts.forEach(function(post) {\r\n if (post.quotes.includes(fullID)) {\r\n return recursive(post, ...args);\r\n }\r\n });\r\n }\r\n};\r\nexport default Recursive;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport DataBoard from \"../classes/DataBoard\";\r\nimport Get from \"../General/Get\";\r\nimport UI from \"../General/UI\";\r\nimport { g, Conf, doc } from \"../globals/globals\";\r\nimport Menu from \"../Menu/Menu\";\r\nimport $ from \"../platform/$\";\r\nimport Recursive from \"./Recursive\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar PostHiding = {\r\n init() {\r\n if (!['index', 'thread'].includes(g.VIEW) || (!Conf['Reply Hiding Buttons'] && !(Conf['Menu'] && Conf['Reply Hiding Link']))) { return; }\r\n\r\n if (Conf['Reply Hiding Buttons']) {\r\n $.addClass(doc, \"reply-hide\");\r\n }\r\n\r\n this.db = new DataBoard('hiddenPosts');\r\n return Callbacks.Post.push({\r\n name: 'Reply Hiding',\r\n cb: this.node\r\n });\r\n },\r\n\r\n isHidden(boardID, threadID, postID) {\r\n return !!(PostHiding.db && PostHiding.db.get({boardID, threadID, postID}));\r\n },\r\n\r\n node() {\r\n let data, sa;\r\n if (!this.isReply || this.isClone || this.isFetchedQuote) { return; }\r\n\r\n if (data = PostHiding.db.get({boardID: this.board.ID, threadID: this.thread.ID, postID: this.ID})) {\r\n if (data.thisPost) {\r\n PostHiding.hide(this, data.makeStub, data.hideRecursively);\r\n } else {\r\n Recursive.apply(PostHiding.hide, this, data.makeStub, true);\r\n Recursive.add(PostHiding.hide, this, data.makeStub, true);\r\n }\r\n }\r\n\r\n if (!Conf['Reply Hiding Buttons']) { return; }\r\n\r\n const button = PostHiding.makeButton(this, 'hide');\r\n if (sa = g.SITE.selectors.sideArrows) {\r\n const sideArrows = $(sa, this.nodes.root);\r\n $.replace(sideArrows.firstChild, button);\r\n return sideArrows.className = 'replacedSideArrows';\r\n } else {\r\n return $.prepend(this.nodes.info, button);\r\n }\r\n },\r\n\r\n menu: {\r\n init() {\r\n if (!['index', 'thread'].includes(g.VIEW) || !Conf['Menu'] || !Conf['Reply Hiding Link']) { return; }\r\n\r\n // Hide\r\n let div = $.el('div', {\r\n className: 'hide-reply-link',\r\n textContent: 'Hide'\r\n }\r\n );\r\n\r\n let apply = $.el('a', {\r\n textContent: 'Apply',\r\n href: 'javascript:;'\r\n }\r\n );\r\n $.on(apply, 'click', PostHiding.menu.hide);\r\n\r\n let thisPost = UI.checkbox('thisPost', 'This post', true);\r\n let replies = UI.checkbox('replies', 'Hide replies', Conf['Recursive Hiding']);\r\n const makeStub = UI.checkbox('makeStub', 'Make stub', Conf['Stubs']);\r\n\r\n Menu.menu.addEntry({\r\n el: div,\r\n order: 20,\r\n open(post) {\r\n if (!post.isReply || post.isClone || post.isHidden) {\r\n return false;\r\n }\r\n PostHiding.menu.post = post;\r\n return true;\r\n },\r\n subEntries: [\r\n {el: apply}\r\n ,\r\n {el: thisPost}\r\n ,\r\n {el: replies}\r\n ,\r\n {el: makeStub}\r\n ]});\r\n\r\n // Show\r\n div = $.el('div', {\r\n className: 'show-reply-link',\r\n textContent: 'Show'\r\n }\r\n );\r\n\r\n apply = $.el('a', {\r\n textContent: 'Apply',\r\n href: 'javascript:;'\r\n }\r\n );\r\n $.on(apply, 'click', PostHiding.menu.show);\r\n\r\n thisPost = UI.checkbox('thisPost', 'This post', false);\r\n replies = UI.checkbox('replies', 'Show replies', false);\r\n const hideStubLink = $.el('a', {\r\n textContent: 'Hide stub',\r\n href: 'javascript:;'\r\n }\r\n );\r\n $.on(hideStubLink, 'click', PostHiding.menu.hideStub);\r\n\r\n Menu.menu.addEntry({\r\n el: div,\r\n order: 20,\r\n open(post) {\r\n let data;\r\n if (!post.isReply || post.isClone || !post.isHidden) {\r\n return false;\r\n }\r\n if (!(data = PostHiding.db.get({boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}))) {\r\n return false;\r\n }\r\n PostHiding.menu.post = post;\r\n thisPost.firstChild.checked = post.isHidden;\r\n replies.firstChild.checked = (data?.hideRecursively != null) ? data.hideRecursively : Conf['Recursive Hiding'];\r\n return true;\r\n },\r\n subEntries: [\r\n {el: apply}\r\n ,\r\n {el: thisPost}\r\n ,\r\n {el: replies}\r\n ]});\r\n\r\n return Menu.menu.addEntry({\r\n el: hideStubLink,\r\n order: 15,\r\n open(post) {\r\n let data;\r\n if (!post.isReply || post.isClone || !post.isHidden) {\r\n return false;\r\n }\r\n if (!(data = PostHiding.db.get({boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}))) {\r\n return false;\r\n }\r\n return PostHiding.menu.post = post;\r\n }\r\n });\r\n },\r\n\r\n hide() {\r\n const parent = this.parentNode;\r\n const thisPost = $('input[name=thisPost]', parent).checked;\r\n const replies = $('input[name=replies]', parent).checked;\r\n const makeStub = $('input[name=makeStub]', parent).checked;\r\n const {post} = PostHiding.menu;\r\n if (thisPost) {\r\n PostHiding.hide(post, makeStub, replies);\r\n } else if (replies) {\r\n Recursive.apply(PostHiding.hide, post, makeStub, true);\r\n Recursive.add(PostHiding.hide, post, makeStub, true);\r\n } else {\r\n return;\r\n }\r\n PostHiding.saveHiddenState(post, true, thisPost, makeStub, replies);\r\n return $.event('CloseMenu');\r\n },\r\n\r\n show() {\r\n let data;\r\n const parent = this.parentNode;\r\n const thisPost = $('input[name=thisPost]', parent).checked;\r\n const replies = $('input[name=replies]', parent).checked;\r\n const {post} = PostHiding.menu;\r\n if (thisPost) {\r\n PostHiding.show(post, replies);\r\n } else if (replies) {\r\n Recursive.apply(PostHiding.show, post, true);\r\n Recursive.rm(PostHiding.hide, post, true);\r\n } else {\r\n return;\r\n }\r\n if (data = PostHiding.db.get({boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID})) {\r\n PostHiding.saveHiddenState(post, !(thisPost && replies), !thisPost, data.makeStub, !replies);\r\n }\r\n return $.event('CloseMenu');\r\n },\r\n hideStub() {\r\n let data;\r\n const {post} = PostHiding.menu;\r\n if (data = PostHiding.db.get({boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID})) {\r\n PostHiding.show(post, data.hideRecursively);\r\n PostHiding.hide(post, false, data.hideRecursively);\r\n PostHiding.saveHiddenState(post, true, true, false, data.hideRecursively);\r\n }\r\n $.event('CloseMenu');\r\n }\r\n },\r\n\r\n makeButton(post, type) {\r\n const span = $.el('span', {\r\n textContent: type === 'hide' ? '➖︎' : '➕︎',\r\n });\r\n const a = $.el('a', {\r\n className: `${type}-reply-button`,\r\n href: 'javascript:;'\r\n }\r\n );\r\n $.add(a, span);\r\n $.on(a, 'click', PostHiding.toggle);\r\n return a;\r\n },\r\n\r\n saveHiddenState(post, isHiding, thisPost, makeStub, hideRecursively) {\r\n const data = {\r\n boardID: post.board.ID,\r\n threadID: post.thread.ID,\r\n postID: post.ID\r\n };\r\n if (isHiding) {\r\n data.val = {\r\n thisPost: thisPost !== false, // undefined -> true\r\n makeStub,\r\n hideRecursively\r\n };\r\n return PostHiding.db.set(data);\r\n } else {\r\n return PostHiding.db.delete(data);\r\n }\r\n },\r\n\r\n toggle() {\r\n const post = Get.postFromNode(this);\r\n PostHiding[(post.isHidden ? 'show' : 'hide')](post);\r\n return PostHiding.saveHiddenState(post, post.isHidden);\r\n },\r\n\r\n hide(post, makeStub=Conf['Stubs'], hideRecursively=Conf['Recursive Hiding']) {\r\n if (post.isHidden) { return; }\r\n post.isHidden = true;\r\n\r\n if (hideRecursively) {\r\n Recursive.apply(PostHiding.hide, post, makeStub, true);\r\n Recursive.add(PostHiding.hide, post, makeStub, true);\r\n }\r\n\r\n for (var quotelink of Get.allQuotelinksLinkingTo(post)) {\r\n $.addClass(quotelink, 'filtered');\r\n }\r\n\r\n if (!makeStub) {\r\n post.nodes.root.hidden = true;\r\n return;\r\n }\r\n\r\n const a = PostHiding.makeButton(post, 'show');\r\n $.add(a, $.tn(` ${post.info.nameBlock}`));\r\n post.nodes.stub = $.el('div',\r\n {className: 'stub'});\r\n $.add(post.nodes.stub, a);\r\n if (Conf['Menu']) {\r\n $.add(post.nodes.stub, Menu.makeButton(post));\r\n }\r\n return $.prepend(post.nodes.root, post.nodes.stub);\r\n },\r\n\r\n show(post, showRecursively=Conf['Recursive Hiding']) {\r\n if (post.nodes.stub) {\r\n $.rm(post.nodes.stub);\r\n delete post.nodes.stub;\r\n } else {\r\n post.nodes.root.hidden = false;\r\n }\r\n post.isHidden = false;\r\n if (showRecursively) {\r\n Recursive.apply(PostHiding.show, post, true);\r\n Recursive.rm(PostHiding.hide, post);\r\n }\r\n for (var quotelink of Get.allQuotelinksLinkingTo(post)) {\r\n $.rmClass(quotelink, 'filtered');\r\n }\r\n }\r\n};\r\nexport default PostHiding;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport Post from \"../classes/Post\";\r\nimport Index from \"../General/Index\";\r\nimport { g, Conf, d, doc } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport { DAY, HOUR, MINUTE, SECOND } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS205: Consider reworking code to avoid use of IIFEs\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar RelativeDates = {\r\n INTERVAL: 30000,\r\n\r\n init() {\r\n if (\r\n (['index', 'thread', 'archive'].includes(g.VIEW) && Conf['Relative Post Dates'] && !Conf['Relative Date Title']) ||\r\n Index.enabled\r\n ) {\r\n this.flush();\r\n $.on(d, 'visibilitychange PostsInserted', this.flush);\r\n }\r\n\r\n if (Conf['Relative Post Dates']) {\r\n return Callbacks.Post.push({\r\n name: 'Relative Post Dates',\r\n cb: this.node\r\n });\r\n }\r\n },\r\n\r\n node() {\r\n if (!this.info.date) { return; }\r\n const dateEl = this.nodes.date;\r\n if (Conf['Relative Date Title']) {\r\n $.on(dateEl, 'mouseover', () => RelativeDates.hover(this));\r\n return;\r\n }\r\n if (this.isClone) { return; }\r\n\r\n // Show original absolute time as tooltip so users can still know exact times\r\n // Since \"Time Formatting\" runs its `node` before us, the title tooltip will\r\n // pick up the user-formatted time instead of 4chan time when enabled.\r\n dateEl.title = dateEl.textContent;\r\n\r\n return RelativeDates.update(this);\r\n },\r\n\r\n // diff is milliseconds from now.\r\n relative(diff, now, date, abbrev) {\r\n let number;\r\n let unit = (() => {\r\n if ((number = (diff / DAY)) >= 1) {\r\n const years = now.getFullYear() - date.getFullYear();\r\n let months = now.getMonth() - date.getMonth();\r\n const days = now.getDate() - date.getDate();\r\n if (years > 1) {\r\n number = years - ((months < 0) || ((months === 0) && (days < 0)));\r\n return 'year';\r\n } else if ((years === 1) && ((months > 0) || ((months === 0) && (days >= 0)))) {\r\n number = years;\r\n return 'year';\r\n } else if ((months = months + (12*years)) > 1) {\r\n number = months - (days < 0);\r\n return 'month';\r\n } else if ((months === 1) && (days >= 0)) {\r\n number = months;\r\n return 'month';\r\n } else {\r\n return 'day';\r\n }\r\n } else if ((number = (diff / HOUR)) >= 1) {\r\n return 'hour';\r\n } else if ((number = (diff / MINUTE)) >= 1) {\r\n return 'minute';\r\n } else {\r\n // prevent \"-1 seconds ago\"\r\n number = Math.max(0, diff) / SECOND;\r\n return 'second';\r\n }\r\n })();\r\n\r\n const rounded = Math.round(number);\r\n\r\n if (abbrev) {\r\n unit = unit === 'month' ? 'mo' : unit[0];\r\n } else {\r\n if (rounded !== 1) { unit += 's'; } // pluralize\r\n }\r\n\r\n if (abbrev) { return `${rounded}${unit}`; } else { return `${rounded} ${unit} ago`; }\r\n },\r\n\r\n // Changing all relative dates as soon as possible incurs many annoying\r\n // redraws and scroll stuttering. Thus, sacrifice accuracy for UX/CPU economy,\r\n // and perform redraws when the DOM is otherwise being manipulated (and scroll\r\n // stuttering won't be noticed), falling back to INTERVAL while the page\r\n // is visible.\r\n //\r\n // Each individual dateTime element will add its update() function to the stale list\r\n // when it is to be called.\r\n stale: [],\r\n flush() {\r\n // No point in changing the dates until the user sees them.\r\n if (d.hidden) { return; }\r\n\r\n const now = new Date();\r\n for (var data of RelativeDates.stale) { RelativeDates.update(data, now); }\r\n RelativeDates.stale = [];\r\n\r\n // Reset automatic flush.\r\n clearTimeout(RelativeDates.timeout);\r\n return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL);\r\n },\r\n\r\n hover(post) {\r\n const {\r\n date\r\n } = post.info;\r\n const now = new Date();\r\n const diff = now - date;\r\n return post.nodes.date.title = RelativeDates.relative(diff, now, date);\r\n },\r\n\r\n // `update()`, when called from `flush()`, updates the elements,\r\n // and re-calls `setOwnTimeout()` to re-add `data` to the stale list later.\r\n update(data, now) {\r\n let abbrev, date;\r\n const isPost = data instanceof Post;\r\n if (isPost) {\r\n ({\r\n date\r\n } = data.info);\r\n abbrev = false;\r\n } else {\r\n date = new Date(+data.dataset.utc);\r\n abbrev = !!data.dataset.abbrev;\r\n }\r\n if (!now) { now = new Date(); }\r\n const diff = now - date;\r\n const relative = RelativeDates.relative(diff, now, date, abbrev);\r\n if (isPost) {\r\n for (var singlePost of [data].concat(data.clones)) {\r\n singlePost.nodes.date.firstChild.textContent = relative;\r\n }\r\n } else {\r\n data.firstChild.textContent = relative;\r\n }\r\n return RelativeDates.setOwnTimeout(diff, data);\r\n },\r\n\r\n setOwnTimeout(diff, data) {\r\n const delay = diff < MINUTE ?\r\n SECOND - ((diff + (SECOND / 2)) % SECOND)\r\n : diff < HOUR ?\r\n MINUTE - ((diff + (MINUTE / 2)) % MINUTE)\r\n : diff < DAY ?\r\n HOUR - ((diff + (HOUR / 2)) % HOUR)\r\n :\r\n DAY - ((diff + (DAY / 2)) % DAY);\r\n return setTimeout(RelativeDates.markStale, delay, data);\r\n },\r\n\r\n markStale(data) {\r\n if (RelativeDates.stale.includes(data)) { return; } // We can call RelativeDates.update() multiple times.\r\n if (data instanceof Post && !g.posts.get(data.fullID)) { return; } // collected post.\r\n if (data instanceof Element && !doc.contains(data)) { return; } // removed catalog reply.\r\n return RelativeDates.stale.push(data);\r\n }\r\n};\r\nexport default RelativeDates;\r\n","import Notice from \"../classes/Notice\";\r\nimport { g, Conf } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport { dict, HOUR } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS104: Avoid inline assignments\r\n * DS205: Consider reworking code to avoid use of IIFEs\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar BoardConfig = {\r\n cbs: [],\r\n\r\n init() {\r\n let middle;\r\n if (g.SITE.software !== 'yotsuba') { return; }\r\n const now = Date.now();\r\n if (now - (2 * HOUR) >= ((middle = Conf['boardConfig'].lastChecked || 0)) || middle > now) {\r\n return $.ajax(`${location.protocol}//a.4cdn.org/boards.json`,\r\n {onloadend: this.load});\r\n } else {\r\n const {boards} = Conf['boardConfig'];\r\n return this.set(boards);\r\n }\r\n },\r\n\r\n load() {\r\n let boards;\r\n if ((this.status === 200) && this.response && this.response.boards) {\r\n boards = dict();\r\n for (var board of this.response.boards) {\r\n boards[board.board] = board;\r\n }\r\n $.set('boardConfig', {boards, lastChecked: Date.now()});\r\n } else {\r\n ({boards} = Conf['boardConfig']);\r\n const err = (() => { switch (this.status) {\r\n case 0: return 'Connection Error';\r\n case 200: return 'Invalid Data';\r\n default: return `Error ${this.statusText} (${this.status})`;\r\n } })();\r\n new Notice('warning', `Failed to load board configuration. ${err}`, 20);\r\n }\r\n return BoardConfig.set(boards);\r\n },\r\n\r\n set(boards) {\r\n this.boards = boards;\r\n for (var ID in g.boards) {\r\n var board = g.boards[ID];\r\n board.config = this.boards[ID] || {};\r\n }\r\n for (var cb of this.cbs) {\r\n $.queueTask(cb);\r\n }\r\n },\r\n\r\n ready(cb) {\r\n if (this.boards) {\r\n return cb();\r\n } else {\r\n return this.cbs.push(cb);\r\n }\r\n },\r\n\r\n sfwBoards(sfw) {\r\n return (() => {\r\n const result = [];\r\n const object = this.boards || Conf['boardConfig'].boards;\r\n for (var board in object) {\r\n var data = object[board];\r\n if (!!data.ws_board === sfw) {\r\n result.push(board);\r\n }\r\n }\r\n return result;\r\n })();\r\n },\r\n\r\n isSFW(board) {\r\n return !!(this.boards || Conf['boardConfig'].boards)[board]?.ws_board;\r\n },\r\n\r\n domain(board) {\r\n return `boards.${BoardConfig.isSFW(board) ? '4channel' : '4chan'}.org`;\r\n },\r\n\r\n isArchived(board) {\r\n // assume archive exists if no data available to prevent cleaning of archived threads\r\n const data = (this.boards || Conf['boardConfig'].boards)[board];\r\n return !data || data.is_archived;\r\n },\r\n\r\n noAudio(boardID) {\r\n if (g.SITE.software !== 'yotsuba') { return false; }\r\n const boards = this.boards || Conf['boardConfig'].boards;\r\n return boards && boards[boardID] && !boards[boardID].webm_audio;\r\n },\r\n\r\n title(boardID) {\r\n return (this.boards || Conf['boardConfig'].boards)?.[boardID]?.title || '';\r\n }\r\n};\r\nexport default BoardConfig;\r\n","import BoardConfig from \"../General/BoardConfig\";\r\nimport { d, g } from \"../globals/globals\";\r\nimport SimpleDict from \"./SimpleDict\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nexport default class Board {\r\n toString() { return this.ID; }\r\n\r\n constructor(ID) {\r\n this.ID = ID;\r\n this.boardID = this.ID;\r\n this.siteID = g.SITE.ID;\r\n this.threads = new SimpleDict();\r\n this.posts = new SimpleDict();\r\n this.config = BoardConfig.boards?.[this.ID] || {};\r\n\r\n g.boards[this] = this;\r\n }\r\n\r\n cooldowns() {\r\n const c2 = (this.config || {}).cooldowns || {};\r\n const c = {\r\n thread: c2.threads || 0,\r\n reply: c2.replies || 0,\r\n image: c2.images || 0,\r\n thread_global: 300 // inter-board thread cooldown\r\n };\r\n // Pass users have reduced cooldowns.\r\n if (d.cookie.indexOf('pass_enabled=1') >= 0) {\r\n for (var key of ['reply', 'image']) {\r\n c[key] = Math.ceil(c[key] / 2);\r\n }\r\n }\r\n return c;\r\n }\r\n}\r\n","import { d } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nconst PostRedirect = {\r\n init() {\r\n return $.on(d, 'QRPostSuccessful', e => {\r\n if (!e.detail.redirect) { return; }\r\n this.event = e;\r\n this.delays = 0;\r\n return $.queueTask(() => {\r\n if ((e === this.event) && (this.delays === 0)) {\r\n return location.href = e.detail.redirect;\r\n }\r\n });\r\n });\r\n },\r\n\r\n delays: 0,\r\n\r\n delay() {\r\n if (!this.event) { return null; }\r\n const e = this.event;\r\n this.delays++;\r\n return () => {\r\n if (e !== this.event) { return; }\r\n this.delays--;\r\n if (this.delays === 0) {\r\n return location.href = e.detail.redirect;\r\n }\r\n };\r\n }\r\n};\r\nexport default PostRedirect;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport Get from \"../General/Get\";\r\nimport { g, Conf } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport $$ from \"../platform/$$\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar ExpandComment = {\r\n init() {\r\n if ((g.VIEW !== 'index') || !Conf['Comment Expansion'] || Conf['JSON Index']) { return; }\r\n\r\n return Callbacks.Post.push({\r\n name: 'Comment Expansion',\r\n cb: this.node\r\n });\r\n },\r\n\r\n node() {\r\n let a;\r\n if (a = $('.abbr > a:not([onclick])', this.nodes.comment)) {\r\n return $.on(a, 'click', ExpandComment.cb);\r\n }\r\n },\r\n\r\n callbacks: [],\r\n\r\n cb(e) {\r\n e.preventDefault();\r\n return ExpandComment.expand(Get.postFromNode(this));\r\n },\r\n\r\n expand(post) {\r\n let a;\r\n if (post.nodes.longComment && !post.nodes.longComment.parentNode) {\r\n $.replace(post.nodes.shortComment, post.nodes.longComment);\r\n post.nodes.comment = post.nodes.longComment;\r\n return;\r\n }\r\n if (!(a = $('.abbr > a', post.nodes.comment))) { return; }\r\n a.textContent = `Post No.${post} Loading...`;\r\n return $.cache(g.SITE.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), function() { return ExpandComment.parse(this, a, post); });\r\n },\r\n\r\n contract(post) {\r\n if (!post.nodes.shortComment) { return; }\r\n const a = $('.abbr > a', post.nodes.shortComment);\r\n a.textContent = 'here';\r\n $.replace(post.nodes.longComment, post.nodes.shortComment);\r\n return post.nodes.comment = post.nodes.shortComment;\r\n },\r\n\r\n parse(req, a, post) {\r\n let postObj, spoilerRange;\r\n const {status} = req;\r\n if (![200, 304].includes(status)) {\r\n a.textContent = status ? `Error ${req.statusText} (${status})` : 'Connection Error';\r\n return;\r\n }\r\n\r\n const {\r\n posts\r\n } = req.response;\r\n if (spoilerRange = posts[0].custom_spoiler) {\r\n g.SITE.Build.spoilerRange[g.BOARD] = spoilerRange;\r\n }\r\n\r\n for (postObj of posts) {\r\n if (postObj.no === post.ID) { break; }\r\n }\r\n if (postObj.no !== post.ID) {\r\n a.textContent = `Post No.${post} not found.`;\r\n return;\r\n }\r\n\r\n const {comment} = post.nodes;\r\n const clone = comment.cloneNode(false);\r\n clone.innerHTML = postObj.com;\r\n // Fix pathnames\r\n for (var quote of $$('.quotelink', clone)) {\r\n var href = quote.getAttribute('href');\r\n if (href[0] === '/') { continue; } // Cross-board quote, or board link\r\n if (href[0] === '#') {\r\n quote.href = `${a.pathname.split(/\\/+/).splice(0,4).join('/')}${href}`;\r\n } else {\r\n quote.href = `${a.pathname.split(/\\/+/).splice(0,3).join('/')}/${href}`;\r\n }\r\n }\r\n post.nodes.shortComment = comment;\r\n $.replace(comment, clone);\r\n post.nodes.comment = (post.nodes.longComment = clone);\r\n post.parseComment();\r\n post.parseQuotes();\r\n\r\n for (var callback of ExpandComment.callbacks) {\r\n callback.call(post);\r\n }\r\n }\r\n};\r\nexport default ExpandComment;;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport DataBoard from \"../classes/DataBoard\";\r\nimport Notice from \"../classes/Notice\";\r\nimport Get from \"../General/Get\";\r\nimport Header from \"../General/Header\";\r\nimport { Conf, d, doc, g } from \"../globals/globals\";\r\nimport Menu from \"../Menu/Menu\";\r\nimport ExpandComment from \"../Miscellaneous/ExpandComment\";\r\nimport $ from \"../platform/$\";\r\nimport $$ from \"../platform/$$\";\r\nimport PostRedirect from \"../Posting/PostRedirect\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar QuoteYou = {\r\n init() {\r\n if (!Conf['Remember Your Posts']) { return; }\r\n\r\n this.db = new DataBoard('yourPosts');\r\n $.sync('Remember Your Posts', enabled => Conf['Remember Your Posts'] = enabled);\r\n $.on(d, 'QRPostSuccessful', function(e) {\r\n const cb = PostRedirect.delay();\r\n return $.get('Remember Your Posts', Conf['Remember Your Posts'], function(items) {\r\n if (!items['Remember Your Posts']) { return; }\r\n const {boardID, threadID, postID} = e.detail;\r\n return QuoteYou.db.set({boardID, threadID, postID, val: true}, cb);\r\n });\r\n });\r\n\r\n if (!['index', 'thread', 'archive'].includes(g.VIEW)) { return; }\r\n\r\n if (Conf['Highlight Own Posts']) {\r\n $.addClass(doc, 'highlight-own');\r\n }\r\n\r\n if (Conf['Highlight Posts Quoting You']) {\r\n $.addClass(doc, 'highlight-you');\r\n }\r\n\r\n if (Conf['Comment Expansion']) {\r\n ExpandComment.callbacks.push(this.node);\r\n }\r\n\r\n // \\u00A0 is nbsp\r\n this.mark = $.el('span', {\r\n textContent: '\\u00A0(You)',\r\n className: 'qmark-you'\r\n }\r\n );\r\n Callbacks.Post.push({\r\n name: 'Mark Quotes of You',\r\n cb: this.node\r\n });\r\n\r\n return QuoteYou.menu.init();\r\n },\r\n\r\n isYou(post) {\r\n return !!QuoteYou.db?.get({\r\n boardID: post.boardID,\r\n threadID: post.threadID,\r\n postID: post.ID\r\n });\r\n },\r\n\r\n node() {\r\n if (this.isClone) { return; }\r\n\r\n if (QuoteYou.isYou(this)) {\r\n $.addClass(this.nodes.root, 'yourPost');\r\n }\r\n\r\n // Stop there if there's no quotes in that post.\r\n if (!this.quotes.length) { return; }\r\n\r\n for (var quotelink of this.nodes.quotelinks) {\r\n if (QuoteYou.db.get(Get.postDataFromLink(quotelink))) {\r\n if (Conf['Mark Quotes of You']) { $.add(quotelink, QuoteYou.mark.cloneNode(true)); }\r\n $.addClass(quotelink, 'you');\r\n $.addClass(this.nodes.root, 'quotesYou');\r\n }\r\n }\r\n },\r\n\r\n menu: {\r\n init() {\r\n const label = $.el('label',\r\n {className: 'toggle-you'}\r\n ,\r\n {innerHTML: ' You'});\r\n const input = $('input', label);\r\n $.on(input, 'change', QuoteYou.menu.toggle);\r\n return Menu.menu?.addEntry({\r\n el: label,\r\n order: 80,\r\n open(post) {\r\n QuoteYou.menu.post = (post.origin || post);\r\n input.checked = QuoteYou.isYou(post);\r\n return true;\r\n }\r\n });\r\n },\r\n\r\n toggle() {\r\n const {post} = QuoteYou.menu;\r\n const data = {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID, val: true};\r\n if (this.checked) {\r\n QuoteYou.db.set(data);\r\n } else {\r\n QuoteYou.db.delete(data);\r\n }\r\n for (var clone of [post].concat(post.clones)) {\r\n clone.nodes.root.classList.toggle('yourPost', this.checked);\r\n }\r\n for (var quotelink of Get.allQuotelinksLinkingTo(post)) {\r\n if (this.checked) {\r\n if (Conf['Mark Quotes of You']) { $.add(quotelink, QuoteYou.mark.cloneNode(true)); }\r\n } else {\r\n $.rm($('.qmark-you', quotelink));\r\n }\r\n quotelink.classList.toggle('you', this.checked);\r\n if ($.hasClass(quotelink, 'quotelink')) {\r\n var quoter = Get.postFromNode(quotelink).nodes.root;\r\n quoter.classList.toggle('quotesYou', !!$('.quotelink.you', quoter));\r\n }\r\n }\r\n }\r\n },\r\n\r\n cb: {\r\n seek(type) {\r\n let highlighted, post;\r\n let result;\r\n const {highlight} = g.SITE.classes;\r\n if (highlighted = $(`.${highlight}`)) { $.rmClass(highlighted, highlight); }\r\n\r\n if (!QuoteYou.lastRead || !doc.contains(QuoteYou.lastRead) || !$.hasClass(QuoteYou.lastRead, 'quotesYou')) {\r\n if (!(post = (QuoteYou.lastRead = $('.quotesYou')))) {\r\n new Notice('warning', 'No posts are currently quoting you, loser.', 20);\r\n return;\r\n }\r\n if (QuoteYou.cb.scroll(post)) { return; }\r\n } else {\r\n post = QuoteYou.lastRead;\r\n }\r\n\r\n const str = `${type}::div[contains(@class,'quotesYou')]`;\r\n\r\n while (post = (result = $.X(str, post)).snapshotItem(type === 'preceding' ? result.snapshotLength - 1 : 0)) {\r\n if (QuoteYou.cb.scroll(post)) { return; }\r\n }\r\n\r\n const posts = $$('.quotesYou');\r\n return QuoteYou.cb.scroll(posts[type === 'following' ? 0 : posts.length - 1]);\r\n },\r\n\r\n scroll(root) {\r\n const post = Get.postFromRoot(root);\r\n if (!post.nodes.post.getBoundingClientRect().height) {\r\n return false;\r\n } else {\r\n QuoteYou.lastRead = root;\r\n location.href = Get.url('post', post);\r\n Header.scrollTo(post.nodes.post);\r\n if (post.isReply) {\r\n const sel = `${g.SITE.selectors.postContainer}${g.SITE.selectors.highlightable.reply}`;\r\n let node = post.nodes.root;\r\n if (!node.matches(sel)) { node = $(sel, node); }\r\n $.addClass(node, g.SITE.classes.highlight);\r\n }\r\n return true;\r\n }\r\n }\r\n }\r\n};\r\nexport default QuoteYou;\r\n","/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nexport default class RandomAccessList {\r\n constructor(items) {\r\n this.length = 0;\r\n if (items) { for (var item of items) { this.push(item); } }\r\n }\r\n\r\n push(data) {\r\n let item;\r\n let {ID} = data;\r\n if (!ID) { ID = data.id; }\r\n if (this[ID]) { return; }\r\n const {last} = this;\r\n this[ID] = (item = {\r\n prev: last,\r\n next: null,\r\n data,\r\n ID\r\n });\r\n item.prev = last;\r\n this.last = last ?\r\n (last.next = item)\r\n :\r\n (this.first = item);\r\n return this.length++;\r\n }\r\n\r\n before(root, item) {\r\n if ((item.next === root) || (item === root)) { return; }\r\n\r\n this.rmi(item);\r\n\r\n const {prev} = root;\r\n root.prev = item;\r\n item.next = root;\r\n item.prev = prev;\r\n if (prev) {\r\n return prev.next = item;\r\n } else {\r\n return this.first = item;\r\n }\r\n }\r\n\r\n after(root, item) {\r\n if ((item.prev === root) || (item === root)) { return; }\r\n\r\n this.rmi(item);\r\n\r\n const {next} = root;\r\n root.next = item;\r\n item.prev = root;\r\n item.next = next;\r\n if (next) {\r\n return next.prev = item;\r\n } else {\r\n return this.last = item;\r\n }\r\n }\r\n\r\n prepend(item) {\r\n const {first} = this;\r\n if ((item === first) || !this[item.ID]) { return; }\r\n this.rmi(item);\r\n item.next = first;\r\n if (first) {\r\n first.prev = item;\r\n } else {\r\n this.last = item;\r\n }\r\n this.first = item;\r\n return delete item.prev;\r\n }\r\n\r\n shift() {\r\n return this.rm(this.first.ID);\r\n }\r\n\r\n order() {\r\n let item;\r\n const order = [(item = this.first)];\r\n while ((item = item.next)) { order.push(item); }\r\n return order;\r\n }\r\n\r\n rm(ID) {\r\n const item = this[ID];\r\n if (!item) { return; }\r\n delete this[ID];\r\n this.length--;\r\n this.rmi(item);\r\n delete item.next;\r\n return delete item.prev;\r\n }\r\n\r\n rmi(item) {\r\n const {prev, next} = item;\r\n if (prev) {\r\n prev.next = next;\r\n } else {\r\n this.first = next;\r\n }\r\n if (next) {\r\n return next.prev = prev;\r\n } else {\r\n return this.last = prev;\r\n }\r\n }\r\n}\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport DataBoard from \"../classes/DataBoard\";\r\nimport RandomAccessList from \"../classes/RandomAccessList\";\r\nimport Get from \"../General/Get\";\r\nimport Header from \"../General/Header\";\r\nimport { g, Conf, d } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport { debounce, SECOND } from \"../platform/helpers\";\r\nimport QuoteYou from \"../Quotelinks/QuoteYou\";\r\nimport Favicon from \"./Favicon\";\r\nimport ThreadWatcher from \"./ThreadWatcher\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar Unread = {\r\n init() {\r\n if ((g.VIEW !== 'thread') || (\r\n !Conf['Unread Count'] &&\r\n !Conf['Unread Favicon'] &&\r\n !Conf['Unread Line'] &&\r\n !Conf['Remember Last Read Post'] &&\r\n !Conf['Desktop Notifications'] &&\r\n !Conf['Quote Threading']\r\n )) { return; }\r\n\r\n if (Conf['Remember Last Read Post']) {\r\n $.sync('Remember Last Read Post', enabled => Conf['Remember Last Read Post'] = enabled);\r\n this.db = new DataBoard('lastReadPosts', this.sync);\r\n }\r\n\r\n this.hr = $.el('hr', {\r\n id: 'unread-line',\r\n className: 'unread-line'\r\n }\r\n );\r\n this.posts = new Set();\r\n this.postsQuotingYou = new Set();\r\n this.order = new RandomAccessList();\r\n this.position = null;\r\n\r\n Callbacks.Thread.push({\r\n name: 'Unread',\r\n cb: this.node\r\n });\r\n\r\n return Callbacks.Post.push({\r\n name: 'Unread',\r\n cb: this.addPost\r\n });\r\n },\r\n\r\n node() {\r\n Unread.thread = this;\r\n Unread.title = d.title;\r\n Unread.lastReadPost = Unread.db?.get({\r\n boardID: this.board.ID,\r\n threadID: this.ID\r\n }) || 0;\r\n Unread.readCount = 0;\r\n for (var ID of this.posts.keys) { if (+ID <= Unread.lastReadPost) { Unread.readCount++; } }\r\n $.one(d, '4chanXInitFinished', Unread.ready);\r\n $.on(d, 'PostsInserted', Unread.onUpdate);\r\n $.on(d, 'ThreadUpdate', function(e) { if (e.detail[404]) { return Unread.update(); } });\r\n const resetLink = $.el('a', {\r\n href: 'javascript:;',\r\n className: 'unread-reset',\r\n textContent: 'Mark all unread'\r\n }\r\n );\r\n $.on(resetLink, 'click', Unread.reset);\r\n return Header.menu.addEntry({\r\n el: resetLink,\r\n order: 70\r\n });\r\n },\r\n\r\n ready() {\r\n if (Conf['Remember Last Read Post'] && Conf['Scroll to Last Read Post']) { Unread.scroll(); }\r\n Unread.setLine(true);\r\n Unread.read();\r\n Unread.update();\r\n $.on(d, 'scroll visibilitychange', Unread.read);\r\n if (Conf['Unread Line']) { return $.on(d, 'visibilitychange', Unread.setLine); }\r\n },\r\n\r\n positionPrev() {\r\n if (Unread.position) { return Unread.position.prev; } else { return Unread.order.last; }\r\n },\r\n\r\n scroll() {\r\n // Let the header's onload callback handle it.\r\n let hash;\r\n if ((hash = location.hash.match(/\\d+/)) && hash[0] in Unread.thread.posts) { return; }\r\n\r\n let position = Unread.positionPrev();\r\n while (position) {\r\n var {bottom} = position.data.nodes;\r\n if (!bottom.getBoundingClientRect().height) {\r\n // Don't try to scroll to posts with display: none\r\n position = position.prev;\r\n } else {\r\n Header.scrollToIfNeeded(bottom, true);\r\n break;\r\n }\r\n }\r\n },\r\n\r\n reset() {\r\n if (Unread.lastReadPost == null) { return; }\r\n\r\n Unread.posts = new Set();\r\n Unread.postsQuotingYou = new Set();\r\n Unread.order = new RandomAccessList();\r\n Unread.position = null;\r\n Unread.lastReadPost = 0;\r\n Unread.readCount = 0;\r\n Unread.thread.posts.forEach(post => Unread.addPost.call(post));\r\n\r\n $.forceSync('Remember Last Read Post');\r\n if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) {\r\n Unread.db.set({\r\n boardID: Unread.thread.board.ID,\r\n threadID: Unread.thread.ID,\r\n val: 0\r\n });\r\n }\r\n\r\n Unread.updatePosition();\r\n Unread.setLine();\r\n return Unread.update();\r\n },\r\n\r\n sync() {\r\n if (Unread.lastReadPost == null) { return; }\r\n const lastReadPost = Unread.db.get({\r\n boardID: Unread.thread.board.ID,\r\n threadID: Unread.thread.ID,\r\n defaultValue: 0\r\n });\r\n if (Unread.lastReadPost >= lastReadPost) { return; }\r\n Unread.lastReadPost = lastReadPost;\r\n\r\n const postIDs = Unread.thread.posts.keys;\r\n for (let i = Unread.readCount, end = postIDs.length; i < end; i++) {\r\n var ID = +postIDs[i];\r\n if (!Unread.thread.posts.get(ID).isFetchedQuote) {\r\n if (ID > Unread.lastReadPost) { break; }\r\n Unread.posts.delete(ID);\r\n Unread.postsQuotingYou.delete(ID);\r\n }\r\n Unread.readCount++;\r\n }\r\n\r\n Unread.updatePosition();\r\n Unread.setLine();\r\n return Unread.update();\r\n },\r\n\r\n addPost() {\r\n if (this.isFetchedQuote || this.isClone) { return; }\r\n Unread.order.push(this);\r\n if ((this.ID <= Unread.lastReadPost) || this.isHidden || QuoteYou.isYou(this)) { return; }\r\n Unread.posts.add((Unread.posts.last = this.ID));\r\n Unread.addPostQuotingYou(this);\r\n return Unread.position != null ? Unread.position : (Unread.position = Unread.order[this.ID]);\r\n },\r\n\r\n addPostQuotingYou(post) {\r\n for (var quotelink of post.nodes.quotelinks) {\r\n if (QuoteYou.db?.get(Get.postDataFromLink(quotelink))) {\r\n Unread.postsQuotingYou.add((Unread.postsQuotingYou.last = post.ID));\r\n Unread.openNotification(post);\r\n return;\r\n }\r\n }\r\n },\r\n\r\n openNotification(post, predicate=' replied to you') {\r\n if (!Header.areNotificationsEnabled) { return; }\r\n const notif = new Notification(`${post.info.nameBlock}${predicate}`, {\r\n body: post.commentDisplay(),\r\n icon: Favicon.logo\r\n }\r\n );\r\n notif.onclick = function() {\r\n Header.scrollToIfNeeded(post.nodes.bottom, true);\r\n return window.focus();\r\n };\r\n return notif.onshow = () => setTimeout(() => notif.close()\r\n , 7 * SECOND);\r\n },\r\n\r\n onUpdate() {\r\n return $.queueTask(function() { // ThreadUpdater may scroll immediately after inserting posts\r\n Unread.setLine();\r\n Unread.read();\r\n return Unread.update();\r\n });\r\n },\r\n\r\n readSinglePost(post) {\r\n const {ID} = post;\r\n if (!Unread.posts.has(ID)) { return; }\r\n Unread.posts.delete(ID);\r\n Unread.postsQuotingYou.delete(ID);\r\n Unread.updatePosition();\r\n Unread.saveLastReadPost();\r\n return Unread.update();\r\n },\r\n\r\n read: debounce(100, function(e) {\r\n // Update the lastReadPost when hidden posts are added to the thread.\r\n if (!Unread.posts.size && (Unread.readCount !== Unread.thread.posts.keys.length)) {\r\n Unread.saveLastReadPost();\r\n }\r\n\r\n if (d.hidden || !Unread.posts.size) { return; }\r\n\r\n let count = 0;\r\n while (Unread.position) {\r\n var {ID, data} = Unread.position;\r\n var {bottom} = data.nodes;\r\n if (!!bottom.getBoundingClientRect().height && // post has been hidden\r\n (Header.getBottomOf(bottom) <= -1)) { break; } // post is completely read\r\n count++;\r\n Unread.posts.delete(ID);\r\n Unread.postsQuotingYou.delete(ID);\r\n Unread.position = Unread.position.next;\r\n }\r\n\r\n if (!count) { return; }\r\n Unread.updatePosition();\r\n Unread.saveLastReadPost();\r\n if (e) { return Unread.update(); }\r\n }),\r\n\r\n updatePosition() {\r\n while (Unread.position && !Unread.posts.has(Unread.position.ID)) {\r\n Unread.position = Unread.position.next;\r\n }\r\n },\r\n\r\n saveLastReadPost: debounce(2 * SECOND, function() {\r\n let ID;\r\n $.forceSync('Remember Last Read Post');\r\n if (!Conf['Remember Last Read Post'] || !Unread.db) { return; }\r\n const postIDs = Unread.thread.posts.keys;\r\n for (let i = Unread.readCount, end = postIDs.length; i < end; i++) {\r\n ID = +postIDs[i];\r\n if (!Unread.thread.posts.get(ID).isFetchedQuote) {\r\n if (Unread.posts.has(ID)) { break; }\r\n Unread.lastReadPost = ID;\r\n }\r\n Unread.readCount++;\r\n }\r\n if (Unread.thread.isDead && !Unread.thread.isArchived) { return; }\r\n return Unread.db.set({\r\n boardID: Unread.thread.board.ID,\r\n threadID: Unread.thread.ID,\r\n val: Unread.lastReadPost\r\n });\r\n }),\r\n\r\n setLine(force) {\r\n if (!Conf['Unread Line']) { return; }\r\n if (Unread.hr.hidden || d.hidden || (force === true)) {\r\n const oldPosition = Unread.linePosition;\r\n if (Unread.linePosition = Unread.positionPrev()) {\r\n if (Unread.linePosition !== oldPosition) {\r\n let node = Unread.linePosition.data.nodes.bottom;\r\n if (node.nextSibling?.tagName === 'BR') { node = node.nextSibling; }\r\n $.after(node, Unread.hr);\r\n }\r\n } else {\r\n $.rm(Unread.hr);\r\n }\r\n }\r\n return Unread.hr.hidden = Unread.linePosition === Unread.order.last;\r\n },\r\n\r\n update() {\r\n const count = Unread.posts.size;\r\n const countQuotingYou = Unread.postsQuotingYou.size;\r\n\r\n if (Conf['Unread Count']) {\r\n const titleQuotingYou = Conf['Quoted Title'] && countQuotingYou ? '(!) ' : '';\r\n const titleCount = count || !Conf['Hide Unread Count at (0)'] ? `(${count}) ` : '';\r\n const titleDead = Unread.thread.isDead ?\r\n Unread.title.replace('-', (Unread.thread.isArchived ? '- Archived -' : '- 404 -'))\r\n :\r\n Unread.title;\r\n d.title = `${titleQuotingYou}${titleCount}${titleDead}`;\r\n }\r\n\r\n Unread.saveThreadWatcherCount();\r\n\r\n if (Conf['Unread Favicon'] && (g.SITE.software === 'yotsuba')) {\r\n const {isDead} = Unread.thread;\r\n return Favicon.set((\r\n countQuotingYou ?\r\n (isDead ? 'unreadDeadY' : 'unreadY')\r\n : count ?\r\n (isDead ? 'unreadDead' : 'unread')\r\n :\r\n (isDead ? 'dead' : 'default')\r\n )\r\n );\r\n }\r\n },\r\n\r\n saveThreadWatcherCount: debounce(2 * SECOND, function() {\r\n $.forceSync('Remember Last Read Post');\r\n if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) {\r\n let posts;\r\n const quotingYou = !Conf['Require OP Quote Link'] && QuoteYou.isYou(Unread.thread.OP) ? Unread.posts : Unread.postsQuotingYou;\r\n if (!quotingYou.size) {\r\n quotingYou.last = 0;\r\n } else if (!quotingYou.has(quotingYou.last)) {\r\n quotingYou.last = 0;\r\n posts = Unread.thread.posts.keys;\r\n for (let i = posts.length - 1; i >= 0; i--) {\r\n if (quotingYou.has(+posts[i])) {\r\n quotingYou.last = posts[i];\r\n break;\r\n }\r\n }\r\n }\r\n return ThreadWatcher.update(g.SITE.ID, Unread.thread.board.ID, Unread.thread.ID, {\r\n last: Unread.thread.lastPost,\r\n isDead: Unread.thread.isDead,\r\n isArchived: Unread.thread.isArchived,\r\n unread: Unread.posts.size,\r\n quotingYou: (quotingYou.last || 0)\r\n }\r\n );\r\n }\r\n })\r\n};\r\nexport default Unread;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport Post from \"../classes/Post\";\r\nimport Get from \"../General/Get\";\r\nimport Index from \"../General/Index\";\r\nimport { g, Conf, d } from \"../globals/globals\";\r\nimport Main from \"../main/Main\";\r\nimport $ from \"../platform/$\";\r\nimport $$ from \"../platform/$$\";\r\nimport { dict } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar ExpandThread = {\r\n statuses: dict(),\r\n init() {\r\n if (!((g.VIEW === 'index') && Conf['Thread Expansion'])) { return; }\r\n if (Conf['JSON Index']) {\r\n return $.on(d, 'IndexRefreshInternal', this.onIndexRefresh);\r\n } else {\r\n return Callbacks.Thread.push({\r\n name: 'Expand Thread',\r\n cb() { return ExpandThread.setButton(this); }\r\n });\r\n }\r\n },\r\n\r\n setButton(thread) {\r\n let a;\r\n if (!(thread.nodes.root && (a = $('.summary', thread.nodes.root)))) { return; }\r\n a.textContent = g.SITE.Build.summaryText('+', ...a.textContent.match(/\\d+/g));\r\n a.style.cursor = 'pointer';\r\n return $.on(a, 'click', ExpandThread.cbToggle);\r\n },\r\n \r\n disconnect(refresh) {\r\n if ((g.VIEW === 'thread') || !Conf['Thread Expansion']) { return; }\r\n for (var threadID in ExpandThread.statuses) {\r\n var oldReq;\r\n var status = ExpandThread.statuses[threadID];\r\n if (oldReq = status.req) {\r\n delete status.req;\r\n oldReq.abort();\r\n }\r\n delete ExpandThread.statuses[threadID];\r\n }\r\n\r\n if (!refresh) { return $.off(d, 'IndexRefreshInternal', this.onIndexRefresh); }\r\n },\r\n\r\n onIndexRefresh() {\r\n ExpandThread.disconnect(true);\r\n return g.BOARD.threads.forEach(thread => ExpandThread.setButton(thread));\r\n },\r\n\r\n cbToggle(e) {\r\n if ($.modifiedClick(e)) { return; }\r\n e.preventDefault();\r\n return ExpandThread.toggle(Get.threadFromNode(this));\r\n },\r\n\r\n cbToggleBottom(e) {\r\n if ($.modifiedClick(e)) { return; }\r\n e.preventDefault();\r\n const thread = Get.threadFromNode(this);\r\n $.rm(this); // remove before fixing bottom of thread position\r\n const {bottom} = thread.nodes.root.getBoundingClientRect();\r\n ExpandThread.toggle(thread);\r\n return window.scrollBy(0, (thread.nodes.root.getBoundingClientRect().bottom - bottom));\r\n },\r\n\r\n toggle(thread) {\r\n let a;\r\n if (!(thread.nodes.root && (a = $('.summary', thread.nodes.root)))) { return; }\r\n if (thread.ID in ExpandThread.statuses) {\r\n return ExpandThread.contract(thread, a, thread.nodes.root);\r\n } else {\r\n return ExpandThread.expand(thread, a);\r\n }\r\n },\r\n\r\n expand(thread, a) {\r\n let status;\r\n ExpandThread.statuses[thread] = (status = {});\r\n a.textContent = g.SITE.Build.summaryText('...', ...a.textContent.match(/\\d+/g));\r\n status.req = $.cache(g.SITE.urls.threadJSON({boardID: thread.board.ID, threadID: thread.ID}), function() {\r\n if (this !== status.req) { return; } // aborted\r\n delete status.req;\r\n return ExpandThread.parse(this, thread, a);\r\n });\r\n return status.numReplies = $$(g.SITE.selectors.replyOriginal, thread.nodes.root).length;\r\n },\r\n\r\n contract(thread, a, threadRoot) {\r\n let oldReq;\r\n const status = ExpandThread.statuses[thread];\r\n delete ExpandThread.statuses[thread];\r\n if (oldReq = status.req) {\r\n delete status.req;\r\n oldReq.abort();\r\n if (a) { a.textContent = g.SITE.Build.summaryText('+', ...a.textContent.match(/\\d+/g)); }\r\n return;\r\n }\r\n\r\n let replies = $$('.thread > .replyContainer', threadRoot);\r\n if (status.numReplies) { replies = replies.slice(0, (-status.numReplies)); }\r\n let postsCount = 0;\r\n let filesCount = 0;\r\n for (var reply of replies) {\r\n // rm clones\r\n if (Conf['Quote Inlining']) { var inlined;\r\n while ((inlined = $('.inlined', reply))) { inlined.click(); } }\r\n postsCount++;\r\n if ('file' in Get.postFromRoot(reply)) { filesCount++; }\r\n $.rm(reply);\r\n }\r\n if (Index.enabled) { // otherwise handled by Main.addPosts\r\n $.event('PostsRemoved', null, a.parentNode);\r\n }\r\n a.textContent = g.SITE.Build.summaryText('+', postsCount, filesCount);\r\n return $.rm($('.summary-bottom', threadRoot));\r\n },\r\n\r\n parse(req, thread, a) {\r\n let root;\r\n if (![200, 304].includes(req.status)) {\r\n a.textContent = req.status ? `Error ${req.statusText} (${req.status})` : 'Connection Error';\r\n return;\r\n }\r\n\r\n g.SITE.Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler;\r\n\r\n const posts = [];\r\n const postsRoot = [];\r\n let filesCount = 0;\r\n for (var postData of req.response.posts) {\r\n var post;\r\n if (postData.no === thread.ID) { continue; }\r\n if ((post = thread.posts.get(postData.no)) && !post.isFetchedQuote) {\r\n if ('file' in post) { filesCount++; }\r\n ({root} = post.nodes);\r\n postsRoot.push(root);\r\n continue;\r\n }\r\n root = g.SITE.Build.postFromObject(postData, thread.board.ID);\r\n post = new Post(root, thread, thread.board);\r\n if ('file' in post) { filesCount++; }\r\n posts.push(post);\r\n postsRoot.push(root);\r\n }\r\n Main.callbackNodes('Post', posts);\r\n $.after(a, postsRoot);\r\n $.event('PostsInserted', null, a.parentNode);\r\n\r\n const postsCount = postsRoot.length;\r\n a.textContent = g.SITE.Build.summaryText('-', postsCount, filesCount);\r\n\r\n if (root) {\r\n const a2 = a.cloneNode(true);\r\n a2.classList.add('summary-bottom');\r\n $.on(a2, 'click', ExpandThread.cbToggleBottom);\r\n return $.after(root, a2);\r\n }\r\n }\r\n};\r\nexport default ExpandThread;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport DataBoard from \"../classes/DataBoard\";\r\nimport Get from \"../General/Get\";\r\nimport Header from \"../General/Header\";\r\nimport Index from \"../General/Index\";\r\nimport { g, Conf, d } from \"../globals/globals\";\r\nimport ExpandThread from \"../Miscellaneous/ExpandThread\";\r\nimport $ from \"../platform/$\";\r\nimport { dict } from \"../platform/helpers\";\r\nimport QuoteYou from \"../Quotelinks/QuoteYou\";\r\nimport ThreadWatcher from \"./ThreadWatcher\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS205: Consider reworking code to avoid use of IIFEs\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar UnreadIndex = {\r\n lastReadPost: dict(),\r\n hr: dict(),\r\n markReadLink: dict(),\r\n\r\n init() {\r\n if ((g.VIEW !== 'index') || !Conf['Remember Last Read Post'] || !Conf['Unread Line in Index']) { return; }\r\n\r\n this.enabled = true;\r\n this.db = new DataBoard('lastReadPosts', this.sync);\r\n\r\n Callbacks.Thread.push({\r\n name: 'Unread Line in Index',\r\n cb: this.node\r\n });\r\n\r\n $.on(d, 'IndexRefreshInternal', this.onIndexRefresh);\r\n return $.on(d, 'PostsInserted PostsRemoved', this.onPostsInserted);\r\n },\r\n\r\n node() {\r\n UnreadIndex.lastReadPost[this.fullID] = UnreadIndex.db.get({\r\n boardID: this.board.ID,\r\n threadID: this.ID\r\n }) || 0;\r\n if (!Index.enabled) { // let onIndexRefresh handle JSON Index\r\n return UnreadIndex.update(this);\r\n }\r\n },\r\n\r\n onIndexRefresh(e) {\r\n if (e.detail.isCatalog) { return; }\r\n return (() => {\r\n const result = [];\r\n for (var threadID of e.detail.threadIDs) {\r\n var thread = g.threads.get(threadID);\r\n result.push(UnreadIndex.update(thread));\r\n }\r\n return result;\r\n })();\r\n },\r\n\r\n onPostsInserted(e) {\r\n if (e.target === Index.root) { return; } // onIndexRefresh handles this case\r\n const thread = Get.threadFromNode(e.target);\r\n if (!thread || (thread.nodes.root !== e.target)) { return; }\r\n const wasVisible = !!UnreadIndex.hr[thread.fullID]?.parentNode;\r\n UnreadIndex.update(thread);\r\n if (Conf['Scroll to Last Read Post'] && (e.type === 'PostsInserted') && !wasVisible && !!UnreadIndex.hr[thread.fullID]?.parentNode) {\r\n return Header.scrollToIfNeeded(UnreadIndex.hr[thread.fullID], true);\r\n }\r\n },\r\n\r\n sync() {\r\n return g.threads.forEach(function(thread) {\r\n const lastReadPost = UnreadIndex.db.get({\r\n boardID: thread.board.ID,\r\n threadID: thread.ID\r\n }) || 0;\r\n if (lastReadPost !== UnreadIndex.lastReadPost[thread.fullID]) {\r\n UnreadIndex.lastReadPost[thread.fullID] = lastReadPost;\r\n if (thread.nodes.root?.parentNode) {\r\n return UnreadIndex.update(thread);\r\n }\r\n }\r\n });\r\n },\r\n\r\n update(thread) {\r\n let divider;\r\n const lastReadPost = UnreadIndex.lastReadPost[thread.fullID];\r\n let repliesShown = 0;\r\n let repliesRead = 0;\r\n let firstUnread = null;\r\n thread.posts.forEach(function(post) {\r\n if (post.isReply && thread.nodes.root.contains(post.nodes.root)) {\r\n repliesShown++;\r\n if (post.ID <= lastReadPost) {\r\n return repliesRead++;\r\n } else if ((!firstUnread || (post.ID < firstUnread.ID)) && !post.isHidden && !QuoteYou.isYou(post)) {\r\n return firstUnread = post;\r\n }\r\n }\r\n });\r\n\r\n let hr = UnreadIndex.hr[thread.fullID];\r\n if (firstUnread && (repliesRead || ((lastReadPost === thread.OP.ID) && (!$(g.SITE.selectors.summary, thread.nodes.root) || thread.ID in ExpandThread.statuses)))) {\r\n if (!hr) {\r\n hr = (UnreadIndex.hr[thread.fullID] = $.el('hr',\r\n {className: 'unread-line'}));\r\n }\r\n $.before(firstUnread.nodes.root, hr);\r\n } else {\r\n $.rm(hr);\r\n }\r\n\r\n const hasUnread = repliesShown ?\r\n firstUnread || !repliesRead\r\n : Index.enabled ?\r\n thread.lastPost > lastReadPost\r\n :\r\n thread.OP.ID > lastReadPost;\r\n thread.nodes.root.classList.toggle('unread-thread', hasUnread);\r\n\r\n let link = UnreadIndex.markReadLink[thread.fullID];\r\n if (!link) {\r\n link = (UnreadIndex.markReadLink[thread.fullID] = $.el('a', {\r\n className: 'unread-mark-read brackets-wrap',\r\n href: 'javascript:;',\r\n textContent: 'Mark Read'\r\n }\r\n ));\r\n $.on(link, 'click', UnreadIndex.markRead);\r\n }\r\n if (divider = $(g.SITE.selectors.threadDivider, thread.nodes.root)) { // divider inside thread as in Tinyboard\r\n return $.before(divider, link);\r\n } else {\r\n return $.add(thread.nodes.root, link);\r\n }\r\n },\r\n\r\n markRead() {\r\n const thread = Get.threadFromNode(this);\r\n UnreadIndex.lastReadPost[thread.fullID] = thread.lastPost;\r\n UnreadIndex.db.set({\r\n boardID: thread.board.ID,\r\n threadID: thread.ID,\r\n val: thread.lastPost\r\n });\r\n $.rm(UnreadIndex.hr[thread.fullID]);\r\n thread.nodes.root.classList.remove('unread-thread');\r\n return ThreadWatcher.update(g.SITE.ID, thread.board.ID, thread.ID, {\r\n last: thread.lastPost,\r\n unread: 0,\r\n quotingYou: 0\r\n }\r\n );\r\n }\r\n};\r\nexport default UnreadIndex;\r\n","import ThreadWatcherPage from './ThreadWatcher/ThreadWatcher.html';\r\nimport $ from \"../platform/$\";\r\nimport Board from '../classes/Board';\r\nimport Callbacks from '../classes/Callbacks';\r\nimport DataBoard from '../classes/DataBoard';\r\nimport Thread from '../classes/Thread';\r\nimport Filter from '../Filtering/Filter';\r\nimport Main from '../main/Main';\r\nimport $$ from '../platform/$$';\r\nimport Config from '../config/Config';\r\nimport CrossOrigin from '../platform/CrossOrigin';\r\nimport PostRedirect from '../Posting/PostRedirect';\r\nimport QuoteYou from '../Quotelinks/QuoteYou';\r\nimport Unread from './Unread';\r\nimport UnreadIndex from './UnreadIndex';\r\nimport Header from '../General/Header';\r\nimport Index from '../General/Index';\r\nimport { Conf, d, doc, g } from '../globals/globals';\r\nimport Menu from '../Menu/Menu';\r\nimport UI from '../General/UI';\r\nimport Get from '../General/Get';\r\nimport { dict, HOUR, MINUTE } from '../platform/helpers';\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS104: Avoid inline assignments\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\n\r\nvar ThreadWatcher = {\r\n init() {\r\n let sc;\r\n if (!(this.enabled = Conf['Thread Watcher'])) { return; }\r\n\r\n this.shortcut = (sc = $.el('a', {\r\n id: 'watcher-link',\r\n textContent: '👁︎',\r\n title: 'Thread Watcher',\r\n href: 'javascript:;',\r\n }\r\n ));\r\n\r\n this.db = new DataBoard('watchedThreads', this.refresh, true);\r\n this.dbLM = new DataBoard('watcherLastModified', null, true);\r\n this.dialog = UI.dialog('thread-watcher', { innerHTML: ThreadWatcherPage });\r\n this.status = $('#watcher-status', this.dialog);\r\n this.list = this.dialog.lastElementChild;\r\n this.refreshButton = $('.refresh', this.dialog);\r\n this.closeButton = $('.move > .close', this.dialog);\r\n this.unreaddb = Unread.db || UnreadIndex.db || new DataBoard('lastReadPosts');\r\n this.unreadEnabled = Conf['Remember Last Read Post'];\r\n\r\n $.on(d, 'QRPostSuccessful', this.cb.post);\r\n $.on(sc, 'click', this.toggleWatcher);\r\n $.on(this.refreshButton, 'click', this.buttonFetchAll);\r\n $.on(this.closeButton, 'click', this.toggleWatcher);\r\n\r\n this.menu.addHeaderMenuEntry();\r\n $.onExists(doc, 'body', this.addDialog);\r\n\r\n switch (g.VIEW) {\r\n case 'index':\r\n $.on(d, 'IndexUpdate', this.cb.onIndexUpdate);\r\n break;\r\n case 'thread':\r\n $.on(d, 'ThreadUpdate', this.cb.onThreadRefresh);\r\n break;\r\n }\r\n\r\n if (Conf['Fixed Thread Watcher']) {\r\n $.addClass(doc, 'fixed-watcher');\r\n }\r\n if (!Conf['Persistent Thread Watcher']) {\r\n $.addClass(ThreadWatcher.shortcut, 'disabled');\r\n this.dialog.hidden = true;\r\n }\r\n\r\n Header.addShortcut('watcher', sc, 510);\r\n\r\n ThreadWatcher.initLastModified();\r\n ThreadWatcher.fetchAuto();\r\n $.on(window, 'visibilitychange focus', () => $.queueTask(ThreadWatcher.fetchAuto));\r\n\r\n if (Conf['Menu'] && Index.enabled) {\r\n Menu.menu.addEntry({\r\n el: $.el('a', {\r\n href: 'javascript:;',\r\n className: 'has-shortcut-text'\r\n }\r\n , {innerHTML: 'Alt+click'}),\r\n order: 6,\r\n open({thread}) {\r\n if (Conf['Index Mode'] !== 'catalog') { return false; }\r\n this.el.firstElementChild.textContent = ThreadWatcher.isWatched(thread) ?\r\n 'Unwatch'\r\n :\r\n 'Watch';\r\n if (this.cb) { $.off(this.el, 'click', this.cb); }\r\n this.cb = function() {\r\n $.event('CloseMenu');\r\n return ThreadWatcher.toggle(thread);\r\n };\r\n $.on(this.el, 'click', this.cb);\r\n return true;\r\n }\r\n });\r\n }\r\n\r\n if (!['index', 'thread'].includes(g.VIEW)) { return; }\r\n\r\n Callbacks.Post.push({\r\n name: 'Thread Watcher',\r\n cb: this.node\r\n });\r\n return Callbacks.CatalogThread.push({\r\n name: 'Thread Watcher',\r\n cb: this.catalogNode\r\n });\r\n },\r\n\r\n isWatched(thread) {\r\n return !!ThreadWatcher.db?.get({boardID: thread.board.ID, threadID: thread.ID});\r\n },\r\n\r\n isWatchedRaw(boardID, threadID) {\r\n return !!ThreadWatcher.db?.get({boardID, threadID});\r\n },\r\n\r\n setToggler(toggler, isWatched) {\r\n toggler.classList.toggle('watched', isWatched);\r\n return toggler.title = `${isWatched ? 'Unwatch' : 'Watch'} Thread`;\r\n },\r\n\r\n node() {\r\n let toggler;\r\n if (this.isReply) { return; }\r\n if (this.isClone) {\r\n toggler = $('.watch-thread-link', this.nodes.info);\r\n } else {\r\n toggler = $.el('a', {\r\n href: 'javascript:;',\r\n className: 'watch-thread-link'\r\n }\r\n );\r\n $.before($('input', this.nodes.info), toggler);\r\n }\r\n const siteID = g.SITE.ID;\r\n const boardID = this.board.ID;\r\n const threadID = this.thread.ID;\r\n const data = ThreadWatcher.db.get({siteID, boardID, threadID});\r\n ThreadWatcher.setToggler(toggler, !!data);\r\n $.on(toggler, 'click', ThreadWatcher.cb.toggle);\r\n // Add missing excerpt for threads added by Auto Watch\r\n if (data && (data.excerpt == null)) {\r\n return $.queueTask(() => {\r\n return ThreadWatcher.update(siteID, boardID, threadID, {excerpt: Get.threadExcerpt(this.thread)});\r\n });\r\n }\r\n },\r\n\r\n catalogNode() {\r\n if (ThreadWatcher.isWatched(this.thread)) { $.addClass(this.nodes.root, 'watched'); }\r\n return $.on(this.nodes.root, 'mousedown click', e => {\r\n if ((e.button !== 0) || !e.altKey) { return; }\r\n if (e.type === 'click') { ThreadWatcher.toggle(this.thread); }\r\n return e.preventDefault();\r\n });\r\n }, // Also on mousedown to prevent highlighting thumbnail in Firefox.\r\n\r\n addDialog() {\r\n if (!Main.isThisPageLegit()) { return; }\r\n ThreadWatcher.build();\r\n return $.prepend(d.body, ThreadWatcher.dialog);\r\n },\r\n\r\n toggleWatcher() {\r\n $.toggleClass(ThreadWatcher.shortcut, 'disabled');\r\n return ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden;\r\n },\r\n\r\n cb: {\r\n openAll() {\r\n if ($.hasClass(this, 'disabled')) { return; }\r\n for (var a of $$('a.watcher-link', ThreadWatcher.list)) {\r\n $.open(a.href);\r\n }\r\n return $.event('CloseMenu');\r\n },\r\n openUnread() {\r\n if ($.hasClass(this, 'disabled')) { return; }\r\n for (var a of $$('.replies-unread > a.watcher-link', ThreadWatcher.list)) {\r\n $.open(a.href);\r\n }\r\n return $.event('CloseMenu');\r\n },\r\n openDeads() {\r\n if ($.hasClass(this, 'disabled')) { return; }\r\n for (var a of $$('.dead-thread > a.watcher-link', ThreadWatcher.list)) {\r\n $.open(a.href);\r\n }\r\n return $.event('CloseMenu');\r\n },\r\n pruneDeads() {\r\n if ($.hasClass(this, 'disabled')) { return; }\r\n for (var {siteID, boardID, threadID, data} of ThreadWatcher.getAll()) {\r\n if (data.isDead) {\r\n ThreadWatcher.db.delete({siteID, boardID, threadID});\r\n }\r\n }\r\n ThreadWatcher.refresh();\r\n return $.event('CloseMenu');\r\n },\r\n dismiss() {\r\n for (var {siteID, boardID, threadID, data} of ThreadWatcher.getAll()) {\r\n if (data.quotingYou) {\r\n ThreadWatcher.update(siteID, boardID, threadID, {dismiss: data.quotingYou || 0});\r\n }\r\n }\r\n return $.event('CloseMenu');\r\n },\r\n toggle() {\r\n const {thread} = Get.postFromNode(this);\r\n return ThreadWatcher.toggle(thread);\r\n },\r\n rm() {\r\n const {siteID} = this.parentNode.dataset;\r\n const [boardID, threadID] = this.parentNode.dataset.fullID.split('.');\r\n return ThreadWatcher.rm(siteID, boardID, +threadID);\r\n },\r\n post(e) {\r\n const {boardID, threadID, postID} = e.detail;\r\n const cb = PostRedirect.delay();\r\n if (postID === threadID) {\r\n if (Conf['Auto Watch']) {\r\n return ThreadWatcher.addRaw(boardID, threadID, {}, cb);\r\n }\r\n } else if (Conf['Auto Watch Reply']) {\r\n return ThreadWatcher.add((g.threads.get(boardID + '.' + threadID) || new Thread(threadID, g.boards[boardID] || new Board(boardID))), cb);\r\n }\r\n },\r\n onIndexUpdate(e) {\r\n const {db} = ThreadWatcher;\r\n const siteID = g.SITE.ID;\r\n const boardID = g.BOARD.ID;\r\n let nKilled = 0;\r\n for (var threadID in db.data[siteID].boards[boardID]) {\r\n // Don't prune threads that have yet to appear in index.\r\n var data = db.data[siteID].boards[boardID][threadID];\r\n if (!data?.isDead && !e.detail.threads.includes(`${boardID}.${threadID}`)) {\r\n if (!e.detail.threads.some(fullID => +fullID.split('.')[1] > threadID)) { continue; }\r\n if (Conf['Auto Prune'] || !(data && (typeof data === 'object'))) { // corrupt data\r\n db.delete({boardID, threadID});\r\n nKilled++;\r\n } else {\r\n ThreadWatcher.fetchStatus({siteID, boardID, threadID, data});\r\n }\r\n }\r\n }\r\n if (nKilled) { return ThreadWatcher.refresh(); }\r\n },\r\n onThreadRefresh(e) {\r\n const thread = g.threads.get(e.detail.threadID);\r\n if (!e.detail[404] || !ThreadWatcher.isWatched(thread)) { return; }\r\n // Update dead status.\r\n return ThreadWatcher.add(thread);\r\n }\r\n },\r\n\r\n requests: [],\r\n fetched: 0,\r\n\r\n fetch(url, {siteID, force}, args, cb) {\r\n if (ThreadWatcher.requests.length === 0) {\r\n ThreadWatcher.status.textContent = '...';\r\n $.addClass(ThreadWatcher.refreshButton, 'spin');\r\n }\r\n const onloadend = function() {\r\n if (this.finished) { return; }\r\n this.finished = true;\r\n ThreadWatcher.fetched++;\r\n if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {\r\n ThreadWatcher.clearRequests();\r\n } else {\r\n ThreadWatcher.status.textContent = `${Math.round((ThreadWatcher.fetched / ThreadWatcher.requests.length) * 100)}%`;\r\n }\r\n return cb.apply(this, args);\r\n };\r\n const ajax = siteID === g.SITE.ID ? $.ajax : CrossOrigin.ajax;\r\n if (force) {\r\n delete $.lastModified.ThreadWatcher?.[url];\r\n }\r\n const req = $.whenModified(\r\n url,\r\n 'ThreadWatcher',\r\n onloadend,\r\n { timeout: MINUTE, ajax }\r\n );\r\n return ThreadWatcher.requests.push(req);\r\n },\r\n\r\n clearRequests() {\r\n ThreadWatcher.requests = [];\r\n ThreadWatcher.fetched = 0;\r\n ThreadWatcher.status.textContent = '';\r\n return $.rmClass(ThreadWatcher.refreshButton, 'spin');\r\n },\r\n\r\n abort() {\r\n delete ThreadWatcher.syncing;\r\n for (var req of ThreadWatcher.requests) {\r\n if (!req.finished) {\r\n req.finished = true;\r\n req.abort();\r\n }\r\n }\r\n return ThreadWatcher.clearRequests();\r\n },\r\n\r\n initLastModified() {\r\n const lm = ($.lastModified['ThreadWatcher'] || ($.lastModified['ThreadWatcher'] = dict()));\r\n for (var siteID in ThreadWatcher.dbLM.data) {\r\n var boards = ThreadWatcher.dbLM.data[siteID];\r\n for (var boardID in boards.boards) {\r\n var data = boards.boards[boardID];\r\n if (ThreadWatcher.db.get({siteID, boardID})) {\r\n for (var url in data) {\r\n var date = data[url];\r\n lm[url] = date;\r\n }\r\n } else {\r\n ThreadWatcher.dbLM.delete({siteID, boardID});\r\n }\r\n }\r\n }\r\n },\r\n\r\n fetchAuto() {\r\n let middle;\r\n clearTimeout(ThreadWatcher.timeout);\r\n if (!Conf['Auto Update Thread Watcher']) { return; }\r\n const {db} = ThreadWatcher;\r\n const interval = Conf['Show Page'] || (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) ? 5 * MINUTE : 2 * HOUR;\r\n const now = Date.now();\r\n if ((now - interval >= ((middle = db.data.lastChecked || 0)) || middle > now) && !d.hidden && !!d.hasFocus()) {\r\n ThreadWatcher.fetchAllStatus(interval);\r\n }\r\n return ThreadWatcher.timeout = setTimeout(ThreadWatcher.fetchAuto, interval);\r\n },\r\n\r\n buttonFetchAll() {\r\n if (ThreadWatcher.syncing || ThreadWatcher.requests.length) {\r\n return ThreadWatcher.abort();\r\n } else {\r\n return ThreadWatcher.fetchAllStatus();\r\n }\r\n },\r\n\r\n fetchAllStatus(interval=0) {\r\n ThreadWatcher.status.textContent = '...';\r\n $.addClass(ThreadWatcher.refreshButton, 'spin');\r\n ThreadWatcher.syncing = true;\r\n const dbs = [ThreadWatcher.db, ThreadWatcher.unreaddb, QuoteYou.db].filter(x => x);\r\n let n = 0;\r\n return dbs.map((dbi) =>\r\n dbi.forceSync(function() {\r\n if ((++n) === dbs.length) {\r\n let middle;\r\n if (!ThreadWatcher.syncing) { return; } // aborted\r\n delete ThreadWatcher.syncing;\r\n if (0 > (middle = Date.now() - (ThreadWatcher.db.data.lastChecked || 0)) || middle >= interval) { // not checked in another tab\r\n // XXX On vichan boards, last_modified field of threads.json does not account for sage posts.\r\n // Occasionally check replies field of catalog.json to find these posts.\r\n let middle1;\r\n const {db} = ThreadWatcher;\r\n const now = Date.now();\r\n const deep = !(now - (2 * HOUR) < ((middle1 = db.data.lastChecked2 || 0)) && middle1 <= now);\r\n const boards = ThreadWatcher.getAll(true);\r\n for (var board of boards) {\r\n ThreadWatcher.fetchBoard(board, deep);\r\n }\r\n db.setLastChecked();\r\n if (deep) { db.setLastChecked('lastChecked2'); }\r\n }\r\n if (ThreadWatcher.fetched === ThreadWatcher.requests.length) {\r\n return ThreadWatcher.clearRequests();\r\n }\r\n }\r\n }));\r\n },\r\n\r\n fetchBoard(board, deep) {\r\n if (!board.some(thread => !thread.data.isDead)) { return; }\r\n let force = false;\r\n for (var thread of board) {\r\n var {data} = thread;\r\n if (!data.isDead && (data.last !== -1)) {\r\n if (Conf['Show Page'] && (data.page == null)) { force = true; }\r\n if ((data.modified == null)) { force = (thread.force = true); }\r\n }\r\n }\r\n const {siteID, boardID} = board[0];\r\n const site = g.sites[siteID];\r\n if (!site) { return; }\r\n const urlF = deep && site.threadModTimeIgnoresSage ? 'catalogJSON' : 'threadsListJSON';\r\n const url = site.urls[urlF]?.({siteID, boardID});\r\n if (!url) { return; }\r\n return ThreadWatcher.fetch(url, {siteID, force}, [board, url], ThreadWatcher.parseBoard);\r\n },\r\n\r\n parseBoard(board, url) {\r\n let page, thread;\r\n if (this.status !== 200) { return; }\r\n const {siteID, boardID} = board[0];\r\n const lmDate = this.getResponseHeader('Last-Modified');\r\n ThreadWatcher.dbLM.extend({siteID, boardID, val: $.item(url, lmDate)});\r\n const threads = dict();\r\n let pageLength = 0;\r\n let nThreads = 0;\r\n let oldest = null;\r\n try {\r\n pageLength = this.response[0]?.threads.length || 0;\r\n for (let i = 0; i < this.response.length; i++) {\r\n page = this.response[i];\r\n for (var item of page.threads) {\r\n threads[item.no] = {\r\n page: i + 1,\r\n index: nThreads,\r\n modified: item.last_modified,\r\n replies: item.replies\r\n };\r\n nThreads++;\r\n if ((oldest == null) || (item.no < oldest)) {\r\n oldest = item.no;\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n for (thread of board) {\r\n ThreadWatcher.fetchStatus(thread);\r\n }\r\n }\r\n for (thread of board) {\r\n var {threadID, data} = thread;\r\n if (threads[threadID]) {\r\n var index, modified, replies;\r\n ({page, index, modified, replies} = threads[threadID]);\r\n if (Conf['Show Page']) {\r\n var lastPage = g.sites[siteID].isPrunedByAge?.({siteID, boardID}) ?\r\n threadID === oldest\r\n :\r\n index >= (nThreads - pageLength);\r\n ThreadWatcher.update(siteID, boardID, threadID, {page, lastPage});\r\n }\r\n if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {\r\n if ((modified !== data.modified) || ((replies != null) && (replies !== data.replies))) {\r\n (thread.newData || (thread.newData = {})).modified = modified;\r\n ThreadWatcher.fetchStatus(thread);\r\n }\r\n }\r\n } else {\r\n ThreadWatcher.fetchStatus(thread);\r\n }\r\n }\r\n },\r\n\r\n fetchStatus(thread) {\r\n const {siteID, boardID, threadID, data, force} = thread;\r\n const url = g.sites[siteID]?.urls.threadJSON?.({siteID, boardID, threadID});\r\n if (!url) { return; }\r\n if (data.isDead && !force) { return; }\r\n if (data.last === -1) { return; } // 404 or no JSON API\r\n return ThreadWatcher.fetch(url, {siteID, force}, [thread], ThreadWatcher.parseStatus);\r\n },\r\n\r\n parseStatus(thread, isArchiveURL) {\r\n let isDead, last;\r\n let {siteID, boardID, threadID, data, newData, force} = thread;\r\n const site = g.sites[siteID];\r\n if ((this.status === 200) && this.response) {\r\n let isArchived;\r\n last = this.response.posts[this.response.posts.length-1].no;\r\n const replies = this.response.posts.length-1;\r\n isDead = (isArchived = !!(this.response.posts[0].archived || isArchiveURL));\r\n if (isDead && Conf['Auto Prune']) {\r\n ThreadWatcher.rm(siteID, boardID, threadID);\r\n return;\r\n }\r\n\r\n if ((last === data.last) && (isDead === data.isDead) && (isArchived === data.isArchived)) { return; }\r\n\r\n const lastReadPost = ThreadWatcher.unreaddb.get({siteID, boardID, threadID, defaultValue: 0});\r\n let unread = data.unread || 0;\r\n let quotingYou = data.quotingYou || 0;\r\n const youOP = !!QuoteYou.db?.get({siteID, boardID, threadID, postID: threadID});\r\n\r\n for (var postObj of this.response.posts) {\r\n if ((postObj.no <= (data.last || 0)) || (postObj.no <= lastReadPost)) { continue; }\r\n if (QuoteYou.db?.get({siteID, boardID, threadID, postID: postObj.no})) { continue; }\r\n\r\n var quotesYou = false;\r\n if (!Conf['Require OP Quote Link'] && youOP) {\r\n quotesYou = true;\r\n } else if (QuoteYou.db && postObj.com) {\r\n var match;\r\n var regexp = site.regexp.quotelinkHTML;\r\n regexp.lastIndex = 0;\r\n while (match = regexp.exec(postObj.com)) {\r\n if (QuoteYou.db.get({\r\n siteID,\r\n boardID: match[1] ? encodeURIComponent(match[1]) : boardID,\r\n threadID: match[2] || threadID,\r\n postID: match[3] || match[2] || threadID\r\n })) {\r\n quotesYou = true;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n if (!unread || (!quotingYou && quotesYou)) {\r\n if (Filter.isHidden(site.Build.parseJSON(postObj, {siteID, boardID}))) { continue; }\r\n }\r\n\r\n unread++;\r\n if (quotesYou) { quotingYou = postObj.no; }\r\n }\r\n\r\n if (!newData) { newData = {}; }\r\n $.extend(newData, {last, replies, isDead, isArchived, unread, quotingYou});\r\n return ThreadWatcher.update(siteID, boardID, threadID, newData);\r\n\r\n } else if (this.status === 404) {\r\n const archiveURL = g.sites[siteID]?.urls.archivedThreadJSON?.({siteID, boardID, threadID});\r\n if (!isArchiveURL && archiveURL) {\r\n return ThreadWatcher.fetch(archiveURL, {siteID, force}, [thread, true], ThreadWatcher.parseStatus);\r\n } else if (site.mayLackJSON && (data.last == null)) {\r\n return ThreadWatcher.update(siteID, boardID, threadID, {last: -1});\r\n } else {\r\n return ThreadWatcher.update(siteID, boardID, threadID, {isDead: true});\r\n }\r\n }\r\n },\r\n\r\n getAll(groupByBoard) {\r\n const all = [];\r\n for (var siteID in ThreadWatcher.db.data) {\r\n var boards = ThreadWatcher.db.data[siteID];\r\n for (var boardID in boards.boards) {\r\n var cont;\r\n var threads = boards.boards[boardID];\r\n if (Conf['Current Board'] && ((siteID !== g.SITE.ID) || (boardID !== g.BOARD.ID))) {\r\n continue;\r\n }\r\n if (groupByBoard) {\r\n all.push((cont = []));\r\n }\r\n for (var threadID in threads) {\r\n var data = threads[threadID];\r\n if (data && (typeof data === 'object')) {\r\n (groupByBoard ? cont : all).push({siteID, boardID, threadID, data});\r\n }\r\n }\r\n }\r\n }\r\n return all;\r\n },\r\n\r\n makeLine(siteID, boardID, threadID, data) {\r\n let page;\r\n const x = $.el('a', {\r\n textContent: '✕',\r\n href: 'javascript:;'\r\n }\r\n );\r\n $.on(x, 'click', ThreadWatcher.cb.rm);\r\n\r\n let {excerpt, isArchived} = data;\r\n if (!excerpt) { excerpt = `/${boardID}/ - No.${threadID}`; }\r\n if (Conf['Show Site Prefix']) { excerpt = ThreadWatcher.prefixes[siteID] + excerpt; }\r\n\r\n const link = $.el('a', {\r\n href: g.sites[siteID]?.urls.thread({siteID, boardID, threadID}, isArchived) || '',\r\n title: excerpt,\r\n className: 'watcher-link'\r\n }\r\n );\r\n\r\n if (Conf['Show Page'] && (data.page != null)) {\r\n page = $.el('span', {\r\n textContent: `[${data.page}]`,\r\n className: 'watcher-page'\r\n }\r\n );\r\n $.add(link, page);\r\n }\r\n\r\n if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count'] && (data.unread != null)) {\r\n const count = $.el('span', {\r\n textContent: `(${data.unread})`,\r\n className: 'watcher-unread'\r\n }\r\n );\r\n $.add(link, count);\r\n }\r\n\r\n const title = $.el('span', {\r\n textContent: excerpt,\r\n className: 'watcher-title'\r\n }\r\n );\r\n $.add(link, title);\r\n\r\n const div = $.el('div');\r\n const fullID = `${boardID}.${threadID}`;\r\n div.dataset.fullID = fullID;\r\n div.dataset.siteID = siteID;\r\n if ((g.VIEW === 'thread') && (fullID === `${g.BOARD}.${g.THREADID}`)) { $.addClass(div, 'current'); }\r\n if (data.isDead) { $.addClass(div, 'dead-thread'); }\r\n if (Conf['Show Page']) {\r\n if (data.lastPage) { $.addClass(div, 'last-page'); }\r\n if (data.page != null) { div.dataset.page = data.page; }\r\n }\r\n if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {\r\n if (data.unread === 0) { $.addClass(div, 'replies-read'); }\r\n if (data.unread) { $.addClass(div, 'replies-unread'); }\r\n if ((data.quotingYou || 0) > (data.dismiss || 0)) { $.addClass(div, 'replies-quoting-you'); }\r\n }\r\n $.add(div, [x, $.tn(' '), link]);\r\n return div;\r\n },\r\n\r\n setPrefixes(threads) {\r\n const prefixes = dict();\r\n for (var {siteID} of threads) {\r\n if (siteID in prefixes) { continue; }\r\n var len = 0;\r\n var prefix = '';\r\n var conflicts = Object.keys(prefixes);\r\n while (conflicts.length > 0) {\r\n len++;\r\n prefix = siteID.slice(0, len);\r\n var conflicts2 = [];\r\n for (var siteID2 of conflicts) {\r\n if (siteID2.slice(0, len) === prefix) {\r\n conflicts2.push(siteID2);\r\n } else if (prefixes[siteID2].length < len) {\r\n prefixes[siteID2] = siteID2.slice(0, len);\r\n }\r\n }\r\n conflicts = conflicts2;\r\n }\r\n prefixes[siteID] = prefix;\r\n }\r\n return ThreadWatcher.prefixes = prefixes;\r\n },\r\n\r\n build() {\r\n const nodes = [];\r\n const threads = ThreadWatcher.getAll();\r\n ThreadWatcher.setPrefixes(threads);\r\n for (var {siteID, boardID, threadID, data} of threads) {\r\n // Add missing excerpt for threads added by Auto Watch\r\n var thread;\r\n if ((data.excerpt == null) && (siteID === g.SITE.ID) && (thread = g.threads.get(`${boardID}.${threadID}`)) && thread.OP) {\r\n ThreadWatcher.db.extend({boardID, threadID, val: {excerpt: Get.threadExcerpt(thread)}});\r\n }\r\n nodes.push(ThreadWatcher.makeLine(siteID, boardID, threadID, data));\r\n }\r\n const {list} = ThreadWatcher;\r\n $.rmAll(list);\r\n $.add(list, nodes);\r\n\r\n return ThreadWatcher.refreshIcon();\r\n },\r\n\r\n refresh() {\r\n ThreadWatcher.build();\r\n\r\n g.threads.forEach(function(thread) {\r\n const isWatched = ThreadWatcher.isWatched(thread);\r\n if (thread.OP) {\r\n for (var post of [thread.OP, ...thread.OP.clones]) {\r\n var toggler;\r\n if (toggler = $('.watch-thread-link', post.nodes.info)) {\r\n ThreadWatcher.setToggler(toggler, isWatched);\r\n }\r\n }\r\n }\r\n if (thread.catalogView) { return thread.catalogView.nodes.root.classList.toggle('watched', isWatched); }\r\n });\r\n\r\n if (Conf['Pin Watched Threads']) {\r\n return $.event('SortIndex', {deferred: Conf['Index Mode'] !== 'catalog'});\r\n }\r\n },\r\n\r\n refreshIcon() {\r\n for (var className of ['replies-unread', 'replies-quoting-you']) {\r\n ThreadWatcher.shortcut.classList.toggle(className, !!$(`.${className}`, ThreadWatcher.dialog));\r\n }\r\n },\r\n\r\n update(siteID, boardID, threadID, newData) {\r\n let data, key, line, val;\r\n if (!(data = ThreadWatcher.db?.get({siteID, boardID, threadID}))) { return; }\r\n if (newData.isDead && Conf['Auto Prune']) {\r\n ThreadWatcher.rm(siteID, boardID, threadID);\r\n return;\r\n }\r\n if (newData.isDead || (newData.last === -1)) {\r\n for (key of ['isArchived', 'page', 'lastPage', 'unread', 'quotingyou']) {\r\n if (!(key in newData)) {\r\n newData[key] = undefined;\r\n }\r\n }\r\n }\r\n if ((newData.last != null) && (newData.last < data.last)) {\r\n newData.modified = undefined;\r\n }\r\n let n = 0;\r\n for (key in newData) { val = newData[key]; if (data[key] !== val) { n++; } }\r\n if (!n) { return; }\r\n ThreadWatcher.db.extend({siteID, boardID, threadID, val: newData});\r\n if (line = $(`#watched-threads > [data-site-i-d='${siteID}'][data-full-i-d='${boardID}.${threadID}']`, ThreadWatcher.dialog)) {\r\n const newLine = ThreadWatcher.makeLine(siteID, boardID, threadID, data);\r\n $.replace(line, newLine);\r\n return ThreadWatcher.refreshIcon();\r\n } else {\r\n return ThreadWatcher.refresh();\r\n }\r\n },\r\n\r\n set404(boardID, threadID, cb) {\r\n let data;\r\n if (!(data = ThreadWatcher.db?.get({boardID, threadID}))) { return cb(); }\r\n if (Conf['Auto Prune']) {\r\n ThreadWatcher.db.delete({boardID, threadID});\r\n return cb();\r\n }\r\n if (data.isDead && !((data.isArchived != null) || (data.page != null) || (data.lastPage != null) || (data.unread != null) || (data.quotingYou != null))) { return cb(); }\r\n return ThreadWatcher.db.extend({boardID, threadID, val: {isDead: true, isArchived: undefined, page: undefined, lastPage: undefined, unread: undefined, quotingYou: undefined}}, cb);\r\n },\r\n\r\n toggle(thread) {\r\n const siteID = g.SITE.ID;\r\n const boardID = thread.board.ID;\r\n const threadID = thread.ID;\r\n if (ThreadWatcher.db.get({boardID, threadID})) {\r\n return ThreadWatcher.rm(siteID, boardID, threadID);\r\n } else {\r\n return ThreadWatcher.add(thread);\r\n }\r\n },\r\n\r\n add(thread, cb) {\r\n const data = {};\r\n const siteID = g.SITE.ID;\r\n const boardID = thread.board.ID;\r\n const threadID = thread.ID;\r\n if (thread.isDead) {\r\n if (Conf['Auto Prune'] && ThreadWatcher.db.get({boardID, threadID})) {\r\n ThreadWatcher.rm(siteID, boardID, threadID, cb);\r\n return;\r\n }\r\n data.isDead = true;\r\n }\r\n if (thread.OP) { data.excerpt = Get.threadExcerpt(thread); }\r\n return ThreadWatcher.addRaw(boardID, threadID, data, cb);\r\n },\r\n\r\n addRaw(boardID, threadID, data, cb) {\r\n const oldData = ThreadWatcher.db.get({ boardID, threadID, defaultValue: dict() });\r\n delete oldData.last;\r\n delete oldData.modified;\r\n $.extend(oldData, data);\r\n ThreadWatcher.db.set({boardID, threadID, val: oldData}, cb);\r\n ThreadWatcher.refresh();\r\n const thread = {siteID: g.SITE.ID, boardID, threadID, data, force: true};\r\n if (Conf['Show Page'] && !data.isDead) {\r\n return ThreadWatcher.fetchBoard([thread]);\r\n } else if (ThreadWatcher.unreadEnabled && Conf['Show Unread Count']) {\r\n return ThreadWatcher.fetchStatus(thread);\r\n }\r\n },\r\n\r\n rm(siteID, boardID, threadID, cb) {\r\n ThreadWatcher.db.delete({siteID, boardID, threadID}, cb);\r\n return ThreadWatcher.refresh();\r\n },\r\n\r\n menu: {\r\n init() {\r\n if (!Conf['Thread Watcher']) { return; }\r\n const menu = (this.menu = new UI.Menu('thread watcher'));\r\n $.on($('.menu-button', ThreadWatcher.dialog), 'click', function(e) {\r\n return menu.toggle(e, this, ThreadWatcher);\r\n });\r\n return this.addMenuEntries();\r\n },\r\n\r\n addHeaderMenuEntry() {\r\n if (g.VIEW !== 'thread') { return; }\r\n const entryEl = $.el('a',\r\n {href: 'javascript:;'});\r\n Header.menu.addEntry({\r\n el: entryEl,\r\n order: 60,\r\n open() {\r\n const [addClass, rmClass, text] = !!ThreadWatcher.db.get({boardID: g.BOARD.ID, threadID: g.THREADID}) ?\r\n ['unwatch-thread', 'watch-thread', 'Unwatch thread']\r\n :\r\n ['watch-thread', 'unwatch-thread', 'Watch thread'];\r\n $.addClass(entryEl, addClass);\r\n $.rmClass(entryEl, rmClass);\r\n entryEl.textContent = text;\r\n return true;\r\n }\r\n });\r\n return $.on(entryEl, 'click', () => ThreadWatcher.toggle(g.threads.get(`${g.BOARD}.${g.THREADID}`)));\r\n },\r\n\r\n addMenuEntries() {\r\n const entries = [];\r\n\r\n // `Open all` entry\r\n entries.push({\r\n text: 'Open all threads',\r\n cb: ThreadWatcher.cb.openAll,\r\n open() {\r\n this.el.classList.toggle('disabled', !ThreadWatcher.list.firstElementChild);\r\n return true;\r\n }\r\n });\r\n\r\n // `Open Unread` entry\r\n entries.push({\r\n text: 'Open unread threads',\r\n cb: ThreadWatcher.cb.openUnread,\r\n open() {\r\n this.el.classList.toggle('disabled', !$('.replies-unread', ThreadWatcher.list));\r\n return true;\r\n }\r\n });\r\n\r\n // `Open dead threads` entry\r\n entries.push({\r\n text: 'Open dead threads',\r\n cb: ThreadWatcher.cb.openDeads,\r\n open() {\r\n this.el.classList.toggle('disabled', !$('.dead-thread', ThreadWatcher.list));\r\n return true;\r\n }\r\n });\r\n\r\n // `Prune dead threads` entry\r\n entries.push({\r\n text: 'Prune dead threads',\r\n cb: ThreadWatcher.cb.pruneDeads,\r\n open() {\r\n this.el.classList.toggle('disabled', !$('.dead-thread', ThreadWatcher.list));\r\n return true;\r\n }\r\n });\r\n\r\n // `Dismiss posts quoting you` entry\r\n entries.push({\r\n text: 'Dismiss posts quoting you',\r\n title: 'Unhighlight the thread watcher icon and threads until there are new replies quoting you.',\r\n cb: ThreadWatcher.cb.dismiss,\r\n open() {\r\n this.el.classList.toggle('disabled', !$.hasClass(ThreadWatcher.shortcut, 'replies-quoting-you'));\r\n return true;\r\n }\r\n });\r\n\r\n for (var {text, title, cb, open} of entries) {\r\n var entry = {\r\n el: $.el('a', {\r\n textContent: text,\r\n href: 'javascript:;'\r\n }\r\n )\r\n };\r\n if (title) { entry.el.title = title; }\r\n $.on(entry.el, 'click', cb);\r\n entry.open = open.bind(entry);\r\n this.menu.addEntry(entry);\r\n }\r\n\r\n // Settings checkbox entries:\r\n for (var name in Config.threadWatcher) {\r\n var conf = Config.threadWatcher[name];\r\n this.addCheckbox(name, conf[1]);\r\n }\r\n\r\n },\r\n\r\n addCheckbox(name, desc) {\r\n const entry = {\r\n type: 'thread watcher',\r\n el: UI.checkbox(name, name.replace(' Thread Watcher', ''))\r\n };\r\n entry.el.title = desc;\r\n const input = entry.el.firstElementChild;\r\n if ((name === 'Show Unread Count') && !ThreadWatcher.unreadEnabled) {\r\n input.disabled = true;\r\n $.addClass(entry.el, 'disabled');\r\n entry.el.title += '\\n[Remember Last Read Post is disabled.]';\r\n }\r\n $.on(input, 'change', $.cb.checked);\r\n if (['Current Board', 'Show Page', 'Show Unread Count', 'Show Site Prefix'].includes(name)) { $.on(input, 'change', ThreadWatcher.refresh); }\r\n if (['Show Page', 'Show Unread Count', 'Auto Update Thread Watcher'].includes(name)) { $.on(input, 'change', ThreadWatcher.fetchAuto); }\r\n return this.menu.addEntry(entry);\r\n }\r\n }\r\n};\r\nexport default ThreadWatcher;\r\n","import Redirect from \"../Archive/Redirect\";\r\nimport Board from \"./Board\";\r\nimport Post from \"./Post\";\r\nimport Thread from \"./Thread\";\r\nimport $ from \"../platform/$\";\r\nimport Main from \"../main/Main\";\r\nimport Index from \"../General/Index\";\r\nimport { E, g, Conf, d } from \"../globals/globals\";\r\nimport ImageHost from \"../Images/ImageHost\";\r\nimport CrossOrigin from \"../platform/CrossOrigin\";\r\nimport Get from \"../General/Get\";\r\nimport { dict } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS205: Consider reworking code to avoid use of IIFEs\r\n * DS206: Consider reworking classes to avoid initClass\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nexport default class Fetcher {\r\n static initClass() {\r\n \r\n this.prototype.archiveTags = {\r\n '\\n': {innerHTML: \"
    \"},\r\n '[b]': {innerHTML: \"\"},\r\n '[/b]': {innerHTML: \"\"},\r\n '[spoiler]': {innerHTML: \"\"},\r\n '[/spoiler]': {innerHTML: \"\"},\r\n '[code]': {innerHTML: \"
    \"},\r\n      '[/code]':    {innerHTML: \"
    \"},\r\n '[moot]': {innerHTML: \"
    \"},\r\n '[/moot]': {innerHTML: \"
    \"},\r\n '[banned]': {innerHTML: \"\"},\r\n '[/banned]': {innerHTML: \"\"},\r\n '[fortune]'(text) { return {innerHTML: \"\"}; },\r\n '[/fortune]': {innerHTML: \"\"},\r\n '[i]': {innerHTML: \"\"},\r\n '[/i]': {innerHTML: \"\"},\r\n '[red]': {innerHTML: \"\"},\r\n '[/red]': {innerHTML: \"\"},\r\n '[green]': {innerHTML: \"\"},\r\n '[/green]': {innerHTML: \"\"},\r\n '[blue]': {innerHTML: \"\"},\r\n '[/blue]': {innerHTML: \"\"}\r\n };\r\n }\r\n constructor(boardID, threadID, postID, root, quoter) {\r\n let post, thread;\r\n this.boardID = boardID;\r\n this.threadID = threadID;\r\n this.postID = postID;\r\n this.root = root;\r\n this.quoter = quoter;\r\n if (post = g.posts.get(`${this.boardID}.${this.postID}`)) {\r\n this.insert(post);\r\n return;\r\n }\r\n\r\n // 4chan X catalog data\r\n if ((post = Index.replyData?.[`${this.boardID}.${this.postID}`]) && (thread = g.threads.get(`${this.boardID}.${this.threadID}`))) {\r\n const board = g.boards[this.boardID];\r\n post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true});\r\n Main.callbackNodes('Post', [post]);\r\n this.insert(post);\r\n return;\r\n }\r\n\r\n this.root.textContent = `Loading post No.${this.postID}...`;\r\n if (this.threadID) {\r\n const that = this;\r\n $.cache(g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID}), function({isCached}) {\r\n return that.fetchedPost(this, isCached);\r\n });\r\n } else {\r\n this.archivedPost();\r\n }\r\n }\r\n\r\n insert(post) {\r\n // Stop here if the container has been removed while loading.\r\n if (!this.root.parentNode) { return; }\r\n if (!this.quoter) { this.quoter = post; }\r\n const clone = post.addClone(this.quoter.context, ($.hasClass(this.root, 'dialog')));\r\n Main.callbackNodes('Post', [clone]);\r\n\r\n // Get rid of the side arrows/stubs.\r\n const {nodes} = clone;\r\n $.rmAll(nodes.root);\r\n $.add(nodes.root, nodes.post);\r\n\r\n // Indicate links to the containing post.\r\n const quotes = [...clone.nodes.quotelinks, ...clone.nodes.backlinks];\r\n for (var quote of quotes) {\r\n var {boardID, postID} = Get.postDataFromLink(quote);\r\n if ((postID === this.quoter.ID) && (boardID === this.quoter.board.ID)) {\r\n $.addClass(quote, 'forwardlink');\r\n }\r\n }\r\n\r\n // Set up flag CSS for cross-board links to boards with flags\r\n if (clone.nodes.flag && !(Fetcher.flagCSS || (Fetcher.flagCSS = $('link[href^=\"//s.4cdn.org/css/flags.\"]')))) {\r\n const cssVersion = $('link[href^=\"//s.4cdn.org/css/\"]')?.href.match(/\\d+(?=\\.css$)|$/)[0] || Date.now();\r\n Fetcher.flagCSS = $.el('link', {\r\n rel: 'stylesheet',\r\n href: `//s.4cdn.org/css/flags.${cssVersion}.css`\r\n }\r\n );\r\n $.add(d.head, Fetcher.flagCSS);\r\n }\r\n\r\n $.rmAll(this.root);\r\n $.add(this.root, nodes.root);\r\n return $.event('PostsInserted', null, this.root);\r\n }\r\n\r\n fetchedPost(req, isCached) {\r\n // In case of multiple callbacks for the same request,\r\n // don't parse the same original post more than once.\r\n let post;\r\n if (post = g.posts.get(`${this.boardID}.${this.postID}`)) {\r\n this.insert(post);\r\n return;\r\n }\r\n\r\n const {status} = req;\r\n if (status !== 200) {\r\n // The thread can die by the time we check a quote.\r\n if (status && this.archivedPost()) { return; }\r\n\r\n $.addClass(this.root, 'warning');\r\n this.root.textContent =\r\n status === 404 ?\r\n `Thread No.${this.threadID} 404'd.`\r\n : !status ?\r\n 'Connection Error'\r\n :\r\n `Error ${req.statusText} (${req.status}).`;\r\n return;\r\n }\r\n\r\n const {posts} = req.response;\r\n g.SITE.Build.spoilerRange[this.boardID] = posts[0].custom_spoiler;\r\n for (post of posts) {\r\n if (post.no === this.postID) { break; }\r\n } // we found it!\r\n\r\n if (post.no !== this.postID) {\r\n // Cached requests can be stale and must be rechecked.\r\n if (isCached) {\r\n const api = g.SITE.urls.threadJSON({boardID: this.boardID, threadID: this.threadID});\r\n $.cleanCache(url => url === api);\r\n const that = this;\r\n $.cache(api, function() {\r\n return that.fetchedPost(this, false);\r\n });\r\n return;\r\n }\r\n\r\n // The post can be deleted by the time we check a quote.\r\n if (this.archivedPost()) { return; }\r\n\r\n $.addClass(this.root, 'warning');\r\n this.root.textContent = `Post No.${this.postID} was not found.`;\r\n return;\r\n }\r\n\r\n const board = g.boards[this.boardID] ||\r\n new Board(this.boardID);\r\n const thread = g.threads.get(`${this.boardID}.${this.threadID}`) ||\r\n new Thread(this.threadID, board);\r\n post = new Post(g.SITE.Build.postFromObject(post, this.boardID), thread, board, {isFetchedQuote: true});\r\n Main.callbackNodes('Post', [post]);\r\n return this.insert(post);\r\n }\r\n\r\n archivedPost() {\r\n let url;\r\n if (!Conf['Resurrect Quotes']) { return false; }\r\n if (!(url = Redirect.to('post', {boardID: this.boardID, postID: this.postID}))) { return false; }\r\n const archive = Redirect.data.post[this.boardID];\r\n const encryptionOK = /^https:\\/\\//.test(url) || (location.protocol === 'http:');\r\n if (encryptionOK || Conf['Exempt Archives from Encryption']) {\r\n const that = this;\r\n CrossOrigin.cache(url, function() {\r\n if (!encryptionOK && this.response?.media) {\r\n const {media} = this.response;\r\n for (var key in media) {\r\n // Image/thumbnail URLs loaded over HTTP can be modified in transit.\r\n // Require them to be from an HTTP host so that no referrer is sent to them from an HTTPS page.\r\n if (/_link$/.test(key)) {\r\n if (!$.getOwn(media, key)?.match(/^http:\\/\\//)) { delete media[key]; }\r\n }\r\n }\r\n }\r\n return that.parseArchivedPost(this.response, url, archive);\r\n });\r\n return true;\r\n }\r\n return false;\r\n }\r\n\r\n parseArchivedPost(data, url, archive) {\r\n // In case of multiple callbacks for the same request,\r\n // don't parse the same original post more than once.\r\n let post;\r\n if (post = g.posts.get(`${this.boardID}.${this.postID}`)) {\r\n this.insert(post);\r\n return;\r\n }\r\n\r\n if (data == null) {\r\n $.addClass(this.root, 'warning');\r\n this.root.textContent = `Error fetching Post No.${this.postID} from ${archive.name}.`;\r\n return;\r\n }\r\n\r\n if (data.error) {\r\n $.addClass(this.root, 'warning');\r\n this.root.textContent = data.error;\r\n return;\r\n }\r\n\r\n // https://github.com/eksopl/asagi/blob/v0.4.0b74/src/main/java/net/easymodo/asagi/YotsubaAbstract.java#L82-L129\r\n // https://github.com/FoolCode/FoolFuuka/blob/800bd090835489e7e24371186db6e336f04b85c0/src/Model/Comment.php#L368-L428\r\n // https://github.com/bstats/b-stats/blob/6abe7bffaf6e5f523498d760e54b110df5331fbb/inc/classes/Yotsuba.php#L157-L168\r\n let comment = (data.comment || '').split(/(\\n|\\[\\/?(?:b|spoiler|code|moot|banned|fortune(?: color=\"#\\w+\")?|i|red|green|blue)\\])/);\r\n comment = (() => {\r\n const result = [];\r\n for (let i = 0; i < comment.length; i++) {\r\n var text = comment[i];\r\n if ((i % 2) === 1) {\r\n var tag = Fetcher.archiveTags[text.replace(/\\ .*\\]/, ']')];\r\n if (typeof tag === 'function') { result.push(tag(text)); } else { result.push(tag); }\r\n } else {\r\n var greentext = text[0] === '>';\r\n text = text.replace(/(\\[\\/?[a-z]+):lit(\\])/g, '$1$2');\r\n text = text.split(/(>>(?:>\\/[a-z\\d]+\\/)?\\d+)/g).map((text2, j) =>\r\n {innerHTML: ((j % 2) ? \"\" + E(text2) + \"\" : E(text2));});\r\n text = {innerHTML: ((greentext) ? \"\" + E.cat(text) + \"\" : E.cat(text))};\r\n result.push(text);\r\n }\r\n }\r\n return result;\r\n })();\r\n comment = {innerHTML: E.cat(comment)};\r\n\r\n this.threadID = +data.thread_num;\r\n const o = {\r\n ID: this.postID,\r\n threadID: this.threadID,\r\n boardID: this.boardID,\r\n isReply: this.postID !== this.threadID\r\n };\r\n o.info = {\r\n subject: data.title,\r\n email: data.email,\r\n name: data.name || '',\r\n tripcode: data.trip,\r\n capcode: (() => { switch (data.capcode) {\r\n // https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/assets/themes/foolz/foolfuuka-theme-fuuka/src/Partial/Board.php#L77\r\n case 'M': return 'Mod';\r\n case 'A': return 'Admin';\r\n case 'D': return 'Developer';\r\n case 'V': return 'Verified';\r\n case 'F': return 'Founder';\r\n case 'G': return 'Manager';\r\n } })(),\r\n uniqueID: data.poster_hash,\r\n flagCode: data.poster_country,\r\n flagCodeTroll: data.troll_country_code,\r\n flag: data.poster_country_name || data.troll_country_name,\r\n dateUTC: data.timestamp,\r\n dateText: data.fourchan_date,\r\n commentHTML: comment\r\n };\r\n if (o.info.capcode) { delete o.info.uniqueID; }\r\n if (data.media && !!+data.media.banned) {\r\n o.fileDeleted = true;\r\n } else if (data.media?.media_filename) {\r\n let {thumb_link} = data.media;\r\n // Fix URLs missing origin\r\n if (thumb_link?.[0] === '/') { thumb_link = url.split('/', 3).join('/') + thumb_link; }\r\n if (!Redirect.securityCheck(thumb_link)) { thumb_link = ''; }\r\n let media_link = Redirect.to('file', {boardID: this.boardID, filename: data.media.media_orig});\r\n if (!Redirect.securityCheck(media_link)) { media_link = ''; }\r\n o.file = {\r\n name: data.media.media_filename,\r\n url: media_link ||\r\n (this.boardID === 'f' ?\r\n `${location.protocol}//${ImageHost.flashHost()}/${this.boardID}/${encodeURIComponent(E(data.media.media_filename))}`\r\n :\r\n `${location.protocol}//${ImageHost.host()}/${this.boardID}/${data.media.media_orig}`),\r\n height: data.media.media_h,\r\n width: data.media.media_w,\r\n MD5: data.media.media_hash,\r\n size: $.bytesToString(data.media.media_size),\r\n thumbURL: thumb_link || `${location.protocol}//${ImageHost.thumbHost()}/${this.boardID}/${data.media.preview_orig}`,\r\n theight: data.media.preview_h,\r\n twidth: data.media.preview_w,\r\n isSpoiler: data.media.spoiler === '1'\r\n };\r\n if (!/\\.pdf$/.test(o.file.url)) { o.file.dimensions = `${o.file.width}x${o.file.height}`; }\r\n if ((this.boardID === 'f') && data.media.exif) { o.file.tag = JSON.parse(data.media.exif).Tag; }\r\n }\r\n o.extra = dict();\r\n\r\n const board = g.boards[this.boardID] ||\r\n new Board(this.boardID);\r\n const thread = g.threads.get(`${this.boardID}.${this.threadID}`) ||\r\n new Thread(this.threadID, board);\r\n post = new Post(g.SITE.Build.post(o), thread, board, {isFetchedQuote: true});\r\n post.kill();\r\n if (post.file) { post.file.thumbURL = o.file.thumbURL; }\r\n Main.callbackNodes('Post', [post]);\r\n return this.insert(post);\r\n }\r\n}\r\nFetcher.initClass();\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport Fetcher from \"../classes/Fetcher\";\r\nimport Get from \"../General/Get\";\r\nimport Header from \"../General/Header\";\r\nimport UI from \"../General/UI\";\r\nimport { Conf, d, g } from \"../globals/globals\";\r\nimport ExpandComment from \"../Miscellaneous/ExpandComment\";\r\nimport $ from \"../platform/$\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar QuotePreview = {\r\n init() {\r\n if (!Conf['Quote Previewing']) { return; }\r\n\r\n if (g.VIEW === 'archive') {\r\n $.on(d, 'mouseover', function(e) {\r\n if ((e.target.nodeName === 'A') && $.hasClass(e.target, 'quotelink')) {\r\n return QuotePreview.mouseover.call(e.target, e);\r\n }\r\n });\r\n }\r\n\r\n if (!['index', 'thread'].includes(g.VIEW)) { return; }\r\n\r\n if (Conf['Comment Expansion']) {\r\n ExpandComment.callbacks.push(this.node);\r\n }\r\n\r\n return Callbacks.Post.push({\r\n name: 'Quote Previewing',\r\n cb: this.node\r\n });\r\n },\r\n\r\n node() {\r\n for (var link of this.nodes.quotelinks.concat([...this.nodes.backlinks], this.nodes.archivelinks)) {\r\n $.on(link, 'mouseover', QuotePreview.mouseover);\r\n }\r\n },\r\n\r\n mouseover(e) {\r\n let origin;\r\n if (($.hasClass(this, 'inlined') && !$.hasClass(doc, 'catalog-mode')) || !d.contains(this)) { return; }\r\n\r\n const {boardID, threadID, postID} = Get.postDataFromLink(this);\r\n\r\n const qp = $.el('div', {\r\n id: 'qp',\r\n className: 'dialog'\r\n }\r\n );\r\n\r\n $.add(Header.hover, qp);\r\n new Fetcher(boardID, threadID, postID, qp, Get.postFromNode(this));\r\n\r\n UI.hover({\r\n root: this,\r\n el: qp,\r\n latestEvent: e,\r\n endEvents: 'mouseout click',\r\n cb: QuotePreview.mouseout\r\n });\r\n\r\n if (Conf['Quote Highlighting'] && (origin = g.posts.get(`${boardID}.${postID}`))) {\r\n const posts = [origin].concat(origin.clones);\r\n // Remove the clone that's in the qp from the array.\r\n posts.pop();\r\n for (var post of posts) {\r\n $.addClass(post.nodes.post, 'qphl');\r\n }\r\n }\r\n },\r\n\r\n mouseout() {\r\n // Stop if it only contains text.\r\n let root;\r\n if (!(root = this.el.firstElementChild)) { return; }\r\n\r\n $.event('PostsRemoved', null, Header.hover);\r\n\r\n const clone = Get.postFromRoot(root);\r\n let post = clone.origin;\r\n post.rmClone(root.dataset.clone);\r\n\r\n if (!Conf['Quote Highlighting']) { return; }\r\n for (post of [post].concat(post.clones)) {\r\n $.rmClass(post.nodes.post, 'qphl');\r\n }\r\n }\r\n};\r\nexport default QuotePreview;\r\n","/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS205: Consider reworking code to avoid use of IIFEs\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nimport Callbacks from '../classes/Callbacks';\r\nimport CatalogThread from '../classes/CatalogThread';\r\nimport Notice from '../classes/Notice';\r\nimport Post from '../classes/Post';\r\nimport Thread from '../classes/Thread';\r\nimport Config from '../config/Config';\r\nimport Filter from '../Filtering/Filter';\r\nimport PostHiding from '../Filtering/PostHiding';\r\nimport ThreadHiding from '../Filtering/ThreadHiding';\r\nimport Main from '../main/Main';\r\nimport CatalogLinks from '../Miscellaneous/CatalogLinks';\r\nimport RelativeDates from '../Miscellaneous/RelativeDates';\r\nimport ThreadWatcher from '../Monitoring/ThreadWatcher';\r\nimport $$ from '../platform/$$';\r\nimport $ from '../platform/$';\r\nimport QuotePreview from '../Quotelinks/QuotePreview';\r\nimport { c, Conf, d, doc, g } from '../globals/globals';\r\nimport Header from './Header';\r\nimport UI from './UI';\r\nimport Menu from '../Menu/Menu';\r\n\r\nimport NavLinksPage from './Index/NavLinks.html';\r\nimport PageList from './Index/PageList.html';\r\nimport BoardConfig from './BoardConfig';\r\nimport Get from './Get';\r\nimport { dict, SECOND } from '../platform/helpers';\r\n\r\nvar Index = {\r\n showHiddenThreads: false,\r\n changed: {},\r\n\r\n enabledOn({siteID, boardID}) {\r\n return Conf['JSON Index'] && (g.sites[siteID].software === 'yotsuba') && (boardID !== 'f');\r\n },\r\n\r\n init() {\r\n let input, inputs, name;\r\n if (g.VIEW !== 'index') { return; }\r\n\r\n // For IndexRefresh events\r\n $.one(d, '4chanXInitFinished', this.cb.initFinished);\r\n $.on(d, 'PostsInserted', this.cb.postsInserted);\r\n\r\n if (!this.enabledOn(g.BOARD)) { return; }\r\n\r\n this.enabled = true;\r\n\r\n Callbacks.Post.push({\r\n name: 'Index Page Numbers',\r\n cb: this.node\r\n });\r\n Callbacks.CatalogThread.push({\r\n name: 'Catalog Features',\r\n cb: this.catalogNode\r\n });\r\n\r\n this.search = history.state?.searched || '';\r\n if (history.state?.mode) {\r\n Conf['Index Mode'] = history.state?.mode;\r\n }\r\n this.currentSort = history.state?.sort;\r\n if (!this.currentSort) { this.currentSort = typeof Conf['Index Sort'] === 'object' ? (\r\n Conf['Index Sort'][g.BOARD.ID] || 'bump'\r\n ) : (\r\n Conf['Index Sort']\r\n ); }\r\n this.currentPage = this.getCurrentPage();\r\n this.processHash();\r\n\r\n $.addClass(doc, 'index-loading', `${Conf['Index Mode'].replace(/\\ /g, '-')}-mode`);\r\n $.on(window, 'popstate', this.cb.popstate);\r\n $.on(d, 'scroll', this.scroll);\r\n $.on(d, 'SortIndex', this.cb.resort);\r\n\r\n // Header refresh button\r\n this.button = $.el('a', {\r\n title: 'Refresh',\r\n href: 'javascript:;',\r\n textContent: '🗘'\r\n }\r\n );\r\n $.on(this.button, 'click', () => Index.update());\r\n Header.addShortcut('index-refresh', this.button, 590);\r\n\r\n // Header \"Index Navigation\" submenu\r\n const entries = [];\r\n this.inputs = (inputs = dict());\r\n for (name in Config.Index) {\r\n var arr = Config.Index[name];\r\n if (arr instanceof Array) {\r\n var label = UI.checkbox(name, `${name[0]}${name.slice(1).toLowerCase()}`);\r\n label.title = arr[1];\r\n entries.push({el: label});\r\n input = label.firstChild;\r\n $.on(input, 'change', $.cb.checked);\r\n inputs[name] = input;\r\n }\r\n }\r\n $.on(inputs['Show Replies'], 'change', this.cb.replies);\r\n $.on(inputs['Catalog Hover Expand'], 'change', this.cb.hover);\r\n $.on(inputs['Pin Watched Threads'], 'change', this.cb.resort);\r\n $.on(inputs['Anchor Hidden Threads'], 'change', this.cb.resort);\r\n\r\n const watchSettings = function(e) {\r\n if (input = $.getOwn(inputs, e.target.name)) {\r\n input.checked = e.target.checked;\r\n return $.event('change', null, input);\r\n }\r\n };\r\n $.on(d, 'OpenSettings', () => $.on($.id('fourchanx-settings'), 'change', watchSettings));\r\n\r\n const sortEntry = UI.checkbox('Per-Board Sort Type', 'Per-board sort type', (typeof Conf['Index Sort'] === 'object'));\r\n sortEntry.title = 'Set the sorting order of each board independently.';\r\n $.on(sortEntry.firstChild, 'change', this.cb.perBoardSort);\r\n entries.splice(3, 0, {el: sortEntry});\r\n\r\n Header.menu.addEntry({\r\n el: $.el('span',\r\n {textContent: 'Index Navigation'}),\r\n order: 100,\r\n subEntries: entries\r\n });\r\n\r\n // Navigation links at top of index\r\n this.navLinks = $.el('div', {className: 'navLinks json-index'});\r\n $.extend(this.navLinks, {innerHTML: NavLinksPage});\r\n $('.cataloglink a', this.navLinks).href = CatalogLinks.catalog();\r\n if (!BoardConfig.isArchived(g.BOARD.ID)) { $('.archlistlink', this.navLinks).hidden = true; }\r\n $.on($('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront);\r\n\r\n // Search field\r\n this.searchInput = $('#index-search', this.navLinks);\r\n this.setupSearch();\r\n $.on(this.searchInput, 'input', this.onSearchInput);\r\n $.on($('#index-search-clear', this.navLinks), 'click', this.clearSearch);\r\n\r\n // Hidden threads toggle\r\n this.hideLabel = $('#hidden-label', this.navLinks);\r\n $.on($('#hidden-toggle a', this.navLinks), 'click', this.cb.toggleHiddenThreads);\r\n\r\n // Drop-down menus and reverse sort toggle\r\n this.selectRev = $('#index-rev', this.navLinks);\r\n this.selectMode = $('#index-mode', this.navLinks);\r\n this.selectSort = $('#index-sort', this.navLinks);\r\n this.selectSize = $('#index-size', this.navLinks);\r\n $.on(this.selectRev, 'change', this.cb.sort);\r\n $.on(this.selectMode, 'change', this.cb.mode);\r\n $.on(this.selectSort, 'change', this.cb.sort);\r\n $.on(this.selectSize, 'change', $.cb.value);\r\n $.on(this.selectSize, 'change', this.cb.size);\r\n for (var select of [this.selectMode, this.selectSize]) {\r\n select.value = Conf[select.name];\r\n }\r\n this.selectRev.checked = /-rev$/.test(Index.currentSort);\r\n this.selectSort.value = Index.currentSort.replace(/-rev$/, '');\r\n\r\n // Last Long Reply options\r\n this.lastLongOptions = $('#lastlong-options', this.navLinks);\r\n this.lastLongInputs = $$('input', this.lastLongOptions);\r\n this.lastLongThresholds = [0, 0];\r\n this.lastLongOptions.hidden = (this.selectSort.value !== 'lastlong');\r\n for (let i = 0; i < this.lastLongInputs.length; i++) {\r\n input = this.lastLongInputs[i];\r\n $.on(input, 'change', this.cb.lastLongThresholds);\r\n var tRaw = Conf[`Last Long Reply Thresholds ${i}`];\r\n input.value = (this.lastLongThresholds[i] =\r\n typeof tRaw === 'object' ? (tRaw[g.BOARD.ID] ?? 100) : tRaw);\r\n }\r\n\r\n // Thread container\r\n this.root = $.el('div', {className: 'board json-index'});\r\n $.on(this.root, 'click', this.cb.hoverToggle);\r\n this.cb.size();\r\n this.cb.hover();\r\n\r\n // Page list\r\n this.pagelist = $.el('div', {className: 'pagelist json-index'});\r\n $.extend(this.pagelist, {innerHTML: PageList});\r\n $('.cataloglink a', this.pagelist).href = CatalogLinks.catalog();\r\n $.on(this.pagelist, 'click', this.cb.pageNav);\r\n\r\n this.update(true);\r\n\r\n $.onExists(doc, 'title + *', () => d.title = d.title.replace(/\\ -\\ Page\\ \\d+/, ''));\r\n\r\n $.onExists(doc, '.board > .thread > .postContainer, .board + *', function() {\r\n let el;\r\n g.SITE.Build.hat = $('.board > .thread > img:first-child');\r\n if (g.SITE.Build.hat) {\r\n g.BOARD.threads.forEach(function(thread) {\r\n if (thread.nodes.root) {\r\n return $.prepend(thread.nodes.root, g.SITE.Build.hat.cloneNode(false));\r\n }\r\n });\r\n $.addClass(doc, 'hats-enabled');\r\n $.addStyle(`.catalog-thread::after {background-image: url(${g.SITE.Build.hat.src});}`);\r\n }\r\n\r\n const board = $('.board');\r\n $.replace(board, Index.root);\r\n if (Index.loaded) {\r\n $.event('PostsInserted', null, Index.root);\r\n }\r\n // Hacks:\r\n // - When removing an element from the document during page load,\r\n // its ancestors will still be correctly created inside of it.\r\n // - Creating loadable elements inside of an origin-less document\r\n // will not download them.\r\n // - Combine the two and you get a download canceller!\r\n // Does not work on Firefox unfortunately. bugzil.la/939713\r\n try {\r\n d.implementation.createDocument(null, null, null).appendChild(board);\r\n } catch (error) {}\r\n\r\n for (el of $$('.navLinks')) { $.rm(el); }\r\n $.rm($.id('ctrl-top'));\r\n const topNavPos = $.id('delform').previousElementSibling;\r\n $.before(topNavPos, $.el('hr'));\r\n $.before(topNavPos, Index.navLinks);\r\n const timeEl = $('#index-last-refresh time', Index.navLinks);\r\n if (timeEl.dataset.utc) { return RelativeDates.update(timeEl); }\r\n });\r\n\r\n return Main.ready(function() {\r\n let pagelist;\r\n if (pagelist = $('.pagelist')) {\r\n $.replace(pagelist, Index.pagelist);\r\n }\r\n return $.rmClass(doc, 'index-loading');\r\n });\r\n },\r\n\r\n scroll() {\r\n if (Index.req || !Index.liveThreadData || (Conf['Index Mode'] !== 'infinite') || (window.scrollY <= (doc.scrollHeight - (300 + window.innerHeight)))) { return; }\r\n if (Index.pageNum == null) { Index.pageNum = Index.currentPage; } // Avoid having to pushState to keep track of the current page\r\n\r\n const pageNum = ++Index.pageNum;\r\n if (pageNum > Index.pagesNum) { return Index.endNotice(); }\r\n\r\n const threadIDs = Index.threadsOnPage(pageNum);\r\n return Index.buildStructure(threadIDs);\r\n },\r\n\r\n endNotice: (function() {\r\n let notify = false;\r\n const reset = () => notify = false;\r\n return function() {\r\n if (notify) { return; }\r\n notify = true;\r\n new Notice('info', \"Last page reached.\", 2);\r\n return setTimeout(reset, 3 * SECOND);\r\n };\r\n })(),\r\n\r\n menu: {\r\n init() {\r\n if ((g.VIEW !== 'index') || !Conf['Menu'] || !Conf['Thread Hiding Link'] || !Index.enabledOn(g.BOARD)) { return; }\r\n\r\n return Menu.menu.addEntry({\r\n el: $.el('a', {\r\n href: 'javascript:;',\r\n className: 'has-shortcut-text'\r\n }\r\n , {innerHTML: \"Shift+click\"}),\r\n order: 20,\r\n open({thread}) {\r\n if (Conf['Index Mode'] !== 'catalog') { return false; }\r\n this.el.firstElementChild.textContent = thread.isHidden ?\r\n 'Unhide'\r\n :\r\n 'Hide';\r\n if (this.cb) { $.off(this.el, 'click', this.cb); }\r\n this.cb = function() {\r\n $.event('CloseMenu');\r\n return Index.toggleHide(thread);\r\n };\r\n $.on(this.el, 'click', this.cb);\r\n return true;\r\n }\r\n });\r\n }\r\n },\r\n\r\n node() {\r\n if (this.isReply || this.isClone || (Index.threadPosition[this.ID] == null)) { return; }\r\n return this.thread.setPage(Math.floor(Index.threadPosition[this.ID] / Index.threadsNumPerPage) + 1);\r\n },\r\n\r\n catalogNode() {\r\n return $.on(this.nodes.root, 'mousedown click', e => {\r\n if ((e.button !== 0) || !e.shiftKey) { return; }\r\n if (e.type === 'click') { Index.toggleHide(this.thread); }\r\n return e.preventDefault();\r\n });\r\n }, // Also on mousedown to prevent highlighting text.\r\n\r\n toggleHide(thread) {\r\n if (Index.showHiddenThreads) {\r\n ThreadHiding.show(thread);\r\n if (!ThreadHiding.db.get({boardID: thread.board.ID, threadID: thread.ID})) { return; }\r\n // Don't save when un-hiding filtered threads.\r\n } else {\r\n ThreadHiding.hide(thread);\r\n }\r\n return ThreadHiding.saveHiddenState(thread);\r\n },\r\n\r\n cycleSortType() {\r\n let i;\r\n const types = Index.selectSort.options.filter(option => !option.disabled);\r\n for (i = 0; i < types.length; i++) {\r\n var type = types[i];\r\n if (type.selected) { break; }\r\n }\r\n types[(i + 1) % types.length].selected = true;\r\n return $.event('change', null, Index.selectSort);\r\n },\r\n\r\n cb: {\r\n initFinished() {\r\n Index.initFinishedFired = true;\r\n return $.queueTask(() => Index.cb.postsInserted());\r\n },\r\n\r\n postsInserted() {\r\n if (!Index.initFinishedFired) { return; }\r\n let n = 0;\r\n g.posts.forEach(function(post) {\r\n if (!post.isFetchedQuote && !post.indexRefreshSeen && doc.contains(post.nodes.root)) {\r\n post.indexRefreshSeen = true;\r\n return n++;\r\n }\r\n });\r\n if (n) { return $.event('IndexRefresh'); }\r\n },\r\n\r\n toggleHiddenThreads() {\r\n $('#hidden-toggle a', Index.navLinks).textContent = (Index.showHiddenThreads = !Index.showHiddenThreads) ?\r\n 'Hide'\r\n :\r\n 'Show';\r\n Index.sort();\r\n return Index.buildIndex();\r\n },\r\n\r\n mode() {\r\n Index.pushState({mode: this.value});\r\n return Index.pageLoad(false);\r\n },\r\n\r\n sort() {\r\n const value = Index.selectRev.checked ? Index.selectSort.value + \"-rev\" : Index.selectSort.value;\r\n Index.pushState({sort: value});\r\n return Index.pageLoad(false);\r\n },\r\n\r\n resort(e) {\r\n Index.changed.order = true;\r\n if (!e?.detail?.deferred) { return Index.pageLoad(false); }\r\n },\r\n\r\n perBoardSort() {\r\n Conf['Index Sort'] = this.checked ? dict() : '';\r\n Index.saveSort();\r\n for (let i = 0; i < 2; i++) {\r\n Conf[`Last Long Reply Thresholds ${i}`] = this.checked ? dict() : '';\r\n Index.saveLastLongThresholds(i);\r\n }\r\n },\r\n\r\n lastLongThresholds() {\r\n const i = [...this.parentNode.children].indexOf(this);\r\n const value = +this.value;\r\n if (!Number.isFinite(value)) {\r\n this.value = Index.lastLongThresholds[i];\r\n return;\r\n }\r\n Index.lastLongThresholds[i] = value;\r\n Index.saveLastLongThresholds(i);\r\n Index.changed.order = true;\r\n return Index.pageLoad(false);\r\n },\r\n\r\n size(e) {\r\n if (Conf['Index Mode'] !== 'catalog') {\r\n $.rmClass(Index.root, 'catalog-small');\r\n $.rmClass(Index.root, 'catalog-large');\r\n } else if (Conf['Index Size'] === 'small') {\r\n $.addClass(Index.root, 'catalog-small');\r\n $.rmClass(Index.root, 'catalog-large');\r\n } else {\r\n $.addClass(Index.root, 'catalog-large');\r\n $.rmClass(Index.root, 'catalog-small');\r\n }\r\n if (e) { return Index.buildIndex(); }\r\n },\r\n\r\n replies() {\r\n return Index.buildIndex();\r\n },\r\n\r\n hover() {\r\n return doc.classList.toggle('catalog-hover-expand', Conf['Catalog Hover Expand']);\r\n },\r\n\r\n hoverToggle(e) {\r\n if (Conf['Catalog Hover Toggle'] && $.hasClass(doc, 'catalog-mode') && !$.modifiedClick(e) && !$.x('ancestor-or-self::a', e.target)) {\r\n let thread;\r\n const input = Index.inputs['Catalog Hover Expand'];\r\n input.checked = !input.checked;\r\n $.event('change', null, input);\r\n if (thread = Get.threadFromNode(e.target)) {\r\n Index.cb.catalogReplies.call(thread);\r\n return Index.cb.hoverAdjust.call(thread.OP.nodes);\r\n }\r\n }\r\n },\r\n\r\n popstate(e) {\r\n if (e?.state) {\r\n const {searched, mode, sort} = e.state;\r\n const page = Index.getCurrentPage();\r\n Index.setState({search: searched, mode, sort, page});\r\n return Index.pageLoad(false);\r\n } else {\r\n // page load or hash change\r\n const nCommands = Index.processHash();\r\n if (Conf['Refreshed Navigation'] && nCommands) {\r\n return Index.update();\r\n } else {\r\n return Index.pageLoad();\r\n }\r\n }\r\n },\r\n\r\n pageNav(e) {\r\n let a;\r\n if ($.modifiedClick(e)) { return; }\r\n switch (e.target.nodeName) {\r\n case 'BUTTON':\r\n e.target.blur();\r\n a = e.target.parentNode;\r\n break;\r\n case 'A':\r\n a = e.target;\r\n break;\r\n default:\r\n return;\r\n }\r\n if (a.textContent === 'Catalog') { return; }\r\n e.preventDefault();\r\n return Index.userPageNav(+a.pathname.split(/\\/+/)[2] || 1);\r\n },\r\n\r\n refreshFront() {\r\n Index.pushState({page: 1});\r\n return Index.update();\r\n },\r\n\r\n catalogReplies() {\r\n if (Conf['Show Replies'] && $.hasClass(doc, 'catalog-hover-expand') && !this.catalogView.nodes.replies) {\r\n return Index.buildCatalogReplies(this);\r\n }\r\n },\r\n\r\n hoverAdjust() {\r\n // Prevent hovered catalog threads from going offscreen.\r\n let x;\r\n if (!$.hasClass(doc, 'catalog-hover-expand')) { return; }\r\n const rect = this.post.getBoundingClientRect();\r\n if (x = $.minmax(0, -rect.left, doc.clientWidth - rect.right)) {\r\n const {style} = this.post;\r\n style.left = `${x}px`;\r\n style.right = `${-x}px`;\r\n return $.one(this.root, 'mouseleave', () => style.left = (style.right = null));\r\n }\r\n }\r\n },\r\n\r\n scrollToIndex() {\r\n // Scroll to navlinks, or top of board if navlinks are hidden.\r\n return Header.scrollToIfNeeded((Index.navLinks.getBoundingClientRect().height ? Index.navLinks : Index.root));\r\n },\r\n\r\n getCurrentPage() {\r\n return +window.location.pathname.split(/\\/+/)[2] || 1;\r\n },\r\n\r\n userPageNav(page) {\r\n Index.pushState({page});\r\n if (Conf['Refreshed Navigation']) {\r\n return Index.update();\r\n } else {\r\n return Index.pageLoad();\r\n }\r\n },\r\n\r\n hashCommands: {\r\n mode: {\r\n 'paged': 'paged',\r\n 'infinite-scrolling': 'infinite',\r\n 'infinite': 'infinite',\r\n 'all-threads': 'all pages',\r\n 'all-pages': 'all pages',\r\n 'catalog': 'catalog'\r\n },\r\n sort: {\r\n 'bump-order': 'bump',\r\n 'last-reply': 'lastreply',\r\n 'last-long-reply': 'lastlong',\r\n 'creation-date': 'birth',\r\n 'reply-count': 'replycount',\r\n 'file-count': 'filecount',\r\n 'posts-per-minute': 'activity'\r\n }\r\n },\r\n\r\n processHash() {\r\n // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=483304\r\n let hash = location.href.match(/#.*/)?.[0] || '';\r\n const state =\r\n {replace: true};\r\n const commands = hash.slice(1).split('/');\r\n const leftover = [];\r\n for (var command of commands) {\r\n var mode, sort;\r\n if (mode = $.getOwn(Index.hashCommands.mode, command)) {\r\n state.mode = mode;\r\n } else if (command === 'index') {\r\n state.mode = Conf['Previous Index Mode'];\r\n state.page = 1;\r\n } else if (sort = $.getOwn(Index.hashCommands.sort, command.replace(/-rev$/, ''))) {\r\n state.sort = sort;\r\n if (/-rev$/.test(command)) { state.sort += '-rev'; }\r\n } else if (/^s=/.test(command)) {\r\n state.search = decodeURIComponent(command.slice(2)).replace(/\\+/g, ' ').trim();\r\n } else {\r\n leftover.push(command);\r\n }\r\n }\r\n hash = leftover.join('/');\r\n if (hash) { state.hash = `#${hash}`; }\r\n Index.pushState(state);\r\n return commands.length - leftover.length;\r\n },\r\n\r\n pushState(state) {\r\n let {search, hash, replace} = state;\r\n let pageBeforeSearch = history.state?.oldpage;\r\n if ((search != null) && (search !== Index.search)) {\r\n state.page = search ? 1 : (pageBeforeSearch || 1);\r\n if (!search) {\r\n pageBeforeSearch = undefined;\r\n } else if (!Index.search) {\r\n pageBeforeSearch = Index.currentPage;\r\n }\r\n }\r\n Index.setState(state);\r\n const pathname = Index.currentPage === 1 ? `/${g.BOARD}/` : `/${g.BOARD}/${Index.currentPage}`;\r\n if (!hash) { hash = ''; }\r\n return history[replace ? 'replaceState' : 'pushState']({\r\n mode: Conf['Index Mode'],\r\n sort: Index.currentSort,\r\n searched: Index.search,\r\n oldpage: pageBeforeSearch\r\n }\r\n , '', `${location.protocol}//${location.host}${pathname}${hash}`);\r\n },\r\n\r\n setState({search, mode, sort, page, hash}) {\r\n if ((search != null) && (search !== Index.search)) {\r\n Index.changed.search = true;\r\n Index.search = search;\r\n }\r\n if ((mode != null) && (mode !== Conf['Index Mode'])) {\r\n Index.changed.mode = true;\r\n Conf['Index Mode'] = mode;\r\n $.set('Index Mode', mode);\r\n if ((mode !== 'catalog') && (Conf['Previous Index Mode'] !== mode)) {\r\n Conf['Previous Index Mode'] = mode;\r\n $.set('Previous Index Mode', mode);\r\n }\r\n }\r\n if ((sort != null) && (sort !== Index.currentSort)) {\r\n Index.changed.sort = true;\r\n Index.currentSort = sort;\r\n Index.saveSort();\r\n }\r\n if (['all pages', 'catalog'].includes(Conf['Index Mode'])) { page = 1; }\r\n if ((page != null) && (page !== Index.currentPage)) {\r\n Index.changed.page = true;\r\n Index.currentPage = page;\r\n }\r\n if (hash != null) {\r\n return Index.changed.hash = true;\r\n }\r\n },\r\n\r\n savePerBoard(key, value) {\r\n if (typeof Conf[key] === 'object') {\r\n Conf[key][g.BOARD.ID] = value;\r\n } else {\r\n Conf[key] = value;\r\n }\r\n return $.set(key, Conf[key]);\r\n },\r\n\r\n saveSort() {\r\n return Index.savePerBoard('Index Sort', Index.currentSort);\r\n },\r\n\r\n saveLastLongThresholds(i) {\r\n return Index.savePerBoard(`Last Long Reply Thresholds ${i}`, Index.lastLongThresholds[i]);\r\n },\r\n\r\n pageLoad(scroll=true) {\r\n if (!Index.liveThreadData) { return; }\r\n let {threads, order, search, mode, sort, page, hash} = Index.changed;\r\n if (!threads) { threads = search; }\r\n if (!order) { order = sort; }\r\n if (threads || order) { Index.sort(); }\r\n if (threads) { Index.buildPagelist(); }\r\n if (search) { Index.setupSearch(); }\r\n if (mode) { Index.setupMode(); }\r\n if (sort) { Index.setupSort(); }\r\n if (threads || mode || page || order) { Index.buildIndex(); }\r\n if (threads || page) { Index.setPage(); }\r\n if (scroll && !hash) { Index.scrollToIndex(); }\r\n if (hash) { Header.hashScroll(); }\r\n return Index.changed = {};\r\n },\r\n\r\n setupMode() {\r\n for (var mode of ['paged', 'infinite', 'all pages', 'catalog']) {\r\n $[mode === Conf['Index Mode'] ? 'addClass' : 'rmClass'](doc, `${mode.replace(/\\ /g, '-')}-mode`);\r\n }\r\n Index.selectMode.value = Conf['Index Mode'];\r\n Index.cb.size();\r\n Index.showHiddenThreads = false;\r\n return $('#hidden-toggle a', Index.navLinks).textContent = 'Show';\r\n },\r\n\r\n setupSort() {\r\n Index.selectRev.checked = /-rev$/.test(Index.currentSort);\r\n Index.selectSort.value = Index.currentSort.replace(/-rev$/, '');\r\n return Index.lastLongOptions.hidden = (Index.selectSort.value !== 'lastlong');\r\n },\r\n\r\n getPagesNum() {\r\n if (Index.search) {\r\n return Math.ceil(Index.sortedThreadIDs.length / Index.threadsNumPerPage);\r\n } else {\r\n return Index.pagesNum;\r\n }\r\n },\r\n\r\n getMaxPageNum() {\r\n return Math.max(1, Index.getPagesNum());\r\n },\r\n\r\n buildPagelist() {\r\n const pagesRoot = $('.pages', Index.pagelist);\r\n const maxPageNum = Index.getMaxPageNum();\r\n if (pagesRoot.childElementCount !== maxPageNum) {\r\n const nodes = [];\r\n for (let i = 1, end = maxPageNum; i <= end; i++) {\r\n var a = $.el('a', {\r\n textContent: i,\r\n href: i === 1 ? './' : i\r\n }\r\n );\r\n nodes.push($.tn('['), a, $.tn('] '));\r\n }\r\n $.rmAll(pagesRoot);\r\n return $.add(pagesRoot, nodes);\r\n }\r\n },\r\n\r\n setPage() {\r\n let a, strong;\r\n const pageNum = Index.currentPage;\r\n const maxPageNum = Index.getMaxPageNum();\r\n const pagesRoot = $('.pages', Index.pagelist);\r\n\r\n // Previous/Next buttons\r\n const prev = pagesRoot.previousElementSibling.firstElementChild;\r\n const next = pagesRoot.nextElementSibling.firstElementChild;\r\n let href = Math.max(pageNum - 1, 1);\r\n prev.href = href === 1 ? './' : href;\r\n prev.firstElementChild.disabled = href === pageNum;\r\n href = Math.min(pageNum + 1, maxPageNum);\r\n next.href = href === 1 ? './' : href;\r\n next.firstElementChild.disabled = href === pageNum;\r\n\r\n // current page\r\n if (strong = $('strong', pagesRoot)) {\r\n if (+strong.textContent === pageNum) { return; }\r\n $.replace(strong, strong.firstChild);\r\n } else {\r\n strong = $.el('strong');\r\n }\r\n\r\n if (a = pagesRoot.children[pageNum - 1]) {\r\n $.before(a, strong);\r\n return $.add(strong, a);\r\n }\r\n },\r\n\r\n updateHideLabel() {\r\n if (!Index.hideLabel) { return; }\r\n let hiddenCount = 0;\r\n for (var threadID of Index.liveThreadIDs) {\r\n if (Index.isHidden(threadID)) {\r\n hiddenCount++;\r\n }\r\n }\r\n if (!hiddenCount) {\r\n Index.hideLabel.hidden = true;\r\n if (Index.showHiddenThreads) { Index.cb.toggleHiddenThreads(); }\r\n return;\r\n }\r\n Index.hideLabel.hidden = false;\r\n return $('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ?\r\n '1 hidden thread'\r\n :\r\n `${hiddenCount} hidden threads`;\r\n },\r\n\r\n update(firstTime) {\r\n let oldReq;\r\n if (oldReq = Index.req) {\r\n delete Index.req;\r\n oldReq.abort();\r\n }\r\n\r\n if (Conf['Index Refresh Notifications']) {\r\n // Optional notification for manual refreshes\r\n if (!Index.notice) { Index.notice = new Notice('info', 'Refreshing index...'); }\r\n if (!Index.nTimeout) { Index.nTimeout = setTimeout(() => {\r\n if (Index.notice) {\r\n Index.notice.el.lastElementChild.textContent += ' (disable JSON Index if this takes too long)';\r\n }\r\n }\r\n , 3 * SECOND); }\r\n } else {\r\n // Also display notice if Index Refresh is taking too long\r\n if (!Index.nTimeout) { Index.nTimeout = setTimeout(() => Index.notice || (Index.notice = new Notice('info', 'Refreshing index... (disable JSON Index if this takes too long)'))\r\n , 3 * SECOND); }\r\n }\r\n\r\n // Hard refresh in case of incomplete page load.\r\n if (!firstTime && (d.readyState !== 'loading') && !$('.board + *')) {\r\n location.reload();\r\n return;\r\n }\r\n\r\n Index.req = $.whenModified(\r\n g.SITE.urls.catalogJSON({boardID: g.BOARD.ID}),\r\n 'Index',\r\n Index.load\r\n );\r\n return $.addClass(Index.button, 'spin');\r\n },\r\n\r\n load() {\r\n let err;\r\n if (this !== Index.req) { return; } // aborted\r\n\r\n $.rmClass(Index.button, 'spin');\r\n const {notice, nTimeout} = Index;\r\n if (nTimeout) { clearTimeout(nTimeout); }\r\n delete Index.nTimeout;\r\n delete Index.req;\r\n delete Index.notice;\r\n\r\n if (![200, 304].includes(this.status)) {\r\n err = `Index refresh failed. ${this.status ? `Error ${this.statusText} (${this.status})` : 'Connection Error'}`;\r\n if (notice) {\r\n notice.setType('warning');\r\n notice.el.lastElementChild.textContent = err;\r\n setTimeout(notice.close, SECOND);\r\n } else {\r\n new Notice('warning', err, 1);\r\n }\r\n return;\r\n }\r\n\r\n try {\r\n if (this.status === 200) {\r\n Index.parse(this.response);\r\n } else if (this.status === 304) {\r\n Index.pageLoad();\r\n }\r\n } catch (error) {\r\n err = error;\r\n c.error(`Index failure: ${err.message}`, err.stack);\r\n if (notice) {\r\n notice.setType('error');\r\n notice.el.lastElementChild.textContent = 'Index refresh failed.';\r\n setTimeout(notice.close, SECOND);\r\n } else {\r\n new Notice('error', 'Index refresh failed.', 1);\r\n }\r\n return;\r\n }\r\n\r\n if (notice) {\r\n if (Conf['Index Refresh Notifications']) {\r\n notice.setType('success');\r\n notice.el.lastElementChild.textContent = 'Index refreshed!';\r\n setTimeout(notice.close, SECOND);\r\n } else {\r\n notice.close();\r\n }\r\n }\r\n\r\n const timeEl = $('#index-last-refresh time', Index.navLinks);\r\n timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified'));\r\n return RelativeDates.update(timeEl);\r\n },\r\n\r\n parse(pages) {\r\n $.cleanCache(url => /^https?:\\/\\/a\\.4cdn\\.org\\//.test(url));\r\n Index.parseThreadList(pages);\r\n Index.changed.threads = true;\r\n return Index.pageLoad();\r\n },\r\n\r\n parseThreadList(pages) {\r\n Index.pagesNum = pages.length;\r\n Index.threadsNumPerPage = pages[0]?.threads.length || 1;\r\n Index.liveThreadData = pages.reduce(((arr, next) => arr.concat(next.threads)), []);\r\n Index.liveThreadIDs = Index.liveThreadData.map(data => data.no);\r\n Index.liveThreadDict = dict();\r\n Index.threadPosition = dict();\r\n Index.parsedThreads = dict();\r\n Index.replyData = dict();\r\n for (let i = 0; i < Index.liveThreadData.length; i++) {\r\n var obj, results;\r\n var data = Index.liveThreadData[i];\r\n Index.liveThreadDict[data.no] = data;\r\n Index.threadPosition[data.no] = i;\r\n Index.parsedThreads[data.no] = (obj = g.SITE.Build.parseJSON(data, g.BOARD));\r\n obj.filterResults = (results = Filter.test(obj));\r\n obj.isOnTop = results.top;\r\n obj.isHidden = results.hide || ThreadHiding.isHidden(obj.boardID, obj.threadID);\r\n if (data.last_replies) {\r\n for (var reply of data.last_replies) {\r\n Index.replyData[`${g.BOARD}.${reply.no}`] = reply;\r\n }\r\n }\r\n }\r\n if (Index.liveThreadData[0]) {\r\n g.SITE.Build.spoilerRange[g.BOARD.ID] = Index.liveThreadData[0].custom_spoiler;\r\n }\r\n g.BOARD.threads.forEach(function(thread) {\r\n if (!Index.liveThreadIDs.includes(thread.ID)) { return thread.collect(); }\r\n });\r\n $.event('IndexUpdate',\r\n {threads: ((Index.liveThreadIDs.map((ID) => `${g.BOARD}.${ID}`)))});\r\n },\r\n\r\n isHidden(threadID) {\r\n let thread;\r\n if ((thread = g.BOARD.threads.get(threadID)) && thread.OP && !thread.OP.isFetchedQuote) {\r\n return thread.isHidden;\r\n } else {\r\n return Index.parsedThreads[threadID].isHidden;\r\n }\r\n },\r\n\r\n isHiddenReply(threadID, replyData) {\r\n return PostHiding.isHidden(g.BOARD.ID, threadID, replyData.no) || Filter.isHidden(g.SITE.Build.parseJSON(replyData, g.BOARD));\r\n },\r\n\r\n buildThreads(threadIDs, isCatalog, withReplies) {\r\n let errors;\r\n const threads = [];\r\n const newThreads = [];\r\n let newPosts = [];\r\n for (var ID of threadIDs) {\r\n var opRoot, thread;\r\n try {\r\n var OP;\r\n var threadData = Index.liveThreadDict[ID];\r\n\r\n if (thread = g.BOARD.threads.get(ID)) {\r\n var isStale = (thread.json !== threadData) && (JSON.stringify(thread.json) !== JSON.stringify(threadData));\r\n if (isStale) {\r\n thread.setCount('post', threadData.replies + 1, threadData.bumplimit);\r\n thread.setCount('file', threadData.images + !!threadData.ext, threadData.imagelimit);\r\n thread.setStatus('Sticky', !!threadData.sticky);\r\n thread.setStatus('Closed', !!threadData.closed);\r\n }\r\n if (thread.catalogView) {\r\n $.rm(thread.catalogView.nodes.replies);\r\n thread.catalogView.nodes.replies = null;\r\n }\r\n } else {\r\n thread = new Thread(ID, g.BOARD);\r\n newThreads.push(thread);\r\n }\r\n var lastPost = threadData.last_replies && threadData.last_replies.length ? threadData.last_replies[threadData.last_replies.length - 1].no : ID;\r\n if (lastPost > thread.lastPost) { thread.lastPost = lastPost; }\r\n thread.json = threadData;\r\n threads.push(thread);\r\n\r\n if ((OP = thread.OP) && !OP.isFetchedQuote) {\r\n OP.setCatalogOP(isCatalog);\r\n thread.setPage(Math.floor(Index.threadPosition[ID] / Index.threadsNumPerPage) + 1);\r\n } else {\r\n var obj = Index.parsedThreads[ID];\r\n opRoot = g.SITE.Build.post(obj);\r\n OP = new Post(opRoot, thread, g.BOARD);\r\n OP.filterResults = obj.filterResults;\r\n newPosts.push(OP);\r\n }\r\n\r\n if (!isCatalog || !thread.nodes.root) {\r\n g.SITE.Build.thread(thread, threadData, withReplies);\r\n }\r\n } catch (err) {\r\n // Skip posts that we failed to parse.\r\n if (!errors) { errors = []; }\r\n errors.push({\r\n message: `Parsing of Thread No.${thread} failed. Thread will be skipped.`,\r\n error: err,\r\n html: opRoot?.outerHTML\r\n });\r\n }\r\n }\r\n if (errors) { Main.handleErrors(errors); }\r\n\r\n if (withReplies) {\r\n newPosts = newPosts.concat(Index.buildReplies(threads));\r\n }\r\n\r\n Main.callbackNodes('Thread', newThreads);\r\n Main.callbackNodes('Post', newPosts);\r\n Index.updateHideLabel();\r\n $.event('IndexRefreshInternal', {threadIDs: (threads.map((t) => t.fullID)), isCatalog});\r\n\r\n return threads;\r\n },\r\n\r\n buildReplies(threads) {\r\n let errors;\r\n const posts = [];\r\n for (var thread of threads) {\r\n var lastReplies;\r\n if (!(lastReplies = Index.liveThreadDict[thread.ID].last_replies)) { continue; }\r\n var nodes = [];\r\n for (var data of lastReplies) {\r\n var node, post;\r\n if ((post = thread.posts.get(data.no)) && !post.isFetchedQuote) {\r\n nodes.push(post.nodes.root);\r\n continue;\r\n }\r\n nodes.push(node = g.SITE.Build.postFromObject(data, thread.board.ID));\r\n try {\r\n posts.push(new Post(node, thread, thread.board));\r\n } catch (err) {\r\n // Skip posts that we failed to parse.\r\n if (!errors) { errors = []; }\r\n errors.push({\r\n message: `Parsing of Post No.${data.no} failed. Post will be skipped.`,\r\n error: err,\r\n html: node?.outerHTML\r\n });\r\n }\r\n }\r\n $.add(thread.nodes.root, nodes);\r\n }\r\n\r\n if (errors) { Main.handleErrors(errors); }\r\n return posts;\r\n },\r\n\r\n buildCatalogViews(threads) {\r\n const catalogThreads = [];\r\n for (var thread of threads) {\r\n if (!thread.catalogView) {\r\n var {ID} = thread;\r\n var page = Math.floor(Index.threadPosition[ID] / Index.threadsNumPerPage) + 1;\r\n var root = g.SITE.Build.catalogThread(thread, Index.liveThreadDict[ID], page);\r\n catalogThreads.push(new CatalogThread(root, thread));\r\n }\r\n }\r\n Main.callbackNodes('CatalogThread', catalogThreads);\r\n },\r\n\r\n sizeCatalogViews(threads) {\r\n // XXX When browsers support CSS3 attr(), use it instead.\r\n const size = Conf['Index Size'] === 'small' ? 150 : 250;\r\n for (var thread of threads) {\r\n var {thumb} = thread.catalogView.nodes;\r\n var {width, height} = thumb.dataset;\r\n if (!width) { continue; }\r\n var ratio = size / Math.max(width, height);\r\n thumb.style.width = (width * ratio) + 'px';\r\n thumb.style.height = (height * ratio) + 'px';\r\n }\r\n },\r\n\r\n buildCatalogReplies(thread) {\r\n let lastReplies;\r\n const {nodes} = thread.catalogView;\r\n if (!(lastReplies = Index.liveThreadDict[thread.ID].last_replies)) { return; }\r\n\r\n const replies = [];\r\n for (var data of lastReplies) {\r\n if (Index.isHiddenReply(thread.ID, data)) { continue; }\r\n var reply = g.SITE.Build.catalogReply(thread, data);\r\n RelativeDates.update($('time', reply));\r\n $.on($('.catalog-reply-preview', reply), 'mouseover', QuotePreview.mouseover);\r\n replies.push(reply);\r\n }\r\n\r\n nodes.replies = $.el('div', {className: 'catalog-replies'});\r\n $.add(nodes.replies, replies);\r\n $.add(thread.OP.nodes.post, nodes.replies);\r\n },\r\n\r\n sort() {\r\n let threadIDs;\r\n const {liveThreadIDs, liveThreadData} = Index;\r\n if (!liveThreadData) { return; }\r\n const tmp_time = new Date().getTime()/1000;\r\n const sortType = Index.currentSort.replace(/-rev$/, '');\r\n Index.sortedThreadIDs = (() => { switch (sortType) {\r\n case 'lastreply': case 'lastlong':\r\n var repliesAvailable = liveThreadData.some(thread => thread.last_replies?.length);\r\n var lastlong = function(thread) {\r\n if (!repliesAvailable) {\r\n return thread.last_modified;\r\n }\r\n const iterable = thread.last_replies || [];\r\n for (let i = iterable.length - 1; i >= 0; i--) {\r\n var r = iterable[i];\r\n if (Index.isHiddenReply(thread.no, r)) { continue; }\r\n if (sortType === 'lastreply') {\r\n return r;\r\n }\r\n var len = r.com ? g.SITE.Build.parseComment(r.com).replace(/[^a-z]/ig, '').length : 0;\r\n if (len >= Index.lastLongThresholds[+!!r.ext]) {\r\n return r;\r\n }\r\n }\r\n if (thread.omitted_posts && thread.last_replies?.length) { return thread.last_replies[0]; } else { return thread; }\r\n };\r\n var lastlongD = dict();\r\n for (var thread of liveThreadData) {\r\n lastlongD[thread.no] = lastlong(thread).no;\r\n }\r\n return [...liveThreadData].sort((a, b) => lastlongD[b.no] - lastlongD[a.no]).map(post => post.no);\r\n case 'bump': return liveThreadIDs;\r\n case 'birth': return [...liveThreadIDs ].sort((a, b) => b - a);\r\n case 'replycount': return [...liveThreadData].sort((a, b) => b.replies - a.replies).map(post => post.no);\r\n case 'filecount': return [...liveThreadData].sort((a, b) => b.images - a.images).map(post => post.no);\r\n 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);\r\n default: return liveThreadIDs;\r\n } })();\r\n if (/-rev$/.test(Index.currentSort)) {\r\n Index.sortedThreadIDs.reverse();\r\n }\r\n if (Index.search && (threadIDs = Index.querySearch(Index.search))) {\r\n Index.sortedThreadIDs = threadIDs;\r\n }\r\n // Sticky threads\r\n Index.sortOnTop(obj => obj.isSticky);\r\n // Highlighted threads\r\n Index.sortOnTop(obj => obj.isOnTop || (Conf['Pin Watched Threads'] && ThreadWatcher.isWatchedRaw(obj.boardID, obj.threadID)));\r\n // Non-hidden threads\r\n if (Conf['Anchor Hidden Threads']) { return Index.sortOnTop(obj => !Index.isHidden(obj.threadID)); }\r\n },\r\n\r\n sortOnTop(match) {\r\n const topThreads = [];\r\n const bottomThreads = [];\r\n for (var ID of Index.sortedThreadIDs) {\r\n (match(Index.parsedThreads[ID]) ? topThreads : bottomThreads).push(ID);\r\n }\r\n return Index.sortedThreadIDs = topThreads.concat(bottomThreads);\r\n },\r\n\r\n buildIndex() {\r\n let threadIDs;\r\n if (!Index.liveThreadData) { return; }\r\n switch (Conf['Index Mode']) {\r\n case 'all pages':\r\n threadIDs = Index.sortedThreadIDs;\r\n break;\r\n case 'catalog':\r\n threadIDs = Index.sortedThreadIDs.filter(ID => !Index.isHidden(ID) !== Index.showHiddenThreads);\r\n break;\r\n default:\r\n threadIDs = Index.threadsOnPage(Index.currentPage);\r\n }\r\n delete Index.pageNum;\r\n $.rmAll(Index.root);\r\n $.rmAll(Header.hover);\r\n if (Index.loaded && Index.root.parentNode) {\r\n $.event('PostsRemoved', null, Index.root);\r\n }\r\n if (Conf['Index Mode'] === 'catalog') {\r\n Index.buildCatalog(threadIDs);\r\n } else {\r\n Index.buildStructure(threadIDs);\r\n }\r\n },\r\n\r\n threadsOnPage(pageNum) {\r\n const nodesPerPage = Index.threadsNumPerPage;\r\n const offset = nodesPerPage * (pageNum - 1);\r\n return Index.sortedThreadIDs.slice(offset , offset + nodesPerPage);\r\n },\r\n\r\n buildStructure(threadIDs) {\r\n const threads = Index.buildThreads(threadIDs, false, Conf['Show Replies']);\r\n const nodes = [];\r\n for (var thread of threads) {\r\n nodes.push(thread.nodes.root, $.el('hr'));\r\n }\r\n $.add(Index.root, nodes);\r\n if (Index.root.parentNode) {\r\n $.event('PostsInserted', null, Index.root);\r\n }\r\n Index.loaded = true;\r\n },\r\n\r\n buildCatalog(threadIDs) {\r\n let i = 0;\r\n const n = threadIDs.length;\r\n let node0 = null;\r\n var fn = function() {\r\n if (node0 && !node0.parentNode) { return; } // Index.root cleared\r\n const j = (i > 0) && Index.root.parentNode ? n : i + 30;\r\n node0 = Index.buildCatalogPart(threadIDs.slice(i, j))[0];\r\n i = j;\r\n if (i < n) {\r\n return $.queueTask(fn);\r\n } else {\r\n if (Index.root.parentNode) {\r\n $.event('PostsInserted', null, Index.root);\r\n }\r\n return Index.loaded = true;\r\n }\r\n };\r\n fn();\r\n },\r\n\r\n buildCatalogPart(threadIDs) {\r\n const threads = Index.buildThreads(threadIDs, true);\r\n Index.buildCatalogViews(threads);\r\n Index.sizeCatalogViews(threads);\r\n const nodes = [];\r\n for (var thread of threads) {\r\n thread.OP.setCatalogOP(true);\r\n $.add(thread.catalogView.nodes.root, thread.OP.nodes.root);\r\n nodes.push(thread.catalogView.nodes.root);\r\n $.on(thread.catalogView.nodes.root, 'mouseenter', Index.cb.catalogReplies.bind(thread));\r\n $.on(thread.OP.nodes.root, 'mouseenter', Index.cb.hoverAdjust.bind(thread.OP.nodes));\r\n }\r\n $.add(Index.root, nodes);\r\n return nodes;\r\n },\r\n\r\n clearSearch() {\r\n Index.searchInput.value = '';\r\n Index.onSearchInput();\r\n return Index.searchInput.focus();\r\n },\r\n\r\n setupSearch() {\r\n Index.searchInput.value = Index.search;\r\n if (Index.search) {\r\n return Index.searchInput.dataset.searching = 1;\r\n } else {\r\n // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289\r\n return Index.searchInput.removeAttribute('data-searching');\r\n }\r\n },\r\n\r\n onSearchInput() {\r\n const search = Index.searchInput.value.trim();\r\n if (search === Index.search) { return; }\r\n Index.pushState({\r\n search,\r\n replace: !!search === !!Index.search\r\n });\r\n return Index.pageLoad(false);\r\n },\r\n\r\n querySearch(query) {\r\n let keywords, match;\r\n if (match = query.match(/^([\\w+]+):\\/(.*)\\/(\\w*)$/)) {\r\n let regexp;\r\n try {\r\n regexp = RegExp(match[2], match[3]);\r\n } catch (error) {\r\n return [];\r\n }\r\n return Index.sortedThreadIDs.filter(ID => regexp.test(Filter.values(match[1], Index.parsedThreads[ID]).join('\\n')));\r\n }\r\n if (!(keywords = query.toLowerCase().match(/\\S+/g))) { return; }\r\n return Index.sortedThreadIDs.filter(ID => Index.searchMatch(Index.parsedThreads[ID], keywords));\r\n },\r\n\r\n searchMatch(obj, keywords) {\r\n const {info, file} = obj;\r\n if (info.comment == null) { info.comment = g.SITE.Build.parseComment(info.commentHTML.innerHTML); }\r\n let text = [];\r\n for (var key of ['comment', 'subject', 'name', 'tripcode']) {\r\n if (key in info) { text.push(info[key]); }\r\n }\r\n if (file) { text.push(file.name); }\r\n text = text.join(' ').toLowerCase();\r\n for (var keyword of keywords) {\r\n if (-1 === text.indexOf(keyword)) { return false; }\r\n }\r\n return true;\r\n }\r\n};\r\nexport default Index;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport DataBoard from \"../classes/DataBoard\";\r\nimport Thread from \"../classes/Thread\";\r\nimport Index from \"../General/Index\";\r\nimport UI from \"../General/UI\";\r\nimport { g, Conf, d, doc } from \"../globals/globals\";\r\nimport Main from \"../main/Main\";\r\nimport Menu from \"../Menu/Menu\";\r\nimport $ from \"../platform/$\";\r\nimport $$ from \"../platform/$$\";\r\nimport { dict } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar ThreadHiding = {\r\n init() {\r\n if (!['index', 'catalog'].includes(g.VIEW) || (!Conf['Thread Hiding Buttons'] && !(Conf['Menu'] && Conf['Thread Hiding Link']) && !Conf['JSON Index'])) { return; }\r\n this.db = new DataBoard('hiddenThreads');\r\n if (g.VIEW === 'catalog') { return this.catalogWatch(); }\r\n this.catalogSet(g.BOARD);\r\n $.on(d, 'IndexRefreshInternal', this.onIndexRefresh);\r\n if (Conf['Thread Hiding Buttons']) {\r\n $.addClass(doc, 'thread-hide');\r\n }\r\n return Callbacks.Post.push({\r\n name: 'Thread Hiding',\r\n cb: this.node\r\n });\r\n },\r\n\r\n catalogSet(board) {\r\n if (!$.hasStorage || (g.SITE.software !== 'yotsuba')) { return; }\r\n const hiddenThreads = ThreadHiding.db.get({\r\n boardID: board.ID,\r\n defaultValue: dict()\r\n });\r\n for (var threadID in hiddenThreads) { hiddenThreads[threadID] = true; }\r\n return localStorage.setItem(`4chan-hide-t-${board}`, JSON.stringify(hiddenThreads));\r\n },\r\n\r\n catalogWatch() {\r\n if (!$.hasStorage || (g.SITE.software !== 'yotsuba')) { return; }\r\n this.hiddenThreads = JSON.parse(localStorage.getItem(`4chan-hide-t-${g.BOARD}`)) || {};\r\n return Main.ready(() => // 4chan's catalog sets the style to \"display: none;\" when hiding or unhiding a thread.\r\n new MutationObserver(ThreadHiding.catalogSave).observe($.id('threads'), {\r\n attributes: true,\r\n subtree: true,\r\n attributeFilter: ['style']\r\n }));\r\n },\r\n\r\n catalogSave() {\r\n let threadID;\r\n const hiddenThreads2 = JSON.parse(localStorage.getItem(`4chan-hide-t-${g.BOARD}`)) || {};\r\n for (threadID in hiddenThreads2) {\r\n if (!$.hasOwn(ThreadHiding.hiddenThreads, threadID)) {\r\n ThreadHiding.db.set({\r\n boardID: g.BOARD.ID,\r\n threadID,\r\n val: {makeStub: Conf['Stubs']}});\r\n }\r\n }\r\n for (threadID in ThreadHiding.hiddenThreads) {\r\n if (!$.hasOwn(hiddenThreads2, threadID)) {\r\n ThreadHiding.db.delete({\r\n boardID: g.BOARD.ID,\r\n threadID\r\n });\r\n }\r\n }\r\n return ThreadHiding.hiddenThreads = hiddenThreads2;\r\n },\r\n\r\n isHidden(boardID, threadID) {\r\n return !!(ThreadHiding.db && ThreadHiding.db.get({boardID, threadID}));\r\n },\r\n\r\n node() {\r\n let data;\r\n if (this.isReply || this.isClone || this.isFetchedQuote) { return; }\r\n\r\n if (Conf['Thread Hiding Buttons']) {\r\n $.prepend(this.nodes.root, ThreadHiding.makeButton(this.thread, 'hide'));\r\n }\r\n\r\n if (data = ThreadHiding.db.get({boardID: this.board.ID, threadID: this.ID})) {\r\n return ThreadHiding.hide(this.thread, data.makeStub);\r\n }\r\n },\r\n\r\n onIndexRefresh() {\r\n return g.BOARD.threads.forEach(function(thread) {\r\n const {root} = thread.nodes;\r\n if (thread.isHidden && thread.stub && !root.contains(thread.stub)) {\r\n return ThreadHiding.makeStub(thread, root);\r\n }\r\n });\r\n },\r\n\r\n menu: {\r\n init() {\r\n if ((g.VIEW !== 'index') || !Conf['Menu'] || !Conf['Thread Hiding Link']) { return; }\r\n\r\n let div = $.el('div', {\r\n className: 'hide-thread-link',\r\n textContent: 'Hide'\r\n }\r\n );\r\n\r\n const apply = $.el('a', {\r\n textContent: 'Apply',\r\n href: 'javascript:;'\r\n }\r\n );\r\n $.on(apply, 'click', ThreadHiding.menu.hide);\r\n\r\n const makeStub = UI.checkbox('Stubs', 'Make stub');\r\n\r\n Menu.menu.addEntry({\r\n el: div,\r\n order: 20,\r\n open({thread, isReply}) {\r\n if (isReply || thread.isHidden || (Conf['JSON Index'] && (Conf['Index Mode'] === 'catalog'))) {\r\n return false;\r\n }\r\n ThreadHiding.menu.thread = thread;\r\n return true;\r\n },\r\n subEntries: [{el: apply}, {el: makeStub}]});\r\n\r\n div = $.el('a', {\r\n className: 'show-thread-link',\r\n textContent: 'Show',\r\n href: 'javascript:;'\r\n }\r\n );\r\n $.on(div, 'click', ThreadHiding.menu.show);\r\n\r\n Menu.menu.addEntry({\r\n el: div,\r\n order: 20,\r\n open({thread, isReply}) {\r\n if (isReply || !thread.isHidden || (Conf['JSON Index'] && (Conf['Index Mode'] === 'catalog'))) {\r\n return false;\r\n }\r\n ThreadHiding.menu.thread = thread;\r\n return true;\r\n }\r\n });\r\n\r\n const hideStubLink = $.el('a', {\r\n textContent: 'Hide stub',\r\n href: 'javascript:;'\r\n }\r\n );\r\n $.on(hideStubLink, 'click', ThreadHiding.menu.hideStub);\r\n\r\n return Menu.menu.addEntry({\r\n el: hideStubLink,\r\n order: 15,\r\n open({thread, isReply}) {\r\n if (isReply || !thread.isHidden || (Conf['JSON Index'] && (Conf['Index Mode'] === 'catalog'))) {\r\n return false;\r\n }\r\n return ThreadHiding.menu.thread = thread;\r\n }\r\n });\r\n },\r\n\r\n hide() {\r\n const makeStub = $('input', this.parentNode).checked;\r\n const {thread} = ThreadHiding.menu;\r\n ThreadHiding.hide(thread, makeStub);\r\n ThreadHiding.saveHiddenState(thread, makeStub);\r\n return $.event('CloseMenu');\r\n },\r\n\r\n show() {\r\n const {thread} = ThreadHiding.menu;\r\n ThreadHiding.show(thread);\r\n ThreadHiding.saveHiddenState(thread);\r\n return $.event('CloseMenu');\r\n },\r\n\r\n hideStub() {\r\n const {thread} = ThreadHiding.menu;\r\n ThreadHiding.show(thread);\r\n ThreadHiding.hide(thread, false);\r\n ThreadHiding.saveHiddenState(thread, false);\r\n $.event('CloseMenu');\r\n }\r\n },\r\n\r\n makeButton(thread, type) {\r\n const a = $.el('a', {\r\n className: `${type}-thread-button`,\r\n href: 'javascript:;'\r\n }\r\n );\r\n $.extend(a, {textContent: type === \"hide\" ? '➖︎' : '➕︎' });\r\n a.dataset.fullID = thread.fullID;\r\n $.on(a, 'click', ThreadHiding.toggle);\r\n return a;\r\n },\r\n\r\n makeStub(thread, root) {\r\n let summary, threadDivider;\r\n let numReplies = $$(g.SITE.selectors.replyOriginal, root).length;\r\n if (summary = $(g.SITE.selectors.summary, root)) { numReplies += +summary.textContent.match(/\\d+/); }\r\n\r\n const a = ThreadHiding.makeButton(thread, 'show');\r\n $.add(a, $.tn(` ${thread.OP.info.nameBlock} (${numReplies === 1 ? '1 reply' : `${numReplies} replies`})`));\r\n thread.stub = $.el('div',\r\n {className: 'stub'});\r\n if (Conf['Menu']) {\r\n $.add(thread.stub, [a, Menu.makeButton(thread.OP)]);\r\n } else {\r\n $.add(thread.stub, a);\r\n }\r\n $.prepend(root, thread.stub);\r\n\r\n // Prevent hiding of thread divider on sites that put it inside the thread\r\n if (threadDivider = $(g.SITE.selectors.threadDivider, root)) {\r\n return $.addClass(threadDivider, 'threadDivider');\r\n }\r\n },\r\n\r\n saveHiddenState(thread, makeStub) {\r\n if (thread.isHidden) {\r\n ThreadHiding.db.set({\r\n boardID: thread.board.ID,\r\n threadID: thread.ID,\r\n val: {makeStub}});\r\n } else {\r\n ThreadHiding.db.delete({\r\n boardID: thread.board.ID,\r\n threadID: thread.ID\r\n });\r\n }\r\n return ThreadHiding.catalogSet(thread.board);\r\n },\r\n\r\n toggle(thread) {\r\n if (!(thread instanceof Thread)) {\r\n thread = g.threads.get(this.dataset.fullID);\r\n }\r\n if (thread.isHidden) {\r\n ThreadHiding.show(thread);\r\n } else {\r\n ThreadHiding.hide(thread);\r\n }\r\n return ThreadHiding.saveHiddenState(thread);\r\n },\r\n\r\n hide(thread, makeStub=Conf['Stubs']) {\r\n if (thread.isHidden) { return; }\r\n const threadRoot = thread.nodes.root;\r\n thread.isHidden = true;\r\n Index.updateHideLabel();\r\n if (thread.catalogView && !Index.showHiddenThreads) {\r\n $.rm(thread.catalogView.nodes.root);\r\n $.event('PostsRemoved', null, Index.root);\r\n }\r\n\r\n if (!makeStub) { return threadRoot.hidden = true; }\r\n\r\n return ThreadHiding.makeStub(thread, threadRoot);\r\n },\r\n\r\n show(thread) {\r\n if (thread.stub) {\r\n $.rm(thread.stub);\r\n delete thread.stub;\r\n }\r\n const threadRoot = thread.nodes.root;\r\n threadRoot.hidden = (thread.isHidden = false);\r\n Index.updateHideLabel();\r\n if (thread.catalogView && Index.showHiddenThreads) {\r\n $.rm(thread.catalogView.nodes.root);\r\n return $.event('PostsRemoved', null, Index.root);\r\n }\r\n }\r\n};\r\nexport default ThreadHiding;\r\n",null,null,null,null,"export default '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==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAadQTFRFAAAAwzw8xDs7cY6O0iws0ysrtF9f0Sws1CwsyzU1zTIy1igoyzQ01icnY7i4t0hI0S4u0ysr1Soq1ikp1ikp1Soq0ysr0S4uu0VFzjEx1Csr1ygo2Ccn2Ccn1ygo1CoqzjExzjAw1Skp2Ccn2Ccn1ikp0TAwxzY21Soq1SoqyzQ00iws1ygo1ygo0yws1Soq1Skp1igo1igo1igo1igo1igo1igo1Coq1Soq0C0t1ygo1ygo0S4unV5e1Csr1ygo1ygo1CsruUdHxzg41Skp1ygo1CsryTU10C4u1igo1ycn1ygo0i0txjo60S0t1ikp1ygo1ikp1Cws1Coq1Coq0yws0S0tyzQ00iws1Soq0ysr0i0txDs72Ccn2CUl2CYm2CQk3EFB2S8v2zw82jY24FZW3D0931FR3EBA3UND8LS04FVV7qys4V9f4WBg+erq766u9t7e7qqq2Ckp54KC9+Pj6pSU+Ojo5XNz9NHR6YqK8bu765ub5G5u9M3N6ImJ88vL5XV165eX3UVF6pWV3UhI2Soq2jU12Coq2jQ02Cgo2Sws////FaxLuAAAAF10Uk5TAAAAAAAAAAAAAAAAAAAAASJnoLy9oWolAhBz1vr72XgTGKf8/a4cCpuiDVvz9mS6xOvy9vzg6aGsPOToRAFv9fh2Awm07XgIMd765UEDOsfemVhhY00nBommbCkEI8horgAAAL5JREFUKBUFwbFKA0EUQNF7387sMq4EmzRpLSSdIBYKFv6Af2prnSYkRT4gWFgkCBJQ0EIFdcZzBCeqqh4qdk7VW2ChPusw02sAYKU7z7wEAAA2piQKFbrWSHazc1J0XWs5pdxPDykcVX+7Y9UxUsSo+s7PibqPFBRV/C5qi4i/UkrJrc7L47Bt4ZWnUaMCAE9GSrtKBQD2fR+bnAEAeOn7dUTOwApe35bDsPz0zsniQlV98IN0tJ3f6P0XAMA/kxou7OXCdnoAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAwUExURTSY22ey5E2l4KbS75rM7Y3F64C/6f///8zl9nS45r/f9PL5/UGe3bPY8Vqr4v///wNjrzUAAAABYktHRA8YugDZAAAAB3RJTUUH4AINEi85AIH95AAAAE9JREFUCNdjYMAGGBWgDGYHCM2a3hkAZmi0dzSBaKaO9o5moCqmLiCjYzNQyw4QowIodQzI6E0AKcpo72gE6+Jyb1kAMehUA9RktgdYbQYAjGIVNGGXBJkAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAnUExURQBk3ff6/trp+kKO5wZt3xx54q7P9Ozz/IS17zOG5WKh653E8sbc9/GbbcoAAABZSURBVAjXY2BAASyhDhAGc9oECMOjyAAiESEEYrBYpLWBGcwHxcvBjDDxHelghpF0yDQwY3kVgweEUeEQDWbMEepqAjO8FMsLIeYsU8o+BrbCdWboTAe4AwALXxWGjW41FwAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAjVBMVEWn3gCo3gSr3w2t4BSu4Bav4Ri35C+45DK45DO55DXA50rA50vB50zC6E/D6FTF6VjG6VvL62vN7G/P7XbQ7XfW74vY8JDa8ZTe8qDe8qLf86Pi9Kzj9K7k9LHp9sDp98Lq98Ps+Mr0++L5/O75/fD6/fH6/fL6/fP7/fT7/fb8/ff8/vj8/vn+/v7///91X4cfAAAAcklEQVR42o3M2xKBUACF4aVQckrIuRJK6H//x2sme4/MuPDfre9i6c/Cc3U5Dj87BuAxsXvGu6JvIIXEHRWwNHCHQNrCzkAFkbSBg4EM8i+Yw7PXBa3zRfuxVyf/Bis7nKwGKAcWxgC8prI5Sc315OlnDfzpDar2S9/oAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABblBMVEXc3NykpKTW1tbb29ugoKCdnZ0AAAACAgIEDRcKCgoMDAwODg4QIzYRDAoTExMUDwwVAg0WICsaEw8aGhoiCBklGxUmERwwKCQ7LSU7Ozs8LSZFLyNINi1JNyxJNy1KSklMOi5VR1FXV1daQTRkZGRseYZwU0F4eHh7dnR8bWV/YE6IdGiKcGCKkJaNgYeNjY2RdGOScWCUcWCZmZmhoaGkpKSoqKirfmaurq6xsbG1tbW6urq+vr7AbmzBb23CwsLGxsbHx8fHyMjJycnJysrMzMzOiYbPi4fQ0NDRoYbT09PU1NTW1tbY2NjZqIzZ2dnb29vd3d3f39/i4uLktZrk5OTl5eXm5ubn5+fo6Ojq6urs7OzttKLu7u7wuqbw8PDx8fHz8/P4+Pj5+fn7uZj8vpz9ya79ybD/tZf/upr/wZ//w6H/xKH/xaL/xrH/yqj/y7T/zqv/z7D/07D/17n/2Lv/2Lz/3L//38n/4Mk3Q/ZuAAAABnRSTlMSFcbGzc5MNKFvAAAA1klEQVQoz2NgYPZHAswMDEwRSclwkBTBxOARn4gE4j0YXBOiJNUDg7y8Ar1UlOITXBkcY73Z2Li42dg42dn4wmIdGeyjQ7nZoEA4PNqewSZKlw0O9KJsGKwjBdl4ZeWkJGQUhNjEIq0ZrMI5+D0ri7Jz8itCRAXCrRgsQ3mUy+xicrPSbfO0REItGSyCVaVL3ONSU9LcCtQUgy0YzIJ85M1LizMzCsv9xF2CzBhMAwN99TV1DI0MtDWcAgNNGUycA5CAswkDi5kDwrMOZiwMjKzGSICVEQDhZj0UQV7PewAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAs5JREFUOE+lk/tvi1EYx98/xT8gW4REIpGFMEQWl2FiM9ZMZhm2xRAyOsmujFFmdFRHu0tWm87UypxStr69zPauN5e5rHVp3IYhbOvHy+wHEQlxkm+ek+d8nm9OznkeSfrfldmgJC7QyUlTymsJTfuTZ25z4HdWYwyLreYhtpgekGPw0+kKvo1Eo+IXRSIiEhkWZuc9tqnsJD9EqTUopCxjSGTpB0iueczSo1HyW8cpsExQ1DbxI2pt45j9cXpexul4FEd79RnZphAa/SD7WvuFtO6UItbU9LC+YQxNI2w0wwYT5LRAdhOU3oBTIXC9gXP3oUSGgz2vST3gYHejR0jptT1C332f8yrUEYHrz8CgxDnpm6DKCUfc0KnmXa/AEVPPwnDcD0cvetA2uYRk67Ive/lpjO7YBO1PPuF8Df3vwf4cbNE4tqdw7YVq8HYyHx6FvhE1hkMEg8HDUqvFkjT4aIjMqkqyqkswDSrcfBfH+Q561YLAZ/B+BLda6FXlU/cPv0AoEPhuoP1h4Av7Wbh9E/Py15NWWUjeSR3nZDfeN+N0DY9hG/7K1eGP3P0S5/EYRFUF/IOTBrUXHPm9fT6mr1xEwupkZqxbzLyiDJYUZ5NSnkdqdSHpxyrYdFpPgdmAsdfJwPMI/Yr65bf7tZLGGBQ7DNdJWFtIYvoOZmbuZE7OXpIKKli86zAr9p9gTVktWTVnKTI2U95uRWe3U2IJUDbVB5p6hVm5x5m9Vc/cnedZUNzC8lILaQesZBy6hEZ3maKzgvJWFzVWD9XtXvVGQbSWASFtMATVRlJIKbOTWtlJXaeXepuPM1f6MNp9GLt8mLvvYLmp0OhQ2Fwvk6m7xaqDTvY0eYWUVtcnllXfYlGpnfklVuraHHg8HjxuN+6fktUHlWWZPaZeUo/ILK0UKttBcbNbSB9GP0yLxWJJUxoZGUn80zD9C/vXQ/4NHY10h3M1zmQAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABcVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB3dIYAAAAAAAAAAAAbGh4BBAcCBgoBBgoCBwsCCQ/QzucCCA7MyuXZ1eUBBQmTh8fo5/i9svIAAADh3vQAAAACCA0CCQ8CCQ4DDBQbGCUDChDr6vgAAAAAAAAREBIDCxK6tdfe2fTv7/cDCxIDDBQEDRUHDhgMJjXk4PZdXWdLUFoUNEYOKDgSMUMRLUBneI4eTGj08/QmW3onW3rTzvfOx/giU3IiVHMkWHdEaYJobHv3+PokWHpua6TNy9xZgZ+1quz8/foQKj0XPFInWn0nW38tZ4o6fqg8gq48grA9hrU/i7pAhrNAiLdBjLtEjr1FksNIjr5Il8pImMtKWnNqhL97odKFqti5q/q5rPq60+nCt/vLw/vPx/jV0vHY0/rc1/rg2/vh3fzn4fzu6/vx8vf19Pv19Pz49/v5+Pv8/Pv8/fr9/vv+/frziVtUAAAAT3RSTlMABQYHCAoNDhARGRobL0ZOV1xdXV5fYGBmZnB0eX2MjZSaoaGio6mqqqustLq7zubo6Ojo6evt7u/x8fLy9/f4+Pj5+vr6+vr6+/39/v7+XKgUSwAAAMhJREFUKM9jYGDg4OZmZgABKINT1dBAhBHIYFMxMBIDisjbhoZbCTExsCu5hoeY8DEwcOkEx8fY6MqpucTGB0izglVEplcU5/gmRYWBVQDNMK+s0hN3SvMyBpsBNJxXw0NfwTEjVQZqHQMHj5RfWW5mliSEC7TPzK6yJD/bXZQRzGdXcisqLy309okA2Q4Eis4peQWmstqBCdGW/CABraC45ERBBs3A6Fh/AbAKTwsHa34QZW8NVsGuLqwswQSjQICTmYMFQaEDAAF8JHLfKGswAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAlNJREFUGBkFwU2LVmUYAODrPu8Z5x1xSpRBXQyFoLsBE+wfiO5atJOgnf9DUPwFgtGinUgEaQsRhHYuMtpEiEWuG5iNjuOcj+c8z911xXcXL/68c3Dw1fzhg0QgEQAAEYGUKXFie9vxlSs/xk/rdavjGEkmkWSih65z4osv9GfOiK6LzEyZ2uGh4dUrmzs72ddlUUhkoiMr4PT167589Mh6c1N0nSRlqrX67dat+PDyZXRT19m5edPnt28rGFHxMcJ6d9fprS1/37tneP3aemPD1uamUydPOru3p5DdGOH0tWsu3LhhxIQJM2qEpRT/Pn3q/du3AhARSmvGTH0lplKMrVkiYpVpQaJlighzhDkzhmEA0fcWoqAfyaFW4zTlgCABxlrNmY4ylUzLsiREprFWc0T2M+ZSjKWY0AEaltZUjJixZJIpuk5pTWlNP2BYFvOyKJkCAKU1tTXHrZlqVWolUxdhxsfVSj9FmJfFMM9GdICGGa01HyMstYpMIFPJVNDPmYZSTOPoOEKHzNRlKpmWWh1j6TpLa2SKTKVWU6Z+Qolwdm/P9QcPZKa2LH69e9eIMs+WCL/cv2/98CGZPrt61am+V9APq1X89eyZ/968obVYaiXT4dGREgG+vnPHeHgYMsH2+fP+efEihtVKv7SWw/6+9/v7KYLMhIywTJPamvOXLomukyRsrNf+ePzYkpl9dJ3SWgSCSCQCfz5/7pMLF2yfO6eLiAQcHRz4/cmT+HR7O+Ob3d0fNt69+7a2BiICQCJbA0EgE5lpvbXl1OXL3/8Pfax4+6SjSukAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAB1FBMVEUAAAAAAAAAAABWYWwAAABbY3BbYm5dZnFdZXJeZnMEBAQHCAhYYGpdZnFdZnBgaHIlJyomKCooKi09QkdESU5eZGtdYmhdYmleY2lrcXdqb3Rqb3Rqb3SSmJ+SlJeWmJutr7GtrrCWm6ChpKhbW1tmZmZvb290dHR3d3d4eHh5eXl6enp8fHx+gIJ/f3+CgoKDg4OEhISFhYWHh4eKioqKjI2Li4uMjIyOjo6Pj4+QkJCRkZGSkpKUlJSVl5mWlpaYmZqZm52ampqbm5ucnJydnZ2enp6fn5+hoaGioqKkpKSkpaalpaWmp6mmp6qnqauoqKioqquoqq2qqqqrrK2srKysra6srrCsrrGurq6vr6+wsLCxsbGysrKztLa0tLS1t7m2tra3t7e4uLi5ubm6urq7u7u8vLy9vb2+vr7AwMDAwsTBwcHExcfFxcXFxsnGxsbHx8fIyMjJycnMzMzNzc3Ozs7O0NLPz8/Q0NDR0dHR09XT09PV1dXV1dbV1tfV19rW1tbX19fX19jY2tzZ2dnZ2tva2tra3N3a3N7c3Nze3t7f39/f4OHg4ODi4uLl5+jm5ubs7Ozs7e3u7u7v7+/v8PDw8PDx8fHy8vLz8/P29vYSoLMZAAAAJHRSTlMABAUGCwsNHCAiLzMzMzZEYGJwgIuOnJycnqmqq9bc3+/w8fkZ0N/uAAAA/klEQVQoU2NgYGDl5YMDdgYGBmZZ3964CYFtIR3e9Q7K/AwMHI55KfaFmcHWMy3K3MwlGRg4wz0zdYpcorRbNbL0LaWAAp3ts2umV8wo6MupTauQBgqUG03VL7W3sfZSb1erAgm02M+yzYrVCXUy6zapAQlUx/dEdyX3J3ZHVUYVywAF8o2rDNN1Go2jzGLMokAC2QbuSc42mXmaOXop9iAtCXrJ5qXWjT59Abl2ESJAAX/tSIMMiyrrqQ3T6uS5gQK6kSqpqkUermGTexQFmYACflqR+hlWZSamzQpCLEDPsSmVVDT1TJw0JUhOAMRnYOARFRMTE5cQF+ZiBPIAII5B3EVG0b4AAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABIFBMVEUhHyAAAABzPBnxaA3CWBEnJSYbGRptbW16enpzc3PTayWhb04hHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyAhHyApIh+0UhMfHiBWMhvsZg7zaQ0hHyAhHyAXHCHzaQ3xaA3xaA3xaA3xaA0hHyAhHyDxaA3xaA3xaA3xaA3xaA3xaA0oJickIiMdGxwUEhPxaA3xaA3xaA1sbGxwcHB3d3eFhYXxaA3xaA3xaA1zc3Nzc3Nzc3Nzc3Nzc3PxaA3xaA3xaA1zc3Nzc3NtdHjxaA3xaA1yc3STcFnvaA/yaAxzc3N4c2/FbDFzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3MhHyDxaA1zc3MAAAAfljyVAAAAXHRSTlMAAAAAAAAAAAAAAAAZkjMBHOLXYArj8p0u2VsJ1XaGL/OhKyXc1WEN2gwk2/SjKgEYiS4B/tYFGosqAdleAxzj12ML9Z8s850rJWbYeYMs1F8Koiri1V0MGZY0AYbIBFIAAAABYktHRAH/Ai3eAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wYXFBUVX81QWQAAAKxJREFUGNNVz9UWgkAQANDBtdbu7lZsxe7ubpH//wxBPKDzNvdMAmi0Oj0QQgAYjCazBX7BStvsDqHoAzTtdLklf+Dx+vwICRAIhsKRaCyOvpAwJ6Up8pXOZHOIAFm+UCzJEQuvMhWrIFBUa/WGkodmq40Ad7q9/kDFwnA05lpYYCbT2ZykFvxQDhhmuVpvcvxaHra7vfp72KflcMSYEOB0vlyx+By+3R9PMSfe+P0enM1454kAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTktMDYtMjRUMDM6MjE6MjEtMDc6MDDse6MAAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE5LTA2LTI0VDAzOjIxOjIxLTA3OjAwnSYbvAAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABsklEQVQ4y5WTy2pUQRCGv2rbzDjJeAlIBmOyipGIIJqFEBDElwh4yULGeRFXPoEIBl/AvQ/gC2RnxCAoxijiwks852S6+3dxzslcHJCpTXVX11/Xv0097gLPgVNMJxnQNfX4zsqleWbnpoMf/oa9d988MM9MC/rp+E0a+A0dsVobMNMCOO8B6McRoABJI+A6gJmN3D2A8jgEBCEkSEMBrcrsDAzDWWn3AjgKFaDMmgRqniGFgsaDp1jrLOngDf1XT1D+A1dFc4MKAkkiCVKjjVu7g9+4Rzx4i1u6hjXbuMWr0O5QPNvCu7IaCZwEKQukLGDrm5x8uI0tr6MkiGlkiv7yLfzN+6S5i6QsIMABkEfcxhbWWYMkVAOjxvYAjc3HNHrbKI9VBQBFwF25XQKSBjqIf1YBuAurEMrczgDygD6/x2LCpFLXLUyQ+PoldphhBhYfIX09XU1+Flaukz7uYqs3SHs7cG4BmTsmkBUF9mmXEwa28BNLPaQPLepuNcbGSWQquQC2/Kdcox1FUGkcB0ykck1nA2+wTzMs8stGnP4rbWGw74EuS/GFQWfK7/wF6P4F7fzIAYkdmdEAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABXFBMVEUPkPoNj/qExv0PkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoNj/oPkPoNj/oNj/qExv0PkPpruvwPkPornfoVk/opnPpnufwPkPqExv0Nj/oPkPoNj/oPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoPkPoOj/opnPsVk/oMjvoOkPoTkfo6pPsblfo3ovva7v7////v9/5Sr/whmPry+f5htvze8P7W7P5itvyl1v0imPu84P3o9P50v/zN6P73+/8lmvs8pfs+pfsKjvr9/v9EqfsNj/oom/v8/v9nufxAp/tJq/sQkPrb7v6t2f0IjPoclvr6/f9luPwUkvrp9f7h8f5ruvy/4f4kmftpuvwxoPum1v32+/8jmfpMrPvu9/7z+f9UsPs7pPv8/f/4/P9oufwalfpDqPsMj/ounvtVsPsnm/qzfQQ9AAAALXRSTlMAAAAggMzw0IYkBPb4iAamsgZ+jPwogpDO1vTYlPoulL4KivyUCiqO1PL01i67tUAWAAAAAWJLR0Q4oAel1gAAAAd0SU1FB+MGFxMuDXVcMbIAAADdSURBVBjTY2AAAmYWVjY2dg5OBgZGJiCXi4VbFwx4ePlAAlz8unAgIAgUENJFAsJMDMw8unp6+gaGRsYmpoa6IqIMYrp6ZuYWllbW5hY2toZ64gwSurp29g6OTs4urm7uHrqSDGy6nhZeet5WPr5+/gGBelJAgSCLYL+Q0LBw3YjIKKAAu250TGxcvE1CYlJySqquNAOHrl9aukVGZla2RU6uoZ4MA6esrl9evnWBYWFRMdBaOQYGXmSHyQNdyieA4CsogjzHpyQL4SqrqIJ9y8Cgpq6hqaWtogPyPgDmvSxRxBWM9AAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wNi0yNFQwMjo0NjoxMy0wNzowMCKUvXUAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTktMDYtMjRUMDI6NDY6MTMtMDc6MDBTyQXJAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAYUExURf///2RBpWRBpWRBpWRBpWRBpWRBpf///+zQyUYAAAAGdFJOUwFdZX0lTzs4r5oAAAABYktHRAcWYYjrAAAAB3RJTUUH4AINEi42iSXRNAAAAD1JREFUCNdjYEiDAAZGGIMtjQEEUBlMCWoEGci6mGEMsxQgIy0BiB3AjLS0FAYQIw0kwABipoI1AhkBQBIAFCIXxiHgq80AAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAEsUExURf///1Cf21Gg3FGi31Gh3VKj4FGh3lKj4VKk4lKl41Ol5FOn51Sp6VSo6FOn5lCf21Gg3FGh3VGi31Gi31Gh3lGg3FGg3FGg3FGg3FGh3lGg3FGi31Kk4lKj4FGh3lGi31Kk4lGh3lGg3FGh3lOm5FOm5VGi31Kj4VSo6FGi31Gh3VGg3FKj4FOn51Gi31So6FWr7VOl5FGi31On51Sq6lKk4lOo51Sp6VOm5FSq61Ws7VOn51Oo51Sq61Ol5FOm5FSq61Wr7VOo51On51Sr7FWs7VSp6lGg3FGh3VOm5FWr7VSp6lKj4VOm5FSo6FSr7FWs7VWs7VWr7VSq6lOo51Om5FOo51So6FOm5VOl5FSq61Ws7VSr7FSp6lSp6VWs7lWr7VKk4lSq6v///6E3MNsAAABVdFJOUwAAAAAAAAAAAAAAAAAAAB0Ii+3xnBVTJhfsMKb+qTEp9GwBF/7lLAbo0m4pLkUTdvk2Ev3+EZnOBo/3Z8ffCRzH/D0OqPxiLnvx3UI8m9n1++GwXQZNS29BAAAAAWJLR0QAiAUdSAAAAAd0SU1FB+ACDRIwBwy67tEAAADKSURBVBjTY2BAB4xogIGRH8IQEBQSFhEVE2eQkJQC8ZmkQ8PCI2Rk5RjkIxUUlRgZlVWioqNjYlXVGNQ14iI1tbR14qLj4+MTdJkZ9PQNosJCE0OjgPz4KEMWBiPjhPiEmKQokIJ4E1MmBmazhHg4MGdlYmCzsLSC8ROsmRkZmFht4Eps7ViADmOzd4DyHZ2YmYACTOzOLmATXd04mIBOd/eQ9owFCXh5c7KB/MLi4+vnHxAYFBzCwcYEEmBi5uLm4eHl42RmAnsSAMZBLgZiFUQ5AAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAxgDGAP8nNqN7AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gMZBjQQLEEqGwAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAA5SURBVDjLY2AYaMDIwMDwn1JD/lPCZhpwL+B1wf///ykzgBhDiAoDfIYQZQAjIyP5BuDTPJqQqAQAvW0ZAMk8+EEAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAACvlBMVEUCWv8HXf8AWv8AAAD///8AVP+bqP8AWv8AVO4AOqUAGkgAyf8APa0AL4QABAsASdEAVv8AUv8AUv8AVP8AWP8BWv8JXv8RYv8QYv8DW/8DXP8xdv9RiP9Af/8IXf8AKP8KXv8JXf8NYf8aaf8ATP0UZP0AVf8AT/8AT/8AVv8ATedvnPVAf/8AT/sYZvl0o/8PYf8udf8aa/8FXf8AVf8AOrRBe/Nvn/8AUv0aaPkXZ/8ATv8AKYQZYuwIXf8ca/wTZP8ASP8AED0HUNwZaf8xdPwDWv8AAAAAQMRcjvQAU/8AMZssb/Jmmf8AU/8AJXsRW+dSif8AUv8AAAAASdQtdP8ATv8AQ/8AQv8APbtKgfQud/8XZ/8TZP8FXP8AKIIcZO4wdP8AF08KU95tnv4gafhZi/Rnl/ZzofcocP8AAAAAQ8Q4efRwnvVmlvVcjvgrcfsAQsQAOK0APrwAQcUEStMLXPgDWv8AHE8APLEARdIAQ80ASeEAVf8AOJkAAAAAAAAAAAAABBMAJJIAY/9rmP+vxv90n/+buPv29/7C1P+zx/n///2Crv/7+fjs8f++z/f///3l6fX9/f/L2fj9/P5ilv9Nh/3h6f6vyf/D0vT///2lwP/Z5Pf3+P9OiP9klvr9/Puzyf+QsPX//fnW4v/k6vfv8/86ev94pfj///uRtf/y8vby9f9Fgv9EgPzt7/jj7f8mcf+eufj///x1pP/Z4fT///52pf9Uivv09fnV4v8ncf64yvj7+/6vxPX///yyyf9ynvr6+vvG1/8ocv3O2/fz9v53ofX8+/nb5v+YuPz//vy0yv8vdP3e5/fn7v/p7PX09//b5P7///6eu/9Df/zq7vjc5//I1vT//v3+/v////9+q/9Tivnn6fPy8/rW4fzI1/2qwv6YtPT8+fX39/jz9PqJrveTsvqfpuxrAAAAhXRSTlMAAAAAAAAAAAAAAAAAAAAABSlERA45nrSzP3TZ7e12Ao2LusMcrJYhFwaR/uhCwP/x5tZzBWHy+n3OvA8u17jmpwgPrOz5jAF2+3FA7PdYG8fuPQaX5jAGAV/39MCmdy/e/RGz/vj5/f/rAXj4//z13n52i5qmmFQ1lqOQaTgIBAYKEAYAKGjtAgAAAKNJREFUGBkFwT0uRGEYBtD3ZJ77uT8iGrXCAixCr7OCyRA2oCKqiUYkOgoJwhqUbMAKLEChVYhk4pxswvcWfFGVEbYtuJutqir9Ibc0uh0+V+mf5gY69yN2PzKJiTjCg8qa3uLRAJpKM9AMoL1VOi9zJ4CQ9z0jwHX+RAwAURUxAMSB/L7u35wCGlKaHrDkPGVmwhlc6FN6l1iHKxupn+djAPgHrEwa+qrzy0oAAAAASUVORK5CYII=';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAIdZUKh6sLlLkLmr4LmsAMp88NrdYVW3MZj7Acstkrt9s1e5E7vN5EfI9JvdtKwuBijp5kpbl30eiDt8aG1uqRr7qTyNehxM+k4PCy3enB3OTg6Ovv9PXw+fz////L9U5WAAAAAXRSTlMAQObYZgAAAIFJREFUeNplz90OwiAMBWAQpAoyxclkP3je/y0H2AQXz0WT8100rRD6kNI9/cRroemQL3hXhoujZYj4OHoAmBvYGcBISwbWBvfXCrytnIDUQMkbsBpagMA7zhtQdyTFQAmIG7IkYniiZuh3XGsPqoOZkMOJOpAcLqUzNFGGu/57fwc1hgtp0mVSyQAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAHCUExURQAAAAC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+jwC+j////54tRLAAAACUdFJOUwAAAEK+9/e+QQIDAwEqzskfUZmUNHz2mrT++V1w+f5tCanNFUDwfEjtjAyyvg027Hki27QMBJzaHE/1+FkNsN0iZvv6bxyAlB589lQeyud0KB8PQO+ZBUrc+eXgcRG/3CoATe316Wxw/P6BAgBt+fp4IAwh0d4zM9q7Fm76qi605EMSrvfX/PRtAivF9IAJNMLxhA2KYlJ9AAAAAWJLR0SVCGB6gwAAAAd0SU1FB+ACDRI2MOJd7FgAAADrSURBVBjTLY9VWwJgGEPfiYWBha2YYHcHditgd3d3odjdivvBfgK727nYsyPiCrw03j6+fv6AaAMCgyAI1lElJBQSFh6hBxDJqOiY2Lh4SEKiIQlITmFqWrqRJkhGJrOA7Bzm5uUXsBBSVMySUpSVs6KyqrqmFmKuY30D0NjU3NLa1t6h9jvZ1Q30WGi19fb1KzAwyKFhYGSUY+MTkwpMTXNmFpibX+Di0rICWFldW9/A5tb2zu7ePtTrg0MeHePklPYzuDRw7uDF5RWvbwC32O0d7x8en55f4DHF6xv5/vHp6f/k6/vH+evuf1LAObptvSvrAAAAAElFTkSuQmCC';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAw9JREFUOE9jYMABuMwYmCyTJKUCGlSnFSy02TTzeOyCiQcDViX26qVz2TAyYtWmEMwuoZ3M7V40LcB79pHkc0svpvzY8jD//87nxf+3Pyn8v/ZO8v+VNyP/2mZJumI1QCWSI8232Hjumitlfw5+qPp/9l8TCt76JP//xkdx/wsXWCzjtWFkwTCkbWFe9plPk/+ga4Txz/xt/D/hkN//gMXif21a+NbyWjIwoRiy6GDT5rP/mlFsPfyp5n/NpOj/22+0gMUXXIz/H7hC/L/bFKFbPDZMrHAD5H35OPt2J9zacDv/f3V7xv9FhwrBGubsT/1//Pjx/1GJ/mD+/nfl/1v3Ovy3KRJNQbHdOlXCvOO03/+pm1P/v3v37n90hhtYw9HPtf8Xb2v937cmHswHeWPRxYj/LvkK3igGKARwicTO07118H3V/5kbi/4vPZMJtK3s/6YH2f+Pfq1B8VbjWrdnMu5s4nAD9CNFhKwz5DTUvLl419zKvAcLtG1P84BRl/b/5M/6/6f/NPzf/qzo84yj0Uus0xUU4Zor54bm9+4OfZG02OCuoAMTb9ZkC9ull1Nvrr2Z+XvRpaRfc65H/68F+jl9svEhzyLFWoccWVc+eyTHq/twydjlKRln7jX9bNMkMJnbhoFRL1xCqmKx6/yi2fYXa/c5/e846PV/5fW0/7OPx/yfcjzop34ulxdGGvDuU8mMXaX507lBuiN6ueadmQeT/p/93vf/1O+G//sP5fw/eL3o/5JLif8zVxs+Tlir9S26UyeFQQvJGBE7FvaFZ9LfN+1y+WjbItSb3GmXvXd15v8zroH/HxgE/D+aGPx/18vi/z07PeZNPRKxe/Kh0Ae8toxscCO4zBkYXArk9C1SxJUYjBkYPPIVtbbuTftz3cz//2O9wP/75iSAXdO72/dt2HL5F6YlfBW4MiJYXMiBiW3t7azHBx+V/t89N+H/8a+1//e9K/9attDp5LQjYX8SuvVL8RoAkmxa65299Erq1FnHo0qrl7t4BddriIs4MrM3rfWcFd+pGwVSAwBZ0bKP8yrZPAAAAABJRU5ErkJggg==';","export default 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAMCAYAAABr5z2BAAABIklEQVQoz53LvUrDUBjG8bOoOammSf1IoBSvoCB4JeIqOHgBLt6AIMRBBQelWurQ2kERnMRBsBUcIp5FJSBI5oQsJVkkUHh8W0o5nhaFHvjBgef/Mq+Q46RJBMkI/vE+aOus956tnEswIZe1LV0QyJ5sE2GzgZfVMtRNIdiDpccEssdlB1mW4bvTwdvWJtRdErM7U+8S/FJykCRJX5qm+KpVce8UMNLRLbulz4iSjTAMh6Iowsd5BeNadp3nUF0VlxAEwZBotXC0Usa4ll3meZdA1iguwvf9vpvDA2wvmKgYGtSud8suDB4TyGr2PF49D/vra9jRZ1BVdknMzgwuCGSnZEObwu6sBnVTCHZiaC7BhFx2PKdxUidiAH/4lLo9Mv0DELVs9qsOHXwAAAAASUVORK5CYII=';","import $ from \"../platform/$\";\r\nimport CSS from \"../css/CSS\";\r\nimport { Conf } from \"../globals/globals\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nconst CustomCSS = {\r\n init() {\r\n if (!Conf['Custom CSS']) { return; }\r\n return this.addStyle();\r\n },\r\n\r\n addStyle() {\r\n return this.style = $.addStyle(CSS.sub(Conf['usercss']), 'custom-css', '#fourchanx-css');\r\n },\r\n\r\n rmStyle() {\r\n if (this.style) {\r\n $.rm(this.style);\r\n return delete this.style;\r\n }\r\n },\r\n\r\n update() {\r\n if (!this.style) {\r\n return this.addStyle();\r\n }\r\n return this.style.textContent = CSS.sub(Conf['usercss']);\r\n }\r\n};\r\nexport default CustomCSS;\r\n","import { Conf, d } from \"../globals/globals\";\r\nimport Main from \"../main/Main\";\r\nimport $ from \"../platform/$\";\r\nimport $$ from \"../platform/$$\";\r\nimport { dict } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nconst SWTinyboard = {\r\n isOPContainerThread: true,\r\n mayLackJSON: true,\r\n threadModTimeIgnoresSage: true,\r\n\r\n disabledFeatures: [\r\n 'Resurrect Quotes',\r\n 'Quick Reply Personas',\r\n 'Quick Reply',\r\n 'Cooldown',\r\n 'Report Link',\r\n 'Delete Link',\r\n 'Edit Link',\r\n 'Quote Inlining',\r\n 'Quote Previewing',\r\n 'Quote Backlinks',\r\n 'File Info Formatting',\r\n 'Image Expansion',\r\n 'Image Expansion (Menu)',\r\n 'Comment Expansion',\r\n 'Thread Expansion',\r\n 'Favicon',\r\n 'Quote Threading',\r\n 'Thread Updater',\r\n 'Banner',\r\n 'Flash Features',\r\n 'Reply Pruning'\r\n ],\r\n\r\n detect() {\r\n for (var script of $$('script:not([src])', d.head)) {\r\n var m;\r\n if (m = script.textContent.match(/\\bvar configRoot=(\".*?\")/)) {\r\n var properties = dict();\r\n try {\r\n var root = JSON.parse(m[1]);\r\n if (root[0] === '/') {\r\n properties.root = location.origin + root;\r\n } else if (/^https?:/.test(root)) {\r\n properties.root = root;\r\n }\r\n } catch (error) {}\r\n return properties;\r\n }\r\n }\r\n return false;\r\n },\r\n\r\n awaitBoard(cb) {\r\n let reactUI;\r\n if (reactUI = $.id('react-ui')) {\r\n const s = (this.selectors = Object.create(this.selectors));\r\n s.boardFor = {index: '.page-container'};\r\n s.thread = 'div[id^=\"thread_\"]';\r\n return Main.mounted(cb);\r\n } else {\r\n return cb();\r\n }\r\n },\r\n\r\n urls: {\r\n thread({siteID, boardID, threadID}, isArchived) {\r\n return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.html`;\r\n },\r\n post({postID}) { return `#${postID}`; },\r\n index({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/`; },\r\n catalog({siteID, boardID}) { return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/catalog.html`; },\r\n threadJSON({siteID, boardID, threadID}, isArchived) {\r\n const root = Conf['siteProperties'][siteID]?.root;\r\n if (root) { return `${root}${boardID}/${isArchived ? 'archive/' : ''}res/${threadID}.json`; } else { return ''; }\r\n },\r\n archivedThreadJSON(thread) {\r\n return SWTinyboard.urls.threadJSON(thread, true);\r\n },\r\n threadsListJSON({siteID, boardID}) {\r\n const root = Conf['siteProperties'][siteID]?.root;\r\n if (root) { return `${root}${boardID}/threads.json`; } else { return ''; }\r\n },\r\n archiveListJSON({siteID, boardID}) {\r\n const root = Conf['siteProperties'][siteID]?.root;\r\n if (root) { return `${root}${boardID}/archive/archive.json`; } else { return ''; }\r\n },\r\n catalogJSON({siteID, boardID}) {\r\n const root = Conf['siteProperties'][siteID]?.root;\r\n if (root) { return `${root}${boardID}/catalog.json`; } else { return ''; }\r\n },\r\n file({siteID, boardID}, filename) {\r\n return `${Conf['siteProperties'][siteID]?.root || `http://${siteID}/`}${boardID}/${filename}`;\r\n },\r\n thumb(board, filename) {\r\n return SWTinyboard.urls.file(board, filename);\r\n }\r\n },\r\n\r\n selectors: {\r\n board: 'form[name=\"postcontrols\"]',\r\n thread: 'input[name=\"board\"] ~ div[id^=\"thread_\"]',\r\n threadDivider: 'div[id^=\"thread_\"] > hr:last-child',\r\n summary: '.omitted',\r\n postContainer: 'div[id^=\"reply_\"]:not(.hidden)', // postContainer is thread for OP\r\n opBottom: '.op',\r\n replyOriginal: 'div[id^=\"reply_\"]:not(.hidden)',\r\n infoRoot: '.intro',\r\n info: {\r\n subject: '.subject',\r\n name: '.name',\r\n email: '.email',\r\n tripcode: '.trip',\r\n uniqueID: '.poster_id',\r\n capcode: '.capcode',\r\n flag: '.flag',\r\n date: 'time',\r\n nameBlock: 'label',\r\n quote: 'a[href*=\"#q\"]',\r\n reply: 'a[href*=\"/res/\"]:not([href*=\"#\"])'\r\n },\r\n icons: {\r\n isSticky: '.fa-thumb-tack',\r\n isClosed: '.fa-lock'\r\n },\r\n file: {\r\n text: '.fileinfo',\r\n link: '.fileinfo > a',\r\n thumb: 'a > .post-image'\r\n },\r\n thumbLink: '.file > a',\r\n multifile: '.files > .file',\r\n highlightable: {\r\n op: ' > .op',\r\n reply: '.reply',\r\n catalog: ' > .thread'\r\n },\r\n comment: '.body',\r\n spoiler: '.spoiler',\r\n quotelink: 'a[onclick*=\"highlightReply(\"]',\r\n catalog: {\r\n board: '#Grid',\r\n thread: '.mix',\r\n thumb: '.thread-image'\r\n },\r\n boardList: '.boardlist',\r\n boardListBottom: '.boardlist.bottom',\r\n styleSheet: '#stylesheet',\r\n psa: '.blotter',\r\n nav: {\r\n prev: '.pages > form > [value=Previous]',\r\n next: '.pages > form > [value=Next]'\r\n }\r\n },\r\n\r\n classes: {\r\n highlight: 'highlighted'\r\n },\r\n\r\n xpath: {\r\n thread: 'div[starts-with(@id,\"thread_\")]',\r\n postContainer: 'div[starts-with(@id,\"reply_\") or starts-with(@id,\"thread_\")]',\r\n replyContainer: 'div[starts-with(@id,\"reply_\")]'\r\n },\r\n\r\n regexp: {\r\n quotelink:\r\n new RegExp(`\\\r\n/\\\r\n([^/]+)\\\r\n/res/\\\r\n(\\\\d+)\\\r\n(?:\\\\.\\\\w+)?#\\\r\n(\\\\d+)\\\r\n$\\\r\n`),\r\n quotelinkHTML:\r\n /]*\\bhref=\"[^\"]*\\/([^\\/]+)\\/res\\/(\\d+)(?:\\.\\w+)?#(\\d+)\"/g\r\n },\r\n\r\n Build: {\r\n parseJSON(data, board) {\r\n const o = this.parseJSON(data, board);\r\n if (data.ext === 'deleted') {\r\n delete o.file;\r\n $.extend(o, {\r\n files: [],\r\n fileDeleted: true,\r\n filesDeleted: [0]\r\n });\r\n }\r\n if (data.extra_files) {\r\n let file;\r\n for (let i = 0; i < data.extra_files.length; i++) {\r\n var extra_file = data.extra_files[i];\r\n if (extra_file.ext === 'deleted') {\r\n o.filesDeleted.push(i);\r\n } else {\r\n file = this.parseJSONFile(data, board);\r\n o.files.push(file);\r\n }\r\n }\r\n if (o.files.length) {\r\n o.file = o.files[0];\r\n }\r\n }\r\n return o;\r\n },\r\n\r\n parseComment(html) {\r\n html = html\r\n .replace(//gi, '\\n')\r\n .replace(/<[^>]*>/g, '');\r\n return $.unescape(html);\r\n }\r\n },\r\n\r\n bgColoredEl() {\r\n return $.el('div', {className: 'post reply'});\r\n },\r\n\r\n isFileURL(url) {\r\n return /\\/src\\/[^\\/]+/.test(url.pathname);\r\n },\r\n\r\n preParsingFixes(board) {\r\n // fixes effects of unclosed link in announcement\r\n let broken;\r\n if (broken = $('a > input[name=\"board\"]', board)) {\r\n return $.before(broken.parentNode, broken);\r\n }\r\n },\r\n\r\n parseNodes(post, nodes) {\r\n // Add vichan's span.poster_id around the ID if not already present.\r\n let m;\r\n if (nodes.uniqueID) { return; }\r\n let text = '';\r\n let node = nodes.nameBlock.nextSibling;\r\n while (node && (node.nodeType === 3)) {\r\n text += node.textContent;\r\n node = node.nextSibling;\r\n }\r\n if (m = text.match(/(\\s*ID:\\s*)(\\S+)/)) {\r\n let uniqueID;\r\n nodes.info.normalize();\r\n let {nextSibling} = nodes.nameBlock;\r\n nextSibling = nextSibling.splitText(m[1].length);\r\n nextSibling.splitText(m[2].length);\r\n nodes.uniqueID = (uniqueID = $.el('span', {className: 'poster_id'}));\r\n $.replace(nextSibling, uniqueID);\r\n return $.add(uniqueID, nextSibling);\r\n }\r\n },\r\n\r\n parseDate(node) {\r\n let date = Date.parse(node.getAttribute('datetime')?.trim());\r\n if (!isNaN(date)) { return new Date(date); }\r\n date = Date.parse(node.textContent.trim() + ' UTC'); // e.g. onesixtwo.club\r\n if (!isNaN(date)) { return new Date(date); }\r\n return undefined;\r\n },\r\n\r\n parseFile(post, file) {\r\n let info, infoNode;\r\n const {text, link, thumb} = file;\r\n if ($.x(`ancestor::${this.xpath.postContainer}[1]`, text) !== post.nodes.root) { return false; } // file belongs to a reply\r\n if (!(infoNode = link.nextSibling?.textContent.includes('(') ? link.nextSibling : link.nextElementSibling)) { return false; }\r\n if (!(info = infoNode.textContent.match(/\\((.*,\\s*)?([\\d.]+ ?[KMG]?B).*\\)/))) { return false; }\r\n const nameNode = $('.postfilename', text);\r\n $.extend(file, {\r\n name: nameNode ? (nameNode.title || nameNode.textContent) : link.pathname.match(/[^/]*$/)[0],\r\n size: info[2],\r\n dimensions: info[0].match(/\\d+x\\d+/)?.[0]\r\n });\r\n if (thumb) {\r\n $.extend(file, {\r\n thumbURL: /\\/static\\//.test(thumb.src) && $.isImage(link.href) ? link.href : thumb.src,\r\n isSpoiler: /^Spoiler/i.test(info[1] || '') || (link.textContent === 'Spoiler Image')\r\n }\r\n );\r\n }\r\n return true;\r\n },\r\n\r\n isThumbExpanded(file) {\r\n // Detect old Tinyboard image expansion that changes src attribute on thumbnail.\r\n return $.hasClass(file.thumb.parentNode, 'expanded') || (file.thumb.parentNode.dataset.expanded === 'true');\r\n },\r\n\r\n isLinkified(link) {\r\n return /\\bnofollow\\b/.test(link.rel);\r\n },\r\n\r\n catalogPin(threadRoot) {\r\n return threadRoot.dataset.sticky = 'true';\r\n }\r\n};\r\nexport default SWTinyboard;\r\n",null,"import { Conf, d } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport PassMessagePage from './PassMessage/PassMessageHtml';\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\n\r\nconst PassMessage = {\r\n init() {\r\n if (Conf['passMessageClosed']) { return; }\r\n const msg = $.el('div',\r\n {className: 'box-outer top-box'}\r\n ,\r\n PassMessagePage);\r\n msg.style.cssText = 'padding-bottom: 0;';\r\n const close = $('a', msg);\r\n $.on(close, 'click', function() {\r\n $.rm(msg);\r\n return $.set('passMessageClosed', true);\r\n });\r\n return $.ready(function() {\r\n let hd;\r\n if (hd = $.id('hd')) {\r\n return $.after(hd, msg);\r\n } else {\r\n return $.prepend(d.body, msg);\r\n }\r\n });\r\n }\r\n};\r\nexport default PassMessage;\r\n","import Redirect from \"../Archive/Redirect\";\r\nimport $ from \"../platform/$\";\r\nimport ReportPage from './Report/ArchiveReport.html';\r\nimport CSS from \"../css/CSS\";\r\nimport Captcha from \"../Posting/Captcha\";\r\nimport { Conf, d, g } from \"../globals/globals\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\n\r\nvar Report = {\r\n init() {\r\n let match;\r\n if (!(match = location.search.match(/\\bno=(\\d+)/))) { return; }\r\n Captcha.replace.init();\r\n this.postID = +match[1];\r\n return $.ready(this.ready);\r\n },\r\n\r\n ready() {\r\n $.addStyle(CSS.report);\r\n\r\n if (Conf['Archive Report']) { Report.archive(); }\r\n\r\n new MutationObserver(function() {\r\n Report.fit('iframe[src^=\"https://www.google.com/recaptcha/api2/frame\"]');\r\n return Report.fit('body');\r\n }).observe(d.body, {\r\n childList: true,\r\n attributes: true,\r\n subtree: true\r\n }\r\n );\r\n return Report.fit('body');\r\n },\r\n\r\n fit(selector) {\r\n let el;\r\n if (!((el = $(selector, doc)) && (getComputedStyle(el).visibility !== 'hidden'))) { return; }\r\n const dy = (el.getBoundingClientRect().bottom - doc.clientHeight) + 8;\r\n if (dy > 0) { return window.resizeBy(0, dy); }\r\n },\r\n\r\n archive() {\r\n let match, urls;\r\n if (!(urls = Redirect.report(g.BOARD.ID)).length) { return; }\r\n\r\n const form = $('form');\r\n const types = $.id('reportTypes');\r\n const message = $('h3');\r\n\r\n const fieldset = $.el('fieldset', {\r\n id: 'archive-report',\r\n hidden: true\r\n }\r\n ,\r\n { innerHTML: ReportPage });\r\n const enabled = $('#archive-report-enabled', fieldset);\r\n const reason = $('#archive-report-reason', fieldset);\r\n const submit = $('#archive-report-submit', fieldset);\r\n\r\n $.on(enabled, 'change', function() {\r\n return reason.disabled = !this.checked;\r\n });\r\n\r\n if (form && types) {\r\n fieldset.hidden = !$('[value=\"31\"]', types).checked;\r\n $.on(types, 'change', function(e) {\r\n fieldset.hidden = (e.target.value !== '31');\r\n return Report.fit('body');\r\n });\r\n $.after(types, fieldset);\r\n Report.fit('body');\r\n $.one(form, 'submit', function(e) {\r\n if (!fieldset.hidden && enabled.checked) {\r\n e.preventDefault();\r\n return Report.archiveSubmit(urls, reason.value, results => {\r\n this.action = '#archiveresults=' + encodeURIComponent(JSON.stringify(results));\r\n return this.submit();\r\n });\r\n }\r\n });\r\n } else if (message) {\r\n fieldset.hidden = /Report submitted!/.test(message.textContent);\r\n $.on(enabled, 'change', function() {\r\n return submit.hidden = !this.checked;\r\n });\r\n $.after(message, fieldset);\r\n $.on(submit, 'click', () => Report.archiveSubmit(urls, reason.value, Report.archiveResults));\r\n }\r\n\r\n if (match = location.hash.match(/^#archiveresults=(.*)$/)) {\r\n try {\r\n return Report.archiveResults(JSON.parse(decodeURIComponent(match[1])));\r\n } catch (error) {}\r\n }\r\n },\r\n\r\n archiveSubmit(urls, reason, cb) {\r\n const form = $.formData({\r\n board: g.BOARD.ID,\r\n num: Report.postID,\r\n reason\r\n });\r\n const results = [];\r\n for (var [name, url] of urls) {\r\n (function(name, url) {\r\n return $.ajax(url, {\r\n onloadend() {\r\n results.push([name, this.response || {error: ''}]);\r\n if (results.length === urls.length) {\r\n return cb(results);\r\n }\r\n },\r\n form\r\n });\r\n })(name, url);\r\n }\r\n },\r\n\r\n archiveResults(results) {\r\n const fieldset = $.id('archive-report');\r\n for (var [name, response] of results) {\r\n var line = $.el('h3',\r\n {className: 'archive-report-response'});\r\n if ('success' in response) {\r\n $.addClass(line, 'archive-report-success');\r\n line.textContent = `${name}: ${response.success}`;\r\n } else {\r\n $.addClass(line, 'archive-report-error');\r\n line.textContent = `${name}: ${response.error || 'Error reporting post.'}`;\r\n }\r\n if (fieldset) {\r\n $.before(fieldset, line);\r\n } else {\r\n $.add(d.body, line);\r\n }\r\n }\r\n }\r\n};\r\nexport default Report;\r\n","import DataBoard from \"../classes/DataBoard\";\r\nimport { Conf, d, g } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nconst PostSuccessful = {\r\n init() {\r\n if (!Conf['Remember Your Posts']) { return; }\r\n return $.ready(this.ready);\r\n },\r\n\r\n ready() {\r\n if (d.title !== 'Post successful!') { return; }\r\n\r\n let [_, threadID, postID] = $('h1').nextSibling.textContent.match(/thread:(\\d+),no:(\\d+)/);\r\n postID = +postID;\r\n threadID = +threadID || postID;\r\n\r\n const db = new DataBoard('yourPosts');\r\n return db.set({\r\n boardID: g.BOARD.ID,\r\n threadID,\r\n postID,\r\n val: true\r\n });\r\n }\r\n};\r\nexport default PostSuccessful;\r\n",null,null,null,null,"import SWTinyboard from \"./SW.tinyboard\";\r\nimport SWYotsuba from \"./SW.yotsuba\";\r\n\r\nconst SW = { tinyboard: SWTinyboard, yotsuba: SWYotsuba };\r\nexport default SW;\r\n",null,null,"import Callbacks from \"../classes/Callbacks\";\r\nimport Header from \"../General/Header\";\r\nimport UI from \"../General/UI\";\r\nimport { g, Conf, E, d } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport QuoteThreading from \"../Quotelinks/QuoteThreading\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS104: Avoid inline assignments\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar ReplyPruning = {\r\n init() {\r\n if ((g.VIEW !== 'thread') || !Conf['Reply Pruning']) { return; }\r\n\r\n this.container = $.frag();\r\n\r\n this.summary = $.el('span', {\r\n hidden: true,\r\n className: 'summary'\r\n }\r\n );\r\n this.summary.style.cursor = 'pointer';\r\n $.on(this.summary, 'click', () => {\r\n this.inputs.enabled.checked = !this.inputs.enabled.checked;\r\n return $.event('change', null, this.inputs.enabled);\r\n });\r\n\r\n const label = UI.checkbox('Prune Replies', 'Show Last', Conf['Prune All Threads']);\r\n const el = $.el('span',\r\n {title: 'Maximum number of replies to show.'}\r\n ,\r\n {innerHTML: \" \"});\r\n $.prepend(el, label);\r\n\r\n this.inputs = {\r\n enabled: label.firstElementChild,\r\n replies: el.lastElementChild\r\n };\r\n\r\n this.setEnabled.call(this.inputs.enabled);\r\n $.on(this.inputs.enabled, 'change', this.setEnabled);\r\n $.on(this.inputs.replies, 'change', $.cb.value);\r\n\r\n Header.menu.addEntry({\r\n el,\r\n order: 190\r\n });\r\n\r\n return Callbacks.Thread.push({\r\n name: 'Reply Pruning',\r\n cb: this.node\r\n });\r\n },\r\n\r\n position: 0,\r\n hidden: 0,\r\n hiddenFiles: 0,\r\n total: 0,\r\n totalFiles: 0,\r\n\r\n setEnabled() {\r\n const other = QuoteThreading.input;\r\n if (this.checked && other?.checked) {\r\n other.checked = false;\r\n $.event('change', null, other);\r\n }\r\n return ReplyPruning.active = this.checked;\r\n },\r\n\r\n showIfHidden(id) {\r\n if (ReplyPruning.container && $(`#${id}`, ReplyPruning.container)) {\r\n ReplyPruning.inputs.enabled.checked = false;\r\n return $.event('change', null, ReplyPruning.inputs.enabled);\r\n }\r\n },\r\n\r\n node() {\r\n let middle;\r\n ReplyPruning.thread = this;\r\n\r\n if (this.isSticky) {\r\n ReplyPruning.active = (ReplyPruning.inputs.enabled.checked = true);\r\n if (QuoteThreading.input) {\r\n // Disable Quote Threading for this thread but don't save the setting.\r\n Conf['Thread Quotes'] = (QuoteThreading.input.checked = false);\r\n }\r\n }\r\n\r\n this.posts.forEach(function(post) {\r\n if (post.isReply) {\r\n ReplyPruning.total++;\r\n if (post.file) { return ReplyPruning.totalFiles++; }\r\n }\r\n });\r\n\r\n // If we're linked to a post that we would hide, don't hide the posts in the first place.\r\n if (\r\n ReplyPruning.active &&\r\n /^#p\\d+$/.test(location.hash) &&\r\n (1 <= (middle = this.posts.keys.indexOf(location.hash.slice(2))) && middle < 1 + Math.max(ReplyPruning.total - +Conf[\"Max Replies\"], 0))\r\n ) {\r\n ReplyPruning.active = (ReplyPruning.inputs.enabled.checked = false);\r\n }\r\n\r\n $.after(this.OP.nodes.root, ReplyPruning.summary);\r\n\r\n $.on(ReplyPruning.inputs.enabled, 'change', ReplyPruning.update);\r\n $.on(ReplyPruning.inputs.replies, 'change', ReplyPruning.update);\r\n $.on(d, 'ThreadUpdate', ReplyPruning.updateCount);\r\n $.on(d, 'ThreadUpdate', ReplyPruning.update);\r\n\r\n return ReplyPruning.update();\r\n },\r\n\r\n updateCount(e) {\r\n if (e.detail[404]) { return; }\r\n for (var fullID of e.detail.newPosts) {\r\n ReplyPruning.total++;\r\n if (g.posts.get(fullID).file) { ReplyPruning.totalFiles++; }\r\n }\r\n },\r\n\r\n update() {\r\n let boardTop, node, post;\r\n const hidden1 = ReplyPruning.hidden;\r\n const hidden2 = ReplyPruning.active ?\r\n Math.max(ReplyPruning.total - +Conf[\"Max Replies\"], 0)\r\n :\r\n 0;\r\n\r\n // Record position from bottom of document\r\n const oldPos = d.body.clientHeight - window.scrollY;\r\n\r\n const {posts} = ReplyPruning.thread;\r\n\r\n if (ReplyPruning.hidden < hidden2) {\r\n while ((ReplyPruning.hidden < hidden2) && (ReplyPruning.position < posts.keys.length)) {\r\n post = posts.get(posts.keys[ReplyPruning.position++]);\r\n if (post.isReply && !post.isFetchedQuote) {\r\n while ((node = ReplyPruning.summary.nextSibling) && (node !== post.nodes.root)) { $.add(ReplyPruning.container, node); }\r\n $.add(ReplyPruning.container, post.nodes.root);\r\n ReplyPruning.hidden++;\r\n if (post.file) { ReplyPruning.hiddenFiles++; }\r\n }\r\n }\r\n\r\n } else if (ReplyPruning.hidden > hidden2) {\r\n const frag = $.frag();\r\n while ((ReplyPruning.hidden > hidden2) && (ReplyPruning.position > 0)) {\r\n post = posts.get(posts.keys[--ReplyPruning.position]);\r\n if (post.isReply && !post.isFetchedQuote) {\r\n while ((node = ReplyPruning.container.lastChild) && (node !== post.nodes.root)) { $.prepend(frag, node); }\r\n $.prepend(frag, post.nodes.root);\r\n ReplyPruning.hidden--;\r\n if (post.file) { ReplyPruning.hiddenFiles--; }\r\n }\r\n }\r\n $.after(ReplyPruning.summary, frag);\r\n $.event('PostsInserted', null, ReplyPruning.summary.parentNode);\r\n }\r\n\r\n ReplyPruning.summary.textContent = ReplyPruning.active ?\r\n g.SITE.Build.summaryText('+', ReplyPruning.hidden, ReplyPruning.hiddenFiles)\r\n :\r\n g.SITE.Build.summaryText('-', ReplyPruning.total, ReplyPruning.totalFiles);\r\n ReplyPruning.summary.hidden = (ReplyPruning.total <= +Conf[\"Max Replies\"]);\r\n\r\n // Maintain position in thread when posts are added/removed above\r\n if ((hidden1 !== hidden2) && ((boardTop = Header.getTopOf($('.board'))) < 0)) {\r\n return window.scrollBy(0, Math.max(d.body.clientHeight - oldPos, window.scrollY + boardTop) - window.scrollY);\r\n }\r\n }\r\n};\r\nexport default ReplyPruning;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport RandomAccessList from \"../classes/RandomAccessList\";\r\nimport Header from \"../General/Header\";\r\nimport { Conf, d, g } from \"../globals/globals\";\r\nimport ReplyPruning from \"../Monitoring/ReplyPruning\";\r\nimport Unread from \"../Monitoring/Unread\";\r\nimport $ from \"../platform/$\";\r\nimport { dict } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\n/*\r\n <3 aeosynth\r\n*/\r\n\r\nvar QuoteThreading = {\r\n init() {\r\n if (!Conf['Quote Threading'] || (g.VIEW !== 'thread')) { return; }\r\n\r\n this.controls = $.el('label',\r\n {innerHTML: \" Threading\"});\r\n\r\n this.threadNewLink = $.el('span', {\r\n className: 'brackets-wrap threadnewlink',\r\n hidden: true\r\n }\r\n );\r\n $.extend(this.threadNewLink, {innerHTML: \"Thread New Posts\"});\r\n\r\n this.input = $('input', this.controls);\r\n this.input.checked = Conf['Thread Quotes'];\r\n\r\n $.on(this.input, 'change', this.setEnabled);\r\n $.on(this.input, 'change', this.rethread);\r\n $.on(this.threadNewLink.firstElementChild, 'click', this.rethread);\r\n $.on(d, '4chanXInitFinished', () => { return this.ready = true; });\r\n\r\n Header.menu.addEntry(this.entry = {\r\n el: this.controls,\r\n order: 99\r\n }\r\n );\r\n\r\n Callbacks.Thread.push({\r\n name: 'Quote Threading',\r\n cb: this.setThread\r\n });\r\n\r\n return Callbacks.Post.push({\r\n name: 'Quote Threading',\r\n cb: this.node\r\n });\r\n },\r\n\r\n parent: dict(),\r\n children: dict(),\r\n inserted: dict(),\r\n\r\n toggleThreading() {\r\n return this.setThreadingState(!Conf['Thread Quotes']);\r\n },\r\n\r\n setThreadingState(enabled) {\r\n this.input.checked = enabled;\r\n this.setEnabled.call(this.input);\r\n return this.rethread.call(this.input);\r\n },\r\n\r\n setEnabled() {\r\n if (this.checked) {\r\n $.set('Prune All Threads', false);\r\n const other = ReplyPruning.inputs?.enabled;\r\n if (other?.checked) {\r\n other.checked = false;\r\n $.event('change', null, other);\r\n }\r\n }\r\n return $.cb.checked.call(this);\r\n },\r\n\r\n setThread() {\r\n QuoteThreading.thread = this;\r\n return $.asap((() => !Conf['Thread Updater'] || $('.navLinksBot > .updatelink')), function() {\r\n let navLinksBot;\r\n if (navLinksBot = $('.navLinksBot')) { return $.add(navLinksBot, [$.tn(' '), QuoteThreading.threadNewLink]); }\r\n });\r\n },\r\n\r\n node() {\r\n let parent;\r\n if (this.isFetchedQuote || this.isClone || !this.isReply) { return; }\r\n\r\n const parents = new Set();\r\n let lastParent = null;\r\n for (var quote of this.quotes) {\r\n if ((parent = g.posts.get(quote))) {\r\n if (!parent.isFetchedQuote && parent.isReply && (parent.ID < this.ID)) {\r\n parents.add(parent.ID);\r\n if (!lastParent || (parent.ID > lastParent.ID)) { lastParent = parent; }\r\n }\r\n }\r\n }\r\n\r\n if (!lastParent) { return; }\r\n\r\n let ancestor = lastParent;\r\n while ((ancestor = QuoteThreading.parent[ancestor.fullID])) {\r\n parents.delete(ancestor.ID);\r\n }\r\n\r\n if (parents.size === 1) {\r\n return QuoteThreading.parent[this.fullID] = lastParent;\r\n }\r\n },\r\n\r\n descendants(post) {\r\n let children;\r\n let posts = [post];\r\n if (children = QuoteThreading.children[post.fullID]) {\r\n for (var child of children) {\r\n posts = posts.concat(QuoteThreading.descendants(child));\r\n }\r\n }\r\n return posts;\r\n },\r\n\r\n insert(post) {\r\n let parent, x;\r\n if (!(\r\n Conf['Thread Quotes'] &&\r\n (parent = QuoteThreading.parent[post.fullID]) &&\r\n !QuoteThreading.inserted[post.fullID]\r\n )) { return false; }\r\n\r\n const descendants = QuoteThreading.descendants(post);\r\n if (!Unread.posts.has(parent.ID)) {\r\n if ((function() { for (var x of descendants) { if (Unread.posts.has(x.ID)) { return true; } } })()) {\r\n QuoteThreading.threadNewLink.hidden = false;\r\n return false;\r\n }\r\n }\r\n\r\n const {order} = Unread;\r\n const children = (QuoteThreading.children[parent.fullID] || (QuoteThreading.children[parent.fullID] = []));\r\n const threadContainer = parent.nodes.threadContainer || $.el('div', {className: 'threadContainer'});\r\n const nodes = [post.nodes.root];\r\n if (post.nodes.threadContainer) { nodes.push(post.nodes.threadContainer); }\r\n\r\n let i = children.length;\r\n for (let j = children.length - 1; j >= 0; j--) { var child = children[j]; if (child.ID >= post.ID) { i--; } }\r\n if (i !== children.length) {\r\n const next = children[i];\r\n for (x of descendants) { order.before(order[next.ID], order[x.ID]); }\r\n children.splice(i, 0, post);\r\n $.before(next.nodes.root, nodes);\r\n } else {\r\n let prev2;\r\n let prev = parent;\r\n while ((prev2 = QuoteThreading.children[prev.fullID]) && prev2.length) {\r\n prev = prev2[prev2.length-1];\r\n }\r\n for (let k = descendants.length - 1; k >= 0; k--) { x = descendants[k]; order.after(order[prev.ID], order[x.ID]); }\r\n children.push(post);\r\n $.add(threadContainer, nodes);\r\n }\r\n\r\n QuoteThreading.inserted[post.fullID] = true;\r\n\r\n if (!parent.nodes.threadContainer) {\r\n parent.nodes.threadContainer = threadContainer;\r\n $.addClass(parent.nodes.root, 'threadOP');\r\n $.after(parent.nodes.root, threadContainer);\r\n }\r\n\r\n return true;\r\n },\r\n\r\n rethread() {\r\n if (!QuoteThreading.ready) { return; }\r\n const {thread} = QuoteThreading;\r\n const {posts} = thread;\r\n\r\n QuoteThreading.threadNewLink.hidden = true;\r\n\r\n if (Conf['Thread Quotes']) {\r\n posts.forEach(QuoteThreading.insert);\r\n } else {\r\n const nodes = [];\r\n Unread.order = new RandomAccessList();\r\n QuoteThreading.inserted = dict();\r\n posts.forEach(function(post) {\r\n if (post.isFetchedQuote) { return; }\r\n Unread.order.push(post);\r\n if (post.isReply) { nodes.push(post.nodes.root); }\r\n if (QuoteThreading.children[post.fullID]) {\r\n delete QuoteThreading.children[post.fullID];\r\n $.rmClass(post.nodes.root, 'threadOP');\r\n $.rm(post.nodes.threadContainer);\r\n return delete post.nodes.threadContainer;\r\n }\r\n });\r\n $.add(thread.nodes.root, nodes);\r\n }\r\n\r\n Unread.position = Unread.order.first;\r\n Unread.updatePosition();\r\n Unread.setLine(true);\r\n Unread.read();\r\n return Unread.update();\r\n }\r\n};\r\nexport default QuoteThreading;\r\n","import Beep from './ThreadUpdater/beep.wav';\r\nimport $ from \"../platform/$\";\r\nimport Callbacks from '../classes/Callbacks';\r\nimport Notice from '../classes/Notice';\r\nimport Post from '../classes/Post';\r\nimport Main from '../main/Main';\r\nimport Config from '../config/Config';\r\nimport Settings from '../General/Settings';\r\nimport QuoteThreading from '../Quotelinks/QuoteThreading';\r\nimport Unread from './Unread';\r\nimport Header from '../General/Header';\r\nimport { g, Conf, d, doc } from '../globals/globals';\r\nimport UI from '../General/UI';\r\nimport { MINUTE, SECOND } from '../platform/helpers';\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS201: Simplify complex destructure assignments\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\n\r\nvar ThreadUpdater = {\r\n init() {\r\n let el, name, sc;\r\n if ((g.VIEW !== 'thread') || !Conf['Thread Updater']) { return; }\r\n this.enabled = true;\r\n\r\n // Chromium won't play audio created in an inactive tab until the tab has been focused, so set it up now.\r\n // XXX Sometimes the loading stalls in Firefox, esp. when opening in private browsing window followed by normal window.\r\n // Don't let it keep the loading icon on indefinitely.\r\n this.audio = $.el('audio');\r\n if ($.engine !== 'gecko') { this.audio.src = this.beep; }\r\n\r\n if (Conf['Updater and Stats in Header']) {\r\n this.dialog = (sc = $.el('span',\r\n {id: 'updater'}));\r\n $.extend(sc, {innerHTML: ''});\r\n Header.addShortcut('updater', sc, 100);\r\n } else {\r\n this.dialog = (sc = UI.dialog('updater',\r\n {innerHTML: '
    '}));\r\n $.addClass(doc, 'float');\r\n $.ready(() => $.add(d.body, sc));\r\n }\r\n\r\n this.checkPostCount = 0;\r\n\r\n this.timer = $('#update-timer', sc);\r\n this.status = $('#update-status', sc);\r\n\r\n $.on(this.timer, 'click', this.update);\r\n $.on(this.status, 'click', this.update);\r\n\r\n const updateLink = $.el('span',\r\n {className: 'brackets-wrap updatelink'});\r\n $.extend(updateLink, {innerHTML: 'Update'});\r\n Main.ready(function() {\r\n let navLinksBot;\r\n if (navLinksBot = $('.navLinksBot')) { return $.add(navLinksBot, [$.tn(' '), updateLink]); }\r\n });\r\n $.on(updateLink.firstElementChild, 'click', this.update);\r\n\r\n const subEntries = [];\r\n for (name in Config.updater.checkbox) {\r\n var conf = Config.updater.checkbox[name];\r\n el = UI.checkbox(name, name);\r\n el.title = conf[1];\r\n var input = el.firstElementChild;\r\n $.on(input, 'change', $.cb.checked);\r\n if (input.name === 'Scroll BG') {\r\n $.on(input, 'change', this.cb.scrollBG);\r\n this.cb.scrollBG();\r\n } else if (input.name === 'Auto Update') {\r\n $.on(input, 'change', this.setInterval);\r\n }\r\n subEntries.push({el});\r\n }\r\n\r\n this.settings = $.el('span',\r\n {innerHTML: 'Interval'});\r\n\r\n $.on(this.settings, 'click', this.intervalShortcut);\r\n\r\n subEntries.push({el: this.settings});\r\n\r\n Header.menu.addEntry(this.entry = {\r\n el: $.el('span',\r\n {textContent: 'Updater'}),\r\n order: 110,\r\n subEntries\r\n }\r\n );\r\n\r\n return Callbacks.Thread.push({\r\n name: 'Thread Updater',\r\n cb: this.node\r\n });\r\n },\r\n\r\n node() {\r\n ThreadUpdater.thread = this;\r\n ThreadUpdater.root = this.nodes.root;\r\n ThreadUpdater.outdateCount = 0;\r\n\r\n // We must keep track of our own list of live posts/files\r\n // to provide an accurate deletedPosts/deletedFiles on update\r\n // as posts may be `kill`ed elsewhere.\r\n ThreadUpdater.postIDs = [];\r\n ThreadUpdater.fileIDs = [];\r\n this.posts.forEach(function(post) {\r\n ThreadUpdater.postIDs.push(post.ID);\r\n if (post.file) { return ThreadUpdater.fileIDs.push(post.ID); }\r\n });\r\n\r\n ThreadUpdater.cb.interval.call($.el('input', {value: Conf['Interval']}));\r\n\r\n $.on(d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost);\r\n $.on(d, 'visibilitychange', ThreadUpdater.cb.visibility);\r\n\r\n return ThreadUpdater.setInterval();\r\n },\r\n\r\n /*\r\n http://freesound.org/people/pierrecartoons1979/sounds/90112/\r\n cc-by-nc-3.0\r\n */\r\n beep: `data:audio/wav;base64,${Beep}`,\r\n\r\n playBeep() {\r\n const {audio} = ThreadUpdater;\r\n if (!audio.src) { audio.src = ThreadUpdater.beep; }\r\n if (audio.paused) {\r\n return audio.play();\r\n } else {\r\n return $.one(audio, 'ended', ThreadUpdater.playBeep);\r\n }\r\n },\r\n\r\n cb: {\r\n checkpost(e) {\r\n if (e.detail.threadID !== ThreadUpdater.thread.ID) { return; }\r\n ThreadUpdater.postID = e.detail.postID;\r\n ThreadUpdater.checkPostCount = 0;\r\n ThreadUpdater.outdateCount = 0;\r\n return ThreadUpdater.setInterval();\r\n },\r\n\r\n visibility() {\r\n if (d.hidden) { return; }\r\n // Reset the counter when we focus this tab.\r\n ThreadUpdater.outdateCount = 0;\r\n if (ThreadUpdater.seconds > ThreadUpdater.interval) {\r\n return ThreadUpdater.setInterval();\r\n }\r\n },\r\n\r\n scrollBG() {\r\n return ThreadUpdater.scrollBG = Conf['Scroll BG'] ?\r\n () => true\r\n :\r\n () => !d.hidden;\r\n },\r\n\r\n interval(e) {\r\n let val = parseInt(this.value, 10);\r\n if (val < 1) { val = 1; }\r\n ThreadUpdater.interval = (this.value = val);\r\n if (e) { return $.cb.value.call(this); }\r\n },\r\n\r\n load() {\r\n if (this !== ThreadUpdater.req) { return; } // aborted\r\n switch (this.status) {\r\n case 200:\r\n ThreadUpdater.parse(this);\r\n if (ThreadUpdater.thread.isArchived) {\r\n return ThreadUpdater.kill();\r\n } else {\r\n return ThreadUpdater.setInterval();\r\n }\r\n case 404:\r\n // XXX workaround for 4chan sending false 404s\r\n return $.ajax(g.SITE.urls.catalogJSON({boardID: ThreadUpdater.thread.board.ID}), { onloadend() {\r\n let confirmed;\r\n if (this.status === 200) {\r\n confirmed = true;\r\n for (var page of this.response) {\r\n for (var thread of page.threads) {\r\n if (thread.no === ThreadUpdater.thread.ID) {\r\n confirmed = false;\r\n break;\r\n }\r\n }\r\n }\r\n } else {\r\n confirmed = false;\r\n }\r\n if (confirmed) {\r\n return ThreadUpdater.kill();\r\n } else {\r\n return ThreadUpdater.error(this);\r\n }\r\n }\r\n }\r\n );\r\n default:\r\n return ThreadUpdater.error(this);\r\n }\r\n }\r\n },\r\n\r\n kill() {\r\n ThreadUpdater.thread.kill();\r\n ThreadUpdater.setInterval();\r\n return $.event('ThreadUpdate', {\r\n 404: true,\r\n threadID: ThreadUpdater.thread.fullID\r\n }\r\n );\r\n },\r\n\r\n error(req) {\r\n if (req.status === 304) {\r\n ThreadUpdater.set('status', '');\r\n }\r\n ThreadUpdater.setInterval();\r\n if (!req.status) {\r\n return ThreadUpdater.set('status', 'Connection Error', 'warning');\r\n } else if (req.status !== 304) {\r\n return ThreadUpdater.set('status', `${req.statusText} (${req.status})`, 'warning');\r\n }\r\n },\r\n\r\n setInterval() {\r\n clearTimeout(ThreadUpdater.timeoutID);\r\n\r\n if (ThreadUpdater.thread.isDead) {\r\n ThreadUpdater.set('status', (ThreadUpdater.thread.isArchived ? 'Archived' : '404'), 'warning');\r\n ThreadUpdater.set('timer', '');\r\n return;\r\n }\r\n\r\n // Fetching your own posts after posting\r\n if (ThreadUpdater.postID && (ThreadUpdater.checkPostCount < 5)) {\r\n ThreadUpdater.set('timer', '...', 'loading');\r\n ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.update, ++ThreadUpdater.checkPostCount * SECOND);\r\n return;\r\n }\r\n\r\n if (!Conf['Auto Update']) {\r\n ThreadUpdater.set('timer', 'Update');\r\n return;\r\n }\r\n\r\n const {interval} = ThreadUpdater;\r\n if (Conf['Optional Increase']) {\r\n // Lower the max refresh rate limit on visible tabs.\r\n const limit = d.hidden ? 10 : 5;\r\n const j = Math.min(ThreadUpdater.outdateCount, limit);\r\n\r\n // 1 second to 100, 30 to 300.\r\n const cur = (Math.floor(interval * 0.1) || 1) * j * j;\r\n ThreadUpdater.seconds = $.minmax(cur, interval, 300);\r\n } else {\r\n ThreadUpdater.seconds = interval;\r\n }\r\n\r\n return ThreadUpdater.timeout();\r\n },\r\n\r\n intervalShortcut() {\r\n Settings.open('Advanced');\r\n const settings = $.id('fourchanx-settings');\r\n return $('input[name=Interval]', settings).focus();\r\n },\r\n\r\n set(name, text, klass) {\r\n let node;\r\n const el = ThreadUpdater[name];\r\n if ((node = el.firstChild)) {\r\n // Prevent the creation of a new DOM Node\r\n // by setting the text node's data.\r\n node.data = text;\r\n } else {\r\n el.textContent = text;\r\n }\r\n return el.className = klass ?? (text === '' ? 'empty' : '');\r\n },\r\n\r\n timeout() {\r\n if (ThreadUpdater.seconds) {\r\n ThreadUpdater.set('timer', ThreadUpdater.seconds);\r\n ThreadUpdater.timeoutID = setTimeout(ThreadUpdater.timeout, 1000);\r\n } else {\r\n ThreadUpdater.outdateCount++;\r\n ThreadUpdater.update();\r\n }\r\n return ThreadUpdater.seconds--;\r\n },\r\n\r\n update() {\r\n let oldReq;\r\n clearTimeout(ThreadUpdater.timeoutID);\r\n ThreadUpdater.set('timer', '...', 'loading');\r\n if (oldReq = ThreadUpdater.req) {\r\n delete ThreadUpdater.req;\r\n oldReq.abort();\r\n }\r\n return ThreadUpdater.req = $.whenModified(\r\n g.SITE.urls.threadJSON({boardID: ThreadUpdater.thread.board.ID, threadID: ThreadUpdater.thread.ID}),\r\n 'ThreadUpdater',\r\n ThreadUpdater.cb.load,\r\n { timeout: MINUTE }\r\n );\r\n },\r\n\r\n updateThreadStatus(type, status) {\r\n let hasChanged;\r\n if (!(hasChanged = ThreadUpdater.thread[`is${type}`] !== status)) { return; }\r\n ThreadUpdater.thread.setStatus(type, status);\r\n if ((type === 'Closed') && ThreadUpdater.thread.isArchived) { return; }\r\n const change = type === 'Sticky' ?\r\n status ?\r\n 'now a sticky'\r\n :\r\n 'not a sticky anymore'\r\n :\r\n status ?\r\n 'now closed'\r\n :\r\n 'not closed anymore';\r\n return new Notice('info', `The thread is ${change}.`, 30);\r\n },\r\n\r\n parse(req) {\r\n let ID, ipCountEl, post;\r\n const postObjects = req.response.posts;\r\n const OP = postObjects[0];\r\n const {thread} = ThreadUpdater;\r\n const {board} = thread;\r\n const lastPost = ThreadUpdater.postIDs[ThreadUpdater.postIDs.length - 1];\r\n\r\n // XXX Reject updates that falsely delete the last post.\r\n if ((postObjects[postObjects.length-1].no < lastPost) &&\r\n ((new Date(req.getResponseHeader('Last-Modified')) - thread.posts.get(lastPost).info.date) < (30 * SECOND))) { return; }\r\n\r\n g.SITE.Build.spoilerRange[board] = OP.custom_spoiler;\r\n thread.setStatus('Archived', !!OP.archived);\r\n ThreadUpdater.updateThreadStatus('Sticky', !!OP.sticky);\r\n ThreadUpdater.updateThreadStatus('Closed', !!OP.closed);\r\n thread.postLimit = !!OP.bumplimit;\r\n thread.fileLimit = !!OP.imagelimit;\r\n if (OP.unique_ips != null) { thread.ipCount = OP.unique_ips; }\r\n\r\n const posts = []; // new post objects\r\n const index = []; // existing posts\r\n const files = []; // existing files\r\n const newPosts = []; // new post fullID list for API\r\n\r\n // Build the index, create posts.\r\n for (var postObject of postObjects) {\r\n ID = postObject.no;\r\n index.push(ID);\r\n if (postObject.fsize) { files.push(ID); }\r\n\r\n // Insert new posts, not older ones.\r\n if (ID <= lastPost) { continue; }\r\n\r\n // XXX Resurrect wrongly deleted posts.\r\n if ((post = thread.posts.get(ID)) && !post.isFetchedQuote) {\r\n post.resurrect();\r\n continue;\r\n }\r\n\r\n newPosts.push(`${board}.${ID}`);\r\n var node = g.SITE.Build.postFromObject(postObject, board.ID);\r\n posts.push(new Post(node, thread, board));\r\n // Fetching your own posts after posting\r\n if (ThreadUpdater.postID === ID) { delete ThreadUpdater.postID; }\r\n }\r\n\r\n // Check for deleted posts.\r\n const deletedPosts = [];\r\n for (ID of ThreadUpdater.postIDs) {\r\n if (!index.includes(ID)) {\r\n thread.posts.get(ID).kill();\r\n deletedPosts.push(`${board}.${ID}`);\r\n }\r\n }\r\n ThreadUpdater.postIDs = index;\r\n\r\n // Check for deleted files.\r\n const deletedFiles = [];\r\n for (ID of ThreadUpdater.fileIDs) {\r\n if (!(files.includes(ID) || deletedPosts.includes(`${board}.${ID}`))) {\r\n thread.posts.get(ID).kill(true);\r\n deletedFiles.push(`${board}.${ID}`);\r\n }\r\n }\r\n ThreadUpdater.fileIDs = files;\r\n\r\n if (!posts.length) {\r\n ThreadUpdater.set('status', '');\r\n } else {\r\n ThreadUpdater.set('status', `+${posts.length}`, 'new');\r\n ThreadUpdater.outdateCount = 0;\r\n\r\n const unreadCount = Unread.posts?.size;\r\n const unreadQYCount = Unread.postsQuotingYou?.size;\r\n\r\n Main.callbackNodes('Post', posts);\r\n\r\n if (d.hidden || !d.hasFocus()) {\r\n if (Conf['Beep Quoting You'] && (Unread.postsQuotingYou?.size > unreadQYCount)) {\r\n ThreadUpdater.playBeep();\r\n if (Conf['Beep']) { ThreadUpdater.playBeep(); }\r\n } else if (Conf['Beep'] && (Unread.posts?.size > 0) && (unreadCount === 0)) {\r\n ThreadUpdater.playBeep();\r\n }\r\n }\r\n\r\n const scroll = Conf['Auto Scroll'] && ThreadUpdater.scrollBG() &&\r\n ((ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight) < 25);\r\n\r\n let firstPost = null;\r\n for (post of posts) {\r\n if (!QuoteThreading.insert(post)) {\r\n if (!firstPost) { firstPost = post.nodes.root; }\r\n $.add(ThreadUpdater.root, post.nodes.root);\r\n }\r\n }\r\n $.event('PostsInserted', null, ThreadUpdater.root);\r\n\r\n if (scroll) {\r\n if (Conf['Bottom Scroll']) {\r\n window.scrollTo(0, d.body.clientHeight);\r\n } else {\r\n if (firstPost) { Header.scrollTo(firstPost); }\r\n }\r\n }\r\n }\r\n\r\n // Update IP count in original post form.\r\n if ((OP.unique_ips != null) && (ipCountEl = $.id('unique-ips'))) {\r\n ipCountEl.textContent = OP.unique_ips;\r\n ipCountEl.previousSibling.textContent = ipCountEl.previousSibling.textContent.replace(/\\b(?:is|are)\\b/, OP.unique_ips === 1 ? 'is' : 'are');\r\n ipCountEl.nextSibling.textContent = ipCountEl.nextSibling.textContent.replace(/\\bposters?\\b/, OP.unique_ips === 1 ? 'poster' : 'posters');\r\n }\r\n\r\n return $.event('ThreadUpdate', {\r\n 404: false,\r\n threadID: thread.fullID,\r\n newPosts,\r\n deletedPosts,\r\n deletedFiles,\r\n postCount: OP.replies + 1,\r\n fileCount: OP.images + !!OP.fsize,\r\n ipCount: OP.unique_ips\r\n }\r\n );\r\n }\r\n};\r\nexport default ThreadUpdater;\r\n",null,"import Callbacks from \"../classes/Callbacks\";\r\nimport Header from \"../General/Header\";\r\nimport UI from \"../General/UI\";\r\nimport { Conf, doc, g } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar FappeTyme = {\r\n init() {\r\n if ((!Conf['Fappe Tyme'] && !Conf['Werk Tyme']) || !['index', 'thread', 'archive'].includes(g.VIEW)) { return; }\r\n\r\n this.nodes = {};\r\n this.enabled = {\r\n fappe: false,\r\n werk: Conf['werk']\r\n };\r\n\r\n for (var type of [\"Fappe\", \"Werk\"]) {\r\n if (Conf[`${type} Tyme`]) {\r\n var lc = type.toLowerCase();\r\n var el = UI.checkbox(lc, `${type} Tyme`, false);\r\n el.title = `${type} Tyme`;\r\n\r\n this.nodes[lc] = el.firstElementChild;\r\n if (Conf[lc]) { this.set(lc, true); }\r\n $.on(this.nodes[lc], 'change', this.toggle.bind(this, lc));\r\n\r\n Header.menu.addEntry({\r\n el,\r\n order: 97\r\n });\r\n\r\n var indicator = $.el('span', {\r\n className: 'indicator',\r\n textContent: type[0],\r\n title: `${type} Tyme active`\r\n }\r\n );\r\n $.on(indicator, 'click', function() {\r\n const check = $.getOwn(FappeTyme.nodes, this.parentNode.id.replace('shortcut-', ''));\r\n check.checked = !check.checked;\r\n return $.event('change', null, check);\r\n });\r\n Header.addShortcut(lc, indicator, 410);\r\n }\r\n }\r\n\r\n if (Conf['Werk Tyme']) {\r\n $.sync('werk', this.set.bind(this, 'werk'));\r\n }\r\n\r\n Callbacks.Post.push({\r\n name: 'Fappe Tyme',\r\n cb: this.node\r\n });\r\n\r\n return Callbacks.CatalogThread.push({\r\n name: 'Werk Tyme',\r\n cb: this.catalogNode\r\n });\r\n },\r\n\r\n node() {\r\n return this.nodes.root.classList.toggle('noFile', !this.files.length);\r\n },\r\n\r\n catalogNode() {\r\n const file = this.thread.OP.files[0];\r\n if (!file) { return; }\r\n const filename = $.el('div', {\r\n textContent: file.name,\r\n className: 'werkTyme-filename'\r\n }\r\n );\r\n return $.add(this.nodes.thumb.parentNode, filename);\r\n },\r\n\r\n set(type, enabled) {\r\n this.enabled[type] = (this.nodes[type].checked = enabled);\r\n return $[`${enabled ? 'add' : 'rm'}Class`](doc, `${type}Tyme`);\r\n },\r\n\r\n toggle(type) {\r\n this.set(type, !this.enabled[type]);\r\n if (type === 'werk') { return $.cb.checked.call(this.nodes[type]); }\r\n }\r\n};\r\nexport default FappeTyme;\r\n","import Callbacks from \"../classes/Callbacks\";\r\nimport Notice from \"../classes/Notice\";\r\nimport Filter from \"../Filtering/Filter\";\r\nimport { g, Conf, doc } from \"../globals/globals\";\r\nimport $ from \"../platform/$\";\r\nimport { dict } from \"../platform/helpers\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS104: Avoid inline assignments\r\n * DS204: Change includes calls to have a more natural evaluation order\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar Sauce = {\r\n init() {\r\n let link;\r\n if (!['index', 'thread'].includes(g.VIEW) || !Conf['Sauce']) { return; }\r\n $.addClass(doc, 'show-sauce');\r\n\r\n const links = [];\r\n for (link of Conf['sauces'].split('\\n')) {\r\n var linkData;\r\n if ((link[0] !== '#') && (linkData = this.parseLink(link))) {\r\n links.push(linkData);\r\n }\r\n }\r\n if (!links.length) { return; }\r\n\r\n this.links = links;\r\n this.link = $.el('a', {\r\n target: '_blank',\r\n className: 'sauce'\r\n }\r\n );\r\n return Callbacks.Post.push({\r\n name: 'Sauce',\r\n cb: this.node\r\n });\r\n },\r\n\r\n parseLink(link) {\r\n if (!(link = link.trim())) { return null; }\r\n const parts = dict();\r\n const iterable = link.split(/;(?=(?:text|boards|types|regexp|sandbox):?)/);\r\n for (let i = 0; i < iterable.length; i++) {\r\n var part = iterable[i];\r\n if (i === 0) {\r\n parts['url'] = part;\r\n } else {\r\n var m = part.match(/^(\\w*):?(.*)$/);\r\n parts[m[1]] = m[2];\r\n }\r\n }\r\n if (!parts['text']) { parts['text'] = parts['url'].match(/(\\w+)\\.\\w+\\//)?.[1] || '?'; }\r\n if ('boards' in parts) {\r\n parts['boards'] = Filter.parseBoards(parts['boards']);\r\n }\r\n if ('regexp' in parts) {\r\n try {\r\n let regexp;\r\n if (regexp = parts['regexp'].match(/^\\/(.*)\\/(\\w*)$/)) {\r\n parts['regexp'] = RegExp(regexp[1], regexp[2]);\r\n } else {\r\n parts['regexp'] = RegExp(parts['regexp']);\r\n }\r\n } catch (err) {\r\n new Notice('warning', [\r\n $.tn(\"Invalid regexp for Sauce link:\"),\r\n $.el('br'),\r\n $.tn(link),\r\n $.el('br'),\r\n $.tn(err.message)\r\n ], 60);\r\n return null;\r\n }\r\n }\r\n return parts;\r\n },\r\n\r\n createSauceLink(link, post, file) {\r\n let a, matches, needle;\r\n const ext = file.url.match(/[^.]*$/)[0];\r\n const parts = dict();\r\n $.extend(parts, link);\r\n\r\n if (!!parts['boards'] && !parts['boards'][`${post.siteID}/${post.boardID}`] && !parts['boards'][`${post.siteID}/*`]) { return null; }\r\n if (!!parts['types'] && (needle = ext, !parts['types'].split(',').includes(needle))) { return null; }\r\n if (!!parts['regexp'] && (!(matches = file.name.match(parts['regexp'])))) { return null; }\r\n\r\n const missing = [];\r\n for (var key of ['url', 'text']) {\r\n parts[key] = parts[key].replace(/%(T?URL|IMG|[sh]?MD5|board|name|%|semi|\\$\\d+)/g, function(orig, parameter) {\r\n let type;\r\n if (parameter[0] === '$') {\r\n if (!matches) { return orig; }\r\n type = matches[parameter.slice(1)] || '';\r\n } else {\r\n type = Sauce.formatters[parameter](post, file, ext);\r\n if ((type == null)) {\r\n missing.push(parameter);\r\n return '';\r\n }\r\n }\r\n\r\n if ((key === 'url') && !['%', 'semi'].includes(parameter)) {\r\n if (/^javascript:/i.test(parts['url'])) { type = JSON.stringify(type); }\r\n type = encodeURIComponent(type);\r\n }\r\n return type;\r\n });\r\n }\r\n\r\n if (g.SITE.areMD5sDeferred?.(post.board) && missing.length && !missing.filter(x => !/^.?MD5$/.test(x)).length) {\r\n a = Sauce.link.cloneNode(false);\r\n a.dataset.skip = '1';\r\n return a;\r\n }\r\n\r\n if (missing.length) { return null; }\r\n\r\n a = Sauce.link.cloneNode(false);\r\n a.href = parts['url'];\r\n a.textContent = parts['text'];\r\n if (/^javascript:/i.test(parts['url'])) { a.removeAttribute('target'); }\r\n return a;\r\n },\r\n\r\n node() {\r\n if (this.isClone) { return; }\r\n for (var file of this.files) {\r\n Sauce.file(this, file);\r\n }\r\n },\r\n\r\n file(post, file) {\r\n let link, node;\r\n const nodes = [];\r\n const skipped = [];\r\n for (link of Sauce.links) {\r\n if (node = Sauce.createSauceLink(link, post, file)) {\r\n nodes.push($.tn(' '), node);\r\n if (node.dataset.skip) { skipped.push([link, node]); }\r\n }\r\n }\r\n $.add(file.text, nodes);\r\n\r\n if (skipped.length) {\r\n var observer = new MutationObserver(function() {\r\n if (file.text.dataset.md5) {\r\n for ([link, node] of skipped) {\r\n var node2;\r\n if (node2 = Sauce.createSauceLink(link, post, file)) {\r\n $.replace(node, node2);\r\n }\r\n }\r\n return observer.disconnect();\r\n }\r\n });\r\n return observer.observe(file.text, {attributes: true});\r\n }\r\n },\r\n\r\n formatters: {\r\n TURL(post, file) { return file.thumbURL; },\r\n URL(post, file) { return file.url; },\r\n IMG(post, file, ext) { if (['gif', 'jpg', 'jpeg', 'png'].includes(ext)) { return file.url; } else { return file.thumbURL; } },\r\n MD5(post, file) { return file.MD5; },\r\n sMD5(post, file) { return file.MD5?.replace(/[+/=]/g, c => ({'+': '-', '/': '_', '=': ''})[c]); },\r\n hMD5(post, file) { if (file.MD5) { return (atob(file.MD5).map((c) => `0${c.charCodeAt(0).toString(16)}`.slice(-2))).join(''); } },\r\n board(post) { return post.board.ID; },\r\n name(post, file) { return file.name; },\r\n '%'() { return '%'; },\r\n semi() { return ';'; }\r\n }\r\n};\r\nexport default Sauce;\r\n","/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS205: Consider reworking code to avoid use of IIFEs\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nimport galleryPage from './Gallery/Gallery.html';\r\nimport $ from '../platform/$';\r\nimport Callbacks from '../classes/Callbacks';\r\nimport Notice from '../classes/Notice';\r\nimport Main from '../main/Main';\r\nimport Keybinds from '../Miscellaneous/Keybinds';\r\nimport $$ from '../platform/$$';\r\nimport ImageCommon from './ImageCommon';\r\nimport Sauce from './Sauce';\r\nimport Volume from './Volume';\r\nimport Header from '../General/Header';\r\nimport { Conf, d, doc, g } from '../globals/globals';\r\nimport UI from '../General/UI';\r\nimport Get from '../General/Get';\r\nimport { debounce, dict, SECOND } from '../platform/helpers';\r\n\r\nvar Gallery = {\r\n init() {\r\n if (!(this.enabled = Conf['Gallery'] && ['index', 'thread'].includes(g.VIEW))) { return; }\r\n\r\n this.delay = Conf['Slide Delay'];\r\n\r\n const el = $.el('a', {\r\n href: 'javascript:;',\r\n title: 'Gallery',\r\n textContent: '🖼︎',\r\n });\r\n\r\n $.on(el, 'click', this.cb.toggle);\r\n\r\n Header.addShortcut('gallery', el, 530);\r\n\r\n return Callbacks.Post.push({\r\n name: 'Gallery',\r\n cb: this.node\r\n });\r\n },\r\n\r\n node() {\r\n return (() => {\r\n const result = [];\r\n for (var file of this.files) {\r\n if (file.thumb) {\r\n if (Gallery.nodes) {\r\n Gallery.generateThumb(this, file);\r\n Gallery.nodes.total.textContent = Gallery.images.length;\r\n }\r\n\r\n if (!Conf['Image Expansion'] && ((g.SITE.software !== 'tinyboard') || !Main.jsEnabled)) {\r\n result.push($.on(file.thumbLink, 'click', Gallery.cb.image));\r\n } else {\r\n result.push(undefined);\r\n }\r\n }\r\n }\r\n return result;\r\n })();\r\n },\r\n\r\n build(image) {\r\n let dialog, thumb;\r\n const {cb} = Gallery;\r\n\r\n if (Conf['Fullscreen Gallery']) {\r\n $.one(d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', () => $.on(d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', cb.close));\r\n doc.mozRequestFullScreen?.();\r\n doc.webkitRequestFullScreen?.(Element.ALLOW_KEYBOARD_INPUT);\r\n }\r\n\r\n Gallery.images = [];\r\n const nodes = (Gallery.nodes = {});\r\n Gallery.fileIDs = dict();\r\n Gallery.slideshow = false;\r\n\r\n nodes.el = (dialog = $.el('div',\r\n {id: 'a-gallery'}));\r\n $.extend(dialog, {innerHTML: galleryPage });\r\n\r\n const object = {\r\n buttons: '.gal-buttons',\r\n frame: '.gal-image',\r\n name: '.gal-name',\r\n count: '.count',\r\n total: '.total',\r\n sauce: '.gal-sauce',\r\n thumbs: '.gal-thumbnails',\r\n next: '.gal-image a',\r\n current: '.gal-image img'\r\n };\r\n for (var key in object) { var value = object[key]; nodes[key] = $(value, dialog); }\r\n\r\n const menuButton = $('.menu-button', dialog);\r\n nodes.menu = new UI.Menu('gallery');\r\n\r\n $.on(nodes.frame, 'click', cb.blank);\r\n if (Conf['Mouse Wheel Volume']) { $.on(nodes.frame, 'wheel', Volume.wheel); }\r\n $.on(nodes.next, 'click', cb.click);\r\n $.on(nodes.name, 'click', ImageCommon.download);\r\n\r\n $.on($('.gal-prev', dialog), 'click', cb.prev);\r\n $.on($('.gal-next', dialog), 'click', cb.next);\r\n $.on($('.gal-start', dialog), 'click', cb.start);\r\n $.on($('.gal-stop', dialog), 'click', cb.stop);\r\n $.on($('.gal-close', dialog), 'click', cb.close);\r\n\r\n $.on(menuButton, 'click', function(e) {\r\n return nodes.menu.toggle(e, this, g);\r\n });\r\n\r\n for (var entry of Gallery.menu.createSubEntries()) {\r\n entry.order = 0;\r\n nodes.menu.addEntry(entry);\r\n }\r\n\r\n $.on(d, 'keydown', cb.keybinds);\r\n if (Conf['Keybinds']) { $.off(d, 'keydown', Keybinds.keydown); }\r\n\r\n $.on(window, 'resize', Gallery.cb.setHeight);\r\n\r\n for (var postThumb of $$(g.SITE.selectors.file.thumb)) {\r\n var post;\r\n if (!(post = Get.postFromNode(postThumb))) { continue; }\r\n for (var file of post.files) {\r\n if (file.thumb) {\r\n Gallery.generateThumb(post, file);\r\n // If no image to open is given, pick image we have scrolled to.\r\n if (!image && Gallery.fileIDs[`${post.fullID}.${file.index}`]) {\r\n var candidate = file.thumbLink;\r\n if ((Header.getTopOf(candidate) + candidate.getBoundingClientRect().height) >= 0) {\r\n image = candidate;\r\n }\r\n }\r\n }\r\n }\r\n }\r\n $.addClass(doc, 'gallery-open');\r\n\r\n $.add(d.body, dialog);\r\n\r\n nodes.thumbs.scrollTop = 0;\r\n nodes.current.parentElement.scrollTop = 0;\r\n\r\n if (image) { thumb = $(`[href='${image.href}']`, nodes.thumbs); }\r\n if (!thumb) { thumb = Gallery.images[Gallery.images.length-1]; }\r\n if (thumb) { Gallery.open(thumb); }\r\n\r\n doc.style.overflow = 'hidden';\r\n return nodes.total.textContent = Gallery.images.length;\r\n },\r\n\r\n generateThumb(post, file) {\r\n if (post.isClone || post.isHidden) { return; }\r\n if (!file || !file.thumb || (!file.isImage && !file.isVideo && !Conf['PDF in Gallery'])) { return; }\r\n if (Gallery.fileIDs[`${post.fullID}.${file.index}`]) { return; }\r\n\r\n Gallery.fileIDs[`${post.fullID}.${file.index}`] = true;\r\n\r\n const thumb = $.el('a', {\r\n className: 'gal-thumb',\r\n href: file.url,\r\n target: '_blank',\r\n title: file.name\r\n }\r\n );\r\n\r\n thumb.dataset.id = Gallery.images.length;\r\n thumb.dataset.post = post.fullID;\r\n thumb.dataset.file = file.index;\r\n\r\n const thumbImg = file.thumb.cloneNode(false);\r\n thumbImg.style.cssText = '';\r\n $.add(thumb, thumbImg);\r\n\r\n $.on(thumb, 'click', Gallery.cb.open);\r\n\r\n Gallery.images.push(thumb);\r\n return $.add(Gallery.nodes.thumbs, thumb);\r\n },\r\n\r\n load(thumb, errorCB) {\r\n const ext = thumb.href.match(/\\w*$/);\r\n const elType = $.getOwn({'webm': 'video', 'mp4': 'video', 'ogv': 'video', 'pdf': 'iframe'}, ext) || 'img';\r\n const file = $.el(elType);\r\n $.extend(file.dataset, thumb.dataset);\r\n $.on(file, 'error', errorCB);\r\n file.src = thumb.href;\r\n return file;\r\n },\r\n\r\n open(thumb) {\r\n let el, file, post;\r\n const {nodes} = Gallery;\r\n const oldID = +nodes.current.dataset.id;\r\n const newID = +thumb.dataset.id;\r\n\r\n // Highlight, center selected thumbnail\r\n if (el = Gallery.images[oldID]) { $.rmClass(el, 'gal-highlight'); }\r\n $.addClass(thumb, 'gal-highlight');\r\n nodes.thumbs.scrollTop = (thumb.offsetTop + (thumb.offsetHeight/2)) - (nodes.thumbs.clientHeight/2);\r\n\r\n // Load image or use preloaded image\r\n if (Gallery.cache?.dataset.id === (''+newID)) {\r\n file = Gallery.cache;\r\n $.off(file, 'error', Gallery.cacheError);\r\n $.on(file, 'error', Gallery.error);\r\n } else {\r\n file = Gallery.load(thumb, Gallery.error);\r\n }\r\n\r\n // Replace old image with new one\r\n $.off(nodes.current, 'error', Gallery.error);\r\n ImageCommon.pause(nodes.current);\r\n $.replace(nodes.current, file);\r\n nodes.current = file;\r\n\r\n if (file.nodeName === 'VIDEO') {\r\n file.loop = true;\r\n Volume.setup(file);\r\n if (Conf['Autoplay']) { file.play(); }\r\n if (Conf['Show Controls']) { ImageCommon.addControls(file); }\r\n }\r\n\r\n doc.classList.toggle('gal-pdf', file.nodeName === 'IFRAME');\r\n Gallery.cb.setHeight();\r\n nodes.count.textContent = +thumb.dataset.id + 1;\r\n nodes.name.download = (nodes.name.textContent = thumb.title);\r\n nodes.name.href = thumb.href;\r\n nodes.frame.scrollTop = 0;\r\n nodes.next.focus();\r\n\r\n // Set sauce links\r\n $.rmAll(nodes.sauce);\r\n if (Conf['Sauce'] && Sauce.links && (post = g.posts.get(file.dataset.post))) {\r\n const sauces = [];\r\n for (var link of Sauce.links) {\r\n var node;\r\n if (node = Sauce.createSauceLink(link, post, post.files[+file.dataset.file])) {\r\n sauces.push($.tn(' '), node);\r\n }\r\n }\r\n $.add(nodes.sauce, sauces);\r\n }\r\n\r\n // Continue slideshow if moving forward, stop otherwise\r\n if (Gallery.slideshow && ((newID > oldID) || ((oldID === (Gallery.images.length-1)) && (newID === 0)))) {\r\n Gallery.setupTimer();\r\n } else {\r\n Gallery.cb.stop();\r\n }\r\n\r\n // Scroll to post\r\n if (Conf['Scroll to Post'] && (post = g.posts.get(file.dataset.post))) {\r\n Header.scrollTo(post.nodes.root);\r\n }\r\n\r\n // Preload next image\r\n if (isNaN(oldID) || (newID === ((oldID + 1) % Gallery.images.length))) {\r\n return Gallery.cache = Gallery.load(Gallery.images[(newID + 1) % Gallery.images.length], Gallery.cacheError);\r\n }\r\n },\r\n\r\n error() {\r\n if (this.error?.code === MediaError.MEDIA_ERR_DECODE) {\r\n return new Notice('error', 'Corrupt or unplayable video', 30);\r\n }\r\n if (ImageCommon.isFromArchive(this)) { return; }\r\n const post = g.posts.get(this.dataset.post);\r\n const file = post.files[+this.dataset.file];\r\n return ImageCommon.error(this, post, file, null, url => {\r\n if (!url) { return; }\r\n Gallery.images[+this.dataset.id].href = url;\r\n if (Gallery.nodes.current === this) { return this.src = url; }\r\n });\r\n },\r\n\r\n cacheError() {\r\n return delete Gallery.cache;\r\n },\r\n\r\n cleanupTimer() {\r\n clearTimeout(Gallery.timeoutID);\r\n const {current} = Gallery.nodes;\r\n $.off(current, 'canplaythrough load', Gallery.startTimer);\r\n return $.off(current, 'ended', Gallery.cb.next);\r\n },\r\n\r\n startTimer() {\r\n return Gallery.timeoutID = setTimeout(Gallery.checkTimer, Gallery.delay * SECOND);\r\n },\r\n\r\n setupTimer() {\r\n Gallery.cleanupTimer();\r\n const {current} = Gallery.nodes;\r\n const isVideo = current.nodeName === 'VIDEO';\r\n if (isVideo) { current.play(); }\r\n if ((isVideo ? current.readyState >= 4 : current.complete) || (current.nodeName === 'IFRAME')) {\r\n return Gallery.startTimer();\r\n } else {\r\n return $.on(current, (isVideo ? 'canplaythrough' : 'load'), Gallery.startTimer);\r\n }\r\n },\r\n\r\n checkTimer() {\r\n const {current} = Gallery.nodes;\r\n if ((current.nodeName === 'VIDEO') && !current.paused) {\r\n $.on(current, 'ended', Gallery.cb.next);\r\n return current.loop = false;\r\n } else {\r\n return Gallery.cb.next();\r\n }\r\n },\r\n\r\n cb: {\r\n keybinds(e) {\r\n let key;\r\n if (!(key = Keybinds.keyCode(e))) { return; }\r\n\r\n const cb = (() => { switch (key) {\r\n case Conf['Close']: case Conf['Open Gallery']:\r\n return Gallery.cb.close;\r\n case Conf['Next Gallery Image']:\r\n return Gallery.cb.next;\r\n case Conf['Advance Gallery']:\r\n return Gallery.cb.advance;\r\n case Conf['Previous Gallery Image']:\r\n return Gallery.cb.prev;\r\n case Conf['Pause']:\r\n return Gallery.cb.pause;\r\n case Conf['Slideshow']:\r\n return Gallery.cb.toggleSlideshow;\r\n case Conf['Rotate image anticlockwise']:\r\n return Gallery.cb.rotateLeft;\r\n case Conf['Rotate image clockwise']:\r\n return Gallery.cb.rotateRight;\r\n case Conf['Download Gallery Image']:\r\n return Gallery.cb.download;\r\n } })();\r\n\r\n if (!cb) { return; }\r\n e.stopPropagation();\r\n e.preventDefault();\r\n return cb();\r\n },\r\n\r\n open(e) {\r\n if (e) { e.preventDefault(); }\r\n if (this) { return Gallery.open(this); }\r\n },\r\n\r\n image(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n return Gallery.build(this);\r\n },\r\n\r\n prev() {\r\n return Gallery.cb.open.call(\r\n Gallery.images[+Gallery.nodes.current.dataset.id - 1] || Gallery.images[Gallery.images.length - 1]\r\n );\r\n },\r\n next() {\r\n return Gallery.cb.open.call(\r\n Gallery.images[+Gallery.nodes.current.dataset.id + 1] || Gallery.images[0]\r\n );\r\n },\r\n\r\n click(e) {\r\n if (ImageCommon.onControls(e)) { return; }\r\n e.preventDefault();\r\n return Gallery.cb.advance();\r\n },\r\n\r\n advance() { if (!Conf['Autoplay'] && Gallery.nodes.current.paused) { return Gallery.nodes.current.play(); } else { return Gallery.cb.next(); } },\r\n toggle() { return (Gallery.nodes ? Gallery.cb.close : Gallery.build)(); },\r\n blank(e) { if (e.target === this) { return Gallery.cb.close(); } },\r\n toggleSlideshow() { return Gallery.cb[Gallery.slideshow ? 'stop' : 'start'](); },\r\n\r\n download() {\r\n const name = $('.gal-name');\r\n return name.click();\r\n },\r\n\r\n pause() {\r\n Gallery.cb.stop();\r\n const {current} = Gallery.nodes;\r\n if (current.nodeName === 'VIDEO') { return current[current.paused ? 'play' : 'pause'](); }\r\n },\r\n\r\n start() {\r\n $.addClass(Gallery.nodes.buttons, 'gal-playing');\r\n Gallery.slideshow = true;\r\n return Gallery.setupTimer();\r\n },\r\n\r\n stop() {\r\n if (!Gallery.slideshow) { return; }\r\n Gallery.cleanupTimer();\r\n const {current} = Gallery.nodes;\r\n if (current.nodeName === 'VIDEO') { current.loop = true; }\r\n $.rmClass(Gallery.nodes.buttons, 'gal-playing');\r\n return Gallery.slideshow = false;\r\n },\r\n\r\n rotateLeft() { return Gallery.cb.rotate(270); },\r\n rotateRight() { return Gallery.cb.rotate(90); },\r\n\r\n rotate: debounce(100, function(delta) {\r\n const {current} = Gallery.nodes;\r\n if (current.nodeName === 'IFRAME') { return; }\r\n current.dataRotate = ((current.dataRotate || 0) + delta) % 360;\r\n current.style.transform = `rotate(${current.dataRotate}deg)`;\r\n return Gallery.cb.setHeight();\r\n }),\r\n\r\n close() {\r\n $.off(Gallery.nodes.current, 'error', Gallery.error);\r\n ImageCommon.pause(Gallery.nodes.current);\r\n $.rm(Gallery.nodes.el);\r\n $.rmClass(doc, 'gallery-open');\r\n if (Conf['Fullscreen Gallery']) {\r\n $.off(d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', Gallery.cb.close);\r\n d.mozCancelFullScreen?.();\r\n d.webkitExitFullscreen?.();\r\n }\r\n delete Gallery.nodes;\r\n delete Gallery.fileIDs;\r\n doc.style.overflow = '';\r\n\r\n $.off(d, 'keydown', Gallery.cb.keybinds);\r\n if (Conf['Keybinds']) { $.on(d, 'keydown', Keybinds.keydown); }\r\n $.off(window, 'resize', Gallery.cb.setHeight);\r\n return clearTimeout(Gallery.timeoutID);\r\n },\r\n\r\n setFitness() {\r\n return (this.checked ? $.addClass : $.rmClass)(doc, `gal-${this.name.toLowerCase().replace(/\\s+/g, '-')}`);\r\n },\r\n\r\n setHeight: debounce(100, function () {\r\n let dim, margin, minHeight;\r\n const {current, frame} = Gallery.nodes;\r\n const {style} = current;\r\n\r\n if (Conf['Stretch to Fit'] && (dim = g.posts.get(current.dataset.post)?.files[+current.dataset.file].dimensions)) {\r\n const [width, height] = dim.split('x');\r\n let containerWidth = frame.clientWidth;\r\n let containerHeight = doc.clientHeight - 25;\r\n if (((current.dataRotate || 0) % 180) === 90) {\r\n [containerWidth, containerHeight] = [containerHeight, containerWidth];\r\n }\r\n minHeight = Math.min(containerHeight, (height / width) * containerWidth);\r\n style.minHeight = minHeight + 'px';\r\n style.minWidth = ((width / height) * minHeight) + 'px';\r\n } else {\r\n style.minHeight = (style.minWidth = '');\r\n }\r\n\r\n if (((current.dataRotate || 0) % 180) === 90) {\r\n style.maxWidth = Conf['Fit Height'] ? `${doc.clientHeight - 25}px` : 'none';\r\n style.maxHeight = Conf['Fit Width'] ? `${frame.clientWidth}px` : 'none';\r\n margin = (current.clientWidth - current.clientHeight)/2;\r\n return style.margin = `${margin}px ${-margin}px`;\r\n } else {\r\n return style.maxWidth = (style.maxHeight = (style.margin = ''));\r\n }\r\n }),\r\n\r\n setDelay() { return Gallery.delay = +this.value; }\r\n },\r\n\r\n menu: {\r\n init() {\r\n if (!Gallery.enabled) { return; }\r\n\r\n const el = $.el('span', {\r\n textContent: 'Gallery',\r\n className: 'gallery-link'\r\n }\r\n );\r\n\r\n return Header.menu.addEntry({\r\n el,\r\n order: 105,\r\n subEntries: Gallery.menu.createSubEntries()\r\n });\r\n },\r\n\r\n createSubEntry(name) {\r\n const label = UI.checkbox(name, name);\r\n const input = label.firstElementChild;\r\n if (['Hide Thumbnails', 'Fit Width', 'Fit Height'].includes(name)) { $.on(input, 'change', Gallery.cb.setFitness); }\r\n $.event('change', null, input);\r\n $.on(input, 'change', $.cb.checked);\r\n if (['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit'].includes(name)) { $.on(input, 'change', Gallery.cb.setHeight); }\r\n return {el: label};\r\n },\r\n\r\n createSubEntries() {\r\n const subEntries = (['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit', 'Scroll to Post'].map((item) => Gallery.menu.createSubEntry(item)));\r\n\r\n const delayLabel = $.el('label', {innerHTML: 'Slide Delay: '});\r\n const delayInput = delayLabel.firstElementChild;\r\n delayInput.value = Gallery.delay;\r\n $.on(delayInput, 'change', Gallery.cb.setDelay);\r\n $.on(delayInput, 'change', $.cb.value);\r\n subEntries.push({el: delayLabel});\r\n\r\n return subEntries;\r\n }\r\n }\r\n};\r\nexport default Gallery;\r\n","import Get from '../General/Get';\r\nimport Header from '../General/Header';\r\nimport UI from '../General/UI';\r\nimport { g, Conf, d, doc, E } from '../globals/globals';\r\nimport ImageHost from '../Images/ImageHost';\r\nimport Main from '../main/Main';\r\nimport $ from '../platform/$';\r\nimport $$ from '../platform/$$';\r\nimport CrossOrigin from '../platform/CrossOrigin';\r\nimport { dict } from '../platform/helpers';\r\nimport EmbeddingPage from './Embedding/Embed.html';\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS205: Consider reworking code to avoid use of IIFEs\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\n\r\nvar Embedding = {\r\n init() {\r\n if (!['index', 'thread', 'archive'].includes(g.VIEW) || !Conf['Linkify'] || (!Conf['Embedding'] && !Conf['Link Title'] && !Conf['Cover Preview'])) { return; }\r\n this.types = dict();\r\n for (var type of this.ordered_types) { this.types[type.key] = type; }\r\n\r\n if (Conf['Embedding'] && (g.VIEW !== 'archive')) {\r\n this.dialog = UI.dialog('embedding',\r\n { innerHTML: EmbeddingPage });\r\n this.media = $('#media-embed', this.dialog);\r\n $.one(d, '4chanXInitFinished', this.ready);\r\n $.on(d, 'IndexRefreshInternal', () => g.posts.forEach(function(post) {\r\n for (post of [post, ...post.clones]) {\r\n for (var embed of post.nodes.embedlinks) {\r\n Embedding.cb.catalogRemove.call(embed);\r\n }\r\n }\r\n }));\r\n }\r\n if (Conf['Link Title']) {\r\n return $.on(d, '4chanXInitFinished PostsInserted', function() {\r\n for (var key in Embedding.types) {\r\n var service = Embedding.types[key];\r\n if (service.title?.batchSize) {\r\n Embedding.flushTitles(service.title);\r\n }\r\n }\r\n });\r\n }\r\n },\r\n\r\n events(post) {\r\n let el, i, items;\r\n if (g.VIEW === 'archive') { return; }\r\n if (Conf['Embedding']) {\r\n i = 0;\r\n items = (post.nodes.embedlinks = $$('.embedder', post.nodes.comment));\r\n while ((el = items[i++])) {\r\n $.on(el, 'click', Embedding.cb.click);\r\n if ($.hasClass(el, 'embedded')) { Embedding.cb.toggle.call(el); }\r\n }\r\n }\r\n if (Conf['Cover Preview']) {\r\n i = 0;\r\n items = $$('.linkify', post.nodes.comment);\r\n while ((el = items[i++])) {\r\n var data;\r\n if (data = Embedding.services(el)) {\r\n Embedding.preview(data);\r\n }\r\n }\r\n return;\r\n }\r\n },\r\n\r\n process(link, post) {\r\n let data;\r\n if (!Conf['Embedding'] && !Conf['Link Title'] && !Conf['Cover Preview']) { return; }\r\n if ($.x('ancestor::pre', link)) { return; }\r\n if (data = Embedding.services(link)) {\r\n data.post = post;\r\n if (Conf['Embedding'] && (g.VIEW !== 'archive')) { Embedding.embed(data); }\r\n if (Conf['Link Title']) { Embedding.title(data); }\r\n if (Conf['Cover Preview'] && (g.VIEW !== 'archive')) { return Embedding.preview(data); }\r\n }\r\n },\r\n\r\n services(link) {\r\n const {href} = link;\r\n for (var type of Embedding.ordered_types) {\r\n var match;\r\n if (match = type.regExp.exec(href)) {\r\n return {key: type.key, uid: match[1], options: match[2], link};\r\n }\r\n }\r\n },\r\n\r\n embed(data) {\r\n const {key, uid, options, link, post} = data;\r\n const {href} = link;\r\n\r\n $.addClass(link, key.toLowerCase());\r\n\r\n const embed = $.el('a', {\r\n className: 'embedder',\r\n href: 'javascript:;'\r\n }\r\n ,\r\n {innerHTML: '(unembed)'});\r\n\r\n const object = {key, uid, options, href};\r\n for (var name in object) { var value = object[name]; embed.dataset[name] = value; }\r\n\r\n $.on(embed, 'click', Embedding.cb.click);\r\n $.after(link, [$.tn(' '), embed]);\r\n post.nodes.embedlinks.push(embed);\r\n\r\n if (Conf['Auto-embed'] && !Conf['Floating Embeds'] && !post.isFetchedQuote) {\r\n if ($.hasClass(doc, 'catalog-mode')) {\r\n return $.addClass(embed, 'embed-removed');\r\n } else {\r\n return Embedding.cb.toggle.call(embed);\r\n }\r\n }\r\n },\r\n\r\n ready() {\r\n if (!Main.isThisPageLegit()) { return; }\r\n $.addClass(Embedding.dialog, 'empty');\r\n $.on($('.close', Embedding.dialog), 'click', Embedding.closeFloat);\r\n $.on($('.move', Embedding.dialog), 'mousedown', Embedding.dragEmbed);\r\n $.on($('.jump', Embedding.dialog), 'click', function() {\r\n if (doc.contains(Embedding.lastEmbed)) { return Header.scrollTo(Embedding.lastEmbed); }\r\n });\r\n return $.add(d.body, Embedding.dialog);\r\n },\r\n\r\n closeFloat() {\r\n delete Embedding.lastEmbed;\r\n $.addClass(Embedding.dialog, 'empty');\r\n return $.replace(Embedding.media.firstChild, $.el('div'));\r\n },\r\n\r\n dragEmbed() {\r\n // only webkit can handle a blocking div\r\n const {style} = Embedding.media;\r\n if (Embedding.dragEmbed.mouseup) {\r\n $.off(d, 'mouseup', Embedding.dragEmbed);\r\n Embedding.dragEmbed.mouseup = false;\r\n style.pointerEvents = '';\r\n return;\r\n }\r\n $.on(d, 'mouseup', Embedding.dragEmbed);\r\n Embedding.dragEmbed.mouseup = true;\r\n return style.pointerEvents = 'none';\r\n },\r\n\r\n title(data) {\r\n let service;\r\n const {key, uid, options, link, post} = data;\r\n if (!(service = Embedding.types[key].title)) { return; }\r\n $.addClass(link, key.toLowerCase());\r\n if (service.batchSize) {\r\n (service.queue || (service.queue = [])).push(data);\r\n if (service.queue.length >= service.batchSize) {\r\n return Embedding.flushTitles(service);\r\n }\r\n } else {\r\n return CrossOrigin.cache(service.api(uid), (function() { return Embedding.cb.title(this, data); }));\r\n }\r\n },\r\n\r\n flushTitles(service) {\r\n let data;\r\n const {queue} = service;\r\n if (!queue?.length) { return; }\r\n service.queue = [];\r\n const cb = function() {\r\n for (data of queue) { Embedding.cb.title(this, data); }\r\n };\r\n return CrossOrigin.cache(service.api((() => {\r\n const result = [];\r\n for (data of queue) { result.push(data.uid);\r\n }\r\n return result;\r\n })()), cb);\r\n },\r\n\r\n preview(data) {\r\n let service;\r\n const {key, uid, link} = data;\r\n if (!(service = Embedding.types[key].preview)) { return; }\r\n return $.on(link, 'mouseover', function(e) {\r\n const src = service.url(uid);\r\n const {height} = service;\r\n const el = $.el('img', {\r\n src,\r\n id: 'ihover'\r\n }\r\n );\r\n $.add(Header.hover, el);\r\n return UI.hover({\r\n root: link,\r\n el,\r\n latestEvent: e,\r\n endEvents: 'mouseout click',\r\n height\r\n });\r\n });\r\n },\r\n\r\n cb: {\r\n click(e) {\r\n e.preventDefault();\r\n if (!$.hasClass(this, 'embedded') && (Conf['Floating Embeds'] || $.hasClass(doc, 'catalog-mode'))) {\r\n let div;\r\n if (!(div = Embedding.media.firstChild)) { return; }\r\n $.replace(div, Embedding.cb.embed(this));\r\n Embedding.lastEmbed = Get.postFromNode(this).nodes.root;\r\n return $.rmClass(Embedding.dialog, 'empty');\r\n } else {\r\n return Embedding.cb.toggle.call(this);\r\n }\r\n },\r\n\r\n toggle() {\r\n if ($.hasClass(this, \"embedded\")) {\r\n $.rm(this.nextElementSibling);\r\n } else {\r\n $.after(this, Embedding.cb.embed(this));\r\n }\r\n return $.toggleClass(this, 'embedded');\r\n },\r\n\r\n embed(a) {\r\n // We create an element to embed\r\n let el, type;\r\n const container = $.el('div', {className: 'media-embed'});\r\n $.add(container, (el = (type = Embedding.types[a.dataset.key]).el(a)));\r\n\r\n // Set style values.\r\n el.style.cssText = (type.style != null) ?\r\n type.style\r\n :\r\n 'border: none; width: 640px; height: 360px;';\r\n\r\n return container;\r\n },\r\n\r\n catalogRemove() {\r\n const isCatalog = $.hasClass(doc, 'catalog-mode');\r\n if ((isCatalog && $.hasClass(this, 'embedded')) || (!isCatalog && $.hasClass(this, 'embed-removed'))) {\r\n Embedding.cb.toggle.call(this);\r\n return $.toggleClass(this, 'embed-removed');\r\n }\r\n },\r\n\r\n title(req, data) {\r\n let text;\r\n const {key, uid, options, link, post} = data;\r\n const service = Embedding.types[key].title;\r\n\r\n let {status} = req;\r\n if ([200, 304].includes(status) && service.status) {\r\n status = service.status(req.response)[0];\r\n }\r\n\r\n if (!status) { return; }\r\n\r\n text = `[${key}] ${(() => { switch (status) {\r\n case 200: case 304:\r\n text = service.text(req.response, uid);\r\n if (typeof text === 'string') {\r\n return text;\r\n } else {\r\n return text = link.textContent;\r\n }\r\n case 404:\r\n return \"Not Found\";\r\n case 403: case 401:\r\n return \"Forbidden or Private\";\r\n default:\r\n return `${status}'d`;\r\n } })()\r\n }`;\r\n\r\n link.dataset.original = link.textContent;\r\n link.textContent = text;\r\n for (var post2 of post.clones) {\r\n for (var link2 of $$('a.linkify', post2.nodes.comment)) {\r\n if (link2.href === link.href) {\r\n if (link2.dataset.original == null) { link2.dataset.original = link2.textContent; }\r\n link2.textContent = text;\r\n }\r\n }\r\n }\r\n }\r\n },\r\n\r\n ordered_types: [{\r\n key: 'audio',\r\n regExp: /^[^?#]+\\.(?:mp3|m4a|oga|wav|flac)(?:[?#]|$)/i,\r\n style: '',\r\n el(a) {\r\n return $.el('audio', {\r\n controls: true,\r\n preload: 'auto',\r\n src: a.dataset.href\r\n }\r\n );\r\n }\r\n }\r\n , {\r\n key: 'image',\r\n regExp: /^[^?#]+\\.(?:gif|png|jpg|jpeg|bmp|webp)(?::\\w+)?(?:[?#]|$)/i,\r\n style: '',\r\n el(a) {\r\n const hrefEsc = E(a.dataset.href);\r\n return $.el('div', { innerHTML: ``});\r\n }\r\n }\r\n , {\r\n key: 'video',\r\n regExp: /^[^?#]+\\.(?:og[gv]|webm|mp4)(?:[?#]|$)/i,\r\n style: 'max-width: 80vw; max-height: 80vh;',\r\n el(a) {\r\n const el = $.el('video', {\r\n hidden: true,\r\n controls: true,\r\n preload: 'auto',\r\n src: a.dataset.href,\r\n loop: ImageHost.test(a.dataset.href.split('/')[2])\r\n });\r\n $.on(el, 'loadedmetadata', function() {\r\n if ((el.videoHeight === 0) && el.parentNode) {\r\n return $.replace(el, Embedding.types.audio.el(a));\r\n } else {\r\n return el.hidden = false;\r\n }\r\n });\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'PeerTube',\r\n regExp: /^(\\w+:\\/\\/[^\\/]+\\/videos\\/watch\\/\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12})(.*)/,\r\n el(a) {\r\n let start;\r\n const options = (start = a.dataset.options.match(/[?&](start=\\w+)/)) ? `?${start[1]}` : '';\r\n const el = $.el('iframe',\r\n {src: a.dataset.uid.replace('/videos/watch/', '/videos/embed/') + options});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'BitChute',\r\n regExp: /^\\w+:\\/\\/(?:www\\.)?bitchute\\.com\\/video\\/([\\w\\-]+)/,\r\n el(a) {\r\n const el = $.el('iframe',\r\n {src: `https://www.bitchute.com/embed/${a.dataset.uid}/`});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'Clyp',\r\n regExp: /^\\w+:\\/\\/(?:www\\.)?clyp\\.it\\/(\\w{8})/,\r\n style: 'border: 0; width: 640px; height: 160px;',\r\n el(a) {\r\n return $.el('iframe',\r\n {src: `https://clyp.it/${a.dataset.uid}/widget`});\r\n },\r\n title: {\r\n api(uid) { return `https://api.clyp.it/oembed?url=https://clyp.it/${uid}`; },\r\n text(_) { return _.title; }\r\n }\r\n }\r\n , {\r\n key: 'Dailymotion',\r\n regExp: /^\\w+:\\/\\/(?:(?:www\\.)?dailymotion\\.com\\/(?:embed\\/)?video|dai\\.ly)\\/([A-Za-z0-9]+)[^?]*(.*)/,\r\n el(a) {\r\n let start;\r\n const options = (start = a.dataset.options.match(/[?&](start=\\d+)/)) ? `?${start[1]}` : '';\r\n const el = $.el('iframe',\r\n {src: `//www.dailymotion.com/embed/video/${a.dataset.uid}${options}`});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n },\r\n title: {\r\n api(uid) { return `https://api.dailymotion.com/video/${uid}`; },\r\n text(_) { return _.title; }\r\n },\r\n preview: {\r\n url(uid) { return `https://www.dailymotion.com/thumbnail/video/${uid}`; },\r\n height: 240\r\n }\r\n }\r\n , {\r\n key: 'Gfycat',\r\n regExp: /^\\w+:\\/\\/(?:www\\.)?gfycat\\.com\\/(?:iframe\\/)?(\\w+)/,\r\n el(a) {\r\n const el = $.el('iframe',\r\n {src: `//gfycat.com/ifr/${a.dataset.uid}`});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'Gist',\r\n regExp: /^\\w+:\\/\\/gist\\.github\\.com\\/[\\w\\-]+\\/(\\w+)/,\r\n style: '',\r\n el: (function() {\r\n let counter = 0;\r\n return function(a) {\r\n const el = $.el('pre', {\r\n hidden: true,\r\n id: `gist-embed-${counter++}`\r\n }\r\n );\r\n CrossOrigin.cache(`https://api.github.com/gists/${a.dataset.uid}`, function() {\r\n el.textContent = Object.values(this.response.files)[0].content;\r\n el.className = 'prettyprint';\r\n $.global(() => window.prettyPrint?.((function() {}), document.getElementById(document.currentScript.dataset.id).parentNode)\r\n , {id: el.id});\r\n return el.hidden = false;\r\n });\r\n return el;\r\n };\r\n })(),\r\n title: {\r\n api(uid) { return `https://api.github.com/gists/${uid}`; },\r\n text({files}) {\r\n for (var file in files) { if (files.hasOwnProperty(file)) { return file; } }\r\n }\r\n }\r\n }\r\n , {\r\n key: 'InstallGentoo',\r\n regExp: /^\\w+:\\/\\/paste\\.installgentoo\\.com\\/view\\/(?:raw\\/|download\\/|embed\\/)?(\\w+)/,\r\n el(a) {\r\n return $.el('iframe',\r\n {src: `https://paste.installgentoo.com/view/embed/${a.dataset.uid}`});\r\n }\r\n }\r\n , {\r\n key: 'LiveLeak',\r\n regExp: /^\\w+:\\/\\/(?:\\w+\\.)?liveleak\\.com\\/.*\\?.*[tif]=(\\w+)/,\r\n el(a) {\r\n const el = $.el('iframe',\r\n {src: `https://www.liveleak.com/e/${a.dataset.uid}`,});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'Loopvid',\r\n 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?:\\/\\/.+)/,\r\n style: 'max-width: 80vw; max-height: 80vh;',\r\n el(a) {\r\n const el = $.el('video', {\r\n controls: true,\r\n preload: 'auto',\r\n loop: true\r\n }\r\n );\r\n if (/^http/.test(a.dataset.uid)) {\r\n $.add(el, $.el('source', {src: a.dataset.uid}));\r\n return el;\r\n }\r\n const [_, host, names] = a.dataset.uid.match(/(\\w+)\\/(.*)/);\r\n const types = (() => { switch (host) {\r\n case 'gd': case 'wu': case 'fc': return [''];\r\n case 'gc': return ['giant', 'fat', 'zippy'];\r\n default: return ['.webm', '.mp4'];\r\n } })();\r\n for (var name of names.split(',')) {\r\n for (var type of types) {\r\n var base = `${name}${type}`;\r\n var urls = (() => { switch (host) {\r\n // list from src/common.py at http://loopvid.appspot.com/source.html\r\n case 'pf': return [`https://kastden.org/_loopvid_media/pf/${base}`, `https://web.archive.org/web/2/http://a.pomf.se/${base}`];\r\n case 'kd': return [`https://kastden.org/loopvid/${base}`];\r\n case 'lv': return [`https://lv.kastden.org/${base}`];\r\n case 'gd': return [`https://docs.google.com/uc?export=download&id=${base}`];\r\n case 'gh': return [`https://googledrive.com/host/${base}`];\r\n case 'db': return [`https://dl.dropboxusercontent.com/u/${base}`];\r\n case 'dx': return [`https://dl.dropboxusercontent.com/${base}`];\r\n case 'nn': return [`https://kastden.org/_loopvid_media/nn/${base}`];\r\n case 'cp': return [`https://copy.com/${base}`];\r\n case 'wu': return [`http://webmup.com/${base}/vid.webm`];\r\n case 'ig': return [`https://i.imgur.com/${base}`];\r\n case 'ky': return [`https://kastden.org/_loopvid_media/ky/${base}`];\r\n case 'mf': return [`https://kastden.org/_loopvid_media/mf/${base}`, `https://web.archive.org/web/2/https://d.maxfile.ro/${base}`];\r\n case 'm2': return [`https://kastden.org/_loopvid_media/m2/${base}`];\r\n case 'pc': return [`https://kastden.org/_loopvid_media/pc/${base}`, `https://web.archive.org/web/2/http://a.pomf.cat/${base}`];\r\n case '1c': return [`http://b.1339.cf/${base}`];\r\n case 'pi': return [`https://kastden.org/_loopvid_media/pi/${base}`, `https://web.archive.org/web/2/https://u.pomf.is/${base}`];\r\n case 'ni': return [`https://kastden.org/_loopvid_media/ni/${base}`, `https://web.archive.org/web/2/https://u.nya.is/${base}`];\r\n case 'wl': return [`http://webm.land/media/${base}`];\r\n case 'ko': return [`https://kordy.kastden.org/loopvid/${base}`];\r\n case 'mm': return [`https://kastden.org/_loopvid_media/mm/${base}`, `https://web.archive.org/web/2/https://my.mixtape.moe/${base}`];\r\n case 'ic': return [`https://media.8ch.net/file_store/${base}`];\r\n case 'fc': return [`//${ImageHost.host()}/${base}.webm`];\r\n case 'gc': return [`https://${type}.gfycat.com/${name}.webm`];\r\n } })();\r\n\r\n for (var url of urls) {\r\n $.add(el, $.el('source', {src: url}));\r\n }\r\n }\r\n }\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'Openings.moe',\r\n regExp: /^\\w+:\\/\\/openings.moe\\/\\?video=([^.&=]+)/,\r\n style: 'width: 1280px; height: 720px; max-width: 80vw; max-height: 80vh;',\r\n el(a) {\r\n const el = $.el('iframe',\r\n {src: `https://openings.moe/?video=${a.dataset.uid}`,});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'Pastebin',\r\n regExp: /^\\w+:\\/\\/(?:\\w+\\.)?pastebin\\.com\\/(?!u\\/)(?:[\\w.]+(?:\\/|\\?i\\=))?(\\w+)/,\r\n el(a) {\r\n let div;\r\n return div = $.el('iframe',\r\n {src: `//pastebin.com/embed_iframe.php?i=${a.dataset.uid}`});\r\n }\r\n }\r\n , {\r\n key: 'SoundCloud',\r\n regExp: /^\\w+:\\/\\/(?:www\\.)?(?:soundcloud\\.com\\/|snd\\.sc\\/)([\\w\\-\\/]+)/,\r\n style: 'border: 0; width: 500px; height: 400px;',\r\n el(a) {\r\n return $.el('iframe',\r\n {src: `https://w.soundcloud.com/player/?visual=true&show_comments=false&url=https%3A%2F%2Fsoundcloud.com%2F${encodeURIComponent(a.dataset.uid)}`});\r\n },\r\n title: {\r\n api(uid) { return `${location.protocol}//soundcloud.com/oembed?format=json&url=https%3A%2F%2Fsoundcloud.com%2F${encodeURIComponent(uid)}`; },\r\n text(_) { return _.title; }\r\n }\r\n }\r\n , {\r\n key: 'StrawPoll',\r\n regExp: /^\\w+:\\/\\/(?:www\\.)?strawpoll\\.me\\/(?:embed_\\d+\\/)?(\\d+(?:\\/r)?)/,\r\n style: 'border: 0; width: 600px; height: 406px;',\r\n el(a) {\r\n return $.el('iframe',\r\n {src: `https://www.strawpoll.me/embed_1/${a.dataset.uid}`});\r\n }\r\n }\r\n , {\r\n key: 'Streamable',\r\n regExp: /^\\w+:\\/\\/(?:www\\.)?streamable\\.com\\/(\\w+)/,\r\n el(a) {\r\n const el = $.el('iframe',\r\n {src: `https://streamable.com/o/${a.dataset.uid}`});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n },\r\n title: {\r\n api(uid) { return `https://api.streamable.com/oembed?url=https://streamable.com/${uid}`; },\r\n text(_) { return _.title; }\r\n }\r\n }\r\n , {\r\n key: 'TwitchTV',\r\n regExp: /^\\w+:\\/\\/(?:www\\.|secure\\.|clips\\.|m\\.)?twitch\\.tv\\/(\\w[^#\\&\\?]*)/,\r\n el(a) {\r\n let url;\r\n let m = a.dataset.href.match(/^\\w+:\\/\\/(?:(clips\\.)|\\w+\\.)?twitch\\.tv\\/(?:\\w+\\/)?(clip\\/)?(\\w[^#\\&\\?]*)/);\r\n if (m[1] || m[2]) {\r\n url = `//clips.twitch.tv/embed?clip=${m[3]}&parent=${location.hostname}`;\r\n } else {\r\n let time;\r\n m = a.dataset.uid.match(/(\\w+)(?:\\/(?:v\\/)?(\\d+))?/);\r\n url = `//player.twitch.tv/?${m[2] ? `video=v${m[2]}` : `channel=${m[1]}`}&autoplay=false&parent=${location.hostname}`;\r\n if (time = a.dataset.href.match(/\\bt=(\\w+)/)) {\r\n url += `&time=${time[1]}`;\r\n }\r\n }\r\n const el = $.el('iframe',\r\n {src: url});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'Twitter',\r\n regExp: /^\\w+:\\/\\/(?:www\\.|mobile\\.)?twitter\\.com\\/(\\w+\\/status\\/\\d+)/,\r\n style: 'border: none; width: 550px; height: 250px; overflow: hidden; resize: both;',\r\n el(a) {\r\n const el = $.el('iframe');\r\n $.on(el, 'load', function() {\r\n return this.contentWindow.postMessage({element: 't', query: 'height'}, 'https://twitframe.com');\r\n });\r\n var onMessage = function(e) {\r\n if ((e.source === el.contentWindow) && (e.origin === 'https://twitframe.com')) {\r\n $.off(window, 'message', onMessage);\r\n return (cont || el).style.height = `${+$.minmax(e.data.height, 250, 0.8 * doc.clientHeight)}px`;\r\n }\r\n };\r\n $.on(window, 'message', onMessage);\r\n el.src = `https://twitframe.com/show?url=https://twitter.com/${a.dataset.uid}`;\r\n if ($.engine === 'gecko') {\r\n // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=680823\r\n el.style.cssText = 'border: none; width: 100%; height: 100%;';\r\n var cont = $.el('div');\r\n $.add(cont, el);\r\n return cont;\r\n } else {\r\n return el;\r\n }\r\n }\r\n }\r\n , {\r\n key: 'VidLii',\r\n regExp: /^\\w+:\\/\\/(?:www\\.)?vidlii\\.com\\/watch\\?v=(\\w{11})/,\r\n style: 'border: none; width: 640px; height: 392px;',\r\n el(a) {\r\n const el = $.el('iframe',\r\n {src: `https://www.vidlii.com/embed?v=${a.dataset.uid}&a=0`});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'Vimeo',\r\n regExp: /^\\w+:\\/\\/(?:www\\.)?vimeo\\.com\\/(\\d+)/,\r\n el(a) {\r\n const el = $.el('iframe',\r\n {src: `//player.vimeo.com/video/${a.dataset.uid}?wmode=opaque`});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n },\r\n title: {\r\n api(uid) { return `https://vimeo.com/api/oembed.json?url=https://vimeo.com/${uid}`; },\r\n text(_) { return _.title; }\r\n }\r\n }\r\n , {\r\n key: 'Vine',\r\n regExp: /^\\w+:\\/\\/(?:www\\.)?vine\\.co\\/v\\/(\\w+)/,\r\n style: 'border: none; width: 500px; height: 500px;',\r\n el(a) {\r\n return $.el('iframe',\r\n {src: `https://vine.co/v/${a.dataset.uid}/card`});\r\n }\r\n }\r\n , {\r\n key: 'Vocaroo',\r\n regExp: /^\\w+:\\/\\/(?:(?:www\\.|old\\.)?vocaroo\\.com|voca\\.ro)\\/((?:i\\/)?\\w+)/,\r\n style: '',\r\n el(a) {\r\n const el = $.el('iframe');\r\n el.width = 300;\r\n el.height = 60;\r\n el.setAttribute('frameborder', 0);\r\n el.src = `https://vocaroo.com/embed/${a.dataset.uid.replace(/^i\\//, '')}?autoplay=0`;\r\n return el;\r\n }\r\n }\r\n , {\r\n key: 'YouTube',\r\n regExp: /^\\w+:\\/\\/(?:youtu.be\\/|[\\w.]*youtube[\\w.]*\\/.*(?:v=|\\bembed\\/|\\bv\\/|live\\/))([\\w\\-]{11})(.*)/,\r\n el(a) {\r\n let start = a.dataset.options.match(/\\b(?:star)?t\\=(\\w+)/);\r\n if (start) { start = start[1]; }\r\n if (start && !/^\\d+$/.test(start)) {\r\n start += ' 0h0m0s';\r\n start = (3600 * start.match(/(\\d+)h/)[1]) + (60 * start.match(/(\\d+)m/)[1]) + (1 * start.match(/(\\d+)s/)[1]);\r\n }\r\n const el = $.el('iframe',\r\n {src: `//www.youtube.com/embed/${a.dataset.uid}?rel=0&wmode=opaque${start ? '&start=' + start : ''}`});\r\n el.setAttribute(\"allowfullscreen\", \"true\");\r\n return el;\r\n },\r\n title: {\r\n api(uid) { return `https://www.youtube.com/oembed?url=https%3A//www.youtube.com/watch%3Fv%3D${uid}&format=json`; },\r\n text(_) { return _.title; },\r\n status(_) {\r\n if (_.error) {\r\n const m = _.error.match(/^(\\d*)\\s*(.*)/);\r\n return [+m[1], m[2]];\r\n } else {\r\n return [200, 'OK'];\r\n }\r\n }\r\n },\r\n preview: {\r\n url(uid) { return `https://img.youtube.com/vi/${uid}/0.jpg`; },\r\n height: 360\r\n }\r\n }\r\n ]\r\n};\r\nexport default Embedding;\r\n","import Notice from \"../classes/Notice\";\r\nimport Config from \"../config/Config\";\r\nimport Filter from \"../Filtering/Filter\";\r\nimport ThreadHiding from \"../Filtering/ThreadHiding\";\r\nimport BoardConfig from \"../General/BoardConfig\";\r\nimport Get from \"../General/Get\";\r\nimport Header from \"../General/Header\";\r\nimport Index from \"../General/Index\";\r\nimport Settings from \"../General/Settings\";\r\nimport { Conf, d, g } from \"../globals/globals\";\r\nimport FappeTyme from \"../Images/FappeTyme\";\r\nimport Gallery from \"../Images/Gallery\";\r\nimport ImageExpand from \"../Images/ImageExpand\";\r\nimport Embedding from \"../Linkification/Embedding\";\r\nimport ThreadUpdater from \"../Monitoring/ThreadUpdater\";\r\nimport ThreadWatcher from \"../Monitoring/ThreadWatcher\";\r\nimport UnreadIndex from \"../Monitoring/UnreadIndex\";\r\nimport $ from \"../platform/$\";\r\nimport $$ from \"../platform/$$\";\r\nimport QR from \"../Posting/QR\";\r\nimport QuoteThreading from \"../Quotelinks/QuoteThreading\";\r\nimport QuoteYou from \"../Quotelinks/QuoteYou\";\r\nimport CatalogLinks from \"./CatalogLinks\";\r\nimport ExpandThread from \"./ExpandThread\";\r\nimport Nav from \"./Nav\";\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS205: Consider reworking code to avoid use of IIFEs\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\nvar Keybinds = {\r\n init() {\r\n if (!Conf['Keybinds']) { return; }\r\n\r\n for (var hotkey in Config.hotkeys) {\r\n $.sync(hotkey, Keybinds.sync);\r\n }\r\n\r\n var init = function() {\r\n $.off(d, '4chanXInitFinished', init);\r\n $.on(d, 'keydown', Keybinds.keydown);\r\n for (var node of $$('[accesskey]')) {\r\n node.removeAttribute('accesskey');\r\n }\r\n };\r\n return $.on(d, '4chanXInitFinished', init);\r\n },\r\n\r\n sync(key, hotkey) {\r\n return Conf[hotkey] = key;\r\n },\r\n\r\n keydown(e) {\r\n let key, thread, threadRoot;\r\n let catalog, notifications;\r\n if (!(key = Keybinds.keyCode(e))) { return; }\r\n const {target} = e;\r\n if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) {\r\n if (!/(Esc|Alt|Ctrl|Meta|Shift\\+\\w{2,})/.test(key) || !!/^Alt\\+(\\d|Up|Down|Left|Right)$/.test(key)) { return; }\r\n }\r\n if (['index', 'thread'].includes(g.VIEW)) {\r\n threadRoot = Nav.getThread();\r\n thread = Get.threadFromRoot(threadRoot);\r\n }\r\n switch (key) {\r\n // QR & Options\r\n case Conf['Toggle board list']:\r\n if (!Conf['Custom Board Navigation']) { return; }\r\n Header.toggleBoardList();\r\n break;\r\n case Conf['Toggle header']:\r\n Header.toggleBarVisibility();\r\n break;\r\n case Conf['Open empty QR']:\r\n if (!QR.postingIsEnabled) { return; }\r\n Keybinds.qr();\r\n break;\r\n case Conf['Open QR']:\r\n if (!QR.postingIsEnabled || !threadRoot) { return; }\r\n Keybinds.qr(threadRoot);\r\n break;\r\n case Conf['Open settings']:\r\n Settings.open();\r\n break;\r\n case Conf['Close']:\r\n if (Settings.dialog) {\r\n Settings.close();\r\n } else if ((notifications = $$('.notification')).length) {\r\n for (var notification of notifications) {\r\n $('.close', notification).click();\r\n }\r\n } else if (QR.nodes && !(QR.nodes.el.hidden || (window.getComputedStyle(QR.nodes.form).display === 'none'))) {\r\n if (Conf['Persistent QR']) {\r\n QR.hide();\r\n } else {\r\n QR.close();\r\n }\r\n } else if (Embedding.lastEmbed) {\r\n Embedding.closeFloat();\r\n } else {\r\n return;\r\n }\r\n break;\r\n case Conf['Spoiler tags']:\r\n if (target.nodeName !== 'TEXTAREA') { return; }\r\n Keybinds.tags('spoiler', target);\r\n break;\r\n case Conf['Code tags']:\r\n if (target.nodeName !== 'TEXTAREA') { return; }\r\n Keybinds.tags('code', target);\r\n break;\r\n case Conf['Eqn tags']:\r\n if (target.nodeName !== 'TEXTAREA') { return; }\r\n Keybinds.tags('eqn', target);\r\n break;\r\n case Conf['Math tags']:\r\n if (target.nodeName !== 'TEXTAREA') { return; }\r\n Keybinds.tags('math', target);\r\n break;\r\n case Conf['SJIS tags']:\r\n if (target.nodeName !== 'TEXTAREA') { return; }\r\n Keybinds.tags('sjis', target);\r\n break;\r\n case Conf['Toggle sage']:\r\n if (!QR.nodes || !!QR.nodes.el.hidden) { return; }\r\n Keybinds.sage();\r\n break;\r\n case Conf['Toggle Cooldown']:\r\n if (!QR.nodes || !!QR.nodes.el.hidden || !$.hasClass(QR.nodes.fileSubmit, 'custom-cooldown')) { return; }\r\n QR.toggleCustomCooldown();\r\n break;\r\n case Conf['Post from URL']:\r\n if (!QR.postingIsEnabled) { return; }\r\n QR.handleUrl('');\r\n break;\r\n case Conf['Add new post']:\r\n if (!QR.postingIsEnabled) { return; }\r\n QR.addPost();\r\n break;\r\n case Conf['Submit QR']:\r\n if (!QR.nodes || !!QR.nodes.el.hidden) { return; }\r\n if (!QR.status()) { QR.submit(); }\r\n break;\r\n // Index/Thread related\r\n case Conf['Update']:\r\n switch (g.VIEW) {\r\n case 'thread':\r\n if (!ThreadUpdater.enabled) { return; }\r\n ThreadUpdater.update();\r\n break;\r\n case 'index':\r\n if (!Index.enabled) { return; }\r\n Index.update();\r\n break;\r\n default:\r\n return;\r\n }\r\n break;\r\n case Conf['Watch']:\r\n if (!ThreadWatcher.enabled || !thread) { return; }\r\n ThreadWatcher.toggle(thread);\r\n break;\r\n case Conf['Update thread watcher']:\r\n if (!ThreadWatcher.enabled) { return; }\r\n ThreadWatcher.buttonFetchAll();\r\n break;\r\n case Conf['Toggle thread watcher']:\r\n if (!ThreadWatcher.enabled) { return; }\r\n ThreadWatcher.toggleWatcher();\r\n break;\r\n case Conf['Toggle threading']:\r\n if (!QuoteThreading.ready) { return; }\r\n QuoteThreading.toggleThreading();\r\n break;\r\n case Conf['Mark thread read']:\r\n if ((g.VIEW !== 'index') || !thread || !UnreadIndex.enabled) { return; }\r\n UnreadIndex.markRead.call(threadRoot);\r\n break;\r\n // Images\r\n case Conf['Expand image']:\r\n if (!ImageExpand.enabled || !threadRoot) { return; }\r\n var post = Get.postFromNode(Keybinds.post(threadRoot));\r\n if (post.file) { ImageExpand.toggle(post); }\r\n break;\r\n case Conf['Expand images']:\r\n if (!ImageExpand.enabled) { return; }\r\n ImageExpand.cb.toggleAll();\r\n break;\r\n case Conf['Open Gallery']:\r\n if (!Gallery.enabled) { return; }\r\n Gallery.cb.toggle();\r\n break;\r\n case Conf['fappeTyme']:\r\n if (!FappeTyme.nodes?.fappe) { return; }\r\n FappeTyme.toggle('fappe');\r\n break;\r\n case Conf['werkTyme']:\r\n if (!FappeTyme.nodes?.werk) { return; }\r\n FappeTyme.toggle('werk');\r\n break;\r\n // Board Navigation\r\n case Conf['Front page']:\r\n if (Index.enabled) {\r\n Index.userPageNav(1);\r\n } else {\r\n location.href = `/${g.BOARD}/`;\r\n }\r\n break;\r\n case Conf['Open front page']:\r\n $.open(`${location.origin}/${g.BOARD}/`);\r\n break;\r\n case Conf['Next page']:\r\n if ((g.VIEW !== 'index') || !!g.SITE.isOnePage?.(g.BOARD)) { return; }\r\n if (Index.enabled) {\r\n if (!['paged', 'infinite'].includes(Conf['Index Mode'])) { return; }\r\n $('.next button', Index.pagelist).click();\r\n } else {\r\n $(g.SITE.selectors.nav.next)?.click();\r\n }\r\n break;\r\n case Conf['Previous page']:\r\n if ((g.VIEW !== 'index') || !!g.SITE.isOnePage?.(g.BOARD)) { return; }\r\n if (Index.enabled) {\r\n if (!['paged', 'infinite'].includes(Conf['Index Mode'])) { return; }\r\n $('.prev button', Index.pagelist).click();\r\n } else {\r\n $(g.SITE.selectors.nav.prev)?.click();\r\n }\r\n break;\r\n case Conf['Search form']:\r\n if (g.VIEW !== 'index') { return; }\r\n var searchInput = Index.enabled ?\r\n Index.searchInput\r\n : g.SITE.selectors.searchBox ?\r\n $(g.SITE.selectors.searchBox)\r\n :\r\n undefined;\r\n if (!searchInput) { return; }\r\n Header.scrollToIfNeeded(searchInput);\r\n searchInput.focus();\r\n break;\r\n case Conf['Paged mode']:\r\n if (!Index.enabledOn(g.BOARD)) { return; }\r\n location.href = g.VIEW === 'index' ? '#paged' : `/${g.BOARD}/#paged`;\r\n break;\r\n case Conf['Infinite scrolling mode']:\r\n if (!Index.enabledOn(g.BOARD)) { return; }\r\n location.href = g.VIEW === 'index' ? '#infinite' : `/${g.BOARD}/#infinite`;\r\n break;\r\n case Conf['All pages mode']:\r\n if (!Index.enabledOn(g.BOARD)) { return; }\r\n location.href = g.VIEW === 'index' ? '#all-pages' : `/${g.BOARD}/#all-pages`;\r\n break;\r\n case Conf['Open catalog']:\r\n if (!(catalog = CatalogLinks.catalog())) { return; }\r\n location.href = catalog;\r\n break;\r\n case Conf['Cycle sort type']:\r\n if (!Index.enabled) { return; }\r\n Index.cycleSortType();\r\n break;\r\n // Thread Navigation\r\n case Conf['Next thread']:\r\n if ((g.VIEW !== 'index') || !threadRoot) { return; }\r\n Nav.scroll(+1);\r\n break;\r\n case Conf['Previous thread']:\r\n if ((g.VIEW !== 'index') || !threadRoot) { return; }\r\n Nav.scroll(-1);\r\n break;\r\n case Conf['Expand thread']:\r\n if ((g.VIEW !== 'index') || !threadRoot) { return; }\r\n ExpandThread.toggle(thread);\r\n // Keep thread from moving off screen when contracted.\r\n Header.scrollTo(threadRoot);\r\n break;\r\n case Conf['Open thread']:\r\n if ((g.VIEW !== 'index') || !threadRoot) { return; }\r\n Keybinds.open(thread);\r\n break;\r\n case Conf['Open thread tab']:\r\n if ((g.VIEW !== 'index') || !threadRoot) { return; }\r\n Keybinds.open(thread, true);\r\n break;\r\n // Reply Navigation\r\n case Conf['Next reply']:\r\n if (!threadRoot) { return; }\r\n Keybinds.hl(+1, threadRoot);\r\n break;\r\n case Conf['Previous reply']:\r\n if (!threadRoot) { return; }\r\n Keybinds.hl(-1, threadRoot);\r\n break;\r\n case Conf['Deselect reply']:\r\n if (!threadRoot) { return; }\r\n Keybinds.hl(0, threadRoot);\r\n break;\r\n case Conf['Hide']:\r\n if (!thread || !ThreadHiding.db) { return; }\r\n Header.scrollTo(threadRoot);\r\n ThreadHiding.toggle(thread);\r\n break;\r\n case Conf['Quick Filter MD5']:\r\n if (!threadRoot) { return; }\r\n post = Keybinds.post(threadRoot);\r\n Keybinds.hl(+1, threadRoot);\r\n Filter.quickFilterMD5.call(post, e);\r\n break;\r\n case Conf['Previous Post Quoting You']:\r\n if (!threadRoot || !QuoteYou.db) { return; }\r\n QuoteYou.cb.seek('preceding');\r\n break;\r\n case Conf['Next Post Quoting You']:\r\n if (!threadRoot || !QuoteYou.db) { return; }\r\n QuoteYou.cb.seek('following');\r\n break;\r\n default:\r\n return;\r\n }\r\n e.preventDefault();\r\n return e.stopPropagation();\r\n },\r\n\r\n keyCode(e) {\r\n let key = (() => { let kc;\r\n switch ((kc = e.keyCode)) {\r\n case 8: // return\r\n return '';\r\n case 13:\r\n return 'Enter';\r\n case 27:\r\n return 'Esc';\r\n case 32:\r\n return 'Space';\r\n case 37:\r\n return 'Left';\r\n case 38:\r\n return 'Up';\r\n case 39:\r\n return 'Right';\r\n case 40:\r\n return 'Down';\r\n case 188:\r\n return 'Comma';\r\n case 190:\r\n return 'Period';\r\n case 191:\r\n return 'Slash';\r\n case 59: case 186:\r\n return 'Semicolon';\r\n default:\r\n if ((48 <= kc && kc <= 57) || (65 <= kc && kc <= 90)) { // 0-9, A-Z\r\n return String.fromCharCode(kc).toLowerCase();\r\n } else if (96 <= kc && kc <= 105) { // numpad 0-9\r\n return String.fromCharCode(kc - 48).toLowerCase();\r\n } else {\r\n return null;\r\n }\r\n } })();\r\n if (key) {\r\n if (e.altKey) { key = 'Alt+' + key; }\r\n if (e.ctrlKey) { key = 'Ctrl+' + key; }\r\n if (e.metaKey) { key = 'Meta+' + key; }\r\n if (e.shiftKey) { key = 'Shift+' + key; }\r\n }\r\n return key;\r\n },\r\n\r\n post(thread) {\r\n const s = g.SITE.selectors;\r\n return (\r\n $(`${s.postContainer}${s.highlightable.reply}.${g.SITE.classes.highlight}`, thread) ||\r\n $(`${g.SITE.isOPContainerThread ? s.thread : s.postContainer}${s.highlightable.op}`, thread)\r\n );\r\n },\r\n\r\n qr(thread) {\r\n QR.open();\r\n if (thread != null) {\r\n QR.quote.call(Keybinds.post(thread));\r\n }\r\n return QR.nodes.com.focus();\r\n },\r\n\r\n tags(tag, ta) {\r\n BoardConfig.ready(function() {\r\n const {config} = g.BOARD;\r\n const supported = (() => { switch (tag) {\r\n case 'spoiler': return !!config.spoilers;\r\n case 'code': return !!config.code_tags;\r\n case 'math': case 'eqn': return !!config.math_tags;\r\n case 'sjis': return !!config.sjis_tags;\r\n } })();\r\n if (!supported) { return new Notice('warning', `[${tag}] tags are not supported on /${g.BOARD}/.`, 20); }\r\n });\r\n\r\n const {\r\n value\r\n } = ta;\r\n const selStart = ta.selectionStart;\r\n const selEnd = ta.selectionEnd;\r\n\r\n ta.value =\r\n value.slice(0, selStart) +\r\n `[${tag}]` + value.slice(selStart, selEnd) + `[/${tag}]` +\r\n value.slice(selEnd);\r\n\r\n // Move the caret to the end of the selection.\r\n const range = (`[${tag}]`).length + selEnd;\r\n ta.setSelectionRange(range, range);\r\n\r\n // Fire the 'input' event\r\n return $.event('input', null, ta);\r\n },\r\n\r\n sage() {\r\n const isSage = /sage/i.test(QR.nodes.email.value);\r\n return QR.nodes.email.value = isSage ?\r\n \"\"\r\n : \"sage\";\r\n },\r\n\r\n open(thread, tab) {\r\n if (g.VIEW !== 'index') { return; }\r\n const url = Get.url('thread', thread);\r\n if (tab) {\r\n return $.open(url);\r\n } else {\r\n return location.href = url;\r\n }\r\n },\r\n\r\n hl(delta, thread) {\r\n const replySelector = `${g.SITE.selectors.postContainer}${g.SITE.selectors.highlightable.reply}`;\r\n const {highlight} = g.SITE.classes;\r\n\r\n const postEl = $(`${replySelector}.${highlight}`, thread);\r\n\r\n if (!delta) {\r\n if (postEl) { $.rmClass(postEl, highlight); }\r\n return;\r\n }\r\n\r\n if (postEl) {\r\n const {height} = postEl.getBoundingClientRect();\r\n if ((Header.getTopOf(postEl) >= -height) && (Header.getBottomOf(postEl) >= -height)) { // We're at least partially visible\r\n let next;\r\n const {root} = Get.postFromNode(postEl).nodes;\r\n const axis = delta === +1 ?\r\n 'following'\r\n :\r\n 'preceding';\r\n if (!(next = $.x(`${axis}-sibling::${g.SITE.xpath.replyContainer}[not(@hidden) and not(child::div[@class='stub'])][1]`, root))) { return; }\r\n if (!next.matches(replySelector)) { next = $(replySelector, next); }\r\n Header.scrollToIfNeeded(next, delta === +1);\r\n $.addClass(next, highlight);\r\n $.rmClass(postEl, highlight);\r\n return;\r\n }\r\n $.rmClass(postEl, highlight);\r\n }\r\n\r\n const replies = $$(replySelector, thread);\r\n if (delta === -1) { replies.reverse(); }\r\n for (var reply of replies) {\r\n if (((delta === +1) && (Header.getTopOf(reply) > 0)) || ((delta === -1) && (Header.getBottomOf(reply) > 0))) {\r\n $.addClass(reply, highlight);\r\n return;\r\n }\r\n }\r\n }\r\n};\r\nexport default Keybinds;\r\n","import $ from \"../platform/$\";\r\nimport CaptchaReplace from \"./Captcha.replace\";\r\nimport CaptchaT from \"./Captcha.t\";\r\nimport meta from '../../package.json';\r\nimport Main from \"../main/Main\";\r\nimport Keybinds from \"../Miscellaneous/Keybinds\";\r\nimport $$ from \"../platform/$$\";\r\nimport QR from \"./QR\";\r\nimport { Conf, d } from \"../globals/globals\";\r\nimport { MINUTE, SECOND } from \"../platform/helpers\";\r\n\r\nconst Captcha = {\r\n Cache: {\r\n init() {\r\n $.on(d, 'SaveCaptcha', e => {\r\n return this.saveAPI(e.detail);\r\n });\r\n return $.on(d, 'NoCaptcha', e => {\r\n return this.noCaptcha(e.detail);\r\n });\r\n },\r\n\r\n captchas: [],\r\n\r\n getCount() {\r\n return this.captchas.length;\r\n },\r\n\r\n neededRaw() {\r\n return !(\r\n this.haveCookie() || this.captchas.length || QR.req || this.submitCB\r\n ) && (\r\n (QR.posts.length > 1) || Conf['Auto-load captcha'] || !QR.posts[0].isOnlyQuotes() || QR.posts[0].file\r\n );\r\n },\r\n\r\n needed() {\r\n return this.neededRaw() && $.event('LoadCaptcha');\r\n },\r\n\r\n prerequest() {\r\n if (!Conf['Prerequest Captcha']) { return; }\r\n // Post count temporarily off by 1 when called from QR.post.rm, QR.close, or QR.submit\r\n return $.queueTask(() => {\r\n if (\r\n !this.prerequested &&\r\n this.neededRaw() &&\r\n !$.event('LoadCaptcha') &&\r\n !QR.captcha.occupied() &&\r\n (QR.cooldown.seconds <= 60) &&\r\n (QR.selected === QR.posts[QR.posts.length - 1]) &&\r\n !QR.selected.isOnlyQuotes()\r\n ) {\r\n const isReply = (QR.selected.thread !== 'new');\r\n if (!$.event('RequestCaptcha', { isReply })) {\r\n this.prerequested = true;\r\n this.submitCB = captcha => {\r\n if (captcha) { return this.save(captcha); }\r\n };\r\n return this.updateCount();\r\n }\r\n }\r\n });\r\n },\r\n\r\n haveCookie() {\r\n return /\\b_ct=/.test(d.cookie) && (QR.posts[0].thread !== 'new');\r\n },\r\n\r\n getOne() {\r\n let captcha;\r\n delete this.prerequested;\r\n this.clear();\r\n if (captcha = this.captchas.shift()) {\r\n this.count();\r\n return captcha;\r\n } else {\r\n return null;\r\n }\r\n },\r\n\r\n request(isReply) {\r\n if (!this.submitCB) {\r\n if ($.event('RequestCaptcha', { isReply })) { return; }\r\n }\r\n return cb => {\r\n this.submitCB = cb;\r\n return this.updateCount();\r\n };\r\n },\r\n\r\n abort() {\r\n if (this.submitCB) {\r\n delete this.submitCB;\r\n $.event('AbortCaptcha');\r\n return this.updateCount();\r\n }\r\n },\r\n\r\n saveAPI(captcha) {\r\n let cb;\r\n if (cb = this.submitCB) {\r\n delete this.submitCB;\r\n cb(captcha);\r\n return this.updateCount();\r\n } else {\r\n return this.save(captcha);\r\n }\r\n },\r\n\r\n noCaptcha(detail) {\r\n let cb;\r\n if (cb = this.submitCB) {\r\n if (!this.haveCookie() || detail?.error) {\r\n QR.error(detail?.error || 'Failed to retrieve captcha.');\r\n QR.captcha.setup(d.activeElement === QR.nodes.status);\r\n }\r\n delete this.submitCB;\r\n cb();\r\n return this.updateCount();\r\n }\r\n },\r\n\r\n save(captcha) {\r\n let cb;\r\n if (cb = this.submitCB) {\r\n this.abort();\r\n cb(captcha);\r\n return;\r\n }\r\n this.captchas.push(captcha);\r\n this.captchas.sort((a, b) => a.timeout - b.timeout);\r\n return this.count();\r\n },\r\n\r\n clear() {\r\n if (this.captchas.length) {\r\n let i;\r\n const now = Date.now();\r\n for (i = 0; i < this.captchas.length; i++) {\r\n var captcha = this.captchas[i];\r\n if (captcha.timeout > now) { break; }\r\n }\r\n if (i) {\r\n this.captchas = this.captchas.slice(i);\r\n return this.count();\r\n }\r\n }\r\n },\r\n\r\n count() {\r\n clearTimeout(this.timer);\r\n if (this.captchas.length) {\r\n this.timer = setTimeout(this.clear.bind(this), this.captchas[0].timeout - Date.now());\r\n }\r\n return this.updateCount();\r\n },\r\n\r\n updateCount() {\r\n return $.event('CaptchaCount', this.captchas.length);\r\n }\r\n }, Replace: CaptchaReplace, t: CaptchaT, v2: {\r\n lifetime: 2 * MINUTE,\r\n\r\n init() {\r\n if (d.cookie.indexOf('pass_enabled=1') >= 0) { return; }\r\n if (!(this.isEnabled = !!$('#g-recaptcha, #captcha-forced-noscript') || !$.id('postForm'))) { return; }\r\n\r\n if (this.noscript = Conf['Force Noscript Captcha'] || !Main.jsEnabled) {\r\n $.addClass(QR.nodes.el, 'noscript-captcha');\r\n }\r\n\r\n Captcha.cache.init();\r\n $.on(d, 'CaptchaCount', this.count.bind(this));\r\n\r\n const root = $.el('div', { className: 'captcha-root' });\r\n $.extend(root, {\r\n innerHTML:\r\n '
    '\r\n }\r\n );\r\n const counter = $('.captcha-counter > a', root);\r\n this.nodes = { root, counter };\r\n this.count();\r\n $.addClass(QR.nodes.el, 'has-captcha', 'captcha-v2');\r\n $.after(QR.nodes.com.parentNode, root);\r\n\r\n $.on(counter, 'click', this.toggle.bind(this));\r\n $.on(counter, 'keydown', e => {\r\n if (Keybinds.keyCode(e) !== 'Space') { return; }\r\n this.toggle();\r\n e.preventDefault();\r\n return e.stopPropagation();\r\n });\r\n return $.on(window, 'captcha:success', () => {\r\n // XXX Greasemonkey 1.x workaround to gain access to GM_* functions.\r\n return $.queueTask(() => this.save(false));\r\n });\r\n },\r\n\r\n timeouts: {},\r\n prevNeeded: 0,\r\n\r\n noscriptURL() {\r\n let lang;\r\n let url = `https://www.google.com/recaptcha/api/fallback?k=${meta.recaptchaKey}`;\r\n if (lang = Conf['captchaLanguage'].trim()) {\r\n url += `&hl=${encodeURIComponent(lang)}`;\r\n }\r\n return url;\r\n },\r\n\r\n moreNeeded() {\r\n // Post count temporarily off by 1 when called from QR.post.rm, QR.close, or QR.submit\r\n return $.queueTask(() => {\r\n const needed = Captcha.cache.needed();\r\n if (needed && !this.prevNeeded) {\r\n this.setup(QR.cooldown.auto && (d.activeElement === QR.nodes.status));\r\n }\r\n return this.prevNeeded = needed;\r\n });\r\n },\r\n\r\n toggle() {\r\n if (this.nodes.container && !this.timeouts.destroy) {\r\n return this.destroy();\r\n } else {\r\n return this.setup(true, true);\r\n }\r\n },\r\n\r\n setup(focus, force) {\r\n if (!this.isEnabled || (!Captcha.cache.needed() && !force)) { return; }\r\n\r\n if (focus) {\r\n $.addClass(QR.nodes.el, 'focus');\r\n this.nodes.counter.focus();\r\n }\r\n\r\n if (this.timeouts.destroy) {\r\n clearTimeout(this.timeouts.destroy);\r\n delete this.timeouts.destroy;\r\n return this.reload();\r\n }\r\n\r\n if (this.nodes.container) {\r\n // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1226835\r\n $.queueTask(() => {\r\n let iframe;\r\n if (this.nodes.container && (d.activeElement === this.nodes.counter) && (iframe = $('iframe[src^=\"https://www.google.com/recaptcha/\"]', this.nodes.container))) {\r\n iframe.focus();\r\n return QR.focus();\r\n }\r\n }); // Event handler not fired in Firefox\r\n return;\r\n }\r\n\r\n this.nodes.container = $.el('div', { className: 'captcha-container' });\r\n $.prepend(this.nodes.root, this.nodes.container);\r\n new MutationObserver(this.afterSetup.bind(this)).observe(this.nodes.container, {\r\n childList: true,\r\n subtree: true\r\n }\r\n );\r\n\r\n if (this.noscript) {\r\n return this.setupNoscript();\r\n } else {\r\n return this.setupJS();\r\n }\r\n },\r\n\r\n setupNoscript() {\r\n const iframe = $.el('iframe', {\r\n id: 'qr-captcha-iframe',\r\n scrolling: 'no',\r\n src: this.noscriptURL()\r\n }\r\n );\r\n const div = $.el('div');\r\n const textarea = $.el('textarea');\r\n $.add(div, textarea);\r\n return $.add(this.nodes.container, [iframe, div]);\r\n },\r\n\r\n setupJS() {\r\n return $.global(function () {\r\n const render = function () {\r\n const { classList } = document.documentElement;\r\n const container = document.querySelector('#qr .captcha-container');\r\n return container.dataset.widgetID = window.grecaptcha.render(container, {\r\n sitekey: meta.recaptchaKey,\r\n theme: classList.contains('tomorrow') || classList.contains('spooky') || classList.contains('dark-captcha') ? 'dark' : 'light',\r\n callback(response) {\r\n return window.dispatchEvent(new CustomEvent('captcha:success', { detail: response }));\r\n }\r\n }\r\n );\r\n };\r\n if (window.grecaptcha) {\r\n return render();\r\n } else {\r\n const cbNative = window.onRecaptchaLoaded;\r\n window.onRecaptchaLoaded = function () {\r\n render();\r\n return cbNative();\r\n };\r\n if (!document.head.querySelector('script[src^=\"https://www.google.com/recaptcha/api.js\"]')) {\r\n const script = document.createElement('script');\r\n script.src = 'https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoaded&render=explicit';\r\n return document.head.appendChild(script);\r\n }\r\n }\r\n });\r\n },\r\n\r\n afterSetup(mutations) {\r\n for (var mutation of mutations) {\r\n for (var node of mutation.addedNodes) {\r\n var iframe, textarea;\r\n if (iframe = $.x('./descendant-or-self::iframe[starts-with(@src, \"https://www.google.com/recaptcha/\")]', node)) { this.setupIFrame(iframe); }\r\n if (textarea = $.x('./descendant-or-self::textarea', node)) { this.setupTextArea(textarea); }\r\n }\r\n }\r\n },\r\n\r\n setupIFrame(iframe) {\r\n let needle;\r\n if (!doc.contains(iframe)) { return; }\r\n Captcha.replace.iframe(iframe);\r\n $.addClass(QR.nodes.el, 'captcha-open');\r\n this.fixQRPosition();\r\n $.on(iframe, 'load', this.fixQRPosition);\r\n if (d.activeElement === this.nodes.counter) { iframe.focus(); }\r\n // XXX Make sure scroll on space prevention (see src/css/style.css) doesn't cause scrolling of div\r\n if (['blink', 'edge'].includes($.engine) && (needle = iframe.parentNode, $$('#qr .captcha-container > div > div:first-of-type').includes(needle))) {\r\n return $.on(iframe.parentNode, 'scroll', function () { return this.scrollTop = 0; });\r\n }\r\n },\r\n\r\n fixQRPosition() {\r\n if (QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight) {\r\n QR.nodes.el.style.top = '';\r\n return QR.nodes.el.style.bottom = '0px';\r\n }\r\n },\r\n\r\n setupTextArea(textarea) {\r\n return $.one(textarea, 'input', () => this.save(true));\r\n },\r\n\r\n destroy() {\r\n if (!this.isEnabled) { return; }\r\n delete this.timeouts.destroy;\r\n $.rmClass(QR.nodes.el, 'captcha-open');\r\n if (this.nodes.container) {\r\n $.global(function () {\r\n const container = document.querySelector('#qr .captcha-container');\r\n return window.grecaptcha.reset(container.dataset.widgetID);\r\n });\r\n $.rm(this.nodes.container);\r\n return delete this.nodes.container;\r\n }\r\n },\r\n\r\n getOne(isReply) {\r\n return Captcha.cache.getOne(isReply);\r\n },\r\n\r\n save(pasted, token) {\r\n Captcha.cache.save({\r\n response: token || $('textarea', this.nodes.container).value,\r\n timeout: Date.now() + this.lifetime\r\n });\r\n\r\n const focus = (d.activeElement?.nodeName === 'IFRAME') && /https?:\\/\\/www\\.google\\.com\\/recaptcha\\//.test(d.activeElement.src);\r\n if (Captcha.cache.needed()) {\r\n if (focus) {\r\n if (QR.cooldown.auto || Conf['Post on Captcha Completion']) {\r\n this.nodes.counter.focus();\r\n } else {\r\n QR.nodes.status.focus();\r\n }\r\n }\r\n this.reload();\r\n } else {\r\n if (pasted) {\r\n this.destroy();\r\n } else {\r\n if (this.timeouts.destroy == null) { this.timeouts.destroy = setTimeout(this.destroy.bind(this), 3 * SECOND); }\r\n }\r\n if (focus) { QR.nodes.status.focus(); }\r\n }\r\n\r\n if (Conf['Post on Captcha Completion'] && !QR.cooldown.auto) { return QR.submit(); }\r\n },\r\n\r\n count() {\r\n const count = Captcha.cache.getCount();\r\n const loading = Captcha.cache.submitCB ? '...' : '';\r\n this.nodes.counter.textContent = `Captchas: ${count}${loading}`;\r\n return this.moreNeeded();\r\n },\r\n\r\n reload() {\r\n if ($('iframe[src^=\"https://www.google.com/recaptcha/api/fallback?\"]', this.nodes.container)) {\r\n this.destroy();\r\n return this.setup(false, true);\r\n } else {\r\n return $.global(function () {\r\n const container = document.querySelector('#qr .captcha-container');\r\n return window.grecaptcha.reset(container.dataset.widgetID);\r\n });\r\n }\r\n },\r\n\r\n occupied() {\r\n return !!this.nodes.container && !this.timeouts.destroy;\r\n }\r\n }\r\n};\r\nexport default Captcha;\r\n","import QuickReplyPage from './QR/QuickReply.html';\r\nimport $ from '../platform/$';\r\nimport Callbacks from '../classes/Callbacks';\r\nimport Notice from '../classes/Notice';\r\nimport Main from '../main/Main';\r\nimport Favicon from '../Monitoring/Favicon';\r\nimport $$ from '../platform/$$';\r\nimport CrossOrigin from '../platform/CrossOrigin';\r\nimport Captcha from './Captcha';\r\nimport meta from '../../package.json';\r\nimport Header from '../General/Header';\r\nimport { Conf, E, d, doc, g } from '../globals/globals';\r\nimport Menu from '../Menu/Menu';\r\nimport UI from '../General/UI';\r\nimport BoardConfig from '../General/BoardConfig';\r\nimport Get from '../General/Get';\r\nimport { DAY, dict, SECOND } from '../platform/helpers';\r\n\r\n/*\r\n * decaffeinate suggestions:\r\n * DS102: Remove unnecessary code created because of implicit returns\r\n * DS202: Simplify dynamic range loops\r\n * DS207: Consider shorter variations of null checks\r\n * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md\r\n */\r\n\r\nvar QR = {\r\n mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm'],\r\n\r\n validExtension: /\\.(jpe?g|png|gif|pdf|swf|webm)$/i,\r\n\r\n typeFromExtension: {\r\n 'jpg': 'image/jpeg',\r\n 'jpeg': 'image/jpeg',\r\n 'png': 'image/png',\r\n 'gif': 'image/gif',\r\n 'pdf': 'application/pdf',\r\n 'swf': 'application/vnd.adobe.flash.movie',\r\n 'webm': 'video/webm'\r\n },\r\n\r\n extensionFromType: {\r\n 'image/jpeg': 'jpg',\r\n 'image/png': 'png',\r\n 'image/gif': 'gif',\r\n 'application/pdf': 'pdf',\r\n 'application/vnd.adobe.flash.movie': 'swf',\r\n 'application/x-shockwave-flash': 'swf',\r\n 'video/webm': 'webm'\r\n },\r\n\r\n init() {\r\n let sc;\r\n if (!Conf['Quick Reply']) { return; }\r\n\r\n this.posts = [];\r\n\r\n $.on(d, '4chanXInitFinished', () => BoardConfig.ready(QR.initReady));\r\n\r\n Callbacks.Post.push({\r\n name: 'Quick Reply',\r\n cb: this.node\r\n });\r\n\r\n this.shortcut = (sc = $.el('a', {\r\n className: 'disabled',\r\n textContent: '↩',\r\n title: 'Quick Reply',\r\n href: 'javascript:;'\r\n }\r\n ));\r\n $.on(sc, 'click', function() {\r\n if (!QR.postingIsEnabled) { return; }\r\n if (Conf['Persistent QR'] || !QR.nodes || QR.nodes.el.hidden) {\r\n QR.open();\r\n return QR.nodes.com.focus();\r\n } else {\r\n return QR.close();\r\n }\r\n });\r\n\r\n return Header.addShortcut('qr', sc, 540);\r\n },\r\n\r\n initReady() {\r\n let origToggle;\r\n const captchaVersion = $('#g-recaptcha, #captcha-forced-noscript') ? 'v2' : 't';\r\n QR.captcha = Captcha[captchaVersion];\r\n QR.postingIsEnabled = true;\r\n\r\n const {config} = g.BOARD;\r\n const prop = (key, def) => +(config[key] ?? def);\r\n\r\n QR.min_width = prop('min_image_width', 1);\r\n QR.min_height = prop('min_image_height', 1);\r\n QR.max_width = (QR.max_height = 10000);\r\n\r\n QR.max_size = prop('max_filesize', 4194304);\r\n QR.max_size_video = prop('max_webm_filesize', QR.max_size);\r\n QR.max_comment = prop('max_comment_chars', 2000);\r\n\r\n QR.max_width_video = (QR.max_height_video = 2048);\r\n QR.max_duration_video = prop('max_webm_duration', 120);\r\n\r\n QR.forcedAnon = !!config.forced_anon;\r\n QR.spoiler = !!config.spoilers;\r\n\r\n if (origToggle = $.id('togglePostFormLink')) {\r\n const link = $.el('h1',\r\n {className: \"qr-link-container\"});\r\n $.extend(link, {\r\n innerHTML:\r\n `${g.VIEW === \"thread\" ? \"Reply to Thread\" : \"Start a Thread\"}`\r\n });\r\n\r\n QR.link = link.firstElementChild;\r\n $.on(link.firstChild, 'click', function() {\r\n QR.open();\r\n return QR.nodes.com.focus();\r\n });\r\n\r\n $.before(origToggle, link);\r\n origToggle.firstElementChild.textContent = 'Original Form';\r\n }\r\n\r\n if (g.VIEW === 'thread') {\r\n let navLinksBot;\r\n const linkBot = $.el('div',\r\n {className: \"brackets-wrap qr-link-container-bottom\"});\r\n $.extend(linkBot, {innerHTML: 'Reply to Thread'});\r\n\r\n $.on(linkBot.firstElementChild, 'click', function() {\r\n QR.open();\r\n return QR.nodes.com.focus();\r\n });\r\n\r\n if (navLinksBot = $('.navLinksBot')) { $.prepend(navLinksBot, linkBot); }\r\n }\r\n\r\n $.on(d, 'QRGetFile', QR.getFile);\r\n $.on(d, 'QRDrawFile', QR.drawFile);\r\n $.on(d, 'QRSetFile', QR.setFile);\r\n\r\n $.on(d, 'paste', QR.paste);\r\n $.on(d, 'dragover', QR.dragOver);\r\n $.on(d, 'drop', QR.dropFile);\r\n $.on(d, 'dragstart dragend', QR.drag);\r\n\r\n $.on(d, 'IndexRefreshInternal', QR.generatePostableThreadsList);\r\n $.on(d, 'ThreadUpdate', QR.statusCheck);\r\n\r\n if (!Conf['Persistent QR']) { return; }\r\n QR.open();\r\n if (Conf['Auto Hide QR']) { return QR.hide(); }\r\n },\r\n\r\n statusCheck() {\r\n if (!QR.nodes) { return; }\r\n const {thread} = QR.posts[0];\r\n if ((thread !== 'new') && g.threads.get(`${g.BOARD}.${thread}`).isDead) {\r\n return QR.abort();\r\n } else {\r\n return QR.status();\r\n }\r\n },\r\n\r\n node() {\r\n $.on(this.nodes.quote, 'click', QR.quote);\r\n if (this.isFetchedQuote) { return QR.generatePostableThreadsList(); }\r\n },\r\n\r\n open() {\r\n if (QR.nodes) {\r\n if (QR.nodes.el.hidden) { QR.captcha.setup(); }\r\n QR.nodes.el.hidden = false;\r\n QR.unhide();\r\n } else {\r\n try {\r\n QR.dialog();\r\n } catch (err) {\r\n delete QR.nodes;\r\n Main.handleErrors({\r\n message: 'Quick Reply dialog creation crashed.',\r\n error: err\r\n });\r\n return;\r\n }\r\n }\r\n return $.rmClass(QR.shortcut, 'disabled');\r\n },\r\n\r\n close() {\r\n if (QR.req) {\r\n QR.abort();\r\n return;\r\n }\r\n QR.nodes.el.hidden = true;\r\n QR.cleanNotifications();\r\n QR.blur();\r\n $.rmClass(QR.nodes.el, 'dump');\r\n $.addClass(QR.shortcut, 'disabled');\r\n new QR.post(true);\r\n for (var post of QR.posts.splice(0, QR.posts.length - 1)) {\r\n post.delete();\r\n }\r\n QR.cooldown.auto = false;\r\n QR.status();\r\n return QR.captcha.destroy();\r\n },\r\n\r\n focus() {\r\n return $.queueTask(function() {\r\n if (!QR.inBubble()) {\r\n QR.hasFocus = d.activeElement && QR.nodes.el.contains(d.activeElement);\r\n return QR.nodes.el.classList.toggle('focus', QR.hasFocus);\r\n }\r\n });\r\n },\r\n\r\n inBubble() {\r\n const bubbles = $$('iframe[src^=\"https://www.google.com/recaptcha/api2/frame\"]');\r\n return bubbles.includes(d.activeElement) || bubbles.some(el => (getComputedStyle(el).visibility !== 'hidden') && (el.getBoundingClientRect().bottom > 0));\r\n },\r\n\r\n hide() {\r\n QR.blur();\r\n $.addClass(QR.nodes.el, 'autohide');\r\n return QR.nodes.autohide.checked = true;\r\n },\r\n\r\n unhide() {\r\n $.rmClass(QR.nodes.el, 'autohide');\r\n return QR.nodes.autohide.checked = false;\r\n },\r\n\r\n toggleHide() {\r\n if (this.checked) {\r\n return QR.hide();\r\n } else {\r\n return QR.unhide();\r\n }\r\n },\r\n\r\n blur() {\r\n if (QR.nodes.el.contains(d.activeElement)) { return d.activeElement.blur(); }\r\n },\r\n\r\n toggleSJIS(e) {\r\n e.preventDefault();\r\n Conf['sjisPreview'] = !Conf['sjisPreview'];\r\n $.set('sjisPreview', Conf['sjisPreview']);\r\n return QR.nodes.el.classList.toggle('sjis-preview', Conf['sjisPreview']);\r\n },\r\n\r\n texPreviewShow() {\r\n if ($.hasClass(QR.nodes.el, 'tex-preview')) { return QR.texPreviewHide(); }\r\n $.addClass(QR.nodes.el, 'tex-preview');\r\n QR.nodes.texPreview.textContent = QR.nodes.com.value;\r\n return $.event('mathjax', null, QR.nodes.texPreview);\r\n },\r\n\r\n texPreviewHide() {\r\n return $.rmClass(QR.nodes.el, 'tex-preview');\r\n },\r\n\r\n addPost() {\r\n const wasOpen = (QR.nodes && !QR.nodes.el.hidden);\r\n QR.open();\r\n if (wasOpen) {\r\n $.addClass(QR.nodes.el, 'dump');\r\n new QR.post(true);\r\n }\r\n return QR.nodes.com.focus();\r\n },\r\n\r\n setCustomCooldown(enabled) {\r\n Conf['customCooldownEnabled'] = enabled;\r\n QR.cooldown.customCooldown = enabled;\r\n return QR.nodes.customCooldown.classList.toggle('disabled', !enabled);\r\n },\r\n\r\n toggleCustomCooldown() {\r\n const enabled = $.hasClass(QR.nodes.customCooldown, 'disabled');\r\n QR.setCustomCooldown(enabled);\r\n return $.set('customCooldownEnabled', enabled);\r\n },\r\n\r\n error(err, focusOverride) {\r\n let el;\r\n QR.open();\r\n if (typeof err === 'string') {\r\n el = $.tn(err);\r\n } else {\r\n el = err;\r\n el.removeAttribute('style');\r\n }\r\n const notice = new Notice('warning', el);\r\n QR.notifications.push(notice);\r\n if (!Header.areNotificationsEnabled) {\r\n if (d.hidden && !QR.cooldown.auto) { return alert(el.textContent); }\r\n } else if (d.hidden || !(focusOverride || d.hasFocus())) {\r\n const notif = new Notification(el.textContent, {\r\n body: el.textContent,\r\n icon: Favicon.logo\r\n }\r\n );\r\n notif.onclick = () => window.focus();\r\n if ($.engine !== 'gecko') {\r\n // Firefox automatically closes notifications\r\n // so we can't control the onclose properly.\r\n notif.onclose = () => notice.close();\r\n return notif.onshow = () => setTimeout(function() {\r\n notif.onclose = null;\r\n return notif.close();\r\n }\r\n , 7 * SECOND);\r\n }\r\n }\r\n },\r\n\r\n connectionError() {\r\n return $.el('span',\r\n { innerHTML:\r\n 'Connection error while posting. ' +\r\n '[More info]'\r\n }\r\n );\r\n },\r\n\r\n notifications: [],\r\n\r\n cleanNotifications() {\r\n for (var notification of QR.notifications) {\r\n notification.close();\r\n }\r\n return QR.notifications = [];\r\n },\r\n\r\n status() {\r\n let disabled, value;\r\n if (!QR.nodes) { return; }\r\n const {thread} = QR.posts[0];\r\n if ((thread !== 'new') && g.threads.get(`${g.BOARD}.${thread}`).isDead) {\r\n value = 'Dead';\r\n disabled = true;\r\n QR.cooldown.auto = false;\r\n }\r\n\r\n value = QR.req ?\r\n QR.req.progress\r\n :\r\n QR.cooldown.seconds || value;\r\n\r\n const {status} = QR.nodes;\r\n status.value = !value ?\r\n 'Submit'\r\n : QR.cooldown.auto ?\r\n `Auto ${value}`\r\n :\r\n value;\r\n return status.disabled = disabled || false;\r\n },\r\n\r\n openPost() {\r\n QR.open();\r\n if (QR.selected.isLocked) {\r\n const index = QR.posts.indexOf(QR.selected);\r\n (QR.posts[index+1] || new QR.post()).select();\r\n $.addClass(QR.nodes.el, 'dump');\r\n return QR.cooldown.auto = true;\r\n }\r\n },\r\n\r\n quote(e) {\r\n let range;\r\n e?.preventDefault();\r\n if (!QR.postingIsEnabled) { return; }\r\n const sel = d.getSelection();\r\n const post = Get.postFromNode(this);\r\n const {root} = post.nodes;\r\n const postRange = new Range();\r\n postRange.selectNode(root);\r\n let text = post.board.ID === g.BOARD.ID ? `>>${post}\\n` : `>>>/${post.board}/${post}\\n`;\r\n for (let i = 0, end = sel.rangeCount, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {\r\n try {\r\n var insideCode, node;\r\n range = sel.getRangeAt(i);\r\n // Trim range to be fully inside post\r\n if (range.compareBoundaryPoints(Range.START_TO_START, postRange) < 0) {\r\n range.setStartBefore(root);\r\n }\r\n if (range.compareBoundaryPoints(Range.END_TO_END, postRange) > 0) {\r\n range.setEndAfter(root);\r\n }\r\n\r\n if (!range.toString().trim()) { continue; }\r\n\r\n var frag = range.cloneContents();\r\n var ancestor = range.commonAncestorContainer;\r\n // Quoting the insides of a spoiler/code tag.\r\n if ($.x('ancestor-or-self::*[self::s or contains(@class,\"removed-spoiler\")]', ancestor)) {\r\n $.prepend(frag, $.tn('[spoiler]'));\r\n $.add(frag, $.tn('[/spoiler]'));\r\n }\r\n if (insideCode = $.x('ancestor-or-self::pre[contains(@class,\"prettyprint\")]', ancestor)) {\r\n $.prepend(frag, $.tn('[code]'));\r\n $.add(frag, $.tn('[/code]'));\r\n }\r\n for (node of $$((insideCode ? 'br' : '.prettyprint br'), frag)) {\r\n $.replace(node, $.tn('\\n'));\r\n }\r\n for (node of $$('br', frag)) {\r\n if (node !== frag.lastChild) { $.replace(node, $.tn('\\n>')); }\r\n }\r\n g.SITE.insertTags?.(frag);\r\n for (node of $$('.linkify[data-original]', frag)) {\r\n $.replace(node, $.tn(node.dataset.original));\r\n }\r\n for (node of $$('.embedder', frag)) {\r\n if (node.previousSibling?.nodeValue === ' ') { $.rm(node.previousSibling); }\r\n $.rm(node);\r\n }\r\n text += `>${frag.textContent.trim()}\\n`;\r\n } catch (error) { }\r\n }\r\n\r\n QR.openPost();\r\n const {com, thread} = QR.nodes;\r\n if (!com.value) { thread.value = Get.threadFromNode(this); }\r\n\r\n const wasOnlyQuotes = QR.selected.isOnlyQuotes();\r\n\r\n const caretPos = com.selectionStart;\r\n // Replace selection for text.\r\n com.value = com.value.slice(0, caretPos) + text + com.value.slice(com.selectionEnd);\r\n // Move the caret to the end of the new quote.\r\n range = caretPos + text.length;\r\n com.setSelectionRange(range, range);\r\n com.focus();\r\n\r\n // This allows us to determine if any text other than quotes has been typed.\r\n if (wasOnlyQuotes) { QR.selected.quotedText = com.value; }\r\n\r\n QR.selected.save(com);\r\n return QR.selected.save(thread);\r\n },\r\n\r\n characterCount() {\r\n const counter = QR.nodes.charCount;\r\n const count = QR.nodes.com.value.replace(/[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g, '_').length;\r\n counter.textContent = count;\r\n counter.hidden = count < (QR.max_comment/2);\r\n return (count > QR.max_comment ? $.addClass : $.rmClass)(counter, 'warning');\r\n },\r\n\r\n getFile() {\r\n return $.event('QRFile', QR.selected?.file);\r\n },\r\n\r\n drawFile(e) {\r\n const file = QR.selected?.file;\r\n if (!file || !/^(image|video)\\//.test(file.type)) { return; }\r\n const isVideo = /^video\\//.test(file);\r\n const el = $.el((isVideo ? 'video' : 'img'));\r\n $.on(el, 'error', () => QR.openError());\r\n $.on(el, (isVideo ? 'loadeddata' : 'load'), function() {\r\n e.target.getContext('2d').drawImage(el, 0, 0);\r\n URL.revokeObjectURL(el.src);\r\n return $.event('QRImageDrawn', null, e.target);\r\n });\r\n return el.src = URL.createObjectURL(file);\r\n },\r\n\r\n openError() {\r\n const div = $.el('div');\r\n $.extend(div, {\r\n innerHTML:\r\n 'Could not open file. [More info]'\r\n });\r\n return QR.error(div);\r\n },\r\n\r\n setFile(e) {\r\n const {file, name, source} = e.detail;\r\n if (name != null) { file.name = name; }\r\n if (source != null) { file.source = source; }\r\n QR.open();\r\n return QR.handleFiles([file]);\r\n },\r\n\r\n drag(e) {\r\n // Let it drag anything from the page.\r\n const toggle = e.type === 'dragstart' ? $.off : $.on;\r\n toggle(d, 'dragover', QR.dragOver);\r\n return toggle(d, 'drop', QR.dropFile);\r\n },\r\n\r\n dragOver(e) {\r\n e.preventDefault();\r\n return e.dataTransfer.dropEffect = 'copy';\r\n }, // cursor feedback\r\n\r\n dropFile(e) {\r\n // Let it only handle files from the desktop.\r\n if (!e.dataTransfer.files.length) { return; }\r\n e.preventDefault();\r\n QR.open();\r\n return QR.handleFiles(e.dataTransfer.files);\r\n },\r\n\r\n paste(e) {\r\n if (!e.clipboardData.items) { return; }\r\n let file = null;\r\n let score = -1;\r\n for (var item of e.clipboardData.items) {\r\n var file2;\r\n if ((item.kind === 'file') && (file2 = item.getAsFile())) {\r\n var score2 = (2*(file2.size <= QR.max_size)) + (file2.type === 'image/png');\r\n if (score2 > score) {\r\n file = file2;\r\n score = score2;\r\n }\r\n }\r\n }\r\n if (file) {\r\n const {type} = file;\r\n const blob = new Blob([file], {type});\r\n blob.name = `${Conf['pastedname']}.${$.getOwn(QR.extensionFromType, type) || 'jpg'}`;\r\n QR.open();\r\n QR.handleFiles([blob]);\r\n $.addClass(QR.nodes.el, 'dump');\r\n }\r\n },\r\n\r\n pasteFF() {\r\n const {pasteArea} = QR.nodes;\r\n if (!pasteArea.childNodes.length) { return; }\r\n const images = $$('img', pasteArea);\r\n $.rmAll(pasteArea);\r\n for (var img of images) {\r\n var m;\r\n var {src} = img;\r\n if (m = src.match(/data:(image\\/(\\w+));base64,(.+)/)) {\r\n var bstr = atob(m[3]);\r\n var arr = new Uint8Array(bstr.length);\r\n for (var i = 0, end = bstr.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {\r\n arr[i] = bstr.charCodeAt(i);\r\n }\r\n var blob = new Blob([arr], {type: m[1]});\r\n blob.name = `${Conf['pastedname']}.${m[2]}`;\r\n QR.handleFiles([blob]);\r\n } else if (/^https?:\\/\\//.test(src)) {\r\n QR.handleUrl(src);\r\n }\r\n }\r\n },\r\n\r\n handleUrl(urlDefault) {\r\n QR.open();\r\n QR.selected.preventAutoPost();\r\n return CrossOrigin.permission(function() {\r\n const url = prompt('Enter a URL:', urlDefault);\r\n if (url === null) { return; }\r\n QR.nodes.fileButton.focus();\r\n return CrossOrigin.file(url, function(blob) {\r\n if (blob && !/^text\\//.test(blob.type)) {\r\n return QR.handleFiles([blob]);\r\n } else {\r\n return QR.error(\"Can't load file.\");\r\n }\r\n });\r\n });\r\n },\r\n\r\n handleFiles(files) {\r\n if (this !== QR) { // file input\r\n files = [...this.files];\r\n this.value = null;\r\n }\r\n if (!files.length) { return; }\r\n QR.cleanNotifications();\r\n for (var file of files) {\r\n QR.handleFile(file, files.length);\r\n }\r\n if (files.length !== 1) { $.addClass(QR.nodes.el, 'dump'); }\r\n if ((d.activeElement === QR.nodes.fileButton) && $.hasClass(QR.nodes.fileSubmit, 'has-file')) {\r\n return QR.nodes.filename.focus();\r\n }\r\n },\r\n\r\n handleFile(file, nfiles) {\r\n let post;\r\n const isText = /^text\\//.test(file.type);\r\n if (nfiles === 1) {\r\n post = QR.selected;\r\n } else {\r\n post = QR.posts[QR.posts.length - 1];\r\n if (isText ? post.com || post.pasting : post.file) {\r\n post = new QR.post();\r\n }\r\n }\r\n return post[isText ? 'pasteText' : 'setFile'](file);\r\n },\r\n\r\n openFileInput() {\r\n if (QR.nodes.fileButton.disabled) { return; }\r\n QR.nodes.fileInput.click();\r\n return QR.nodes.fileButton.focus();\r\n },\r\n\r\n generatePostableThreadsList() {\r\n if (!QR.nodes) { return; }\r\n const list = QR.nodes.thread;\r\n const options = [list.firstElementChild];\r\n for (var thread of g.BOARD.threads.keys) {\r\n options.push($.el('option', {\r\n value: thread,\r\n textContent: `Thread ${thread}`\r\n }\r\n )\r\n );\r\n }\r\n const val = list.value;\r\n $.rmAll(list);\r\n $.add(list, options);\r\n list.value = val;\r\n if (list.value === val) { return; }\r\n // Fix the value if the option disappeared.\r\n list.value = g.VIEW === 'thread' ?\r\n g.THREADID\r\n :\r\n 'new';\r\n return (g.VIEW === 'thread' ? $.addClass : $.rmClass)(QR.nodes.el, 'reply-to-thread');\r\n },\r\n\r\n dialog() {\r\n let dialog, event, nodes;\r\n let name;\r\n QR.nodes = (nodes = {\r\n el: (dialog = UI.dialog('qr',\r\n { innerHTML: QuickReplyPage }))\r\n });\r\n\r\n const setNode = (name, query) => nodes[name] = $(query, dialog);\r\n\r\n setNode('move', '.move');\r\n setNode('autohide', '#autohide');\r\n setNode('close', '.close');\r\n setNode('thread', 'select');\r\n setNode('form', 'form');\r\n setNode('sjisToggle', '#sjis-toggle');\r\n setNode('texButton', '#tex-preview-button');\r\n setNode('name', '[data-name=name]');\r\n setNode('email', '[data-name=email]');\r\n setNode('sub', '[data-name=sub]');\r\n setNode('com', '[data-name=com]');\r\n setNode('charCount', '#char-count');\r\n setNode('texPreview', '#tex-preview');\r\n setNode('dumpList', '#dump-list');\r\n setNode('addPost', '#add-post');\r\n setNode('oekaki', '.oekaki');\r\n setNode('drawButton', '#qr-draw-button');\r\n setNode('fileSubmit', '#file-n-submit');\r\n setNode('fileButton', '#qr-file-button');\r\n setNode('noFile', '#qr-no-file');\r\n setNode('filename', '#qr-filename');\r\n setNode('spoiler', '#qr-file-spoiler');\r\n setNode('oekakiButton', '#qr-oekaki-button');\r\n setNode('fileRM', '#qr-filerm');\r\n setNode('urlButton', '#url-button');\r\n setNode('pasteArea', '#paste-area');\r\n setNode('customCooldown', '#custom-cooldown-button');\r\n setNode('dumpButton', '#dump-button');\r\n setNode('status', '[type=submit]');\r\n setNode('flashTag', '[name=filetag]');\r\n setNode('fileInput', '[type=file]');\r\n\r\n const {config} = g.BOARD;\r\n const {classList} = QR.nodes.el;\r\n classList.toggle('forced-anon', QR.forcedAnon);\r\n classList.toggle('has-spoiler', QR.spoiler);\r\n classList.toggle('has-sjis', !!config.sjis_tags);\r\n classList.toggle('has-math', !!config.math_tags);\r\n classList.toggle('sjis-preview', !!config.sjis_tags && Conf['sjisPreview']);\r\n classList.toggle('show-new-thread-option', Conf['Show New Thread Option in Threads']);\r\n\r\n if (parseInt(Conf['customCooldown'], 10) > 0) {\r\n $.addClass(QR.nodes.fileSubmit, 'custom-cooldown');\r\n $.get('customCooldownEnabled', Conf['customCooldownEnabled'], function({customCooldownEnabled}) {\r\n QR.setCustomCooldown(customCooldownEnabled);\r\n return $.sync('customCooldownEnabled', QR.setCustomCooldown);\r\n });\r\n }\r\n\r\n QR.flagsInput();\r\n\r\n $.on(nodes.autohide, 'change', QR.toggleHide);\r\n $.on(nodes.close, 'click', QR.close);\r\n $.on(nodes.status, 'click', QR.submit);\r\n $.on(nodes.form, 'submit', QR.submit);\r\n $.on(nodes.sjisToggle, 'click', QR.toggleSJIS);\r\n $.on(nodes.texButton, 'mousedown', QR.texPreviewShow);\r\n $.on(nodes.texButton, 'mouseup', QR.texPreviewHide);\r\n $.on(nodes.addPost, 'click', () => new QR.post(true));\r\n $.on(nodes.drawButton, 'click', QR.oekaki.draw);\r\n $.on(nodes.fileButton, 'click', QR.openFileInput);\r\n $.on(nodes.noFile, 'click', QR.openFileInput);\r\n $.on(nodes.filename, 'focus', function() { return $.addClass(this.parentNode, 'focus'); });\r\n $.on(nodes.filename, 'blur', function() { return $.rmClass(this.parentNode, 'focus'); });\r\n $.on(nodes.spoiler, 'change', () => QR.selected.nodes.spoiler.click());\r\n $.on(nodes.oekakiButton, 'click', QR.oekaki.button);\r\n $.on(nodes.fileRM, 'click', () => QR.selected.rmFile());\r\n $.on(nodes.urlButton, 'click', () => QR.handleUrl(''));\r\n $.on(nodes.customCooldown, 'click', QR.toggleCustomCooldown);\r\n $.on(nodes.dumpButton, 'click', () => nodes.el.classList.toggle('dump'));\r\n $.on(nodes.fileInput, 'change', QR.handleFiles);\r\n\r\n window.addEventListener('focus', QR.focus, true);\r\n window.addEventListener('blur', QR.focus, true);\r\n // We don't receive blur events from captcha iframe.\r\n $.on(d, 'click', QR.focus);\r\n\r\n // XXX Workaround for image pasting in Firefox, obsolete as of v50.\r\n // https://bugzilla.mozilla.org/show_bug.cgi?id=906420\r\n if (($.engine === 'gecko') && !window.DataTransferItemList) {\r\n nodes.pasteArea.hidden = false;\r\n }\r\n new MutationObserver(QR.pasteFF).observe(nodes.pasteArea, {childList: true});\r\n\r\n // save selected post's data\r\n const items = ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag'];\r\n let i = 0;\r\n const save = function() { return QR.selected.save(this); };\r\n while ((name = items[i++])) {\r\n var node;\r\n if (!(node = nodes[name])) { continue; }\r\n event = node.nodeName === 'SELECT' ? 'change' : 'input';\r\n $.on(nodes[name], event, save);\r\n }\r\n\r\n // XXX Blink and WebKit treat width and height of