- Fixed thread not scrolling to last read post.
- Set default 'Exempt Archives from Encryption' to false. This setting will _not_ change automatically when updating.
- Enabled automatic updates. If you don't want updates, turn them off in your user script manager.
- Removed build files related to upstream 4chan X
- Updated CONTRIBUTING.md and README.md
This commit is contained in:
Tuxedo Takodachi 2023-10-28 20:14:40 +02:00
parent b047925392
commit d19d34e91a
32 changed files with 21202 additions and 21588 deletions

View File

@ -3,6 +3,12 @@
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.2.1 (2023-10-28)
- Fixed thread not scrolling to last read post.
- Set default 'Exempt Archives from Encryption' to false. This setting will _not_ change automatically when updating.
- Enabled automatic updates. If you don't want updates, turn them off in your user script manager.
### XT v2.2.0 (2023-10-27)
- Added ability to restore deleted posts from an external archive. This can be found in the drop down menu at the top

View File

@ -1,6 +1,6 @@
## Reporting bugs
Bug reports and feature requests for 4chan X are tracked at **https://github.com/ccd0/4chan-x/issues?q=is%3Aopen+sort%3Aupdated-desc**.
Bug reports and feature requests for 4chan XT are tracked at **https://github.com/TuxedoTako/4chan-xt/issues?q=is%3Aopen+sort%3Aupdated-desc**.
You can submit a bug report / feature request via your Github account.
@ -12,38 +12,37 @@ If you're reporting a bug, the more detail you can give, the better. If I can't
4. If other people (including me) aren't having your problem, **test whether it happens in a fresh profile**. Here are instructions for [Firefox](https://support.mozilla.org/en-US/kb/profile-manager-create-and-remove-firefox-profiles) and [Chromium](https://developer.chrome.com/devtools/docs/clean-testing-environment).
5. **Please mention any other extensions / scripts you are using.** To check if a bug is due to a conflict with another extension, temporarily disable any other extensions and userscripts. If the bug goes away, turn them back on one by one until you find the one causing the problem.
6. To test if the bug occurs under the default settings or only with specific settings, back up your settings and reset them using the **Export** and **Reset Settings** links in the settings panel. If the bug only occurs under specific settings, upload your exported settings to a site like https://paste.installgentoo.com/, and link to it in your bug report. If your settings contains sensitive information (e.g. personas), edit the text file manually.
7. Test if the bug occurs using the **native extension** with 4chan X disabled. If it does, it's likely a problem with 4chan or your browser rather than with 4chan X.
7. Test if the bug occurs using the **native extension** with 4chan XT disabled. If it does, it's likely a problem with 4chan or your browser rather than with 4chan X.
## Development & Contribution
### Get started
- Install [git](https://git-scm.com/), [node.js](https://nodejs.org/), [npm](https://www.npmjs.com/) (usually distributed with node), and GNU Make (on Windows, the [MinGW](http://www.mingw.org/) port will work, and the [GnuWin](http://gnuwin32.sourceforge.net/) port has been reported to work as well).
- Clone 4chan X: `git clone https://github.com/ccd0/4chan-x.git`<br>(If this is taking too long, you can add `--depth 100` to fetch only recent history.)
- Open the directory: `cd 4chan-x`
- Install [git](https://git-scm.com/), [node.js](https://nodejs.org/), and [npm](https://www.npmjs.com/).
- Clone 4chan X: `git clone https://github.com/TuxedoTako/4chan-xt.git`<br>(If this is taking too long, you can add `--depth 100` to fetch only recent history.)
- Open the directory: `cd 4chan-xt`
- Fetch needed dependencies with: `npm install`
### Build
- Build with `make`.
- Build with `npm run build`.
### Contribute
- 4chan X is mostly written in [CoffeeScript](http://coffeescript.org/). If you're already familiar with Javascript, it doesn't take long to pick up.
- Use TypeScript for new files. If you want to convert a .js file to .ts, use a separate commit so the file history is
tracked past the rename
- Edit the sources in the src/ directory (not the compiled scripts in builds/).
- Fetch needed dependencies with: `npm install`
- Compile the script with: `make`
- Install the compiled script (found in the testbuilds/ directory), and test your changes.
- Compile the script with: `npm run build`
- Install the compiled script (found in the build/ directory), and test your changes.
- Make sure you have set your name and email as you want them, as they will be published in your commit message:<br>`git config user.name yourname`<br>`git config user.email youremail`
- Commit your changes: `git commit -a`
- Open a pull request by doing any of the following:
- Fork this repository on Github, push your changes to your fork, and make a pull request through the Github website.
- Push your changes to any online Git repository, and send an email with an explanation of your changes and the URL, branch, and commit you want me to pull from.
- Export your changes via `git bundle` (e.g. `git bundle create file.bundle master..your-branch`), and upload them to a file host. Then send an email with an explanation of your changes and the URL of the file.
- Open a pull request on GitHub.
Pull requests to archive.json should be sent upstream: https://github.com/4chenz/archives.json
4chan X updates from there automatically.
4chan XT updates from there automatically.
### More info
Further documentation is available at https://github.com/ccd0/4chan-x/wiki/Developer-Documentation.
Further documentation is available at the wiki for the original 4chan X: https://github.com/ccd0/4chan-x/wiki/Developer-Documentation.
At the moment 4chan XT doesn't have its own wiki yet.

View File

@ -60,6 +60,7 @@
* btmcsweeney
* AppleBloom
* detharonil
* TuxedoTako
*
* All the people who've taken the time to write bug reports.
*

View File

@ -1,9 +1,7 @@
# 4chan XT
I would prefer that this is merged into [the repo this is forked from](https://github.com/ccd0/4chan-x), but in the mean
time, you can try this fork as well.
PR to upstream: https://github.com/ccd0/4chan-x/pull/3341.
Originally forked from [4chan X](https://github.com/ccd0/4chan-x) for [this PR](https://github.com/ccd0/4chan-x/pull/3341),
this fork started getting some features on its own. See the releases.
The 4chan XT project is a migration of 4chan X from coffeescript to TypeScript/JavaScript. It is named XT both as a continuation of eXTended, and a T for TypeScript. The goals of this project is to first get a working bundle from js/ts files, and then gradually convert js files to ts and add types as needed.

File diff suppressed because one or more lines are too long

113
builds/4chan-XT.meta.js Normal file
View File

@ -0,0 +1,113 @@
// ==UserScript==
// @name 4chan XT
// @version XT 2.2.1
// @minGMVer 1.14
// @minFFVer 74
// @namespace 4chan-XT
// @description 4chan XT is a script that adds various features to anonymous imageboards.
// @license MIT; https://github.com/TuxedoTako/4chan-xt/blob/project-XT/LICENSE
// @include http://boards.4chan.org/*
// @include https://boards.4chan.org/*
// @include http://sys.4chan.org/*
// @include https://sys.4chan.org/*
// @include http://www.4chan.org/*
// @include https://www.4chan.org/*
// @include http://boards.4channel.org/*
// @include https://boards.4channel.org/*
// @include http://sys.4channel.org/*
// @include https://sys.4channel.org/*
// @include http://www.4channel.org/*
// @include https://www.4channel.org/*
// @include http://i.4cdn.org/*
// @include https://i.4cdn.org/*
// @include http://is.4chan.org/*
// @include https://is.4chan.org/*
// @include http://is2.4chan.org/*
// @include https://is2.4chan.org/*
// @include http://is.4channel.org/*
// @include https://is.4channel.org/*
// @include http://is2.4channel.org/*
// @include https://is2.4channel.org/*
// @include https://erischan.org/*
// @include https://www.erischan.org/*
// @include https://fufufu.moe/*
// @include https://gnfos.com/*
// @include https://himasugi.blog/*
// @include https://www.himasugi.blog/*
// @include https://kakashinenpo.com/*
// @include https://www.kakashinenpo.com/*
// @include https://kissu.moe/*
// @include https://www.kissu.moe/*
// @include https://lainchan.org/*
// @include https://www.lainchan.org/*
// @include https://merorin.com/*
// @include https://ota-ch.com/*
// @include https://www.ota-ch.com/*
// @include https://ponyville.us/*
// @include https://www.ponyville.us/*
// @include https://smuglo.li/*
// @include https://notso.smuglo.li/*
// @include https://smugloli.net/*
// @include https://smug.nepu.moe/*
// @include https://sportschan.org/*
// @include https://www.sportschan.org/*
// @include https://sushigirl.us/*
// @include https://www.sushigirl.us/*
// @include https://tvch.moe/*
// @exclude http://www.4chan.org/advertise
// @exclude https://www.4chan.org/advertise
// @exclude http://www.4chan.org/advertise?*
// @exclude https://www.4chan.org/advertise?*
// @exclude http://www.4chan.org/donate
// @exclude https://www.4chan.org/donate
// @exclude http://www.4chan.org/donate?*
// @exclude https://www.4chan.org/donate?*
// @exclude http://www.4channel.org/advertise
// @exclude https://www.4channel.org/advertise
// @exclude http://www.4channel.org/advertise?*
// @exclude https://www.4channel.org/advertise?*
// @exclude http://www.4channel.org/donate
// @exclude https://www.4channel.org/donate
// @exclude http://www.4channel.org/donate?*
// @exclude https://www.4channel.org/donate?*
// @connect 4chan.org
// @connect 4channel.org
// @connect 4cdn.org
// @connect 4chenz.github.io
// @connect archive.4plebs.org
// @connect warosu.org
// @connect desuarchive.org
// @connect boards.fireden.net
// @connect arch.b4k.co
// @connect archived.moe
// @connect thebarchive.com
// @connect archiveofsins.com
// @connect www.tokyochronos.net
// @connect archive.palanq.win
// @connect eientei.xyz
// @connect api.clyp.it
// @connect api.dailymotion.com
// @connect api.github.com
// @connect soundcloud.com
// @connect api.streamable.com
// @connect vimeo.com
// @connect www.youtube.com
// @connect *
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addValueChangeListener
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM.openInTab
// @grant GM.xmlHttpRequest
// @run-at document-start
// @updateURL https://github.com/TuxedoTako/4chan-xt/releases/latest/download/4chan-XT.meta.js
// @downloadURL https://github.com/TuxedoTako/4chan-xt/releases/latest/download/4chan-XT.user.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
// ==/UserScript==

113
builds/4chan-XT.min.meta.js Normal file
View File

@ -0,0 +1,113 @@
// ==UserScript==
// @name 4chan XT
// @version XT 2.2.1
// @minGMVer 1.14
// @minFFVer 74
// @namespace 4chan-XT
// @description 4chan XT is a script that adds various features to anonymous imageboards.
// @license MIT; https://github.com/TuxedoTako/4chan-xt/blob/project-XT/LICENSE
// @include http://boards.4chan.org/*
// @include https://boards.4chan.org/*
// @include http://sys.4chan.org/*
// @include https://sys.4chan.org/*
// @include http://www.4chan.org/*
// @include https://www.4chan.org/*
// @include http://boards.4channel.org/*
// @include https://boards.4channel.org/*
// @include http://sys.4channel.org/*
// @include https://sys.4channel.org/*
// @include http://www.4channel.org/*
// @include https://www.4channel.org/*
// @include http://i.4cdn.org/*
// @include https://i.4cdn.org/*
// @include http://is.4chan.org/*
// @include https://is.4chan.org/*
// @include http://is2.4chan.org/*
// @include https://is2.4chan.org/*
// @include http://is.4channel.org/*
// @include https://is.4channel.org/*
// @include http://is2.4channel.org/*
// @include https://is2.4channel.org/*
// @include https://erischan.org/*
// @include https://www.erischan.org/*
// @include https://fufufu.moe/*
// @include https://gnfos.com/*
// @include https://himasugi.blog/*
// @include https://www.himasugi.blog/*
// @include https://kakashinenpo.com/*
// @include https://www.kakashinenpo.com/*
// @include https://kissu.moe/*
// @include https://www.kissu.moe/*
// @include https://lainchan.org/*
// @include https://www.lainchan.org/*
// @include https://merorin.com/*
// @include https://ota-ch.com/*
// @include https://www.ota-ch.com/*
// @include https://ponyville.us/*
// @include https://www.ponyville.us/*
// @include https://smuglo.li/*
// @include https://notso.smuglo.li/*
// @include https://smugloli.net/*
// @include https://smug.nepu.moe/*
// @include https://sportschan.org/*
// @include https://www.sportschan.org/*
// @include https://sushigirl.us/*
// @include https://www.sushigirl.us/*
// @include https://tvch.moe/*
// @exclude http://www.4chan.org/advertise
// @exclude https://www.4chan.org/advertise
// @exclude http://www.4chan.org/advertise?*
// @exclude https://www.4chan.org/advertise?*
// @exclude http://www.4chan.org/donate
// @exclude https://www.4chan.org/donate
// @exclude http://www.4chan.org/donate?*
// @exclude https://www.4chan.org/donate?*
// @exclude http://www.4channel.org/advertise
// @exclude https://www.4channel.org/advertise
// @exclude http://www.4channel.org/advertise?*
// @exclude https://www.4channel.org/advertise?*
// @exclude http://www.4channel.org/donate
// @exclude https://www.4channel.org/donate
// @exclude http://www.4channel.org/donate?*
// @exclude https://www.4channel.org/donate?*
// @connect 4chan.org
// @connect 4channel.org
// @connect 4cdn.org
// @connect 4chenz.github.io
// @connect archive.4plebs.org
// @connect warosu.org
// @connect desuarchive.org
// @connect boards.fireden.net
// @connect arch.b4k.co
// @connect archived.moe
// @connect thebarchive.com
// @connect archiveofsins.com
// @connect www.tokyochronos.net
// @connect archive.palanq.win
// @connect eientei.xyz
// @connect api.clyp.it
// @connect api.dailymotion.com
// @connect api.github.com
// @connect soundcloud.com
// @connect api.streamable.com
// @connect vimeo.com
// @connect www.youtube.com
// @connect *
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addValueChangeListener
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM.openInTab
// @grant GM.xmlHttpRequest
// @run-at document-start
// @updateURL https://github.com/TuxedoTako/4chan-xt/releases/latest/download/4chan-XT.min.meta.js
// @downloadURL https://github.com/TuxedoTako/4chan-xt/releases/latest/download/4chan-XT.min.user.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
// ==/UserScript==

View File

@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan XT
// @version XT 2.2.0
// @version XT 2.2.1
// @minGMVer 1.14
// @minFFVer 74
// @namespace 4chan-XT
@ -107,6 +107,8 @@
// @grant GM.openInTab
// @grant GM.xmlHttpRequest
// @run-at document-start
// @updateURL https://github.com/TuxedoTako/4chan-xt/releases/latest/download/4chan-XT.min.meta.js
// @downloadURL https://github.com/TuxedoTako/4chan-xt/releases/latest/download/4chan-XT.min.user.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
// ==/UserScript==
/*
@ -171,6 +173,7 @@
* btmcsweeney
* AppleBloom
* detharonil
* TuxedoTako
*
* All the people who've taken the time to write bug reports.
*
@ -187,9 +190,9 @@
!function(){"use strict";var e={name:"4chan XT",path:"4chan-XT",fork:"TuxedoTako",page:"https://github.com/TuxedoTako/4chan-xt",downloads:"https://github.com/TuxedoTako/4chan-xt/releases",oldVersions:"https://raw.githubusercontent.com/ccd0/4chan-x/",faq:"https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions",captchaFAQ:"https://github.com/ccd0/4chan-x/wiki/Captcha-FAQ",cssGuide:"https://github.com/ccd0/4chan-x/wiki/Styling-Guide",license:"https://github.com/TuxedoTako/4chan-xt/blob/project-XT/LICENSE",changelog:"https://github.com/TuxedoTako/4chan-xt/blob/project-XT/CHANGELOG.md",issues:"https://github.com/TuxedoTako/4chan-xt/issues",newIssue:"https://github.com/TuxedoTako/4chan-xt/issues",newIssueMaxLength:8181,alternatives:"https://www.4chan-x.net/4chan_alternatives.html",appid:"lacclbnghgdicfifcamcmcnilckjamag",appidGecko:"4chan-x@4chan-x.net",chromeStoreID:"ohnjgmpcibpbafdlkimncjhflgedgpam",recaptchaKey:"6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc",distBranch:"gh-pages",
includes_only:["*://boards.4chan.org/*","*://sys.4chan.org/*","*://www.4chan.org/*","*://boards.4channel.org/*","*://sys.4channel.org/*","*://www.4channel.org/*","*://i.4cdn.org/*","*://is.4chan.org/*","*://is2.4chan.org/*","*://is.4channel.org/*","*://is2.4channel.org/*"],matches_only:["*://*.4chan.org/*","*://*.4channel.org/*","*://*.4cdn.org/*"],
matches:["https://erischan.org/*","https://www.erischan.org/*","https://fufufu.moe/*","https://gnfos.com/*","https://himasugi.blog/*","https://www.himasugi.blog/*","https://kakashinenpo.com/*","https://www.kakashinenpo.com/*","https://kissu.moe/*","https://www.kissu.moe/*","https://lainchan.org/*","https://www.lainchan.org/*","https://merorin.com/*","https://ota-ch.com/*","https://www.ota-ch.com/*","https://ponyville.us/*","https://www.ponyville.us/*","https://smuglo.li/*","https://notso.smuglo.li/*","https://smugloli.net/*","https://smug.nepu.moe/*","https://sportschan.org/*","https://www.sportschan.org/*","https://sushigirl.us/*","https://www.sushigirl.us/*","https://tvch.moe/*"],matches_extra:[],exclude_matches:["*://www.4chan.org/advertise","*://www.4chan.org/advertise?*","*://www.4chan.org/donate","*://www.4chan.org/donate?*","*://www.4channel.org/advertise","*://www.4channel.org/advertise?*","*://www.4channel.org/donate","*://www.4channel.org/donate?*"],
grants:["GM_getValue","GM_setValue","GM_deleteValue","GM_listValues","GM_addValueChangeListener","GM_openInTab","GM_xmlhttpRequest","GM.getValue","GM.setValue","GM.deleteValue","GM.listValues","GM.openInTab","GM.xmlHttpRequest"],min:{chrome:"80",firefox:"74",greasemonkey:"1.14"}};const t=Object.create(null),o={VERSION:"XT 2.2.0",NAMESPACE:e.name,sites:Object.create(null),boards:Object.create(null)},n=function(){const e={"&":"&amp;","'":"&#039;",'"':"&quot;","<":"&lt;",">":"&gt;"},t=/[&"'<>]/g,o=function(t){return e[t]},n=function(e){return e.toString().replace(t,o)};return n.cat=function(e){let t="";for(let o=0;o<e.length;o++)t+=e[o].innerHTML;return t},n}(),a=document,i=a.documentElement,r=console,s=function(){return i};class l{static initClass(){this.Post=new l("Post"),this.Thread=new l("Thread"),this.CatalogThread=new l("Catalog Thread"),this.CatalogThreadNative=new l("Catalog Thread")}constructor(e){this.type=e,this.keys=[]}push({name:e,cb:t}){return this[e]||this.keys.push(e),
grants:["GM_getValue","GM_setValue","GM_deleteValue","GM_listValues","GM_addValueChangeListener","GM_openInTab","GM_xmlhttpRequest","GM.getValue","GM.setValue","GM.deleteValue","GM.listValues","GM.openInTab","GM.xmlHttpRequest"],min:{chrome:"80",firefox:"74",greasemonkey:"1.14"}};const t=Object.create(null),o={VERSION:"XT 2.2.1",NAMESPACE:e.name,sites:Object.create(null),boards:Object.create(null)},n=function(){const e={"&":"&amp;","'":"&#039;",'"':"&quot;","<":"&lt;",">":"&gt;"},t=/[&"'<>]/g,o=function(t){return e[t]},n=function(e){return e.toString().replace(t,o)};return n.cat=function(e){let t="";for(let o=0;o<e.length;o++)t+=e[o].innerHTML;return t},n}(),a=document,i=a.documentElement,r=console,s=function(){return i};class l{static initClass(){this.Post=new l("Post"),this.Thread=new l("Thread"),this.CatalogThread=new l("Catalog Thread"),this.CatalogThreadNative=new l("Catalog Thread")}constructor(e){this.type=e,this.keys=[]}push({name:e,cb:t}){return this[e]||this.keys.push(e),
this[e]=t}execute(e,t=this.keys,o=!1){let n;if(!e.callbacksExecuted||o){for(var a of(e.callbacksExecuted=!0,t))try{this[a]?.call(e)}catch(t){n||(n=[]),n.push({message:['"',a,'" crashed on node ',this.type," No.",e.ID," (",e.board,")."].join(""),error:t,html:e.nodes?.root?.outerHTML})}return n?jt.handleErrors(n):void 0}}}l.initClass();const c={main:{Miscellaneous:{"Redirect to HTTPS":[!0,"Redirect to the HTTPS version of 4chan."],"JSON Index":[!0,"Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode."],[`Use ${e.name} Catalog`]:[!0,`Link to ${e.name}'s catalog instead of the native 4chan one.`,1],"Index Refresh Notifications":[!1,"Show a notice at the top of the page when the index is refreshed.",1],"Follow Cursor":[!0,"Image Hover and Quote Preview move with the mouse cursor."],"Open Threads in New Tab":[!1,`Make links to threads in the index / ${e.name} catalog open in a new tab.`],
"External Catalog":[!1,"Link to external catalog instead of the internal one."],"Catalog Links":[!1,"Add toggle link in header menu to turn Navigation links into links to each board's catalog."],"Announcement Hiding":[!0,"Add button to hide 4chan announcements."],"Desktop Notifications":[!0,`Enables desktop notifications across various ${e.name} features.`],"404 Redirect":[!0,"Redirect dead threads and images to the archives."],"Archive Report":[!0,"Enable reporting posts to supported archives."],"Exempt Archives from Encryption":[!0,"Permit loading content from, and warningless redirects to, HTTP-only archives from HTTPS pages."],Keybinds:[!0,"Bind actions to keyboard shortcuts."],"Time Formatting":[!0,"Localize and format timestamps."],"Relative Post Dates":[!0,'Display dates like "3 minutes ago". Tooltip shows the timestamp.'],"Relative Date Title":[!0,"Show Relative Post Date only when hovering over dates.",1],
"External Catalog":[!1,"Link to external catalog instead of the internal one."],"Catalog Links":[!1,"Add toggle link in header menu to turn Navigation links into links to each board's catalog."],"Announcement Hiding":[!0,"Add button to hide 4chan announcements."],"Desktop Notifications":[!0,`Enables desktop notifications across various ${e.name} features.`],"404 Redirect":[!0,"Redirect dead threads and images to the archives."],"Archive Report":[!0,"Enable reporting posts to supported archives."],"Exempt Archives from Encryption":[!1,"Permit loading content from, and warningless redirects to, HTTP-only archives from HTTPS pages."],Keybinds:[!0,"Bind actions to keyboard shortcuts."],"Time Formatting":[!0,"Localize and format timestamps."],"Relative Post Dates":[!0,'Display dates like "3 minutes ago". Tooltip shows the timestamp.'],"Relative Date Title":[!0,"Show Relative Post Date only when hovering over dates.",1],
"Comment Expansion":[!0,"Expand comments that are too long to display on the index. Not applicable with JSON Index."],"File Info Formatting":[!0,"Reformat the file information."],"Thread Expansion":[!0,"Add buttons to expand threads."],"Index Navigation":[!1,"Add buttons to navigate between threads."],"Reply Navigation":[!1,"Add buttons to navigate to top / bottom of thread."],"Unique ID and Capcode Navigation":[!1,"Add buttons to navigate to posts having the same unique ID or capcode."],"Custom Board Titles":[!0,"Allow editing of the board title and subtitle by ctrl/⌘+clicking them."],"Persistent Custom Board Titles":[!1,"Force custom board titles to be persistent, even if the board titles are updated.",1],"Show Updated Notifications":[!0,`Show notifications when ${e.name} is successfully updated.`],"Color User IDs":[!0,"Assign unique colors to user IDs on boards that use them"],"Count Posts by ID":[!0,"Display number of posts in the thread when hovering over an ID."],
"Remove Spoilers":[!1,"Remove all spoilers in text."],"Reveal Spoilers":[!1,"Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled."],"Normalize URL":[!0,"Rewrite the URL of the current page, removing slugs and excess slashes, and changing /res/ to /thread/."],"Work around CORB Bug":[!0,"Leave this checked until your garbage browser is fixed."],"Disable Autoplaying Sounds":[!1,"Prevent sounds on the page from autoplaying."],"Disable Native Extension":[!0,`${e.name} is NOT designed to work with the native extension.`],"Enable Native Flash Embedding":[!0,"Activate the native extension's Flash embedding if the native extension is disabled."]},Linkification:{Linkify:[!0,"Convert text into links where applicable."],"Link Title":[!0,"Replace the link of a supported site with its actual title.",1],"Cover Preview":[!0,"Show preview of supported links on hover.",1],
Embedding:[!0,"Embed supported services. Note: Some services don't work on HTTPS.",1],"Auto-embed":[!1,"Auto-embed Linkify Embeds.",2],"Floating Embeds":[!1,"Embed content in a frame that remains in place when the page is scrolled.",2]},Filtering:{Anonymize:[!1,"Make everyone Anonymous."],Filter:[!0,"Self-moderation placebo."],"Filtered Backlinks":[!1,"When enabled, shows backlinks to filtered posts with a line-through decoration. Otherwise, hides the backlinks.",1],"Filter in Native Catalog":[!0,"Apply 4chan X filters in native catalog.",1],"MD5 Quick Filter Notifications":[!0,"Show notification when quick filtering MD5s using the button or keybind.",1],"Recursive Hiding":[!0,"Hide replies of hidden posts, recursively."],"Thread Hiding Buttons":[!0,"Add buttons to hide entire threads."],"Reply Hiding Buttons":[!0,"Add buttons to hide single replies."],Stubs:[!0,"Show stubs of hidden threads / replies."]},"Images and Videos":{"Image Expansion":[!0,"Expand images / videos."],
@ -280,7 +283,7 @@ textContent:" (You)",className:"qmark-you"}),l.Post.push({name:"Mark Quotes of
Xe.scrollTo(t.nodes.post),t.isReply){const e=`${o.SITE.selectors.postContainer}${o.SITE.selectors.highlightable.reply}`;let n=t.nodes.root;n.matches(e)||(n=Qe(e,n)),Qe.addClass(n,o.SITE.classes.highlight)}return!0}return!1}}};class Z{constructor(e){if(this.length=0,e)for(var t of e)this.push(t)}push(e){let t,{ID:o}=e;if(o||(o=e.id),this[o])return;const{last:n}=this;return this[o]=t={prev:n,next:null,data:e,ID:o},t.prev=n,this.last=n?n.next=t:this.first=t,this.length++}before(e,t){if(t.next===e||t===e)return;this.rmi(t);const{prev:o}=e;return e.prev=t,t.next=e,t.prev=o,o?o.next=t:this.first=t}after(e,t){if(t.prev===e||t===e)return;this.rmi(t);const{next:o}=e;return e.next=t,t.prev=e,t.next=o,o?o.prev=t:this.last=t}prepend(e){const{first:t}=this;if(e!==t&&this[e.ID])return this.rmi(e),e.next=t,t?t.prev=e:this.last=e,this.first=e,delete e.prev}shift(){return this.rm(this.first.ID)}order(){let e;const t=[e=this.first];for(;e=e.next;)t.push(e);return t}rm(e){const t=this[e]
;if(t)return delete this[e],this.length--,this.rmi(t),delete t.next,delete t.prev}rmi(e){const{prev:t,next:o}=e;return t?t.next=o:this.first=o,o?o.prev=t:this.last=t}}var ee={init(){if("thread"===o.VIEW&&(t["Unread Count"]||t["Unread Favicon"]||t["Unread Line"]||t["Remember Last Read Post"]||t["Desktop Notifications"]||t["Quote Threading"]))return t["Remember Last Read Post"]&&(Qe.sync("Remember Last Read Post",(e=>t["Remember Last Read Post"]=e)),this.db=new y("lastReadPosts",this.sync)),this.hr=Qe.el("hr",{id:"unread-line",className:"unread-line"}),this.posts=new Set,this.postsQuotingYou=new Set,this.order=new Z,this.position=null,l.Thread.push({name:"Unread",cb:this.node}),l.Post.push({name:"Unread",cb:this.addPost})},node(){for(var e of(ee.thread=this,ee.title=a.title,ee.lastReadPost=ee.db?.get({boardID:this.board.ID,threadID:this.ID})||0,ee.readCount=0,this.posts.keys))+e<=ee.lastReadPost&&ee.readCount++;Qe.one(a,"4chanXInitFinished",ee.ready),Qe.on(a,"PostsInserted",ee.onUpdate),
Qe.on(a,"ThreadUpdate",(function(e){if(e.detail[404])return ee.update()}));const t=Qe.el("a",{href:"javascript:;",className:"unread-reset",textContent:"Mark all unread"});return Qe.on(t,"click",ee.reset),Xe.menu.addEntry({el:t,order:70})},ready(){if(t["Remember Last Read Post"]&&t["Scroll to Last Read Post"]&&ee.scroll(),ee.setLine(!0),ee.read(),ee.update(),Qe.on(a,"scroll visibilitychange",ee.read),t["Unread Line"])return Qe.on(a,"visibilitychange",ee.setLine)},positionPrev:()=>ee.position?ee.position.prev:ee.order.last,scroll(){let e;if((e=location.hash.match(/\d+/))&&e[0]in ee.thread.posts)return;let t=ee.positionPrev();for(;t;){var{bottom:o}=t.data.nodes;if(o.getBoundingClientRect().height){Xe.scrollToIfNeeded(o,!0);break}t=t.prev}},reset(){if(null!=ee.lastReadPost)return ee.posts=new Set,ee.postsQuotingYou=new Set,ee.order=new Z,ee.position=null,ee.lastReadPost=0,ee.readCount=0,ee.thread.posts.forEach((e=>ee.addPost.call(e))),Qe.forceSync("Remember Last Read Post"),
!t["Remember Last Read Post"]||ee.thread.isDead&&!ee.thread.isArchived||ee.db.set({boardID:ee.thread.board.ID,threadID:ee.thread.ID,val:0}),ee.updatePosition(),ee.setLine(),ee.update()},sync(){if(null==ee.lastReadPost)return;const e=ee.db.get({boardID:ee.thread.board.ID,threadID:ee.thread.ID,defaultValue:0});if(ee.lastReadPost>=e)return;ee.lastReadPost=e;const t=ee.thread.posts.keys;for(let e=ee.readCount,n=t.length;e<n;e++){var o=+t[e];if(!ee.thread.posts.get(o).isFetchedQuote){if(o>ee.lastReadPost)break;ee.posts.delete(o),ee.postsQuotingYou.delete(o)}ee.readCount++}return ee.updatePosition(),ee.setLine(),ee.update()},addPost(){if(!(this.isFetchedQuote||this.isClone||this.ID<=ee.lastReadPost||(ee.order.push(this),this.isHidden||_.isYou(this))))return ee.posts.add(ee.posts.last=this.ID),ee.addPostQuotingYou(this),null!=ee.position?ee.position:ee.position=ee.order[this.ID]},addPostQuotingYou(e){
!t["Remember Last Read Post"]||ee.thread.isDead&&!ee.thread.isArchived||ee.db.set({boardID:ee.thread.board.ID,threadID:ee.thread.ID,val:0}),ee.updatePosition(),ee.setLine(),ee.update()},sync(){if(null==ee.lastReadPost)return;const e=ee.db.get({boardID:ee.thread.board.ID,threadID:ee.thread.ID,defaultValue:0});if(ee.lastReadPost>=e)return;ee.lastReadPost=e;const t=ee.thread.posts.keys;for(let e=ee.readCount,n=t.length;e<n;e++){var o=+t[e];if(!ee.thread.posts.get(o).isFetchedQuote){if(o>ee.lastReadPost)break;ee.posts.delete(o),ee.postsQuotingYou.delete(o)}ee.readCount++}return ee.updatePosition(),ee.setLine(),ee.update()},addPost(){if(!(this.isFetchedQuote||this.isClone||(ee.order.push(this),this.ID<=ee.lastReadPost||this.isHidden||_.isYou(this))))return ee.posts.add(ee.posts.last=this.ID),ee.addPostQuotingYou(this),null!=ee.position?ee.position:ee.position=ee.order[this.ID]},addPostQuotingYou(e){
for(var t of e.nodes.quotelinks)if(_.db?.get(We.postDataFromLink(t)))return ee.postsQuotingYou.add(ee.postsQuotingYou.last=e.ID),void ee.openNotification(e)},openNotification(e,t=" replied to you"){if(!Xe.areNotificationsEnabled)return;const o=new Notification(`${e.info.nameBlock}${t}`,{body:e.commentDisplay(),icon:h.logo});return o.onclick=function(){return Xe.scrollToIfNeeded(e.nodes.bottom,!0),window.focus()},o.onshow=()=>setTimeout((()=>o.close()),7e3)},onUpdate:()=>Qe.queueTask((function(){return ee.setLine(),ee.read(),ee.update()})),readSinglePost(e){const{ID:t}=e;if(ee.posts.has(t))return ee.posts.delete(t),ee.postsQuotingYou.delete(t),ee.updatePosition(),ee.saveLastReadPost(),ee.update()},read:f(100,(function(e){if(ee.posts.size||ee.readCount===ee.thread.posts.keys.length||ee.saveLastReadPost(),a.hidden||!ee.posts.size)return;let t=0;for(;ee.position;){var{ID:o,data:n}=ee.position,{bottom:i}=n.nodes;if(i.getBoundingClientRect().height&&Xe.getBottomOf(i)<=-1)break;t++,
ee.posts.delete(o),ee.postsQuotingYou.delete(o),ee.position=ee.position.next}return t?(ee.updatePosition(),ee.saveLastReadPost(),e?ee.update():void 0):void 0})),updatePosition(){for(;ee.position&&!ee.posts.has(ee.position.ID);)ee.position=ee.position.next},saveLastReadPost:f(2e3,(function(){let e;if(Qe.forceSync("Remember Last Read Post"),!t["Remember Last Read Post"]||!ee.db)return;const o=ee.thread.posts.keys;for(let t=ee.readCount,n=o.length;t<n;t++){if(e=+o[t],!ee.thread.posts.get(e).isFetchedQuote){if(ee.posts.has(e))break;ee.lastReadPost=e}ee.readCount++}return!ee.thread.isDead||ee.thread.isArchived?ee.db.set({boardID:ee.thread.board.ID,threadID:ee.thread.ID,val:ee.lastReadPost}):void 0})),setLine(e){if(t["Unread Line"]){if(ee.hr.hidden||a.hidden||!0===e){const e=ee.linePosition;if(ee.linePosition=ee.positionPrev()){if(ee.linePosition!==e){let e=ee.linePosition.data.nodes.bottom;"BR"===e.nextSibling?.tagName&&(e=e.nextSibling),Qe.after(e,ee.hr)}}else Qe.rm(ee.hr)}
return ee.hr.hidden=ee.linePosition===ee.order.last}},update(){const e=ee.posts.size,n=ee.postsQuotingYou.size;if(t["Unread Count"]){const o=t["Quoted Title"]&&n?"(!) ":"",i=e||!t["Hide Unread Count at (0)"]?`(${e}) `:"",r=ee.thread.isDead?ee.title.replace("-",ee.thread.isArchived?"- Archived -":"- 404 -"):ee.title;a.title=`${o}${i}${r}`}if(ee.saveThreadWatcherCount(),t["Unread Favicon"]&&"yotsuba"===o.SITE.software){const{isDead:t}=ee.thread;return h.set(n?t?"unreadDeadY":"unreadY":e?t?"unreadDead":"unread":t?"dead":"default")}},saveThreadWatcherCount:f(2e3,(function(){if(Qe.forceSync("Remember Last Read Post"),t["Remember Last Read Post"]&&(!ee.thread.isDead||ee.thread.isArchived)){let e;const n=!t["Require OP Quote Link"]&&_.isYou(ee.thread.OP)?ee.posts:ee.postsQuotingYou;if(n.size){if(!n.has(n.last)){n.last=0,e=ee.thread.posts.keys;for(let t=e.length-1;t>=0;t--)if(n.has(+e[t])){n.last=e[t];break}}}else n.last=0;return ae.update(o.SITE.ID,ee.thread.board.ID,ee.thread.ID,{
@ -639,24 +642,24 @@ Qe.on(t.firstElementChild,"click",(()=>window.open(`//sys.${location.hostname.sp
name:"Mark OP Quotes",cb:this.node})},node(){let e,t,o;if(this.isClone&&this.thread===this.context.thread)return;if(!(o=this.quotes).length)return;const{quotelinks:n}=this.nodes;if(this.isClone&&o.includes(this.thread.fullID))for(e=0;t=n[e++];)Qe.rm(Qe(".qmark-op",t));const{fullID:a}=this.context.thread;if(o.includes(a))for(e=0;t=n[e++];){var{boardID:i,postID:r}=We.postDataFromLink(t);`${i}.${r}`===a&&Qe.add(t,Nt.mark.cloneNode(!0))}}};var Lt={init(){if(["index","thread"].includes(o.VIEW)&&t["Resurrect Quotes"])return Qe.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))Lt.parseArchivelink.call(this,e);for(var t of u(".deadlink",this.nodes.comment))Lt.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 o=t[1],n=t[2],a=e.hash.match(/^#[pq]?(\d+)$|$/)[1]||n;return et.to("post",{boardID:o,postID:a})?(Qe.addClass(e,"quotelink"),Qe.extend(e.dataset,{boardID:o,threadID:n,postID:a}),this.nodes.archivelinks.push(e)):void 0},parseDeadlink(e){let t,n,a,i;if(Qe.hasClass(e.parentNode,"prettyprint"))return void Lt.fixDeadlink(e);const r=e.textContent;if(!(i=r.match(/\d+$/)?.[0]))return;if("0"===i[0])return void Lt.fixDeadlink(e);const s=(n=r.match(/^>>>\/([a-z\d]+)/))?n[1]:this.board.ID,l=`${s}.${i}`;if(a=o.posts.get(l))a.isDead?(t=Qe.el("a",{href:o.SITE.Build.postURL(s,a.thread.ID,i),className:"quotelink deadlink",textContent:r}),Qe.add(t,H.deadMark.cloneNode(!0)),Qe.extend(t.dataset,{boardID:s,threadID:a.thread.ID,postID:i})):t=Qe.el("a",{href:o.SITE.Build.postURL(s,a.thread.ID,i),className:"quotelink",textContent:r});else{const e=et.to("thread",{
boardID:s,threadID:0,postID:i}),o=et.to("post",{boardID:s,postID:i});(e||o)&&(t=Qe.el("a",{href:e||"javascript:;",className:"deadlink",textContent:r}),Qe.add(t,H.deadMark.cloneNode(!0)),o&&(Qe.addClass(t,"quotelink"),Qe.extend(t.dataset,{boardID:s,postID:i})))}if(this.quotes.includes(l)||this.quotes.push(l),t)return Qe.replace(e,t),Qe.hasClass(t,"quotelink")?this.nodes.quotelinks.push(t):void 0;Qe.add(e,H.deadMark.cloneNode(!0))},fixDeadlink(e){let t;if(!(t=e.previousSibling)||"BR"===t.nodeName){const t=Qe.el("span",{className:"quote"});Qe.before(e,t),Qe.add(t,e)}return Qe.replace(e,[...e.childNodes])}};const Ft={init(){this.toBlob(),Qe.global(this.toBlob),Element.prototype.matches||(Element.prototype.matches=Element.prototype.mozMatchesSelector||Element.prototype.webkitMatchesSelector)},toBlob:function(){HTMLCanvasElement.prototype.toBlob||(HTMLCanvasElement.prototype.toBlob=function(e,t,o){const n=this.toDataURL(t,o),a=atob(n.slice(n.indexOf(",")+1)),i=a.length,r=new Uint8Array(i)
;for(let e=0,t=i;e<t;e++)r[e]=a.charCodeAt(e);return e(new Blob([r],{type:t||"image/png"}))})}},Ot={restore(){console.log(o);const e=et.to("threadJSON",{boardID:o.boardID,threadID:o.threadID});if(console.log(e),!e)return void new _e("warning","No archive found",3);(e.startsWith("https://")||t["Exempt Archives from Encryption"])&&He.cache(e,(function(){console.log(this);let e=0;const t=this.response[o.threadID.toString()].posts;for(const[n,a]of Object.entries(t)){const t=`${o.boardID}.${n}`;if(!o.posts.keys.includes(t)){const i=+n;let r=o.posts.keys.findIndex((e=>+e.split(".")[1]>i));-1===r&&(r=o.posts.keys.length);const s=de(a);s.kill(),o.posts.push(t,s),o.posts.keys.pop(),o.posts.keys.splice(r,0,t),Re.insert(s)||o.posts.get(o.posts.keys[r-1]).root.insertAdjacentElement("afterend",s.root),++e}}let n;n=0===e?"No removed posts found":1===e?"1 post restored":`${e} posts restored`,new _e("info",n,3)}))},init(){if("thread"!==o.VIEW)return;const e=Qe.el("a",{href:"javascript:;",
textContent:"Restore from archive"});Qe.on(e,"click",(()=>{Ot.restore(),Xe.menu.close()})),Xe.menu.addEntry({el:e,order:10})}};var $t={init(){let n;try{let t=window;if("crx"===x&&(t=t.wrappedJSObject||t),`${e.name} antidup`in t)return;t[`${e.name} antidup`]=!0}catch(e){}try{if(window.frameElement&&["","about:blank"].includes(window.frameElement.src))return}catch(e){}if(i&&Qe.hasClass(i,"fourchan-x"))return;Qe.asap(s,(function(){if(Qe.addClass(i,"fourchan-x","seaweedchan"),Qe.engine)return Qe.addClass(i,`ua-${Qe.engine}`)})),Qe.on(a,"4chanXInitFinished",(function(){return $t.expectInitFinished?delete $t.expectInitFinished:(new _e("error","Error: Multiple copies of 4chan X are enabled."),Qe.addClass(i,"tainted"))}));var r=function(){return a.removeEventListener("mounted",r,!0),$t.isMounted=!0,$t.mountedCBs.map((e=>(()=>{try{return e()}catch(e){}})()))};a.addEventListener("mounted",r,!0);var l=function(e,o){
if(o instanceof Array)t[e]=m.clone(o[0]);else if("object"==typeof o)for(var n in o){var a=o[n];l(n,a)}else t[e]=o};for(var d of(["boards.4chan.org","boards.4channel.org"].includes(location.hostname)&&(Qe.global((function(){const e=String.fromCharCode;return String.fromCharCode=function(){if(document.body)String.fromCharCode=e;else if(document.currentScript&&!document.currentScript.src)throw Error();return e.apply(this,arguments)}})),Qe.asap(s,(()=>Qe.onExists(i,"iframe[srcdoc]",Qe.rm)))),l(null,c),y.keys))t[d]=m();t.customTitles=m.clone({"4chan.org":{boards:{qa:{boardTitle:{orig:"/qa/ - Question & Answer",title:"/qa/ - 2D/Random"}}}}}),t.boardConfig={boards:m()},t.archives=et.archives,t.selectedArchives=m(),t.cooldowns=m(),t["Index Sort"]=m();for(let e=0;e<2;e++)t[`Last Long Reply Thresholds ${e}`]=m();t.siteProperties=m(),t["Except Archives from Encryption"]=!1,t["JSON Navigation"]=!0,t["Oekaki Links"]=!0,t["Show Name and Subject"]=!1,t["QR Shortcut"]=!0,t["Bottom QR Link"]=!0,
t["Toggleable Thread Watcher"]=!0,t.siteSoftware="",t["Use Faster Image Host"]="true",t["Captcha Fixes"]=!0,t.captchaServiceDomain="",t.captchaServiceKey=m(),!/\.4chan(?:nel)?\.org$/.test(location.hostname)||De.yotsuba.regexp.pass.test(location.href)||De.yotsuba.regexp.captcha.test(location.href)||u("script:not([src])",a).filter((e=>/this\[/.test(e.textContent))).length||(Qe.getSync||Qe.get)({jsWhitelist:t.jsWhitelist},(({jsWhitelist:e})=>Qe.addCSP(`script-src ${e.replace(/^#.*$/gm,"").replace(/[\s;]+/g," ").trim()}`)));const h=m();for(n in t)h[n]=void 0;return h.previousversion=void 0,(Qe.getSync||Qe.get)(h,(function(e){if(Qe.perProtocolSettings||!/\.4chan(?:nel)?\.org$/.test(location.hostname)||!(e["Redirect to HTTPS"]??t["Redirect to HTTPS"])||"https:"===location.protocol)return Qe.asap(s,(function(){for(n in Qe.cantSet||(null==e.previousversion?($t.isFirstRun=!0,$t.ready((function(){return Qe.set("previousversion",o.VERSION),Pe.open()
}))):e.previousversion!==o.VERSION&&$t.upgrade(e)),t){var a=t[n];t[n]=e[n]??a}return Ye.init($t.initFeatures)}));location.replace("https://"+location.host+location.pathname+location.search+location.hash)}))},upgrade(t){const{previousversion:n}=t,a=Pe.upgrade(t,n);return t.previousversion=a.previousversion=o.VERSION,Qe.set(a,(function(){if(t["Show Updated Notifications"]??1){const t=Qe.el("span",{innerHTML:`${e.name} has been updated to <a href="${e.changelog}" target="_blank">version ${o.VERSION}</a>.`});return new _e("info",t,15)}}))},parseURL(e=o.SITE,t=location){const n={};if(!e)return n;if(n.siteID=e.ID,e.isBoardlessPage?.(t))return n;const a=t.pathname.split(/\/+/);return n.boardID=a[1],e.isFileURL(t)?n.VIEW="file":e.isAuxiliaryPage?.(t)||(["thread","res"].includes(a[2])?(n.VIEW="thread",n.threadID=n.THREADID=+a[3].replace(/\.\w+$/,"")):"archive"===a[2]&&"res"===a[3]?(n.VIEW="thread",n.threadID=n.THREADID=+a[4].replace(/\.\w+$/,""),
n.threadArchived=!0):/^(?:catalog|archive)(?:\.\w+)?$/.test(a[2])?n.VIEW=a[2].replace(/\.\w+$/,""):/^(?:index|\d*)(?:\.\w+)?$/.test(a[2])&&(n.VIEW="index")),n},initFeatures(){if(Qe.global((function(){return document.documentElement.classList.add("js-enabled"),window.FCX={}})),$t.jsEnabled=Qe.hasClass(i,"js-enabled"),Qe.ajaxPageInit?.(),Qe.extend(o,$t.parseURL()),o.boardID&&(o.BOARD=new J(o.boardID)),o.VIEW){if("file"!==o.VIEW){for(var[e,n]of(o.threads=new k,o.posts=new k,Qe.onExists(i,"body",$t.initStyle),$t.features))if(!o.SITE.disabledFeatures||!o.SITE.disabledFeatures.includes(e))try{n.init()}catch(t){$t.handleErrors({message:`"${e}" initialization crashed.`,error:t})}return Qe.ready($t.initReady)}Qe.asap((()=>"loading"!==a.readyState),(function(){let e;if("yotsuba"===o.SITE.software&&t["404 Redirect"]&&o.SITE.is404?.()){const e=location.pathname.split(/\/+/);return et.navigate("file",{boardID:o.BOARD.ID,filename:e[e.length-1]})}
if((e=Qe("video"))&&(t["Volume in New Tab"]&&$.setup(e),t["Loop in New Tab"]))return e.loop=!0,e.controls=!1,e.play(),j.addControls(e)}))}else o.SITE.initAuxiliary?.()},initStyle(){if(!$t.isThisPageLegit())return;const e=Qe("link[href*=mobile]",a.head);e&&(e.disabled=!0),i.dataset.host=location.host,Qe.addClass(i,`sw-${o.SITE.software}`),Qe.addClass(i,"thread"===o.VIEW?"thread-view":o.VIEW),Qe.onExists(i,".ad-cnt, .adg-rects > .desktop",(e=>Qe.onExists(e,"img, iframe",(()=>Qe.addClass(i,"ads-loaded"))))),t["Autohiding Scrollbar"]&&Qe.addClass(i,"autohiding-scrollbar"),Qe.ready((function(){if(a.body.clientHeight>i.clientHeight&&window.innerWidth===i.clientWidth!==t["Autohiding Scrollbar"])return t["Autohiding Scrollbar"]=!t["Autohiding Scrollbar"],Qe.set("Autohiding Scrollbar",t["Autohiding Scrollbar"]),Qe.toggleClass(i,"autohiding-scrollbar")})),Qe.addStyle(be.sub(be.boards),"fourchanx-css"),$t.bgColorStyle=Qe.el("style",{id:"fourchanx-bgcolor-css"});let n=!1
;return Qe.on(a,"mousedown",(()=>n=!1)),Qe.on(a,"keydown",(function(e){if(9===e.keyCode)return n=!0})),window.addEventListener("focus",(()=>i.classList.toggle("keyboard-focus",n)),!0),$t.setClass()},setClass(){let e,t,n;const r=["yotsuba","yotsuba-b","futaba","burichan","photon","tomorrow","spooky"];if("yotsuba"===o.SITE.software&&"catalog"===o.VIEW&&(e=Qe.id("base-css"))&&(t=e.href.match(/catalog_(\w+)/)?.[1].replace("_new","").replace(/_+/g,"-"),r.includes(t)))return void Qe.addClass(i,t);t=e=n=null;const s=function(){if("yotsuba"===o.SITE.software){for(var s of(Qe.rmClass(i,t),t=null,n))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 Qe.addClass(i,t),void Qe.rm($t.bgColorStyle)}const l=o.SITE.bgColoredEl();l.style.position="absolute",l.style.visibility="hidden",Qe.add(a.body,l);let d=window.getComputedStyle(l).backgroundColor;Qe.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 Qe.luma(c)<100&&(h+=".watch-thread-link {\n background-image: url(\"data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(200,200,200)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>\");\n}"),$t.bgColorStyle.textContent=h,Qe.after(Qe.id("fourchanx-css"),$t.bgColorStyle)}
;if(Qe.onExists(a.head,o.SITE.selectors.styleSheet,(function(t){return e=t,"yotsuba"===o.SITE.software&&(n=u('link[rel="alternate stylesheet"]',a.head)),new MutationObserver(s).observe(e,{attributes:!0,attributeFilter:["href"]}),Qe.on(e,"load",s),s()})),!e){for(var l of u('link[rel="stylesheet"]',a.head))Qe.on(l,"load",s);return s()}},initReady(){if(!o.SITE.is404?.()){if(o.SITE.isIncomplete?.()){const e=Qe.el("div",{innerHTML:'The page didn&#039;t load completely.<br>Some features may not work unless you <a href="javascript:;">reload</a>.'});Qe.on(Qe("a",e),"click",(()=>location.reload())),new _e("warning",e)}return"catalog"===o.VIEW?$t.initCatalog():pe.enabled?($t.expectInitFinished=!0,Qe.event("4chanXInitFinished")):o.SITE.awaitBoard?o.SITE.awaitBoard($t.initThread):$t.initThread()}"thread"===o.VIEW&&ae.set404(o.BOARD.ID,o.THREADID,(function(){if(t["404 Redirect"])return et.navigate("thread",{boardID:o.BOARD.ID,threadID:o.THREADID,postID:+location.hash.match(/\d+/)},`/${o.BOARD}/`)
}))},initThread(){let e;const t=o.SITE.selectors;if(e=Qe(t.boardFor?.[o.VIEW]||t.board)){const n=[],a=[],i=[];try{o.SITE.preParsingFixes?.(e)}catch(e){}return $t.addThreadsObserver=new MutationObserver($t.addThreads),$t.addPostsObserver=new MutationObserver($t.addPosts),$t.addThreadsObserver.observe(e,{childList:!0}),$t.parseThreads(u(t.thread,e),n,a,i),i.length&&$t.handleErrors(i),"thread"===o.VIEW&&(o.threadArchived&&(n[0].isArchived=!0,n[0].kill()),o.SITE.parseThreadMetadata?.(n[0])),$t.callbackNodes("Thread",n),$t.callbackNodesDB("Post",a,(function(){for(var e of a)Re.insert(e);return $t.expectInitFinished=!0,Qe.event("4chanXInitFinished")}))}return $t.expectInitFinished=!0,Qe.event("4chanXInitFinished")},parseThreads(e,t,n,a){for(var i of e){var r=(()=>{let e;return(e=i.dataset.board)?(e=encodeURIComponent(e),o.boards[e]||new J(e)):o.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(o.SITE.selectors.postContainer,i);o.SITE.isOPContainerThread&&d.unshift(i),$t.parsePosts(d,l,n,a),$t.addPostsObserver.observe(i,{childList:!0})}},parsePosts(e,t,n,a){for(var i of e)if((!i.dataset.fullID||!o.posts.get(i.dataset.fullID))&&Qe(o.SITE.selectors.comment,i))try{n.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 n of e)for(var a of n.addedNodes)a.nodeType===Node.ELEMENT_NODE&&a.matches(o.SITE.selectors.thread)&&t.push(a);if(!t.length)return;const i=[],r=[],s=[];return $t.parseThreads(t,i,r,s),s.length&&$t.handleErrors(s),$t.callbackNodes("Thread",i),$t.callbackNodesDB("Post",r,(()=>Qe.event("PostsInserted",null,e[0].target)))},addPosts(e){let t;const n=[],a=[],r=[],s=[];for(var l of e){t=We.threadFromRoot(l.target);var d=[]
;for(var c of l.addedNodes)c.nodeType===Node.ELEMENT_NODE&&(c.matches(o.SITE.selectors.postContainer)||(c=Qe(o.SITE.selectors.postContainer,c)))&&d.push(c);var h=r.length;$t.parsePosts(d,t,r,s),r.length>h&&!n.includes(t)&&n.push(t);var u=!1;for(var p of l.removedNodes)if(We.postFromRoot(p)?.nodes.root===p&&!i.contains(p)){u=!0;break}u&&!a.includes(t)&&a.push(t)}return s.length&&$t.handleErrors(s),$t.callbackNodesDB("Post",r,(function(){for(t of n)Qe.event("PostsInserted",null,t.nodes.root);for(t of a)Qe.event("PostsRemoved",null,t.nodes.root)}))},initCatalog(){let e;const t=o.SITE.selectors.catalog;if(t&&(e=Qe(t.board))){const o=[],n=[];$t.addCatalogThreadsObserver=new MutationObserver($t.addCatalogThreads),$t.addCatalogThreadsObserver.observe(e,{childList:!0}),$t.parseCatalogThreads(u(t.thread,e),o,n),n.length&&$t.handleErrors(n),$t.callbackNodes("CatalogThreadNative",o)}return $t.expectInitFinished=!0,Qe.event("4chanXInitFinished")},parseCatalogThreads(e,t,o){for(var n of e)try{
var a=new tt(n);a.thread.catalogViewNative?.nodes.root!==n&&(a.thread.catalogViewNative=a,t.push(a))}catch(e){o.push({message:`Parsing of Catalog Thread No.${(n.dataset.id||n.id).match(/\d+/)} failed. Thread will be skipped.`,error:e,html:n.outerHTML})}},addCatalogThreads(e){const t=[];for(var n of e)for(var a of n.addedNodes)a.nodeType===Node.ELEMENT_NODE&&a.matches(o.SITE.selectors.catalog.thread)&&t.push(a);if(!t.length)return;const i=[],r=[];return $t.parseCatalogThreads(t,i,r),r.length&&$t.handleErrors(r),$t.callbackNodes("CatalogThreadNative",i)},callbackNodes(e,t){let o,n=0;const a=l[e];for(;o=t[n++];)a.execute(o)},callbackNodesDB(e,t,o){let n=0;const a=l[e],i=function(){let e;return!!(e=t[n])&&(a.execute(e),++n%250)};var r=function(){for(;i(););if(t[n])return setTimeout(r,0);o&&o()};return r()},handleErrors(r){let s;if(a.body&&Qe.hasClass(a.body,"fourchan_x")&&!Qe.hasClass(i,"tainted")&&(new _e("error","Error: Multiple copies of 4chan X are enabled."),Qe.addClass(i,"tainted")),
o.SITE.testNativeExtension&&!Qe.hasClass(i,"tainted")){const{enabled:a}=o.SITE.testNativeExtension();if(a&&(Qe.addClass(i,"tainted"),t["Disable Native Extension"]&&!$t.isFirstRun)){const t=Qe.el("div",{innerHTML:'Failed to disable the native extension. You may need to <a href="'+n(e.faq)+'#blocking-native-extension" target="_blank">block it</a>.'});new _e("error",t)}}if(r instanceof Array?1===r.length&&(s=r[0]):s=r,s)return void new _e("error",$t.parseError(s,$t.reportLink([s])),15);const l=Qe.el("div",{innerHTML:`${r.length} errors occurred.${$t.reportLink(r).innerHTML} [<a href="javascript:;">show</a>]`});Qe.on(l.lastElementChild,"click",(function(){return[this.textContent,d.hidden]="show"===this.textContent?["hide",!1]:["show",!0]}));var d=Qe.el("div",{hidden:!0});for(s of r)Qe.add(d,$t.parseError(s));return new _e("error",[l,d],30)},parseError(t,a){r.error(t.message,t.error.stack);const i=Qe.el("div",{innerHTML:n(t.message)+(a?a.innerHTML:"")}),s=Qe.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,Qe.el("div",{textContent:`(${e.name} ${e.fork} v${o.VERSION} ${x} on ${Qe.engine}${l})`})]},reportLink(t){let n;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${o.VERSION} ${x}\nURL: ${location.href}\nUser agent: ${navigator.userAgent}`),"userscript"===x&&(n="undefined"!=typeof GM&&null!==GM?GM.info:"undefined"!=typeof GM_info&&null!==GM_info?GM_info:void 0)&&s(`Userscript manager: ${n.scriptHandler} ${n.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:`<span class="report-error"> [<a href="${e.newIssue.replace("%title",encodeURIComponent(i)).replace("%details",encodeURIComponent(r))}" target="_blank">report</a>]</span>`}},isThisPageLegit:()=>("thisPageIsLegit"in $t||($t.thisPageIsLegit=o.SITE.isThisPageLegit?o.SITE.isThisPageLegit():!/^[45]\d\d\b/.test(document.title)&&!/\.(?:json|rss)$/.test(location.pathname)),$t.thisPageIsLegit),ready:e=>Qe.ready((function(){if($t.isThisPageLegit())return e()})),mounted:e=>$t.isMounted?e():$t.mountedCBs.push(e),mountedCBs:[],
;for(let e=0,t=i;e<t;e++)r[e]=a.charCodeAt(e);return e(new Blob([r],{type:t||"image/png"}))})}},Ot={restore(){const e=et.to("threadJSON",{boardID:o.boardID,threadID:o.threadID});if(!e)return void new _e("warning","No archive found",3);(e.startsWith("https://")||t["Exempt Archives from Encryption"])&&He.cache(e,(function(){let e=0;const t=this.response[o.threadID.toString()].posts;for(const[n,a]of Object.entries(t)){const t=`${o.boardID}.${n}`;if(!o.posts.keys.includes(t)){const i=+n;let r=o.posts.keys.findIndex((e=>+e.split(".")[1]>i));-1===r&&(r=o.posts.keys.length);const s=de(a);s.kill(),o.posts.push(t,s),o.posts.keys.pop(),o.posts.keys.splice(r,0,t),Re.insert(s)||o.posts.get(o.posts.keys[r-1]).root.insertAdjacentElement("afterend",s.root),++e}}let n;n=0===e?"No removed posts found":1===e?"1 post restored":`${e} posts restored`,new _e("info",n,3)}))},init(){if("thread"!==o.VIEW)return;const e=Qe.el("a",{href:"javascript:;",textContent:"Restore from archive"});Qe.on(e,"click",(()=>{
Ot.restore(),Xe.menu.close()})),Xe.menu.addEntry({el:e,order:10})}};var $t={init(){let n;try{let t=window;if("crx"===x&&(t=t.wrappedJSObject||t),`${e.name} antidup`in t)return;t[`${e.name} antidup`]=!0}catch(e){}try{if(window.frameElement&&["","about:blank"].includes(window.frameElement.src))return}catch(e){}if(i&&Qe.hasClass(i,"fourchan-x"))return;Qe.asap(s,(function(){if(Qe.addClass(i,"fourchan-x","seaweedchan"),Qe.engine)return Qe.addClass(i,`ua-${Qe.engine}`)})),Qe.on(a,"4chanXInitFinished",(function(){return $t.expectInitFinished?delete $t.expectInitFinished:(new _e("error","Error: Multiple copies of 4chan X are enabled."),Qe.addClass(i,"tainted"))}));var r=function(){return a.removeEventListener("mounted",r,!0),$t.isMounted=!0,$t.mountedCBs.map((e=>(()=>{try{return e()}catch(e){}})()))};a.addEventListener("mounted",r,!0);var l=function(e,o){if(o instanceof Array)t[e]=m.clone(o[0]);else if("object"==typeof o)for(var n in o){var a=o[n];l(n,a)}else t[e]=o}
;for(var d of(["boards.4chan.org","boards.4channel.org"].includes(location.hostname)&&(Qe.global((function(){const e=String.fromCharCode;return String.fromCharCode=function(){if(document.body)String.fromCharCode=e;else if(document.currentScript&&!document.currentScript.src)throw Error();return e.apply(this,arguments)}})),Qe.asap(s,(()=>Qe.onExists(i,"iframe[srcdoc]",Qe.rm)))),l(null,c),y.keys))t[d]=m();t.customTitles=m.clone({"4chan.org":{boards:{qa:{boardTitle:{orig:"/qa/ - Question & Answer",title:"/qa/ - 2D/Random"}}}}}),t.boardConfig={boards:m()},t.archives=et.archives,t.selectedArchives=m(),t.cooldowns=m(),t["Index Sort"]=m();for(let e=0;e<2;e++)t[`Last Long Reply Thresholds ${e}`]=m();t.siteProperties=m(),t["Except Archives from Encryption"]=!1,t["JSON Navigation"]=!0,t["Oekaki Links"]=!0,t["Show Name and Subject"]=!1,t["QR Shortcut"]=!0,t["Bottom QR Link"]=!0,t["Toggleable Thread Watcher"]=!0,t.siteSoftware="",t["Use Faster Image Host"]="true",t["Captcha Fixes"]=!0,
t.captchaServiceDomain="",t.captchaServiceKey=m(),!/\.4chan(?:nel)?\.org$/.test(location.hostname)||De.yotsuba.regexp.pass.test(location.href)||De.yotsuba.regexp.captcha.test(location.href)||u("script:not([src])",a).filter((e=>/this\[/.test(e.textContent))).length||(Qe.getSync||Qe.get)({jsWhitelist:t.jsWhitelist},(({jsWhitelist:e})=>Qe.addCSP(`script-src ${e.replace(/^#.*$/gm,"").replace(/[\s;]+/g," ").trim()}`)));const h=m();for(n in t)h[n]=void 0;return h.previousversion=void 0,(Qe.getSync||Qe.get)(h,(function(e){if(Qe.perProtocolSettings||!/\.4chan(?:nel)?\.org$/.test(location.hostname)||!(e["Redirect to HTTPS"]??t["Redirect to HTTPS"])||"https:"===location.protocol)return Qe.asap(s,(function(){for(n in Qe.cantSet||(null==e.previousversion?($t.isFirstRun=!0,$t.ready((function(){return Qe.set("previousversion",o.VERSION),Pe.open()}))):e.previousversion!==o.VERSION&&$t.upgrade(e)),t){var a=t[n];t[n]=e[n]??a}return Ye.init($t.initFeatures)}))
;location.replace("https://"+location.host+location.pathname+location.search+location.hash)}))},upgrade(t){const{previousversion:n}=t,a=Pe.upgrade(t,n);return t.previousversion=a.previousversion=o.VERSION,Qe.set(a,(function(){if(t["Show Updated Notifications"]??1){const t=Qe.el("span",{innerHTML:`${e.name} has been updated to <a href="${e.changelog}" target="_blank">version ${o.VERSION}</a>.`});return new _e("info",t,15)}}))},parseURL(e=o.SITE,t=location){const n={};if(!e)return n;if(n.siteID=e.ID,e.isBoardlessPage?.(t))return n;const a=t.pathname.split(/\/+/);return n.boardID=a[1],e.isFileURL(t)?n.VIEW="file":e.isAuxiliaryPage?.(t)||(["thread","res"].includes(a[2])?(n.VIEW="thread",n.threadID=n.THREADID=+a[3].replace(/\.\w+$/,"")):"archive"===a[2]&&"res"===a[3]?(n.VIEW="thread",n.threadID=n.THREADID=+a[4].replace(/\.\w+$/,""),n.threadArchived=!0):/^(?:catalog|archive)(?:\.\w+)?$/.test(a[2])?n.VIEW=a[2].replace(/\.\w+$/,""):/^(?:index|\d*)(?:\.\w+)?$/.test(a[2])&&(n.VIEW="index")),n},
initFeatures(){if(Qe.global((function(){return document.documentElement.classList.add("js-enabled"),window.FCX={}})),$t.jsEnabled=Qe.hasClass(i,"js-enabled"),Qe.ajaxPageInit?.(),Qe.extend(o,$t.parseURL()),o.boardID&&(o.BOARD=new J(o.boardID)),o.VIEW){if("file"!==o.VIEW){for(var[e,n]of(o.threads=new k,o.posts=new k,Qe.onExists(i,"body",$t.initStyle),$t.features))if(!o.SITE.disabledFeatures||!o.SITE.disabledFeatures.includes(e))try{n.init()}catch(t){$t.handleErrors({message:`"${e}" initialization crashed.`,error:t})}return Qe.ready($t.initReady)}Qe.asap((()=>"loading"!==a.readyState),(function(){let e;if("yotsuba"===o.SITE.software&&t["404 Redirect"]&&o.SITE.is404?.()){const e=location.pathname.split(/\/+/);return et.navigate("file",{boardID:o.BOARD.ID,filename:e[e.length-1]})}if((e=Qe("video"))&&(t["Volume in New Tab"]&&$.setup(e),t["Loop in New Tab"]))return e.loop=!0,e.controls=!1,e.play(),j.addControls(e)}))}else o.SITE.initAuxiliary?.()},initStyle(){if(!$t.isThisPageLegit())return
;const e=Qe("link[href*=mobile]",a.head);e&&(e.disabled=!0),i.dataset.host=location.host,Qe.addClass(i,`sw-${o.SITE.software}`),Qe.addClass(i,"thread"===o.VIEW?"thread-view":o.VIEW),Qe.onExists(i,".ad-cnt, .adg-rects > .desktop",(e=>Qe.onExists(e,"img, iframe",(()=>Qe.addClass(i,"ads-loaded"))))),t["Autohiding Scrollbar"]&&Qe.addClass(i,"autohiding-scrollbar"),Qe.ready((function(){if(a.body.clientHeight>i.clientHeight&&window.innerWidth===i.clientWidth!==t["Autohiding Scrollbar"])return t["Autohiding Scrollbar"]=!t["Autohiding Scrollbar"],Qe.set("Autohiding Scrollbar",t["Autohiding Scrollbar"]),Qe.toggleClass(i,"autohiding-scrollbar")})),Qe.addStyle(be.sub(be.boards),"fourchanx-css"),$t.bgColorStyle=Qe.el("style",{id:"fourchanx-bgcolor-css"});let n=!1;return Qe.on(a,"mousedown",(()=>n=!1)),Qe.on(a,"keydown",(function(e){if(9===e.keyCode)return n=!0})),window.addEventListener("focus",(()=>i.classList.toggle("keyboard-focus",n)),!0),$t.setClass()},setClass(){let e,t,n
;const r=["yotsuba","yotsuba-b","futaba","burichan","photon","tomorrow","spooky"];if("yotsuba"===o.SITE.software&&"catalog"===o.VIEW&&(e=Qe.id("base-css"))&&(t=e.href.match(/catalog_(\w+)/)?.[1].replace("_new","").replace(/_+/g,"-"),r.includes(t)))return void Qe.addClass(i,t);t=e=n=null;const s=function(){if("yotsuba"===o.SITE.software){for(var s of(Qe.rmClass(i,t),t=null,n))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 Qe.addClass(i,t),void Qe.rm($t.bgColorStyle)}const l=o.SITE.bgColoredEl();l.style.position="absolute",l.style.visibility="hidden",Qe.add(a.body,l);let d=window.getComputedStyle(l).backgroundColor;Qe.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 Qe.luma(c)<100&&(h+=".watch-thread-link {\n background-image: url(\"data:image/svg+xml,<svg viewBox='0 0 26 26' preserveAspectRatio='true' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(200,200,200)' d='M24.132,7.971c-2.203-2.205-5.916-2.098-8.25,0.235L15.5,8.588l-0.382-0.382c-2.334-2.333-6.047-2.44-8.25-0.235c-2.204,2.203-2.098,5.916,0.235,8.249l8.396,8.396l8.396-8.396C26.229,13.887,26.336,10.174,24.132,7.971z'/></svg>\");\n}"),$t.bgColorStyle.textContent=h,Qe.after(Qe.id("fourchanx-css"),$t.bgColorStyle)};if(Qe.onExists(a.head,o.SITE.selectors.styleSheet,(function(t){return e=t,"yotsuba"===o.SITE.software&&(n=u('link[rel="alternate stylesheet"]',a.head)),new MutationObserver(s).observe(e,{attributes:!0,attributeFilter:["href"]}),
Qe.on(e,"load",s),s()})),!e){for(var l of u('link[rel="stylesheet"]',a.head))Qe.on(l,"load",s);return s()}},initReady(){if(!o.SITE.is404?.()){if(o.SITE.isIncomplete?.()){const e=Qe.el("div",{innerHTML:'The page didn&#039;t load completely.<br>Some features may not work unless you <a href="javascript:;">reload</a>.'});Qe.on(Qe("a",e),"click",(()=>location.reload())),new _e("warning",e)}return"catalog"===o.VIEW?$t.initCatalog():pe.enabled?($t.expectInitFinished=!0,Qe.event("4chanXInitFinished")):o.SITE.awaitBoard?o.SITE.awaitBoard($t.initThread):$t.initThread()}"thread"===o.VIEW&&ae.set404(o.BOARD.ID,o.THREADID,(function(){if(t["404 Redirect"])return et.navigate("thread",{boardID:o.BOARD.ID,threadID:o.THREADID,postID:+location.hash.match(/\d+/)},`/${o.BOARD}/`)}))},initThread(){let e;const t=o.SITE.selectors;if(e=Qe(t.boardFor?.[o.VIEW]||t.board)){const n=[],a=[],i=[];try{o.SITE.preParsingFixes?.(e)}catch(e){}return $t.addThreadsObserver=new MutationObserver($t.addThreads),
$t.addPostsObserver=new MutationObserver($t.addPosts),$t.addThreadsObserver.observe(e,{childList:!0}),$t.parseThreads(u(t.thread,e),n,a,i),i.length&&$t.handleErrors(i),"thread"===o.VIEW&&(o.threadArchived&&(n[0].isArchived=!0,n[0].kill()),o.SITE.parseThreadMetadata?.(n[0])),$t.callbackNodes("Thread",n),$t.callbackNodesDB("Post",a,(function(){for(var e of a)Re.insert(e);return $t.expectInitFinished=!0,Qe.event("4chanXInitFinished")}))}return $t.expectInitFinished=!0,Qe.event("4chanXInitFinished")},parseThreads(e,t,n,a){for(var i of e){var r=(()=>{let e;return(e=i.dataset.board)?(e=encodeURIComponent(e),o.boards[e]||new J(e)):o.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(o.SITE.selectors.postContainer,i);o.SITE.isOPContainerThread&&d.unshift(i),$t.parsePosts(d,l,n,a),$t.addPostsObserver.observe(i,{childList:!0})}},parsePosts(e,t,n,a){
for(var i of e)if((!i.dataset.fullID||!o.posts.get(i.dataset.fullID))&&Qe(o.SITE.selectors.comment,i))try{n.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 n of e)for(var a of n.addedNodes)a.nodeType===Node.ELEMENT_NODE&&a.matches(o.SITE.selectors.thread)&&t.push(a);if(!t.length)return;const i=[],r=[],s=[];return $t.parseThreads(t,i,r,s),s.length&&$t.handleErrors(s),$t.callbackNodes("Thread",i),$t.callbackNodesDB("Post",r,(()=>Qe.event("PostsInserted",null,e[0].target)))},addPosts(e){let t;const n=[],a=[],r=[],s=[];for(var l of e){t=We.threadFromRoot(l.target);var d=[];for(var c of l.addedNodes)c.nodeType===Node.ELEMENT_NODE&&(c.matches(o.SITE.selectors.postContainer)||(c=Qe(o.SITE.selectors.postContainer,c)))&&d.push(c);var h=r.length;$t.parsePosts(d,t,r,s),r.length>h&&!n.includes(t)&&n.push(t);var u=!1
;for(var p of l.removedNodes)if(We.postFromRoot(p)?.nodes.root===p&&!i.contains(p)){u=!0;break}u&&!a.includes(t)&&a.push(t)}return s.length&&$t.handleErrors(s),$t.callbackNodesDB("Post",r,(function(){for(t of n)Qe.event("PostsInserted",null,t.nodes.root);for(t of a)Qe.event("PostsRemoved",null,t.nodes.root)}))},initCatalog(){let e;const t=o.SITE.selectors.catalog;if(t&&(e=Qe(t.board))){const o=[],n=[];$t.addCatalogThreadsObserver=new MutationObserver($t.addCatalogThreads),$t.addCatalogThreadsObserver.observe(e,{childList:!0}),$t.parseCatalogThreads(u(t.thread,e),o,n),n.length&&$t.handleErrors(n),$t.callbackNodes("CatalogThreadNative",o)}return $t.expectInitFinished=!0,Qe.event("4chanXInitFinished")},parseCatalogThreads(e,t,o){for(var n of e)try{var a=new tt(n);a.thread.catalogViewNative?.nodes.root!==n&&(a.thread.catalogViewNative=a,t.push(a))}catch(e){o.push({message:`Parsing of Catalog Thread No.${(n.dataset.id||n.id).match(/\d+/)} failed. Thread will be skipped.`,error:e,
html:n.outerHTML})}},addCatalogThreads(e){const t=[];for(var n of e)for(var a of n.addedNodes)a.nodeType===Node.ELEMENT_NODE&&a.matches(o.SITE.selectors.catalog.thread)&&t.push(a);if(!t.length)return;const i=[],r=[];return $t.parseCatalogThreads(t,i,r),r.length&&$t.handleErrors(r),$t.callbackNodes("CatalogThreadNative",i)},callbackNodes(e,t){let o,n=0;const a=l[e];for(;o=t[n++];)a.execute(o)},callbackNodesDB(e,t,o){let n=0;const a=l[e],i=function(){let e;return!!(e=t[n])&&(a.execute(e),++n%250)};var r=function(){for(;i(););if(t[n])return setTimeout(r,0);o&&o()};return r()},handleErrors(r){let s;if(a.body&&Qe.hasClass(a.body,"fourchan_x")&&!Qe.hasClass(i,"tainted")&&(new _e("error","Error: Multiple copies of 4chan X are enabled."),Qe.addClass(i,"tainted")),o.SITE.testNativeExtension&&!Qe.hasClass(i,"tainted")){const{enabled:a}=o.SITE.testNativeExtension();if(a&&(Qe.addClass(i,"tainted"),t["Disable Native Extension"]&&!$t.isFirstRun)){const t=Qe.el("div",{
innerHTML:'Failed to disable the native extension. You may need to <a href="'+n(e.faq)+'#blocking-native-extension" target="_blank">block it</a>.'});new _e("error",t)}}if(r instanceof Array?1===r.length&&(s=r[0]):s=r,s)return void new _e("error",$t.parseError(s,$t.reportLink([s])),15);const l=Qe.el("div",{innerHTML:`${r.length} errors occurred.${$t.reportLink(r).innerHTML} [<a href="javascript:;">show</a>]`});Qe.on(l.lastElementChild,"click",(function(){return[this.textContent,d.hidden]="show"===this.textContent?["hide",!1]:["show",!0]}));var d=Qe.el("div",{hidden:!0});for(s of r)Qe.add(d,$t.parseError(s));return new _e("error",[l,d],30)},parseError(t,a){r.error(t.message,t.error.stack);const i=Qe.el("div",{innerHTML:n(t.message)+(a?a.innerHTML:"")}),s=Qe.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,Qe.el("div",{
textContent:`(${e.name} ${e.fork} v${o.VERSION} ${x} on ${Qe.engine}${l})`})]},reportLink(t){let n;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${o.VERSION} ${x}\nURL: ${location.href}\nUser agent: ${navigator.userAgent}`),"userscript"===x&&(n="undefined"!=typeof GM&&null!==GM?GM.info:"undefined"!=typeof GM_info&&null!==GM_info?GM_info:void 0)&&s(`Userscript manager: ${n.scriptHandler} ${n.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:`<span class="report-error"> [<a href="${e.newIssue.replace("%title",encodeURIComponent(i)).replace("%details",encodeURIComponent(r))}" target="_blank">report</a>]</span>`}},isThisPageLegit:()=>("thisPageIsLegit"in $t||($t.thisPageIsLegit=o.SITE.isThisPageLegit?o.SITE.isThisPageLegit():!/^[45]\d\d\b/.test(document.title)&&!/\.(?:json|rss)$/.test(location.pathname)),$t.thisPageIsLegit),ready:e=>Qe.ready((function(){if($t.isThisPageLegit())return e()})),mounted:e=>$t.isMounted?e():$t.mountedCBs.push(e),mountedCBs:[],
features:[["Polyfill",Ft],["Board Configuration",Y],["Normalize URL",xt],["Delay Redirect on Post",K],["Captcha Configuration",p],["Image Host Rewriting",O],["Redirect",et],["Header",Xe],["Catalog Links",Je],["Settings",Pe],["Index Generator",pe],["Disable Autoplay",pt],["Announcement Hiding",It],["Fourchan thingies",mt],["Tinyboard Glue",Et],["Color User IDs",bt],["Highlight by User ID",At],["Count Posts by ID",vt],["Custom CSS",ve],["Thread Links",Dt],["Linkify",st],["Reveal Spoilers",Ct],["Resurrect Quotes",Lt],["Filter",Ge],["Thread Hiding Buttons",ge],["Reply Hiding Buttons",W],["Recursive",z],["Strike-through Quotes",{init(){if(["index","thread"].includes(o.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:n}=We.postDataFromLink(e);o.posts.get(`${t}.${n}`)?.isHidden&&Qe.addClass(e,"filtered")}}
}],["Quick Reply Personas",je.persona],["Quick Reply",je],["Cooldown",je.cooldown],["Post Jumper",yt],["Pass Link",Rt],["Menu",Q],["Index Generator (Menu)",pe.menu],["Report Link",ut],["Copy Text Link",dt],["Thread Hiding (Menu)",ge.menu],["Reply Hiding (Menu)",W.menu],["Delete Link",ct],["Filter (Menu)",Ge.menu],["Edit Link",je.oekaki.menu],["Download Link",ht],["Archive Link",lt],["Quote Inlining",Bt],["Quote Previewing",he],["Quote Backlinks",Pt],["Mark Quotes of You",_],["Mark OP Quotes",Nt],["Mark Cross-thread Quotes",Mt],["Anonymize",ot],["Time Formatting",Se],["Relative Post Dates",G],["File Info Formatting",Ee],["Fappe Tyme",Me],["Gallery",Le],["Gallery (menu)",Le.menu],["Sauce",Ne],["Image Expansion",U],["Image Expansion (Menu)",U.menu],["Reveal Spoiler Thumbnails",rt],["Image Loading",at],["Image Hover",nt],["Volume Control",$],["WEBM Metadata",it],["Comment Expansion",X],["Thread Expansion",te],["Favicon",h],["Unread",ee],["Unread Line in Index",oe],["Quote Threading",Re],["Thread Stats",Tt],["Thread Updater",Be],["Thread Watcher",ae],["Thread Watcher (Menu)",ae.menu],["Mark New IPs",St],["Index Navigation",F],["Keybinds",Oe],["Banner",gt],["Announcements",kt],["Flash Features",ft],["Reply Pruning",Te],["Mod Contact Links",wt],["Restore deleted posts from archive",Ot]]
},jt=$t;Qe.ready((()=>$t.init()))}();
//# sourceMappingURL=4chan-XT-noupdate.user.min.js.map
//# sourceMappingURL=4chan-XT.min.user.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "4chan XT",
"version": "XT 2.2.0",
"version": "XT 2.2.1",
"manifest_version": 2,
"description": "4chan XT is a script that adds various features to anonymous imageboards.",
"icons": {

View File

@ -60,6 +60,7 @@
* btmcsweeney
* AppleBloom
* detharonil
* TuxedoTako
*
* All the people who've taken the time to write bug reports.
*
@ -79,8 +80,8 @@
'use strict';
var version = {
"version": "XT 2.2.0",
"date": "2023-10-27T13:58:44.136Z"
"version": "XT 2.2.1",
"date": "2023-10-28T17:56:24.449Z"
};
var meta = {
@ -346,7 +347,7 @@ div.boardTitle {
'Enable reporting posts to supported archives.'
],
'Exempt Archives from Encryption': [
true,
false,
'Permit loading content from, and warningless redirects to, HTTP-only archives from HTTPS pages.'
],
'Keybinds': [
@ -5443,9 +5444,9 @@ https://*.hcaptcha.com
},
addPost() {
if (this.isFetchedQuote || this.isClone || (this.ID <= Unread.lastReadPost)) return;
if (this.isFetchedQuote || this.isClone) return;
Unread.order.push(this);
if (this.isHidden || QuoteYou.isYou(this)) return;
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]);
@ -26411,9 +26412,7 @@ aero|asia|biz|cat|com|coop|dance|info|int|jobs|mobi|moe|museum|name|net|org|post
const RestoreDeletedFromArchive = {
restore() {
console.log(g);
const url = Redirect$1.to('threadJSON', { boardID: g.boardID, threadID: g.threadID });
console.log(url);
if (!url) {
new Notice('warning', 'No archive found', 3);
return;
@ -26421,7 +26420,6 @@ aero|asia|biz|cat|com|coop|dance|info|int|jobs|mobi|moe|museum|name|net|org|post
const encryptionOK = url.startsWith('https://');
if (encryptionOK || Conf['Exempt Archives from Encryption']) {
CrossOrigin$1.cache(url, function () {
console.log(this);
let nrRestored = 0;
const archivePosts = this.response[g.threadID.toString()].posts;
for (const [postID, raw] of Object.entries(archivePosts)) {

View File

@ -1,139 +0,0 @@
<!doctype html>
<html><head>
<meta charset="utf-8">
<title>4chan X</title>
<link rel="stylesheet" href="web.css">
<link rel="icon" href="img/icon.gif">
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/ohnjgmpcibpbafdlkimncjhflgedgpam">
</head><body>
<div id="header">
<h1 id="4chan-x">4chan X</h1>
<div id="links">
<a href="https://github.com/ccd0/4chan-x">Source Code</a>
<a href="https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md">Changelog</a>
<a href="https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions">FAQ</a>
<a href="https://github.com/ccd0/4chan-x/wiki/Privacy">Privacy</a>
<a href="https://github.com/ccd0/4chan-x/issues">Report Bugs</a>
</div>
</div>
<a class="screenshot" href="img/screenshot.png"><img src="img/screenshot.png" alt="Screenshot"></a>
<p>4chan X is a script that adds various features to anonymous imageboards. It was originally developed for 4chan but has no affiliation with it.</p>
<p>It was previously developed by <a href="https://github.com/aeosynth/4chan-x">aeosynth</a>, <a href="https://github.com/MayhemYDG/4chan-x">Mayhem</a>, <a href="https://github.com/ihavenoface/4chan-x">ihavenoface</a>, <a href="https://github.com/zixaphir/appchan-x">Zixaphir</a>, <a href="https://github.com/seaweedchan/4chan-x">Seaweed</a>, and <a href="https://github.com/Spittie/4chan-x">Spittie</a>, with contributions from many others.</p>
<p>If you're looking for a maintained fork of OneeChan (a style script used in addition to 4chan X), try
<a href="https://github.com/KevinParnell/OneeChan">https://github.com/KevinParnell/OneeChan</a>.</p>
<h2 id="please-note">Please note</h2>
<p><strong>Uninstalling</strong>: 4chan X disables the native extension, so if you uninstall 4chan X, you'll need to re-enable it. To do this, click the <code>[Settings]</code> link in the top right corner, uncheck &quot;<code>Disable the native extension</code>&quot; in the panel that appears, and click the &quot;<code>Save Settings</code>&quot; button. If you don't see a &quot;<code>Save Settings</code>&quot; button, it may be being hidden by your ad blocker.</p>
<p><strong>Private browsing</strong>: By default, 4chan X remembers your last read post in a thread and which posts were made by you, even if you are in private browsing / incognito mode. If you want to turn this off, uncheck the <code>Remember Last Read Post</code> and <code>Remember Your Posts</code> options in the settings panel. You can clear all 4chan browsing history saved by 4chan X by resetting your settings.</p>
<p>Use of the &quot;Link Title&quot; feature to fetch titles of Youtube links is subject to Youtube's <a href="https://www.youtube.com/t/terms">Terms of Service</a> and <a href="http://www.google.com/policies/privacy">Privacy Policy</a>. For more details on what information is sent to Youtube and other sites, and how to turn it off if you don't want the feature, see 4chan X's <a href="https://github.com/ccd0/4chan-x/wiki/Privacy">privacy documentation</a>.</p>
<h2 id="install">Install</h2>
<input hidden type="checkbox" id="firefox-hide"><div><h3 id="firefox"><label for="firefox-hide">Firefox</label></h3>
<p>Install <a href="https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/">Violentmonkey</a>, <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/">Tampermonkey</a>, or <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">Greasemonkey</a> (issues since v4: <a href="https://github.com/greasemonkey/greasemonkey/issues/2526">#2526</a>, <a href="https://github.com/greasemonkey/greasemonkey/issues/2574">#2576</a>), then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
<p>Ports of Greasemonkey are available for <a href="https://sourceforge.net/projects/gmport/">SeaMonkey</a> and <a href="https://github.com/janekptacijarabaci/greasemonkey/releases/latest">Pale Moon</a>.</p>
</div><input hidden type="checkbox" id="chromium-hide"><div><h3 id="chromium"><label for="chromium-hide">Chromium</label></h3>
<p><strong>Userscript</strong>: Install <a href="https://chrome.google.com/webstore/detail/violent-monkey/jinjaccalgkegednnccohejagnlnfdag">Violentmonkey</a> or <a href="https://tampermonkey.net/">Tampermonkey</a>, then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
<p><strong>Chrome extension</strong>: 4chan X is also available as a standalone Chrome extension. The Chrome extension has the additional feature of being able to sync your settings and data with other devices via Chrome Sync. But there is an issue when the script updates: Whenever the Chrome extension is updated, until you hard refresh (F5) the tab, 4chan X is unable to save any data (such as posts marked as yours and settings changes). The userscript version above does not have this problem when 4chan X updates, only when Violentmonkey / Tampermonkey is updated. To install as a Chrome extension:</p>
<ul>
<li><strong>Chromium</strong>, <strong>Vivaldi</strong>: <strong><a href="https://www.4chan-x.net/builds/4chan-X.crx">Download 4chan X</a></strong>, then open <code>chrome://extensions</code> and drag the downloaded file onto the page. Alternatively, you can install 4chan X from the <strong><a href="https://chrome.google.com/webstore/detail/ohnjgmpcibpbafdlkimncjhflgedgpam">Chrome store</a></strong>.</li>
<li><strong>Opera</strong>: <strong><a href="https://www.4chan-x.net/builds/4chan-X.crx">Click to install 4chan X</a></strong>, then follow the prompts to activate it in your extension manager.</li>
<li><strong>Chrome</strong>: Install 4chan X from the <strong><a href="https://chrome.google.com/webstore/detail/ohnjgmpcibpbafdlkimncjhflgedgpam">Chrome store</a></strong>.</li>
</ul>
<p>Note: This version of 4chan X does not work with Opera 12. If you need Opera 12 support, try <a href="https://github.com/loadletter/4chan-x">loadletter's fork</a> instead.</p>
</div><input hidden type="checkbox" id="safari-hide"><div><h3 id="safari"><label for="safari-hide">Safari</label></h3>
<p>Install the <a href="https://itunes.apple.com/us/app/userscripts/id1463298887">Userscripts</a> extension. Enable it by pressing <code>⌘,</code>, navigating to the extensions pane and checking <code>Userscripts</code> checkbox. Now open the Userscripts editor by clicking on the <code>&lt;/&gt;</code> button in the taskbar. Then click on the <code>+</code> button and select the <code>New Javascript</code> option. Replace the default text with the contents of the 4chan X <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">script</a></strong>. Finally save it by pressing <code>⌘s</code>.</p>
</div><input hidden type="checkbox" id="webkitgtk-qtwebkit-qtwebengine-hide"><div><h3 id="webkitgtk-qtwebkit-qtwebengine"><label for="webkitgtk-qtwebkit-qtwebengine-hide">WebKitGTK+ / QtWebKit / QtWebEngine</label></h3>
<p>Several minimal browsers have support for userscripts and can run 4chan X. Due to the lack of the cross-site GM_* API, and lack of support for userscripts in iframes, not all features will work. You may experience crashes when repeatedly solving the default image-based captchas. You can avoid this problem by enabling <code>Use Recaptcha v1</code> in your settings.</p>
<ul>
<li>
<p><strong>dwb</strong>: Install the userscripts extension, then save the <a href="https://www.4chan-x.net/builds/4chan-X.user.js">script</a> to the <code>$XDG_CONFIG_HOME/dwb/greasemonkey</code> or <code>$HOME/.config/dwb/greasemonkey</code> directory (creating it if necessary):</p>
<pre><code>dwbem -N -i userscripts
wget -P ${XDG_CONFIG_HOME:-$HOME/.config}/dwb/greasemonkey https://www.4chan-x.net/builds/4chan-X.user.js
</code></pre>
</li>
<li>
<p><strong>Midori</strong>: Enable <code>User addons</code> in your preferences, under the Extensions tab. In the Privacy tab, check <code>Enable HTML5 local storage support</code>. Optionally, if you want 4chan X to be able to open new tabs when you start or reply to a thread, you will need to check <code>Allow scripts to open popups</code> under the Behavior tab. Then click the link to the <a href="https://www.4chan-x.net/builds/4chan-X.user.js">script</a> to install it.</p>
</li>
<li>
<p><strong>Luakit</strong>: Navigate to the <a href="https://www.4chan-x.net/builds/4chan-X.user.js">script</a>, then type the command <code>:usi</code> to install it.</p>
</li>
<li>
<p><strong>uzbl</strong>: Install the script from <a href="https://github.com/singpolyma/singpolyma/blob/master/uzbl/data/scripts/userscript.sh">https://github.com/singpolyma/singpolyma/blob/master/uzbl/data/scripts/userscript.sh</a>, enable it in your config file, and then save <a href="https://www.4chan-x.net/builds/4chan-X.user.js">4chan X</a> to <code>$XDG_DATA_HOME/uzbl/userscripts</code> (or <code>$HOME/.local/share/uzbl/userscripts</code>). The commands below assume you have run uzbl at least once to create its config file.</p>
<pre><code>wget -P &quot;${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts&quot; https://raw.githubusercontent.com/singpolyma/singpolyma/master/uzbl/data/scripts/userscript.sh
chmod +x &quot;${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts/userscript.sh&quot;
echo '@on_event LOAD_COMMIT spawn @scripts_dir/userscript.sh document-start' &gt;&gt; &quot;${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config&quot;
echo '@on_event LOAD_FINISH spawn @scripts_dir/userscript.sh document-end' &gt;&gt; &quot;${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config&quot;
wget -P &quot;${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/userscripts&quot; https://www.4chan-x.net/builds/4chan-X.user.js
</code></pre>
</li>
<li>
<p><strong>qutebrowser</strong>: Save the <a href="https://www.4chan-x.net/builds/4chan-X.user.js">script</a> to the <code>$XDG_DATA_HOME/qutebrowser/greasemonkey</code> or <code>$HOME/.local/share/qutebrowser/greasemonkey</code> directory:</p>
<pre><code>wget -P ${XDG_DATA_HOME:-$HOME/.local/share}/qutebrowser/greasemonkey https://www.4chan-x.net/builds/4chan-X.user.js
</code></pre>
</li>
</ul>
</div><input hidden type="checkbox" id="ms-edge-hide"><div><h3 id="ms-edge"><label for="ms-edge-hide">MS Edge</label></h3>
<p>Install <a href="https://www.microsoft.com/en-us/store/p/tampermonkey/9nblggh5162s">Tampermonkey</a>, then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
</div><input hidden type="checkbox" id="other-browsers-hide"><div><h3 id="other-browsers"><label for="other-browsers-hide">Other browsers</label></h3>
<p>4chan X can be used in some browsers that do not support userscripts using <a href="https://github.com/ccd0/4chan-x-proxy">a local proxy</a>. Not all features will work.</p>
</div><h2 id="beta-version">Beta version</h2>
<p>New features and non-urgent bugfixes are released on the beta channel for further testing before they are moved the stable version. Please <a href="https://github.com/ccd0/4chan-x/issues?q=is%3Aopen+sort%3Aupdated-desc">report</a> any issues you find, and be sure to mention which version you're using. You should back up your settings regularly to prevent them from being lost due to bugs.</p>
<p>To install the <strong>beta</strong> version and get updates whenever there's a new <strong>beta</strong> version:</p>
<ul>
<li><a href="https://www.4chan-x.net/builds/4chan-X-beta.user.js">Install userscript</a> (use with Greasemonkey / Violentmonkey / Tampermonkey / JS Blocker / etc.)</li>
<li><a href="https://www.4chan-x.net/builds/4chan-X-beta.crx">Download Chrome extension</a> (download and drag to <code>chrome://extensions</code>)</li>
</ul>
<p>To install the current <strong>beta</strong> version but get updates from the <strong>stable</strong> channel (for example, if just you want a particular recent feature):</p>
<ul>
<li><a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js">Install userscript</a></li>
<li><a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx">Download Chrome extension</a></li>
</ul>
<h2 id="troubleshooting">Troubleshooting</h2>
<p>If you encounter a bug, try the steps <a href="https://github.com/ccd0/4chan-x/blob/master/CONTRIBUTING.md#reporting-bugs">here</a>, then report it to the <a href="https://github.com/ccd0/4chan-x/issues?q=is%3Aopen+sort%3Aupdated-desc">issue tracker</a>. If the bug seems to be caused by a script update, you can install a old version from the <a href="https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md">changelog</a>.</p>
<script>
function imagePreview() {
this.removeEventListener('mouseover', imagePreview, false);
var img = new Image();
img.src = this.href;
img.alt = 'preview';
var span = document.createElement('span');
span.className = 'hover';
span.appendChild(img);
this.parentNode.insertBefore(span, this.nextSibling);
}
function storeInstall(e) {
if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.button === 0) {
var url = this.href;
chrome.webstore.install(url, function(){}, function(){
location.href = url;
});
e.preventDefault();
}
}
for (var i = 0; i < document.links.length; i++) {
var link = document.links[i];
if (/\.png$/.test(link.pathname) && !link.querySelector('.hover')) {
link.addEventListener('mouseover', imagePreview, false);
} else if (window.chrome && link.host === 'chrome.google.com') {
link.addEventListener('click', storeInstall, false);
}
}
var engine = (function() {
if (/Edge\//.test(navigator.userAgent)) return 'edge';
if (/Chrome\//.test(navigator.userAgent)) return 'blink';
if (/WebKit\//.test(navigator.userAgent)) return 'webkit';
if (/Gecko\/|Goanna/.test(navigator.userAgent)) return 'gecko';
if (/Presto\//.test(navigator.userAgent)) return 'presto';
})();
var engines = {'firefox': 'gecko', 'chromium': 'blink presto', 'safari': 'webkit', 'webkitgtk-qtwebkit-qtwebengine': 'webkit', 'ms-edge': 'edge', 'other-browsers': ''};
if (location.hash.slice(1) in engines) {
for (browser in engines) {
document.getElementById(browser + '-hide').checked = (browser !== location.hash.slice(1));
}
} else if (engine) {
for (browser in engines) {
document.getElementById(browser + '-hide').checked = (engines[browser].indexOf(engine) < 0);
}
}
</script>
</body></html>

View File

@ -130,7 +130,8 @@
"Zixaphir <zixaphirmoxphar@gmail.com>",
"seaweedchan <jtbates@asu.edu>",
"Kabir Sala <spittiepie@gmail.com>",
"ccd0 <admin@containerchan.org>"
"ccd0 <admin@containerchan.org>",
"Tuxedo Takodachi <TuxedoTako@proton.me>"
],
"license": "MIT",
"readmeFilename": "README.md",
@ -140,7 +141,6 @@
"type": "module",
"scripts": {
"build": "node ./tools/rollup",
"build:beta": "node ./tools/rollup -beta",
"build:noupdate": "node ./tools/rollup -noupdate"
"build:min": "node ./tools/rollup -min"
}
}

View File

@ -9,9 +9,7 @@ import QuoteThreading from '../Quotelinks/QuoteThreading';
const RestoreDeletedFromArchive = {
restore() {
console.log(g);
const url = Redirect.to('threadJSON', { boardID: g.boardID, threadID: g.threadID });
console.log(url);
if (!url) {
new Notice('warning', 'No archive found', 3);
return;
@ -19,7 +17,6 @@ const RestoreDeletedFromArchive = {
const encryptionOK = url.startsWith('https://');
if (encryptionOK || Conf['Exempt Archives from Encryption']) {
CrossOrigin.cache(url, function (this: XMLHttpRequest) {
console.log(this);
let nrRestored = 0;
const archivePosts = this.response[g.threadID.toString()].posts as Record<string, RawArchivePost>;
for (const [postID, raw] of Object.entries(archivePosts)) {

View File

@ -161,9 +161,9 @@ var Unread = {
},
addPost() {
if (this.isFetchedQuote || this.isClone || (this.ID <= Unread.lastReadPost)) return;
if (this.isFetchedQuote || this.isClone) return;
Unread.order.push(this);
if (this.isHidden || QuoteYou.isYou(this)) return;
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]);

View File

@ -56,7 +56,7 @@ const Config = {
'Enable reporting posts to supported archives.'
],
'Exempt Archives from Encryption': [
true,
false,
'Permit loading content from, and warningless redirects to, HTTP-only archives from HTTPS pages.'
],
'Keybinds': [

View File

@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default async function generateMetadata(packageJson, channel) {
export default async function generateMetadata(packageJson, channel, fileName, metaFileName) {
const meta = packageJson.meta;
const versionFile = await readFile(resolve(__dirname, '../../version.json'));
@ -82,8 +82,8 @@ export default async function generateMetadata(packageJson, channel) {
if (channel !== '-noupdate') {
output += `
// @updateURL ${meta.downloads}${packageJson.name}${channel}.meta.js
// @downloadURL ${meta.downloads}${packageJson.name}${channel}.user.js`;
// @updateURL ${meta.downloads}/latest/download/${metaFileName}
// @downloadURL ${meta.downloads}/latest/download/${fileName}`;
}
output += `
// @icon data:image/png;base64,${icon}

View File

@ -1,19 +0,0 @@
#!/usr/bin/env python3
import urllib.request, urllib.error, json
banners = []
for ext in ['jpg', 'png', 'gif']:
for i in range(300):
banner = str(i) + '.' + ext
req = urllib.request.Request('http://s.4cdn.org/image/title/' + banner, method='HEAD')
try:
try:
status = urllib.request.urlopen(req).status
except urllib.error.URLError:
status = urllib.request.urlopen(req).status
except urllib.error.HTTPError as e:
status = e.status
print(banner, status)
if status == 200:
banners.append(banner)
with open('src/config/banners.json', 'w') as f:
f.write(json.dumps(banners))

View File

@ -1,26 +0,0 @@
var fs = require('fs');
function bump(version, level) {
var parts = version.split('.');
var i;
for (i = 0; i < level; i++) {
parts[i] = (parts[i] || '0');
}
parts[level-1] = +parts[level-1] + 1;
for (i = level; i < parts.length; i++) {
parts[i] = '0';
}
return parts.join('.');
}
function setversion(version) {
var data = {version: version, date: new Date()};
fs.writeFileSync('version.json', JSON.stringify(data, null, 2));
}
var level = +process.argv[2];
var v = JSON.parse(fs.readFileSync('version.json', 'utf8'));
var oldversion = v.version;
var version = bump(oldversion, level);
setversion(version);
console.log(`Version updated from v${oldversion} to v${version}.`);

View File

@ -1,30 +0,0 @@
var fs = require('fs');
var template = require('./template.js');
var coffee = require('coffeescript');
for (var name of process.argv.slice(2)) {
try {
var parts = name.match(/^tmp\/([^_]*)(?:_(.*))?-(.*)\.(.*)\.js$/);
var sourceName = `src/${parts[1]}/${parts[3]}.${parts[4]}`;
var script = fs.readFileSync(sourceName, 'utf8');
script = script.replace(/\r\n/g, '\n');
script = template(script, {type: parts[2]}, sourceName);
if (parts[4] === 'coffee') {
var definesVar = /^[$A-Z][$\w]*$/.test(parts[3]);
if (definesVar) {
script = `${script}\nreturn ${parts[3]};\n`;
}
script = coffee.compile(script);
if (definesVar) {
script = `${parts[3]} = ${script}`;
}
}
script += '\n';
fs.writeFileSync(name, script);
} catch (err) {
console.error(`Error processing ${name}`);
throw err;
}
}

View File

@ -1,18 +0,0 @@
var fs = require('fs');
var names = [];
for (var d of fs.readdirSync('src')) {
for (var f of fs.readdirSync(`src/${d}`)) {
var m = f.match(/^([$A-Z][$\w]*)\.(?:coffee|js)$/);
if (m) names.push(m[1]);
}
}
var decl = `var ${names.sort().join(', ')};\n`;
var oldDecl;
try {
oldDecl = fs.readFileSync('tmp/declaration.js', 'utf8');
} catch(err) {
}
if (decl !== oldDecl) {
fs.writeFileSync('tmp/declaration.js', decl, 'utf8');
}

View File

@ -1,8 +0,0 @@
var fs = require('fs');
var installMap = JSON.parse(fs.readFileSync('install.json', 'utf8'));
for (var src in installMap) {
for (var dest of installMap[src]) {
fs.writeFileSync(dest, fs.readFileSync(src));
}
}

View File

@ -1,10 +0,0 @@
var fs = require('fs');
var md = require('markdown-it')({linkify: true}).use(require('markdown-it-anchor'), {slugify: s => String(s).trim().toLowerCase().replace(/\W+/g, '-')});
var template = require('lodash.template');
var readme = fs.readFileSync('README.md', 'utf8');
var content = md.render(readme);
var webtemplate = fs.readFileSync('template.jst', 'utf8');
var output = template(webtemplate)({content: content});
output = output.replace(/\r\n/g, '\n');
fs.writeFileSync('test.html', output);

View File

@ -1,5 +0,0 @@
var fs = require('fs');
var text = fs.readFileSync(process.argv[2], 'utf8');
text = text.replace(/\r\n/g, '\n');
fs.writeFileSync(process.argv[3], text);

View File

@ -1,18 +0,0 @@
var fs = require('fs');
var pkg = JSON.parse(fs.readFileSync('package.json'));
var vars = {};
var k;
vars.name = pkg.name;
for (k in pkg.meta) {
vars[`meta_${k}`] = pkg.meta[k];
}
for (k in pkg.devDependencies) {
vars[`version_${k}`] = pkg.devDependencies[k];
}
for (k in vars) {
console.log(`\$(eval ${k} := ${vars[k]})`);
}

View File

@ -25,13 +25,15 @@ const minify = process.argv.includes('-min');
(async () => {
const packageJson = JSON.parse(await readFile(resolve(__dirname, '../package.json'), 'utf-8'));
const metadata = await generateMetadata(packageJson, channel);
const fileName = `${packageJson.meta.path}${channel}${minify ? '.min' : ''}.user.js`;
const metaFileName = `${packageJson.meta.path}${channel}${minify ? '.min' : ''}.meta.js`;
const metadata = await generateMetadata(packageJson, channel, fileName, metaFileName);
const license = await readFile(resolve(__dirname, '../LICENSE'), 'utf8');
const version = JSON.parse(await readFile(resolve(__dirname, '../version.json'), 'utf-8'));
const inlineFile = await setupFileInliner(packageJson);
const bundle = await rollup({
@ -97,7 +99,7 @@ const minify = process.argv.includes('-min');
...sharedBundleOpts,
banner: metadata + license,
// file: '../builds/test/rollupOutput.js',
file: resolve(buildDir, `${packageJson.meta.path}${channel}.user${minify ? '.min' : ''}.js`),
file: resolve(buildDir, fileName),
plugins: minify ? [terser({
format: {
max_line_len: 1000,
@ -107,6 +109,8 @@ const minify = process.argv.includes('-min');
sourcemap: minify,
});
await writeFile(resolve(buildDir, metaFileName), metadata);
// chrome extension
const crxDir = resolve(buildDir, 'crx');
await bundle.write({
@ -117,7 +121,11 @@ const minify = process.argv.includes('-min');
await copyFile(resolve(__dirname, '../src/meta/eventPage.js'), resolve(crxDir, 'eventPage.js'));
writeFile(resolve(crxDir, 'manifest.json'), generateManifestJson(packageJson, version, channel));
await writeFile(
resolve(crxDir, 'manifest.json'),
// There's no auto update for the extension.
generateManifestJson(packageJson, version, channel || '-noupdate'),
);
for (const file of ['icon16.png', 'icon48.png', 'icon128.png']) {
await copyFile(resolve(__dirname, '../src/meta/', file), resolve(crxDir, file));

View File

@ -1,247 +0,0 @@
/* jshint evil: true */
var fs = require('fs');
var path = require('path');
var _template = require('lodash.template');
var esprima = require('esprima');
// disable ES6 delimiters
var _templateSettings = {interpolate: /<%=([\s\S]+?)%>/g};
// Functions used in templates.
var tools = {};
var read = tools.read = filename => fs.readFileSync(filename, 'utf8').replace(/\r\n/g, '\n');
var readJSON = tools.readJSON = filename => JSON.parse(read(filename));
tools.readBase64 = filename => fs.readFileSync(filename).toString('base64');
tools.readHTML = function(filename) {
var text = read(filename).replace(/^ +/gm, '').replace(/\r?\n/g, '');
text = _template(text, _templateSettings)(pkg); // package.json data only; no recursive imports
return tools.html(text);
};
tools.multiline = function(text) {
return text.replace(/\n+/g, '\n').split(/^/m).map(JSON.stringify).join('+').replace(/"\+"/g, '\\\n');
};
// Convert JSONify-able object to Javascript expression.
var constExpression = data => JSON.stringify(data).replace(/`/g, '\\`');
function TextStream(text) {
this.text = text;
}
TextStream.prototype.eat = function(regexp) {
var match = regexp.exec(this.text);
if (match && match.index === 0) {
this.text = this.text.slice(match[0].length);
}
return match;
};
function parseHTMLTemplate(stream, context) {
var template = stream.text; // text from beginning, for error messages
var expression = new HTMLExpression(context);
var match;
try {
while (stream.text) {
// Literal HTML
if ((match = stream.eat(
// characters not indicating start or end of placeholder, using backslash as escape
/^(?:[^\\{}]|\\.)+(?!{)/
))) {
var unescaped = match[0].replace(/\\(.)/g, '$1');
expression.addLiteral(unescaped);
// Placeholder
} else if ((match = stream.eat(
// symbol identifying placeholder type and first argument (enclosed by {})
// backtick not allowed in arguments as it can end embedded JS in Coffeescript
/^([^}]){([^}`]*)}/
))) {
var type = match[1];
var args = [match[2]];
if (type === '?') {
// conditional expression can take up to two subtemplate arguments
for (var i = 0; i < 2 && stream.eat(/^{/); i++) {
var subtemplate = parseHTMLTemplate(stream, context);
args.push(subtemplate);
if (!stream.eat(/^}/)) {
throw new Error(`Unexpected characters in subtemplate (${stream.text})`);
}
}
}
expression.addPlaceholder(new Placeholder(type, args));
// No match: end of subtemplate (} next) or error
} else {
break;
}
}
return expression.build();
} catch(err) {
throw new Error(`${err.message}: ${template}`);
}
}
function HTMLExpression(context) {
this.parts = [];
this.startContext = this.endContext = (context || '');
}
HTMLExpression.prototype.addLiteral = function(text) {
this.parts.push(constExpression(text));
this.endContext = (
this.endContext
.replace(/(=['"])[^'"<>]*/g, '$1') // remove values from quoted attributes (no '"<> allowed)
.replace(/(<\w+)( [\w-]+((?=[ >])|=''|=""))*/g, '$1') // remove attributes from tags
.replace(/^([^'"<>]+|<\/?\w+>)*/, '') // remove text (no '"<> allowed) and tags
);
};
HTMLExpression.prototype.addPlaceholder = function(placeholder) {
if (!placeholder.allowed(this.endContext)) {
throw new Error(`Illegal insertion of placeholder (type ${placeholder.type}) into HTML template (at ${this.endContext})`);
}
this.parts.push(placeholder.build());
};
HTMLExpression.prototype.build = function() {
if (this.startContext !== this.endContext) {
throw new Error(`HTML template is ill-formed (at ${this.endContext})`);
}
return (this.parts.length === 0 ? '""' : this.parts.join(' + '));
};
function Placeholder(type, args) {
this.type = type;
this.args = args;
}
Placeholder.prototype.allowed = function(context) {
switch(this.type) {
case '$':
// escaped text allowed outside tags or in quoted attributes
return (context === '' || /\=['"]$/.test(context));
case '&':
case '@':
// contents of one/many HTML element or template allowed outside tags only
return (context === '');
case '?':
// conditionals allowed anywhere so long as their contents don't change context (checked by HTMLExpression.prototype.build)
return true;
}
throw new Error(`Unrecognized placeholder type (${this.type})`);
};
Placeholder.prototype.build = function() {
// first argument is always JS expression; validate it so we don't accidentally break out of placeholders
var expr = this.args[0];
var ast;
try {
ast = esprima.parse(expr);
} catch (err) {
throw new Error(`Invalid JavaScript in template (${expr})`);
}
if (!(ast.type === 'Program' && ast.body.length == 1 && ast.body[0].type === 'ExpressionStatement')) {
throw new Error(`JavaScript in template is not an expression (${expr})`);
}
switch(this.type) {
case '$': return `E(${expr})`; // $ : escaped text
case '&': return `(${expr}).innerHTML`; // & : contents of HTML element or template (of form {innerHTML: "safeHTML"})
case '@': return `E.cat(${expr})`; // @ : contents of array of HTML elements or templates (see src/General/Globals.coffee for E.cat)
case '?':
return `((${expr}) ? ${this.args[1] || '""'} : ${this.args[2] || '""'})`; // ? : conditional expression
}
throw new Error(`Unrecognized placeholder type (${this.type})`);
};
// HTML template generator with placeholders of forms ${}, &{}, @{}, and ?{}{}{} (see Placeholder.prototype.build)
// that checks safety of generated expressions at compile time.
tools.html = function(template) {
var stream = new TextStream(template);
var output = parseHTMLTemplate(stream);
if (stream.text) {
throw new Error(`Unexpected characters in template (${stream.text}): ${template}`);
}
return `{innerHTML: ${output}}`;
};
function includesDir(templateName) {
var dir = path.dirname(templateName);
var subdir = path.basename(templateName).replace(/\.[^.]+$/, '');
if (fs.readdirSync(dir).indexOf(subdir) >= 0) {
return path.join(dir, subdir);
} else {
return dir;
}
}
function resolvePath(includeName, templateName) {
var dir;
if (includeName[0] === '/') {
dir = process.cwd();
} else {
dir = includesDir(templateName);
}
return path.join(dir, includeName);
}
function wrapTool(tool, templateName) {
return function(includeName) {
return tool(resolvePath(includeName, templateName));
};
}
function loadModules(templateName) {
var dir = includesDir(templateName);
var moduleNames = fs.readdirSync(dir).filter(f => /\.inc$/.test(f));
var modules = {};
for (var name of moduleNames) {
var code = read(path.join(dir, name));
modules[name.replace(/\.inc$/, '')] = new Function(code)();
}
return modules;
}
// Import variables from package.json.
var pkg = readJSON('package.json');
function interpolate(text, data, filename) {
var context = {}, key;
for (key in tools) {
context[key] = /^read/.test(key) ? wrapTool(tools[key], filename) : tools[key];
}
for (key in pkg) {
context[key] = pkg[key];
}
if (data) {
for (key in data) {
context[key] = data[key];
}
}
context.files = fs.readdirSync(includesDir(filename));
context.require = loadModules(filename);
return _template(text, _templateSettings)(context);
}
module.exports = interpolate;
if (require.main === module) {
(function() {
// Take variables from command line.
var data = {};
for (var i = 4; i < process.argv.length; i++) {
var m = process.argv[i].match(/(.*?)=(.*)/);
data[m[1]] = m[2];
}
var text = read(process.argv[2]);
text = interpolate(text, data, process.argv[2]);
fs.writeFileSync(process.argv[3], text);
})();
}

View File

@ -1,40 +0,0 @@
var fs = require('fs');
var child_process = require('child_process');
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
var v = JSON.parse(fs.readFileSync('version.json', 'utf8'));
var name = pkg.name;
var oldVersions = pkg.meta.oldVersions;
var version = v.version;
var date = v.date;
var branch = version.replace(/\.\d+$/, '');
var headerLevel = branch.replace(/(\.0)*$/, '').split('.').length;
var headerPrefix = new Array(headerLevel + 1).join('#');
var separator = `${headerPrefix} v${branch}`;
var today = date.split('T')[0];
var filename = `/builds/${name}-noupdate`;
var ffLink = `${oldVersions}${version}${filename}.user.js`;
var crLink = `${oldVersions}${version}${filename}.crx`;
var line = `**v${version}** *(${today})* - [[Userscript](${ffLink})] [[Chrome extension](${crLink})]`;
var changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
var breakPos = changelog.indexOf(separator);
if (breakPos >= 0) {
breakPos += separator.length;
} else {
breakPos = Math.max(changelog.indexOf('\n\n#'), 0);
line = `${separator}\n\n${line}`;
}
var prevVersion = changelog.substr(breakPos).match(/\*\*v([\d\.]+)\*\*/)[1];
if (prevVersion.replace(/\.\d+$/, '') !== branch) {
line += `\n- Based on v${prevVersion}.`;
}
line += '\n- ' + child_process.execSync(`git log --pretty=format:%s ${prevVersion}..HEAD`).toString().replace(/\n/g, '\n- ');
fs.writeFileSync('CHANGELOG.md', `${changelog.substr(0, breakPos)}\n\n${line}${changelog.substr(breakPos)}`, 'utf8');
console.log(`Changelog updated for v${version}.`);

View File

@ -1,38 +0,0 @@
var fs = require('fs');
var child_process = require('child_process');
var request = require('request');
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
var v = JSON.parse(child_process.execSync('git show stable:version.json').toString());
var secrets = JSON.parse(fs.readFileSync(`../${pkg.meta.path}.keys/chrome-store.json`, 'utf8'));
var refresh = JSON.parse(fs.readFileSync(`../${pkg.meta.path}.keys/refresh-token.json`, 'utf8'));
import('chrome-webstore-upload').then(chromeWebstoreUpload => {
var webStore = chromeWebstoreUpload.default({
extensionId: pkg.meta.chromeStoreID,
clientId: secrets.installed.client_id,
clientSecret: secrets.installed.client_secret,
refreshToken: refresh.refresh_token
});
request(`https://chrome.google.com/webstore/detail/${pkg.meta.chromeStoreID}`, function (error, response, body) {
if (body && body.indexOf(`<meta itemprop="version" content="${v.version}"/>`) > 0 && process.argv[2] !== 'force') {
console.log(`Version ${v.version} already uploaded.`);
return;
}
var myZipFile = fs.createReadStream(`dist/builds/${pkg.name}.zip`);
var token;
webStore.fetchToken().then(t => {
token = t;
return webStore.uploadExisting(myZipFile, token);
}).then(() =>
webStore.publish()
).catch(res => {
console.error(res);
process.exit(1);
});
});
});

View File

@ -1,24 +0,0 @@
var fs = require('fs');
var JSZip = require('jszip');
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
var v = JSON.parse(fs.readFileSync('version.json', 'utf8'));
var channel = process.argv[2] || '';
var zip = new JSZip();
for (var file of ['script.js', 'eventPage.js', 'icon16.png', 'icon48.png', 'icon128.png', 'manifest.json']) {
zip.file(
file,
fs.readFileSync(`testbuilds/crx${channel}/${file}`),
{date: new Date(v.date)}
);
}
zip.generateAsync({
type: 'nodebuffer',
compression: 'DEFLATE',
compressionOptions: {level: 9},
}).then(function(output) {
fs.writeFileSync(`testbuilds/${pkg.name}${channel}.crx.zip`, output);
}, function() {
process.exit(1);
});

View File

@ -1,4 +1,4 @@
{
"version": "XT 2.2.0",
"date": "2023-10-27T13:58:44.136Z"
"version": "XT 2.2.1",
"date": "2023-10-28T17:56:24.449Z"
}