Merge branch 'master' into palemoon

This commit is contained in:
ccd0 2015-08-20 00:57:49 -07:00
commit 50bbb9010f
69 changed files with 6741 additions and 5407 deletions

View File

@ -1,9 +1,251 @@
**Note**: Installing the script from one of the links below will disable automatic updates. If you want automatic updates, install the script from the links on the [main page](https://www.4chan-x.net/).
Sometimes the changelog has notes (not comprehensive) acknowledging people's work. This does not mean the changes are their fault, only that their code was used. All changes to the script are chosen by and the fault of the maintainer (ccd0).
The links to individual versions below are to copies of the script with the update URL removed. If you want automatic updates, install the script from the links on the [main page](https://github.com/ccd0/4chan-x).
### v1.11.9
**v1.11.9.2** *(2015-08-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.11.8.9: Fix regression from v1.11.8.0 that caused the cooldown to stop working in certain circumstances.
**v1.11.9.1** *(2015-08-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Add more info and a reporting link to error messages.
**v1.11.9.0** *(2015-08-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.8.8.
- Add `Randomize Filename` option: Replaces filenames with a random timestamp from the past year.
- Fix bugs with cached captchas when you change captcha settings.
### v1.11.8
**v1.11.8.9** *(2015-08-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.9/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.9/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix regression from v1.11.8.0 that caused the cooldown to stop working in certain circumstances.
**v1.11.8.8** *(2015-08-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.8/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.8/builds/4chan-X-noupdate.crx "Chromium version")]
- Add link to new anonymous bug reporting form: https://gitreports.com/issue/ccd0/4chan-x
**v1.11.8.7** *(2015-08-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.7/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.7/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix captcha issues in Pale Moon in some cases where it was still not working.
**v1.11.8.6** *(2015-08-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.6/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.6/builds/4chan-X-noupdate.crx "Chromium version")]
- Add .xyz to TLDs recognized by linkifier without http://.
**v1.11.8.5** *(2015-08-12)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.5/builds/4chan-X-noupdate.crx "Chromium version")]
- Change script home page and update URLs to the new https://www.4chan-x.net/ site.
**v1.11.8.4** *(2015-08-10)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.4/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.11.7.5: Fix captcha issues in Pale Moon.
**v1.11.8.3** *(2015-08-09)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.3/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix `Show Updated Notifications` setting.
**v1.11.8.2** *(2015-08-08)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix bug in infinite scrolling causing skipped pages.
**v1.11.8.1** *(2015-08-08)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.11.7.4: Fix cooldown bug.
**v1.11.8.0** *(2015-08-08)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.7.3.
- Posts selected for deletion will now be auto-deleted when 4chan's 60-second deletion cooldown expires.
- The setting `Except Archives from Encryption` has been changed to `Exempt Archives from Encryption`.
- Bug fixes.
### v1.11.7
**v1.11.7.5** *(2015-08-10)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.5/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix captcha issues in Pale Moon.
**v1.11.7.4** *(2015-08-08)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.4/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix cooldown bug.
**v1.11.7.3** *(2015-08-06)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.3/builds/4chan-X-noupdate.crx "Chromium version")]
- Turn `Custom Board Navigation` back on by default for now until we support better migration of old settings.
**v1.11.7.2** *(2015-08-05)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.11.6.2: Fix bug where clicking on scrollbar of captcha image selection bubble could mess up the captcha.
- Redirect threads deleted but with stubs left behind to archive.
**v1.11.7.1** *(2015-08-02)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.11.6.1: Update banner list.
**v1.11.7.0** *(2015-08-02)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.6.0.
- Turn `Custom Board Navigation` off by default. To turn it back on, check `Header` > `Custom board navigation` in the header menu.
- Experimental MS Edge support via https://github.com/ccd0/4chan-x-proxy.
### v1.11.6
**v1.11.6.2** *(2015-08-05)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix bug where clicking on scrollbar of captcha image selection bubble could mess up the captcha.
**v1.11.6.1** *(2015-08-02)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Update banner list.
**v1.11.6.0** *(2015-07-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.5.2.
- Implement 404 Redirect for `sys.4chan.org/board/imgboard.php?res=` URLs.
- Index navigation bugfixes.
### v1.11.5
**v1.11.5.2** *(2015-07-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Update `Disable Autoplaying Sounds` so the video in https://boards.4chan.org/g/thread/49036627 is visible.
- Tweak position of expanded images.
**v1.11.5.1** *(2015-07-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Bugfixes.
**v1.11.5.0** *(2015-07-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.4.1.
- When posting a reply with a file on /f/, add a link to it in the comment.
### v1.11.4
**v1.11.4.1** *(2015-07-13)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.4.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.4.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.11.3.5: Fix video not being displayed when 'Disable Autoplaying Sounds' is enabled.
**v1.11.4.0** *(2015-07-12)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.4.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.4.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.3.4.
- Show files in replies on /f/.
- Remove code that disabled the updater if you were offline since detection was too unreliable.
### v1.11.3
**v1.11.3.5** *(2015-07-13)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.5/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix video not being displayed when 'Disable Autoplaying Sounds' is enabled.
**v1.11.3.4** *(2015-07-12)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.4/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix captcha in original post form obscuring file field for some users.
- Turn `Ignore Offline Status` on by default.
**v1.11.3.3** *(2015-07-07)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.3/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.11.2.5: Add fireden.net archive.
**v1.11.3.2** *(2015-07-05)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Get posting from URLs, WebM titles, and Vocaroo/Clyp embedding working in Safari.
**v1.11.3.1** *(2015-07-04)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Make posting from URL more efficient, and make it work in Tampermonkey.
**v1.11.3.0** *(2015-07-04)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.2.4.
- Improved Tampermonkey and WebKit support.
- Minor bugfixes.
### v1.11.2
**v1.11.2.5** *(2015-07-07)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.5/builds/4chan-X-noupdate.crx "Chromium version")]
- Add fireden.net archive.
**v1.11.2.4** *(2015-07-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.4/builds/4chan-X-noupdate.crx "Chromium version")]
- Minor bugfixes.
**v1.11.2.3** *(2015-06-30)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.3/builds/4chan-X-noupdate.crx "Chromium version")]
- Add 'webkit' CSS class to document when WebKit engine is detected.
- Various CSS-related bugfixes.
**v1.11.2.2** *(2015-06-30)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix bug from v1.11.2.0 that broke `Use Recaptcha v1` when the original post form was hidden.
**v1.11.2.1** *(2015-06-30)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.1/builds/4chan-X-noupdate.crx "Chromium version")]
- CSS bugfixes for thread watcher and header.
**v1.11.2.0** *(2015-06-29)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.1.3.
- `Use Recaptcha v1` option now works on the original post form.
- Captcha section of QR can now be opened with the space bar.
- Minor bugfixes.
### v1.11.1
**v1.11.1.3** *(2015-06-26)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.3/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.11.0.10: (Hasumi) Update archive list.
**v1.11.1.2** *(2015-06-23)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Improved compatibility: Fix some issues in dwb and Midori; add partial support for Luakit.
**v1.11.1.1** *(2015-06-22)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Add `Use Recaptcha v2 in Reports` option to use the image selection captcha in the report window.
**v1.11.1.0** *(2015-06-22)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.11.0.9.
- Add partial support for Midori and other browsers whose userscript engines don't implement the Greasemonkey API. Some features will not work in these browsers.
## v1.11.0
**v1.11.0.10** *(2015-06-25)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.10/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.10/builds/4chan-X-noupdate.crx "Chromium version")]
- (Hasumi) Update archive list.
**v1.11.0.9** *(2015-06-21)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.9/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.9/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix caching of v1 captchas.
- Other minor bugfixes.
**v1.11.0.8** *(2015-06-21)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.8/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.8/builds/4chan-X-noupdate.crx "Chromium version")]
- Support noscript version of Recaptcha v1.
- Captcha-related bugfixes/improvements.
**v1.11.0.7** *(2015-06-21)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.7/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.7/builds/4chan-X-noupdate.crx "Chromium version")]
- Add `Use Recaptcha v1` option to use the old text-based captchas in the Quick Reply.
**v1.11.0.6** *(2015-06-20)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.6/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.6/builds/4chan-X-noupdate.crx "Chromium version")]
- Support toggling images in the captcha with the number keys (as arranged in the numpad) and the UIOJKLM,. keys.
- Arrow key navigation now works in noscript captcha.
- Various captcha-related improvements/bugfixes.
- Support space bar, numbers in numpad, comma, and space in keybinds.
**v1.11.0.5** *(2015-06-20)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.5/builds/4chan-X-noupdate.crx "Chromium version")]
- Add `Captcha Language` setting in the `Advanced` panel.
- Minor bugfixes.
**v1.11.0.4** *(2015-06-20)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.4/builds/4chan-X-noupdate.crx "Chromium version")]
- Noscript captcha improvements, including the ability to click the image rather than the little checkbox.
**v1.11.0.3** *(2015-06-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.3/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.10.14.4: Update script for new non-Javascript captcha using image selection.
**v1.11.0.2** *(2015-06-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Remove code that focused on first image of the captcha as Google now focuses on the refresh button.
**v1.11.0.1** *(2015-06-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.10.14.3: Fix words being cut off in non-Javascript captcha.
**v1.11.0.0** *(2015-06-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.10.14.2.
- Gallery now preloads the next image in sequence.
- `Stretch to Fit` option added to gallery.
- Various bug fixes.
- Drop workarounds for old versions of Chromium (< v34).
### v1.10.14
**v1.10.14.4** *(2015-06-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.4/builds/4chan-X-noupdate.crx "Chromium version")]
- Update script for new non-Javascript captcha using image selection.
**v1.10.14.3** *(2015-06-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.3/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix words being cut off in non-Javascript captcha.
**v1.10.14.2** *(2015-06-11)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Bring back workaround for scrolling to top caused by captcha as some users are still reporting it happening.
**v1.10.14.1** *(2015-06-11)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Add ImgOps to default sauce examples: `//imgops.com/%URL;types:gif,jpg,png`
**v1.10.14.0** *(2015-06-07)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.0/builds/4chan-X-noupdate.crx "Chromium version")]
- Based on v1.10.13.4.
- Fix board-specific filters in thread watcher (filters apply to unread posts quoting you).
### v1.10.13
**v1.10.13.4** *(2015-06-05)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.4/builds/4chan-X-noupdate.crx "Chromium version")]
- Support selecting images in the image captcha with the space bar in addition to the enter key.
**v1.10.13.3** *(2015-06-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.3/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.10.12.10: Revert workaround for scrolling to top as it seems to have been fixed on Google's end.
**v1.10.13.2** *(2015-06-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.2/builds/4chan-X-noupdate.crx "Chromium version")]
- Merge v1.10.12.9: Update for captcha changes.
- Merge v1.10.12.9: Work around issue where the captcha causes scrolling to the top of the page.
**v1.10.13.1** *(2015-05-27)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.1/builds/4chan-X-noupdate.crx "Chromium version")]
- Fix manual page number updating not working when `Updater and Stats in Header` is off.
@ -20,6 +262,13 @@ The links to individual versions below are to copies of the script with the upda
### v1.10.12
**v1.10.12.10** *(2015-06-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.10/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.10/builds/4chan-X-noupdate.crx "Chromium version")]
- Revert workaround for scrolling to top as it seems to have been fixed on Google's end.
**v1.10.12.9** *(2015-06-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.9/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.9/builds/4chan-X-noupdate.crx "Chromium version")]
- Update for captcha changes.
- Work around issue where the captcha causes scrolling to the top of the page.
**v1.10.12.8** *(2015-05-22)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.8/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.8/builds/4chan-X-noupdate.crx "Chromium version")]
- Remove guard against dropping files into captcha due to continued reports of disabled captchas.

View File

@ -1,6 +1,10 @@
## Reporting bugs
If I can't reproduce a bug, I probably won't be able to fix it. You can help by doing the following when you [report a bug](https://github.com/ccd0/4chan-x/issues):
Bug reports and feature requests for 4chan X are tracked at **https://github.com/ccd0/4chan-x/issues**.
You can submit a bug report / feature request either via your Github account or the [anonymous report form](https://gitreports.com/issue/ccd0/4chan-x).
If you're reporting a bug, the more detail you can give, the better. If I can't reproduce your bug, I probably won't be able to fix it. You can help by doing the following:
1. Include precise steps to reproduce the problem, with the expected and actual results.
2. Make sure your **browser**, **4chan X**, and (if applicable) **Greasemonkey** are up to date. Include the versions you're using in bug reports.
@ -14,10 +18,10 @@ If I can't reproduce a bug, I probably won't be able to fix it. You can help by
### Get started
- Install [node.js](http://nodejs.org/).
- Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`.
- Clone 4chan X.
- `cd` into it.
- Install/Update 4chan X dependencies with `npm install`.
- Install [Grunt's CLI](http://gruntjs.com/): `npm install -g grunt-cli`
- Clone 4chan X: `git clone https://github.com/ccd0/4chan-x.git`
- Open the directory: `cd 4chan-x`
- Install/Update 4chan X dependencies: `npm install`
### Build
@ -29,6 +33,9 @@ If I can't reproduce a bug, I probably won't be able to fix it. You can help by
- Edit the sources (not the compiled scripts in the builds/ directory).
- Compile the script with `grunt`.
- Install the compiled script (found in the testbuilds/ directory), and test your changes.
- Open a pull request.
- 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 via Github's mechanism.
- Push your changes to any online Git repository, and [open an issue](https://gitreports.com/issue/ccd0/4chan-x) 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 like https://jii.moe/. Then [open an issue](https://gitreports.com/issue/ccd0/4chan-x) with an explanation of your changes and the URL of the file.
Archive list updates should go to https://github.com/MayhemYDG/archives.json.

View File

@ -8,6 +8,12 @@ module.exports = (grunt) ->
json = (data) ->
"`#{JSON.stringify(data).replace(/`/g, '\\`')}`"
importCSS = (filenames...) ->
grunt.template.process(
filenames.map((name) -> grunt.file.read "src/General/css/#{name}.css").join(''),
{data: grunt.config 'pkg'}
).trim().replace(/\n+/g, '\n').split(/^/m).map(JSON.stringify).join(' +\n').replace(/`/g, '\\`')
importHTML = (filename) ->
html grunt.template.process(grunt.file.read("src/General/html/#{filename}.html").replace(/^ +/gm, '').replace(/\r?\n/g, ''), data: grunt.config('pkg'))
@ -69,6 +75,7 @@ module.exports = (grunt) ->
options: process: Object.create(null, data:
get: ->
pkg = grunt.config 'pkg'
pkg.importCSS = importCSS
pkg.importHTML = importHTML
pkg.html = html
pkg.assert = assert
@ -166,8 +173,6 @@ module.exports = (grunt) ->
stdout: true
stderr: true
failOnError: true
checkout:
command: 'git checkout <%= pkg.meta.mainBranch %>'
commit:
command: """
git commit -am "Release <%= pkg.meta.name %> v<%= pkg.meta.version %>."
@ -185,12 +190,12 @@ module.exports = (grunt) ->
""".split('\n').join('&&')
stable:
command: """
git push . HEAD:bstable
git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.meta.version %>."
git checkout gh-pages
git pull
git merge --no-commit -s ours stable
git checkout stable builds
git checkout HEAD "builds/*<%= pkg.meta.suffix.beta %>.*"
git checkout stable "builds/<%= pkg.name %>.*" builds/updates.xml
git commit -am "Move <%= pkg.meta.name %> v<%= pkg.meta.version %> to stable channel."
git checkout -
""".split('\n').join('&&')
@ -201,12 +206,24 @@ module.exports = (grunt) ->
git checkout gh-pages
git pull
git merge --no-commit -s ours -
git checkout - README.md index.html web.css img src/General/img/icon.gif
git checkout - README.md index.html web.css img
git commit -am "Update web page."
git checkout -
""".split('\n').join('&&')
push:
command: 'git push origin --tags -f && git push origin --all'
aws:
command: """
git checkout gh-pages
aws s3 cp builds/ s3://<%= pkg.meta.awsBucket %>/builds/ --recursive --exclude "*" --include "*.js" --cache-control "max-age=600" --content-type "application/javascript; charset=utf-8"
aws s3 cp builds/ s3://<%= pkg.meta.awsBucket %>/builds/ --recursive --exclude "*" --include "*.crx" --cache-control "max-age=600" --content-type "application/x-chrome-extension"
aws s3 cp builds/ s3://<%= pkg.meta.awsBucket %>/builds/ --recursive --exclude "*" --include "*.xml" --cache-control "max-age=600" --content-type "text/xml; charset=utf-8"
aws s3 cp builds/ s3://<%= pkg.meta.awsBucket %>/builds/ --recursive --exclude "*" --include "*.zip" --cache-control "max-age=600" --content-type "application/zip"
aws s3 cp img/ s3://<%= pkg.meta.awsBucket %>/img/ --recursive --cache-control "max-age=600"
aws s3 cp index.html s3://<%= pkg.meta.awsBucket %> --cache-control "max-age=600" --content-type "text/html; charset=utf-8"
aws s3 cp web.css s3://<%= pkg.meta.awsBucket %> --cache-control "max-age=600" --content-type "text/css; charset=utf-8"
git checkout -
""".split('\n').join('&&')
npm:
command: 'npm install'
update:
@ -282,9 +299,10 @@ module.exports = (grunt) ->
GM_setValue: true
GM_deleteValue: true
GM_listValues: true
GM_addValueChangeListener: true
GM_openInTab: true
GM_info: true
GM_xmlhttpRequest: true
GM_info: true
cloneInto: true
unsafeWindow: true
chrome: true
@ -432,6 +450,10 @@ module.exports = (grunt) ->
'shell:push'
]
grunt.registerTask 'aws', [
'shell:aws'
]
grunt.registerTask 'store', [
'webstore_upload'
]

View File

@ -1,4 +1,4 @@
![screenshot](https://ccd0.github.io/4chan-x/img/screenshot.png)
![screenshot](https://www.4chan-x.net/img/screenshot.png)
# 4chan X
Adds various features to 4chan.
Previously developed by [aeosynth](https://github.com/aeosynth/4chan-x), [Mayhem](https://github.com/MayhemYDG/4chan-x), [ihavenoface](https://github.com/ihavenoface/4chan-x), [Zixaphir](https://github.com/zixaphir/appchan-x), [Seaweed](https://github.com/seaweedchan/4chan-x), and [Spittie](https://github.com/Spittie/4chan-x), with contributions from many others.
@ -6,47 +6,71 @@ Previously developed by [aeosynth](https://github.com/aeosynth/4chan-x), [Mayhem
If you're looking for a maintained fork of OneeChan (a style script used in addition to 4chan X), try
https://github.com/Nebukazar/OneeChan.
## Firefox version: [Click to Install](https://ccd0.github.io/4chan-x/builds/4chan-X.user.js)
Install [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/), then click the link above to install 4chan X. If you're using a fork of Firefox (e.g. Pale Moon), you may need to use [Greasemonkey 1.15](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/versions/#version-1.15) instead of the most recent version.
**Note**: 4chan X disables the native extension, so if you uninstall 4chan X, you'll need to re-enable it. To do this, click the `[Settings]` link in the top right corner and uncheck "`Disable the native extension`" in the panel that appears.
**WARNING**:
If you're switching to this fork from someone else's fork of 4chan X, back up your old script before installing this one as the old one may be overwritten.
## Install
**Known issues**:
Greasemonkey 3.0 has a [bug](https://github.com/greasemonkey/greasemonkey/issues/2094) causing 4chan X to open multiple tabs when you open a new tab (for example, when starting a thread). If you're having this problem, [updating](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/) to Greasemonkey 3.1 or later should fix it.
### Firefox
Install [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
## Chromium version: [Click to Install](https://ccd0.github.io/4chan-x/builds/4chan-X.crx)
Download the file from the link above and add drag it to your `chrome://extensions` page.
This should also work for non-Windows/dev/canary Chrome and Chromium-based versions of Opera.
- **Pale Moon** users should use [Greasemonkey 1.15](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/versions/1.15.1-signed).
- **SeaMonkey** users should use Greasemonkey 2.3 converted with [this tool](http://addonconverter.fotokraina.com/?url=https://addons.mozilla.org/firefox/downloads/file/282084/greasemonkey-2.3-fx.xpi).
**The above will not work in Chrome (stable or beta) users on Windows; you must install from the [Chrome store](https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam).**
### Chromium
4chan X is available as a Chrome extension. The Chrome extension has the additional feature of being able to sync your settings and data with other devices via Chrome Sync.
## Chromium version (Chrome store): [Click to Install](https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam)
The stable and beta releases of Chrome on Windows will disable extensions not installed from the Chrome store, so users will need to install 4chan X from the link above.
Only the latest stable version of 4chan X is available.
- **Chromium**: **[Download 4chan X](https://www.4chan-x.net/builds/4chan-X.crx)**, then open `chrome://extensions` and drag the downloaded file onto the page. Alternatively, you can install 4chan X from the **[Chrome store](https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam)**.
- **Opera**: **[Click to install 4chan X](https://www.4chan-x.net/builds/4chan-X.crx)**, then follow the prompts to activate it in your extension manager. Note: This version does not work with Opera 12; try [loadletter's fork](https://github.com/loadletter/4chan-x) instead.
- **Chrome**, **Vivaldi**: Install 4chan X from the **[Chrome store](https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam)**.
## Other browsers
This fork of 4chan X is not guaranteed to work correctly in other browsers, but you are welcome to try your luck. Pull requests to fix the bugs you will likely find are always welcome. You may fare better with [loadletter's fork](https://github.com/loadletter/4chan-x), which has fewer features but less dependence on browser-specific APIs.
You can also use the [userscript version of 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js) with [Tampermonkey](https://tampermonkey.net/).
- Some people have reported success in Safari using [JS Blocker](http://jsblocker.toggleable.com/) to install the Firefox/Greasemonkey version.
- Instructions are available for [installing 4chan X in dwb](https://github.com/ccd0/4chan-x/wiki/Installing-4chan-X-in-dwb).
### Safari
Install [JS Blocker](http://jsblocker.toggleable.com/), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
### WebKitGTK+
Several WebKitGTK+ based 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 `Use Recaptcha v1` in your settings.
- **dwb**: Install the userscripts extension, then save the [script](https://www.4chan-x.net/builds/4chan-X.user.js) to the `$XDG_CONFIG_HOME/dwb/greasemonkey` or `$HOME/.config/dwb/greasemonkey` directory (creating it if necessary):
dwbem -N -i userscripts
wget -P ${XDG_CONFIG_HOME:-$HOME/.config}/dwb/greasemonkey https://www.4chan-x.net/builds/4chan-X.user.js
- **Midori**: Enable `User addons` in your preferences, under the Extensions tab. In the Privacy tab, check `Enable HTML5 local storage support`. 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 `Allow scripts to open popups` under the Behavior tab. Then click the link to the [script](https://www.4chan-x.net/builds/4chan-X.user.js) to install it.
- **Luakit**: Navigate to the [script](https://www.4chan-x.net/builds/4chan-X.user.js), then type the command `:usi` to install it.
- **uzbl**: Install the script from https://github.com/singpolyma/singpolyma/blob/master/uzbl/data/scripts/userscript.sh, enable it in your config file, and then save [4chan X](https://www.4chan-x.net/builds/4chan-X.user.js) to `$XDG_DATA_HOME/uzbl/userscripts` (or `$HOME/.local/share/uzbl/userscripts`).
wget -P ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts https://raw.githubusercontent.com/singpolyma/singpolyma/master/uzbl/data/scripts/userscript.sh
chmod +x ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts/userscript.sh
echo '@on_event LOAD_COMMIT spawn @scripts_dir/userscript.sh document-start' >> ${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config
echo '@on_event LOAD_FINISH spawn @scripts_dir/userscript.sh document-end' >> ${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config
wget -P ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/userscripts https://www.4chan-x.net/builds/4chan-X.user.js
### Other browsers
4chan X can be used in some browsers that do not support userscripts, such as **Microsoft Edge**, using [a local proxy](https://github.com/ccd0/4chan-x-proxy). Not all features will work.
## Beta version
New features and non-urgent bugfixes are released on the beta channel for further testing before they are moved the stable version. Please [report](https://github.com/ccd0/4chan-x/issues) 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.
- [Firefox version](https://ccd0.github.io/4chan-x/builds/4chan-X-beta.user.js)
- [Chromium version](https://ccd0.github.io/4chan-x/builds/4chan-X-beta.crx)
If you want to install the current beta version but get updates from the stable channel after that, install it from [here](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js) for Firefox or [here](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx) for Chromium.
To install the current **beta** version but get updates from the **stable** channel (recommended if you want a particular recent feature):
- [Install userscript](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js) (use with Greasemonkey / Tampermonkey / JS Blocker / etc.)
- [Download Chrome extension](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx) (download and drag to `chrome://extensions`)
To install the **beta** version and get updates whenever there's a new **beta** version:
- [Install userscript](https://www.4chan-x.net/builds/4chan-X-beta.user.js)
- [Download Chrome extension](https://www.4chan-x.net/builds/4chan-X-beta.crx)
## Security note
4chan X currently shares your settings and post history between the HTTP and HTTPS versions of 4chan. If you are concerned about protecting your privacy against a man-in-the-middle attack, you should disable 4chan X on the HTTP version of 4chan and/or install [HTTPS Everywhere](https://www.eff.org/https-everywhere).
## Uninstalling
4chan X disables the native extension, so if you uninstall 4chan X, you'll need to re-enable it. To do this, click the `[Settings]` link in the top right corner and uncheck "`Disable the native extension`" in the panel that appears.
## Troubleshooting
If you encounter a bug, try the steps [here](https://github.com/ccd0/4chan-x/blob/master/CONTRIBUTING.md#reporting-bugs), then report it to the [issue tracker](https://github.com/ccd0/4chan-x/issues). You can report bugs without a Github account via [this form](https://gitreports.com/issue/ccd0/4chan-x). If the bug seems to be caused by a script update, you can install a old version from the [changelog](https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md).
## More information
- [Changelog](https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md)
- [Frequently Asked Questions](https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions)
- [Report Bugs](https://github.com/ccd0/4chan-x/issues)
- [Report Bugs](https://gitreports.com/issue/ccd0/4chan-x)
- [Contributing](https://github.com/ccd0/4chan-x/blob/master/CONTRIBUTING.md)

Binary file not shown.

View File

@ -1,26 +1,38 @@
// ==UserScript==
// @name 4chan X beta
// @version 1.10.13.1
// @version 1.11.9.2
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
// @description Cross-browser userscript for maximum lurking on 4chan.
// @license MIT; https://github.com/ccd0/4chan-x/blob/master/LICENSE
// @match *://boards.4chan.org/*
// @match *://sys.4chan.org/*
// @match *://a.4cdn.org/*
// @match *://i.4cdn.org/*
// @match https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @match https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @match *://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc
// @include http://boards.4chan.org/*
// @include https://boards.4chan.org/*
// @include http://sys.4chan.org/*
// @include https://sys.4chan.org/*
// @include http://a.4cdn.org/*
// @include https://a.4cdn.org/*
// @include http://i.4cdn.org/*
// @include https://i.4cdn.org/*
// @include http://www.4chan.org/banned
// @include https://www.4chan.org/banned
// @include http://www.4chan.org/feedback
// @include https://www.4chan.org/feedback
// @include https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include http://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include http://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include https://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addValueChangeListener
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @run-at document-start
// @updateURL https://ccd0.github.io/4chan-x/builds/4chan-X-beta.meta.js
// @downloadURL https://ccd0.github.io/4chan-x/builds/4chan-X-beta.user.js
// @updateURL https://www.4chan-x.net/builds/4chan-X-beta.meta.js
// @downloadURL https://www.4chan-x.net/builds/4chan-X-beta.user.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
// ==/UserScript==

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -1,26 +1,38 @@
// ==UserScript==
// @name 4chan X
// @version 1.10.13.1
// @version 1.11.9.2
// @minGMVer 1.14
// @minFFVer 26
// @namespace 4chan-X
// @description Cross-browser userscript for maximum lurking on 4chan.
// @license MIT; https://github.com/ccd0/4chan-x/blob/master/LICENSE
// @match *://boards.4chan.org/*
// @match *://sys.4chan.org/*
// @match *://a.4cdn.org/*
// @match *://i.4cdn.org/*
// @match https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @match https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @match *://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc
// @include http://boards.4chan.org/*
// @include https://boards.4chan.org/*
// @include http://sys.4chan.org/*
// @include https://sys.4chan.org/*
// @include http://a.4cdn.org/*
// @include https://a.4cdn.org/*
// @include http://i.4cdn.org/*
// @include https://i.4cdn.org/*
// @include http://www.4chan.org/banned
// @include https://www.4chan.org/banned
// @include http://www.4chan.org/feedback
// @include https://www.4chan.org/feedback
// @include https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include http://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include http://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @include https://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addValueChangeListener
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @run-at document-start
// @updateURL https://ccd0.github.io/4chan-x/builds/4chan-X.meta.js
// @downloadURL https://ccd0.github.io/4chan-x/builds/4chan-X.user.js
// @updateURL https://www.4chan-x.net/builds/4chan-X.meta.js
// @downloadURL https://www.4chan-x.net/builds/4chan-X.user.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
// ==/UserScript==

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X-beta.crx' version='1.10.13.1' />
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.11.9.2' />
</app>
</gupdate>

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X.crx' version='1.10.13.1' />
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.11.9.2' />
</app>
</gupdate>

BIN
img/icon.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

@ -3,7 +3,7 @@
<meta charset="utf-8">
<title>4chan X</title>
<link rel="stylesheet" href="web.css">
<link rel="icon" href="src/General/img/icon.gif">
<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">
@ -12,7 +12,7 @@
<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/issues">Report Bugs</a>
<a href="https://gitreports.com/issue/ccd0/4chan-x">Report Bugs</a>
</div>
</div>
<a class="screenshot" href="img/screenshot.png"><img src="img/screenshot.png" alt="Screenshot"></a>
@ -21,36 +21,61 @@
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&#39;re looking for a maintained fork of OneeChan (a style script used in addition to 4chan X), try
<a href="https://github.com/Nebukazar/OneeChan">https://github.com/Nebukazar/OneeChan</a>.</p>
<h2 id="firefox-version-click-to-install-https-ccd0-github-io-4chan-x-builds-4chan-x-user-js-">Firefox version: <a href="https://ccd0.github.io/4chan-x/builds/4chan-X.user.js">Click to Install</a></h2>
<p>Install <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">Greasemonkey</a>, then click the link above to install 4chan X. If you&#39;re using a fork of Firefox (e.g. Pale Moon), you may need to use <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/versions/#version-1.15">Greasemonkey 1.15</a> instead of the most recent version.</p>
<p><strong>WARNING</strong>:
If you&#39;re switching to this fork from someone else&#39;s fork of 4chan X, back up your old script before installing this one as the old one may be overwritten.</p>
<p><strong>Known issues</strong>:
Greasemonkey 3.0 has a <a href="https://github.com/greasemonkey/greasemonkey/issues/2094">bug</a> causing 4chan X to open multiple tabs when you open a new tab (for example, when starting a thread). If you&#39;re having this problem, <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">updating</a> to Greasemonkey 3.1 or later should fix it.</p>
<h2 id="chromium-version-click-to-install-https-ccd0-github-io-4chan-x-builds-4chan-x-crx-">Chromium version: <a href="https://ccd0.github.io/4chan-x/builds/4chan-X.crx">Click to Install</a></h2>
<p>Download the file from the link above and add drag it to your <code>chrome://extensions</code> page.
This should also work for non-Windows/dev/canary Chrome and Chromium-based versions of Opera.</p>
<p><strong>The above will not work in Chrome (stable or beta) users on Windows; you must install from the <a href="https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam">Chrome store</a>.</strong></p>
<h2 id="chromium-version-chrome-store-click-to-install-https-chrome-google-com-webstore-detail-4chan-x-ohnjgmpcibpbafdlkimncjhflgedgpam-">Chromium version (Chrome store): <a href="https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam">Click to Install</a></h2>
<p>The stable and beta releases of Chrome on Windows will disable extensions not installed from the Chrome store, so users will need to install 4chan X from the link above.
Only the latest stable version of 4chan X is available.</p>
<h2 id="other-browsers">Other browsers</h2>
<p>This fork of 4chan X is not guaranteed to work correctly in other browsers, but you are welcome to try your luck. Pull requests to fix the bugs you will likely find are always welcome. You may fare better with <a href="https://github.com/loadletter/4chan-x">loadletter&#39;s fork</a>, which has fewer features but less dependence on browser-specific APIs.</p>
<p><strong>Note</strong>: 4chan X disables the native extension, so if you uninstall 4chan X, you&#39;ll need to re-enable it. To do this, click the <code>[Settings]</code> link in the top right corner and uncheck &quot;<code>Disable the native extension</code>&quot; in the panel that appears.</p>
<h2 id="install">Install</h2>
<input hidden type="checkbox" id="firefox-hide"><div><label for="firefox-hide"><h3 id="firefox">Firefox</h3></label>
<p>Install <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">Greasemonkey</a>, then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
<ul>
<li>Some people have reported success in Safari using <a href="http://jsblocker.toggleable.com/">JS Blocker</a> to install the Firefox/Greasemonkey version.</li>
<li>Instructions are available for <a href="https://github.com/ccd0/4chan-x/wiki/Installing-4chan-X-in-dwb">installing 4chan X in dwb</a>.</li>
<li><strong>Pale Moon</strong> users should use <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/versions/1.15.1-signed">Greasemonkey 1.15</a>.</li>
<li><strong>SeaMonkey</strong> users should use Greasemonkey 2.3 converted with <a href="http://addonconverter.fotokraina.com/?url=https://addons.mozilla.org/firefox/downloads/file/282084/greasemonkey-2.3-fx.xpi">this tool</a>.</li>
</ul>
<h2 id="beta-version">Beta version</h2>
</div><input hidden type="checkbox" id="chromium-hide"><div><label for="chromium-hide"><h3 id="chromium">Chromium</h3></label>
<p>4chan X is available as a Chrome extension. The Chrome extension has the additional feature of being able to sync your settings and data with other devices via Chrome Sync.</p>
<ul>
<li><strong>Chromium</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/4chan-x/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. Note: This version does not work with Opera 12; try <a href="https://github.com/loadletter/4chan-x">loadletter&#39;s fork</a> instead.</li>
<li><strong>Chrome</strong>, <strong>Vivaldi</strong>: Install 4chan X from the <strong><a href="https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam">Chrome store</a></strong>.</li>
</ul>
<p>You can also use the <a href="https://www.4chan-x.net/builds/4chan-X.user.js">userscript version of 4chan X</a> with <a href="https://tampermonkey.net/">Tampermonkey</a>.</p>
</div><input hidden type="checkbox" id="safari-hide"><div><label for="safari-hide"><h3 id="safari">Safari</h3></label>
<p>Install <a href="http://jsblocker.toggleable.com/">JS Blocker</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="webkitgtk--hide"><div><label for="webkitgtk--hide"><h3 id="webkitgtk-">WebKitGTK+</h3></label>
<p>Several WebKitGTK+ based 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>).</p>
<pre><code> wget -P ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts https://raw.githubusercontent.com/singpolyma/singpolyma/master/uzbl/data/scripts/userscript.sh
chmod +x ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts/userscript.sh
echo &#39;@on_event LOAD_COMMIT spawn @scripts_dir/userscript.sh document-start&#39; &gt;&gt; ${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config
echo &#39;@on_event LOAD_FINISH spawn @scripts_dir/userscript.sh document-end&#39; &gt;&gt; ${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config
wget -P ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/userscripts https://www.4chan-x.net/builds/4chan-X.user.js
</code></pre></li>
</ul>
</div><input hidden type="checkbox" id="other-browsers-hide"><div><label for="other-browsers-hide"><h3 id="other-browsers">Other browsers</h3></label>
<p>4chan X can be used in some browsers that do not support userscripts, such as <strong>Microsoft Edge</strong>, 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">report</a> any issues you find, and be sure to mention which version you&#39;re using. You should back up your settings regularly to prevent them from being lost due to bugs.</p>
<p>To install the current <strong>beta</strong> version but get updates from the <strong>stable</strong> channel (recommended if you want a particular recent feature):</p>
<ul>
<li><a href="https://ccd0.github.io/4chan-x/builds/4chan-X-beta.user.js">Firefox version</a></li>
<li><a href="https://ccd0.github.io/4chan-x/builds/4chan-X-beta.crx">Chromium version</a></li>
<li><a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js">Install userscript</a> (use with Greasemonkey / Tampermonkey / JS Blocker / etc.)</li>
<li><a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx">Download Chrome extension</a> (download and drag to <code>chrome://extensions</code>)</li>
</ul>
<p>To install the <strong>beta</strong> version and get updates whenever there&#39;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></li>
<li><a href="https://www.4chan-x.net/builds/4chan-X-beta.crx">Download Chrome extension</a></li>
</ul>
<p>If you want to install the current beta version but get updates from the stable channel after that, install it from <a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js">here</a> for Firefox or <a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx">here</a> for Chromium.</p>
<h2 id="security-note">Security note</h2>
<p>4chan X currently shares your settings and post history between the HTTP and HTTPS versions of 4chan. If you are concerned about protecting your privacy against a man-in-the-middle attack, you should disable 4chan X on the HTTP version of 4chan and/or install <a href="https://www.eff.org/https-everywhere">HTTPS Everywhere</a>.</p>
<h2 id="uninstalling">Uninstalling</h2>
<p>4chan X disables the native extension, so if you uninstall 4chan X, you&#39;ll need to re-enable it. To do this, click the <code>[Settings]</code> link in the top right corner and uncheck &quot;<code>Disable the native extension</code>&quot; in the panel that appears.</p>
<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">issue tracker</a>. You can report bugs without a Github account via <a href="https://gitreports.com/issue/ccd0/4chan-x">this form</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() {
@ -65,7 +90,9 @@ function imagePreview() {
}
function storeInstall(e) {
if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.button === 0) {
chrome.webstore.install();
chrome.webstore.install(this.href, function(){}, function(){
location.href = this.href;
});
e.preventDefault();
}
}
@ -77,5 +104,18 @@ for (var i = 0; i < document.links.length; i++) {
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';
})();
if (engine) {
var engines = {'firefox': 'gecko', 'chromium': 'blink presto', 'safari': 'webkit', 'webkitgtk-': 'webkit', 'other-browsers': 'edge'};
for (browser in engines) {
document.getElementById(browser + '-hide').checked = (engines[browser].indexOf(engine) < 0);
}
}
</script>
</body></html>

3080
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

32
package.json Executable file → Normal file
View File

@ -3,27 +3,33 @@
"description": "Cross-browser userscript for maximum lurking on 4chan.",
"meta": {
"name": "4chan X",
"version": "1.10.13.1",
"date": "2015-05-27T18:54:32.769Z",
"repo": "https://github.com/ccd0/4chan-x/",
"page": "https://github.com/ccd0/4chan-x",
"downloads": "https://ccd0.github.io/4chan-x/builds/",
"fork": "ccd0",
"version": "1.11.9.2",
"date": "2015-08-17T01:13:28.335Z",
"page": "https://www.4chan-x.net/",
"downloads": "https://www.4chan-x.net/builds/",
"oldVersions": "https://raw.githubusercontent.com/ccd0/4chan-x/",
"faq": "https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions",
"license": "https://github.com/ccd0/4chan-x/blob/master/LICENSE",
"changelog": "https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md",
"issues": "https://gitreports.com/issue/ccd0/4chan-x",
"newIssue": "https://gitreports.com/issue/ccd0/4chan-x?issue_title=%title&details=%details",
"appid": "lacclbnghgdicfifcamcmcnilckjamag",
"chromeStoreID": "ohnjgmpcibpbafdlkimncjhflgedgpam",
"recaptchaKey": "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc",
"youtubeAPIKey": "AIzaSyB5_zaen_-46Uhz1xGR-lz1YoUMHqCD6CE",
"buildsPath": "builds/",
"mainBranch": "master",
"awsBucket": "4chan-x",
"matches": [
"*://boards.4chan.org/*",
"*://sys.4chan.org/*",
"*://a.4cdn.org/*",
"*://i.4cdn.org/*",
"*://www.4chan.org/banned",
"*://www.4chan.org/feedback",
"https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*",
"https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*",
"*://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"
"*://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*",
"*://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*"
],
"suffix": {
"stable": "",
@ -38,16 +44,16 @@
"dev": " dev"
},
"min": {
"chrome": "32",
"chrome": "33",
"firefox": "26",
"greasemonkey": "1.14"
}
},
"devDependencies": {
"crx": "^3.0.2",
"font-awesome": "4.3.0",
"crx": "^3.0.3",
"font-awesome": "^4.4.0",
"grunt": "^0.4.5",
"grunt-concurrent": "^1.0.0",
"grunt-concurrent": "^2.0.1",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-coffee": "^0.13.0",
"grunt-contrib-concat": "^0.5.1",
@ -56,7 +62,7 @@
"grunt-contrib-watch": "^0.6.1",
"grunt-markdown": "^0.7.0",
"grunt-shell": "^1.1.2",
"grunt-webstore-upload": "^0.8.2",
"grunt-webstore-upload": "^0.8.5",
"jszip": "^2.5.0",
"load-grunt-tasks": "^3.2.0",
"npm-shrinkwrap": "^5.4.0"

View File

@ -95,11 +95,13 @@ Redirect =
location.protocol is 'http:' or
Conf['Except Archives from Encryption']
navigate: (URL, alternative) ->
if URL and (
Redirect.securityCheck(URL) or
confirm "Redirect to #{URL}?\n\nYour connection will not be encrypted."
navigate: (dest, data, alternative) ->
Redirect.init() unless Redirect.data
url = Redirect.to dest, data
if url and (
Redirect.securityCheck(url) or
confirm "Redirect to #{url}?\n\nYour connection will not be encrypted."
)
location.replace URL
location.replace url
else if alternative
location.replace alternative

View File

@ -6,7 +6,7 @@
"https": true,
"software": "foolfuuka",
"boards": ["a", "an", "biz", "c", "co", "diy", "fit", "gd", "gif", "h", "i", "int", "jp", "k", "m", "mlp", "out", "po", "qa", "r9k", "s4s", "sci", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "c", "co", "diy", "fit", "gd", "h", "i", "jp", "k", "m", "mlp", "po", "qa", "r9k", "s4s", "sci", "tg", "u", "v", "vg", "vp", "vr", "wsg"]
"files": ["a", "an", "biz", "c", "co", "diy", "fit", "gd", "gif", "h", "i", "int", "jp", "k", "m", "mlp", "out", "po", "qa", "r9k", "s4s", "sci", "tg", "u", "v", "vg", "vp", "vr", "wsg"]
}, {
"uid": 3,
"name": "4plebs Archive",
@ -79,4 +79,13 @@
"software": "foolfuuka",
"boards": ["mlp", "qa"],
"files": ["mlp", "qa"]
}, {
"uid": 24,
"name": "fireden.net",
"domain": "boards.fireden.net",
"http": false,
"https": true,
"software": "foolfuuka",
"boards": ["cm", "ic", "vg", "y"],
"files": ["cm", "ic", "vg", "y"]
}]

View File

@ -17,12 +17,10 @@ Filter =
# Don't mix up filter flags with the regular expression.
filter = line.replace regexp[0], ''
# Do not add this filter to the list if it's not a global one
# and it's not specifically applicable to the current board.
# Comma-separated list of the boards this filter applies to.
# Defaults to global.
boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global'
if boards isnt 'global' and g.BOARD.ID not in boards.split ','
continue
boards = if boards is 'global' then null else boards.split(',')
if key in ['uniqueID', 'MD5']
# MD5 filter will use strings instead of regular expressions.
@ -66,7 +64,7 @@ Filter =
top = filter.match(/top:(yes|no)/)?[1] or 'yes'
top = top is 'yes' # Turn it into a boolean
@filters[key].push @createFilter regexp, op, stub, hl, top
@filters[key].push @createFilter regexp, boards, op, stub, hl, top
# Only execute filter types that contain valid filters.
unless @filters[key].length
@ -77,7 +75,7 @@ Filter =
name: 'Filter'
cb: @node
createFilter: (regexp, op, stub, hl, top) ->
createFilter: (regexp, boards, op, stub, hl, top) ->
test =
if typeof regexp is 'string'
# MD5 checking
@ -91,7 +89,9 @@ Filter =
class: hl
top: top
(value, isReply) ->
(value, boardID, isReply) ->
if boards and boardID not in boards
return false
if isReply and op is 'only' or !isReply and op is 'no'
return false
unless test value
@ -103,7 +103,7 @@ Filter =
for key of Filter.filters when (value = Filter[key] @)?
# Continue if there's nothing to filter (no tripcode for example).
for filter in Filter.filters[key] when result = filter value, @isReply
for filter in Filter.filters[key] when result = filter value, @board.ID, @isReply
# Hide
if result.hide and not @isFetchedQuote
if @isReply
@ -123,7 +123,7 @@ Filter =
isHidden: (post) ->
for key of Filter.filters when (value = Filter[key] post)?
for filter in Filter.filters[key] when result = filter value, post.isReply
for filter in Filter.filters[key] when result = filter value, post.boardID, post.isReply
return true if result.hide
false

View File

@ -39,7 +39,7 @@ Config =
true
'Enable reporting posts to supported archives.'
]
'Except Archives from Encryption': [
'Exempt Archives from Encryption': [
false
'Permit loading content from, and warningless redirects to, HTTP-only archives from HTTPS pages.'
]
@ -382,7 +382,7 @@ Config =
'Bookmark threads.'
]
'Fixed Thread Watcher': [
null # XXX temporarily set in Main.coffee so old versions update to correct setting
true
'Makes the thread watcher scroll with the page.'
1
]
@ -396,7 +396,7 @@ Config =
'Label each post from a new IP with the thread\'s current IP count.'
]
'Posting':
'Posting and Captchas':
'Quick Reply': [
true
'All-in-one form to reply, create threads, automate dumping and more.'
@ -421,23 +421,27 @@ Config =
'Open new threads or replies to a thread from the index in a new tab.'
1
]
<% if (type === 'userscript') { %>
'Remember QR Size': [
false
'Remember the size of the Quick reply.'
1
]
<% } %>
'Remember Spoiler': [
false
'Remember the spoiler state, instead of resetting after posting.'
1
]
'Randomize Filename': [
false
'Set the filename to a random timestamp within the past year. Disabled on /f/.'
1
]
'Show New Thread Option in Threads': [
false
'Show the option to post a new / different thread from inside a thread.'
1
]
# XXX This has been migrated to Name Sync and will be removed from 4chan X in a future version.
'Show Name and Subject': [
false
'Show the classic name, email, and subject fields in the QR, even when 4chan doesn\'t use them all.'
@ -480,7 +484,15 @@ Config =
]
'Captcha Fixes': [
true
'Make captcha more keyboard-navigable.'
'Make captcha easier to use, especially with the keyboard.'
]
'Use Recaptcha v1': [
false
'Use the old text version of Recaptcha.'
]
'Use Recaptcha v2 in Reports': [
false
'Use the image selection captcha in the report window.'
]
'Quote Links':
@ -587,6 +599,9 @@ Config =
'Fit Height': [
true
]
'Stretch to Fit': [
false
]
'Scroll to Post': [
true
]
@ -653,6 +668,10 @@ Config =
comment: """
# Filter Stallman copypasta on /g/:
#/what you\'re refer+ing to as linux/i;boards:g
# Filter posts with 20 or more quote links:
#/(?:>>\\d(?:(?!>>\\d)[^])*){20}/
# Filter posts like T H I S / H / I / S:
#/^>?\\s?\\w\\s?(\\w)\\s?(\\w)\\s?(\\w).*$[\\s>]+\\1[\\s>]+\\2[\\s>]+\\3/im
"""
flag: ''
@ -674,7 +693,9 @@ Config =
#https://www.yandex.com/images/search?rpt=imageview&img_url=%IMG
#//saucenao.com/search.php?url=%IMG
#http://3d.iqdb.org/?url=%IMG
# tools:
#http://regex.info/exif.cgi?imgurl=%URL
#//imgops.com/%URL;types:gif,jpg,png
# uploaders:
#//imgur.com/upload?url=%URL;types:gif,jpg,png,pdf;text:Upload to imgur
# "View Same" in archives:
@ -738,6 +759,8 @@ Config =
#options:"sage";boards:jp;always
"""
captchaLanguage: ''
time: '%m/%d/%y(%a)%H:%M:%S'
backlink: '>>%id'
@ -946,10 +969,6 @@ Config =
true
'Automatically fetch new posts.'
]
'Ignore Offline Status': [
false
'Update even if your browser reports you are offline.'
]
'Optional Increase': [
false
'Increase the intervals between updates on threads without new posts.'

View File

@ -31,25 +31,41 @@ CrossOrigin = do ->
cb new Uint8Array(response), contentType, contentDisposition
<% } %>
<% if (type === 'userscript') { %>
GM_xmlhttpRequest
# Use workaround for binary data in Greasemonkey versions < 3.2 and in JS Blocker (Safari)
workaround = $.engine is 'gecko' and GM_info? and /^[0-2]\.|^3\.[01](?!\d)/.test(GM_info.version)
workaround or= GM_info?.script?.includeJSB?
options =
method: "GET"
url: url
headers: headers
overrideMimeType: "text/plain; charset=x-user-defined"
onload: (xhr) ->
r = xhr.responseText
data = new Uint8Array r.length
i = 0
while i < r.length
data[i] = r.charCodeAt i
i++
contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1]
contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1]
if workaround
r = xhr.responseText
data = new Uint8Array r.length
i = 0
while i < r.length
data[i] = r.charCodeAt i
i++
else
data = new Uint8Array xhr.response
if typeof xhr.responseHeaders is 'object'
# XXX https://github.com/infernoboy/JavaScript-Blocker/issues/35
contentType = xhr.responseHeaders['Content-Type']
contentDisposition = xhr.responseHeaders['Content-Disposition']
else
contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1]
contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1]
cb data, contentType, contentDisposition
onerror: ->
cb null
onabort: ->
cb null
if workaround
# XXX https://github.com/infernoboy/JavaScript-Blocker/issues/35
options.overrideMimeType = options.mimeType = 'text/plain; charset=x-user-defined'
else
options.responseType = 'arraybuffer'
GM_xmlhttpRequest options
<% } %>
file: (url, cb) ->
@ -62,6 +78,9 @@ CrossOrigin = do ->
contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1]
if match
name = match.replace /\\"/g, '"'
if GM_info?.script?.includeJSB?
# Content type comes back as 'text/plain; charset=x-user-defined'; guess from filename instead.
mime = QR.typeFromExtension[name.match(/[^.]*$/)[0].toLowerCase()] or 'application/octet-stream'
blob = new Blob([data], {type: mime})
blob.name = name
cb blob

View File

@ -105,7 +105,7 @@ Index =
d.implementation.createDocument(null, null, null).appendChild board
$.rm el for el in $$ '.navLinks'
$.id('search-box')?.parentNode.remove()
$.rm $.id('ctrl-top')
topNavPos = $.id('delform').previousElementSibling
$.before topNavPos, $.el 'hr'
$.before topNavPos, Index.navLinks
@ -227,13 +227,13 @@ Index =
popstate: (e) ->
if e?.state
{search, mode} = e.state
page = Index.getCurrentPage()
state = {}
if Index.search isnt search
state.search = Index.search = search
if Conf['Index Mode'] isnt mode
state.mode = mode
Index.saveMode mode
page = Index.getCurrentPage()
if Index.currentPage isnt page
state.page = Index.currentPage = page
if state.search? or state.mode? or state.page?
@ -412,7 +412,6 @@ Index =
"#{hiddenCount} hidden threads"
update: (state) ->
delete Index.pageNum
Index.req?.abort()
Index.notice?.close()
@ -631,8 +630,11 @@ Index =
i = 0
i++ while Index.followedThreadID isnt Get.threadFromRoot(Index.sortedNodes[i]).ID
page = i // Index.threadsNumPerPage + 1
Index.pushState {page} if page isnt Index.getCurrentPage()
if page isnt Index.getCurrentPage()
Index.pushState {page}
Index.setPage()
nodes = Index.buildSinglePage Index.getCurrentPage()
delete Index.pageNum
$.rmAll Index.root
$.rmAll Header.hover
if Conf['Index Mode'] is 'catalog'

View File

@ -1,12 +1,21 @@
Main =
init: ->
# XXX Work around Pale Moon / old Firefox + GM 1.15 bug where script runs in iframe with wrong window.location.
return if d.body and not $ 'title', d.head
# XXX dwb userscripts extension reloads scripts run at document-start when replaceState/pushState is called.
return if window['<%= meta.name %> antidup']
window['<%= meta.name %> antidup'] = true
if location.hostname is 'www.google.com'
if location.pathname is '/recaptcha/api/fallback'
if location.pathname is '/recaptcha/api/noscript'
$.ready -> Captcha.noscript.initFrame()
else
$.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) ->
if enabled
$.ready -> Captcha.fixes.init()
return
if location.pathname is '/recaptcha/api/fallback'
$.ready -> Captcha.v2.initFrame()
$.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) ->
if enabled
$.ready -> Captcha.fixes.init()
return
g.threads = new SimpleDict()
@ -28,8 +37,7 @@ Main =
if g.VIEW is 'thread'
g.THREADID = +pathname[3]
# flatten Config into Conf
# and get saved or default values
# Flatten default values from Config into Conf
flatten = (parent, obj) ->
if obj instanceof Array
Conf[parent] = obj[0]
@ -45,37 +53,83 @@ Main =
for db in DataBoard.keys
Conf[db] = boards: {}
Conf['selectedArchives'] = {}
Conf['cooldowns'] = {}
$.get Conf, (items) ->
$.extend Conf, items
# XXX temporarily set here so old versions update to correct setting
Conf['Fixed Thread Watcher'] ?= Conf['Toggleable Thread Watcher']
$.asap (-> doc = d.documentElement), Main.initFeatures
# XXX old key names
Conf['Except Archives from Encryption'] = false
# Get saved values as items
items = {}
items[key] = undefined for key of Conf
items['previousversion'] = undefined
$.get items, (items) ->
$.asap (-> doc = d.documentElement), ->
# Fresh install
if !items.previousversion?
Main.ready ->
$.set 'previousversion', g.VERSION
Settings.open()
# Migrate old settings
else if items.previousversion isnt g.VERSION
Main.upgrade items
# Combine default values with saved values
for key, val of Conf
Conf[key] = items[key] ? val
Main.initFeatures()
# set up CSS when <head> is completely loaded
$.asap (-> doc = d.documentElement), ->
$.onExists doc, 'body', false, Main.initStyle
upgrade: (items) ->
{previousversion} = items
items2 = {previousversion: g.VERSION}
compareString = previousversion.replace(/\d+/g, (x) -> ('0000'+x)[-5..])
if compareString < '00001.00011.00008.00000'
unless items['Fixed Thread Watcher']?
items2['Fixed Thread Watcher'] = items['Toggleable Thread Watcher'] ? true
unless items['Exempt Archives from Encryption']?
items2['Exempt Archives from Encryption'] = items['Except Archives from Encryption'] ? false
$.extend items, items2
$.set items2, ->
if items['Show Updated Notifications'] ? true
el = $.el 'span',
<%= html(meta.name + ' has been updated to <a href="' + meta.changelog + '" target="_blank">version ${g.VERSION}</a>.') %>
new Notice 'info', el, 15
initFeatures: ->
if location.hostname in ['boards.4chan.org', 'sys.4chan.org']
if location.hostname in ['boards.4chan.org', 'sys.4chan.org', 'www.4chan.org']
$.globalEval 'document.documentElement.classList.add("js-enabled");'
switch location.hostname
when 'www.4chan.org'
Captcha.replace.init()
return
when 'a.4cdn.org'
return
when 'sys.4chan.org'
Report.init()
PostSuccessful.init() if g.VIEW is 'post'
if Conf['404 Redirect'] and /\/imgboard\.php$/.test(location.pathname) and (match = location.search.match /\bres=(\d+)/)
$.ready ->
if $.id('errmsg')?.textContent is 'Error: Specified thread does not exist.'
Redirect.navigate 'thread',
boardID: g.BOARD.ID
postID: +match[1]
return
when 'i.4cdn.org'
$.asap (-> d.readyState isnt 'loading'), ->
if Conf['404 Redirect'] and d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
Redirect.init()
pathname = location.pathname.split '/'
URL = Redirect.to 'file',
Redirect.navigate 'file',
boardID: g.BOARD.ID
filename: pathname[pathname.length - 1]
Redirect.navigate URL
else if video = $ 'video'
if Conf['Volume in New Tab']
Volume.setup video
@ -109,12 +163,15 @@ Main =
$.ready Main.initReady
initStyle: ->
$.addStyle Main.cssWWW if location.hostname is 'www.4chan.org'
return if !Main.isThisPageLegit() or $.hasClass doc, 'fourchan-x'
# disable the mobile layout
$('link[href*=mobile]', d.head)?.disabled = true
$.addClass doc, 'fourchan-x', 'seaweedchan'
$.addClass doc, if g.VIEW is 'thread' then 'thread-view' else g.VIEW
$.addClass doc, if chrome? then 'blink' else 'gecko'
$.addClass doc, $.engine if $.engine
$.addStyle Main.css, 'fourchanx-css'
keyboard = false
@ -146,17 +203,19 @@ Main =
attributeFilter: ['href']
initReady: ->
if d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
if g.VIEW is 'thread'
ThreadWatcher.set404 g.BOARD.ID, g.THREADID, ->
if Conf['404 Redirect']
href = Redirect.to 'thread',
boardID: g.BOARD.ID
threadID: g.THREADID
postID: +location.hash.match /\d+/ # post number or 0
Redirect.navigate href, "/#{g.BOARD}/"
# XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post.
if g.VIEW is 'thread' and (d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] or ($('.board') and not $('.opContainer')))
ThreadWatcher.set404 g.BOARD.ID, g.THREADID, ->
if Conf['404 Redirect']
Redirect.navigate 'thread',
boardID: g.BOARD.ID
threadID: g.THREADID
postID: +location.hash.match /\d+/ # post number or 0
, "/#{g.BOARD}/"
return
return if d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
# 4chan Pass Link
if styleSelector = $.id 'styleSelector'
passLink = $.el 'a',
@ -174,25 +233,7 @@ Main =
else
$.event '4chanXInitFinished'
$.get 'previousversion', null, ({previousversion}) ->
return if previousversion is g.VERSION
if previousversion
el = $.el 'span',
<%= html(meta.name + ' has been updated to <a href="' + meta.repo + 'blob/' + meta.mainBranch + '/CHANGELOG.md" target="_blank">version ${g.VERSION}</a>.') %>
new Notice 'info', el, 15
else
Settings.open()
$.set 'previousversion', g.VERSION
if Conf['Show Support Message']
<% if (type === 'userscript') { %>
GMver = GM_info.version.split '.'
for v, i in "<%= meta.min.greasemonkey %>".split '.'
continue if v is GMver[i]
(v < GMver[i]) or new Notice 'warning', "Your version of Greasemonkey is outdated (v#{GM_info.version} instead of v<%= meta.min.greasemonkey %> minimum) and <%= meta.name %> may not operate correctly.", 30
break
<% } %>
try
localStorage.getItem '4chan-settings'
catch err
@ -263,11 +304,11 @@ Main =
else if errors.length is 1
error = errors[0]
if error
new Notice 'error', Main.parseError(error), 15
new Notice 'error', Main.parseError(error, Main.reportLink([error])), 15
return
div = $.el 'div',
<%= html('${errors.length} errors occurred. [<a href="javascript:;">show</a>]') %>
<%= html('${errors.length} errors occurred.&{Main.reportLink(errors)} [<a href="javascript:;">show</a>]') %>
$.on div.lastElementChild, 'click', ->
[@textContent, logs.hidden] = if @textContent is 'show'
['hide', false]
@ -281,13 +322,34 @@ Main =
new Notice 'error', [div, logs], 30
parseError: (data) ->
parseError: (data, reportLink) ->
c.error data.message, data.error.stack
message = $.el 'div',
textContent: data.message
<%= html('${data.message}?{reportLink}{&{reportLink}}') %>
error = $.el 'div',
textContent: "#{data.error.name or 'Error'}: #{data.error.message or 'see console for details'}"
[message, error]
lines = data.error.stack?.match(/\d+(?=:\d+\)?$)/mg)?.join().replace(/^/, ' at ') or ''
context = $.el 'div',
textContent: "(<%= meta.name %> <%= meta.fork %> v#{g.VERSION} <%= type %> on #{$.engine}#{lines})"
[message, error, context]
reportLink: (errors) ->
data = errors[0]
title = data.message
title += " (+#{errors.length - 1} other errors)" if errors.length > 1
details = """
[Please describe the steps needed to reproduce this error.]
Script: <%= meta.name %> <%= meta.fork %> v#{g.VERSION} <%= type %>
User agent: #{navigator.userAgent}
URL: #{location.href}
#{data.error}
#{data.error.stack?.replace(data.error.toString(), '').trim() or ''}
"""
details = details.replace /file:\/{3}.+\//g, '' # Remove local file paths
url = "<%= meta.newIssue.replace('%title', '#{encodeURIComponent title}').replace('%details', '#{encodeURIComponent details}') %>"
<%= html(' [<a href="${url}" target="_blank">report</a>]') %>
isThisPageLegit: ->
# 404 error page or similar.
@ -301,17 +363,13 @@ Main =
$.ready ->
cb() if Main.isThisPageLegit()
css: `<%=
grunt.template.process(
['font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'].map(function(name) {
return grunt.file.read('src/General/css/'+name+'.css');
}).join(''),
{data: {type: type}}
).trim().replace(/\n+/g, '\n').split(/^/m).map(JSON.stringify).join(' +\n').replace(/`/g, '\\`')
%>`
css: `<%= importCSS('font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon') %>`
cssWWW: `<%= importCSS('www') %>`
features: [
['Polyfill', Polyfill]
['Captcha Replacement', Captcha.replace]
['Redirect', Redirect]
['Header', Header]
['Catalog Links', CatalogLinks]
@ -332,6 +390,7 @@ Main =
['Recursive', Recursive]
['Strike-through Quotes', QuoteStrikeThrough]
['Quick Reply', QR]
['Cooldown', QR.cooldown]
['Menu', Menu]
['Index Generator (Menu)', Index.menu]
['Report Link', ReportLink]

View File

@ -103,6 +103,7 @@ Settings =
description = arr[1]
div = $.el 'div',
<%= html('<label><input type="checkbox" name="${key}">${key}</label><span class="description">: ${description}</span>') %>
div.hidden = true if $.engine isnt 'gecko' and key is 'Remember QR Size' # XXX not supported
input = $ 'input', div
$.on input, 'change', ->
@parentNode.parentNode.dataset.checked = @checked
@ -155,11 +156,9 @@ Settings =
a = $.el 'a',
download: "<%= meta.name %> v#{g.VERSION}-#{data.date}.json"
href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}"
<% if (type === 'userscript') { %>
p = $ '.imp-exp-result', Settings.dialog
$.rmAll p
$.add p, a
<% } %>
a.click()
import: ->
$('input[type=file]', @parentNode).click()
@ -314,7 +313,7 @@ Settings =
items = {}
inputs = {}
for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss', 'customCooldown']
for name in ['captchaLanguage', 'boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss', 'customCooldown']
input = $ "[name='#{name}']", section
items[name] = Conf[name]
inputs[name] = input
@ -323,9 +322,9 @@ Settings =
else if name is 'favicon'
$.on input, 'change', $.cb.value
$.on input, 'change', Settings[name]
else
else
$.on input, 'input', $.cb.value
$.on input, 'input', Settings[name]
$.on input, 'input', Settings[name] if name of Settings
# Quick Reply Personas
ta = $ '.personafield', section
@ -338,8 +337,8 @@ Settings =
for key, val of items
input = inputs[key]
input.value = val
continue if key in ['usercss', 'customCooldown']
Settings[key].call input
if key of Settings and key isnt 'usercss'
Settings[key].call input
return
interval = $ 'input[name="Interval"]', section

View File

@ -102,7 +102,13 @@ UI = do ->
insertEntry: (entry, parent, data) ->
if typeof entry.open is 'function'
return unless entry.open data
try
return unless entry.open data
catch err
Main.handleErrors
message: "Error in building the #{@type} menu."
error: err
return
$.add parent, entry.el
return unless entry.subEntries
@ -331,11 +337,9 @@ UI = do ->
$.on d, 'keydown', o.hoverend
$.on root, 'mousemove', o.hover
<% if (type === 'userscript') { %>
# Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955
o.workaround = (e) -> o.hoverend(e) unless root.contains e.target
$.on doc, 'mousemove', o.workaround
<% } %>
hover = (e) ->
@latestEvent = e
@ -365,10 +369,8 @@ UI = do ->
$.off @root, @endEvents, @hoverend
$.off d, 'keydown', @hoverend
$.off @root, 'mousemove', @hover
<% if (type === 'userscript') { %>
# Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955
$.off doc, 'mousemove', @workaround
<% } %>
@cb.call @ if @cb
checkbox = (name, text, checked) ->

View File

@ -0,0 +1,6 @@
:root:not(.js-enabled) #captchaContainerAlt {
height: auto;
}
noscript > iframe, #recaptcha_challenge_field {
width: 500px;
}

View File

@ -105,9 +105,16 @@ hr + div.center:not(.ad-cnt):not(.topad):not(.middlead):not(.bottomad) {
/* party hats */
pointer-events: none;
}
#g-recaptcha {
min-height: 78px;
height: auto;
}
:root:not(.js-enabled) #postForm {
display: table;
}
#captchaContainerAlt td:nth-child(2) {
display: table-cell !important;
}
/* Anti-autoplay */
audio.controls-added {
@ -120,6 +127,11 @@ audio.controls-added {
height: auto;
text-align: center;
}
:root.anti-autoplay .autoplay-removed {
display: block !important;
min-width: 640px;
min-height: 390px;
}
/* fixed, z-index */
#overlay,
@ -161,9 +173,15 @@ audio.controls-added {
#embedding {
z-index: 11;
}
#thread-watcher {
:root.fixed-watcher #thread-watcher {
z-index: 10;
}
:root.fixed:not(.gallery-open) #header-bar:not(:hover) {
z-index: 8;
}
#thread-watcher {
z-index: 5;
}
/* Header */
.fixed.top-header body {
@ -239,7 +257,7 @@ audio.controls-added {
height: 10px;
position: absolute;
}
:root:not(.autohide) #scroll-marker {
#header-bar:not(.autohide) #scroll-marker {
pointer-events: none;
}
#header-bar #scroll-marker {
@ -318,6 +336,7 @@ audio.controls-added {
flex: auto;
display: -webkit-flex;
display: flex;
width: 0px; /* XXX Fixes Edge not shrinking the board list below default size when needed */
}
:root.fixed:not(.centered-links) #full-board-list > .boardList > a,
:root.fixed:not(.centered-links) #full-board-list > .boardList > span:not(.space):not(.spacer) {
@ -530,7 +549,7 @@ div[data-checked="false"] > .suboption-list {
.section-filter textarea {
height: 500px;
}
.section-filter a {
.section-filter a, .section-advanced a {
text-decoration: underline;
}
.section-sauce textarea {
@ -621,10 +640,10 @@ div[data-checked="false"] > .suboption-list {
left: -1em;
width: 0;
}
<% if (type === 'crx') { %>
/* ``::-webkit-*'' selectors break selector lists on Firefox. */
#index-search::-webkit-search-cancel-button,
<% } %>
#index-search::-webkit-search-cancel-button {
display: none;
}
#index-search:not([data-searching]) + #index-search-clear {
display: none;
}
@ -792,10 +811,11 @@ span.hide-announcement {
#thread-watcher {
padding-bottom: 3px;
padding-left: 3px;
overflow: hidden;
white-space: nowrap;
min-width: 146px;
max-height: 92%;
}
#watched-threads {
overflow-x: hidden;
overflow-y: auto;
}
#thread-watcher .refresh {
@ -804,7 +824,12 @@ span.hide-announcement {
:root.fixed-watcher #thread-watcher {
position: fixed;
}
:root:not(.fixed-watcher) #thread-watcher:not(:hover) {
:root.fixed-watcher #watched-threads {
/* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
max-height: 85vh;
max-height: calc(100vh - 75px);
}
:root:not(.fixed-watcher) #watched-threads:not(:hover) {
max-height: 210px;
overflow-y: hidden;
}
@ -976,6 +1001,11 @@ span.hide-announcement {
:root.fit-height .full-image {
max-height: 100vh;
}
:root.fit-height.fixed .full-image {
/* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
max-height: 93vh;
max-height: calc(100vh - 35px);
}
:root.fit-width .full-image {
max-width: 100%;
}
@ -1150,6 +1180,13 @@ input[name="Default Volume"] {
min-width: 300px;
border-radius: 3px 3px 0 0;
}
#qr > form {
/* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
max-height: 85vh;
max-height: calc(100vh - 75px);
overflow-y: auto;
overflow-x: hidden;
}
#qrtab {
border-radius: 3px 3px 0 0;
}
@ -1222,7 +1259,7 @@ input.field.tripped:not(:hover):not(:focus) {
top: 2px;
}
/* Noscript Recaptcha */
/* Recaptcha v1 */
.captcha-img {
margin: 0px;
text-align: center;
@ -1231,11 +1268,11 @@ input.field.tripped:not(:hover):not(:focus) {
min-height: 59px;
min-width: 302px;
}
.captcha-input{
.captcha-input {
width: 100%;
margin: 1px 0 0;
}
#qr-captcha-iframe {
#qr.captcha-v1 #qr-captcha-iframe {
display: none;
}
@ -1263,6 +1300,13 @@ input.field.tripped:not(:hover):not(:focus) {
display: block;
width: 100%;
}
#qr.captcha-v2 #qr-captcha-iframe {
width: 302px;
height: 423px;
border: 0;
display: block;
margin: auto;
}
.goog-bubble-content {
max-width: 100vw;
max-height: 100vh;
@ -1317,6 +1361,7 @@ input#qr-filename {
.has-file #qr-filename {
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
width: 0px; /* XXX Fixes filename not shrinking to allow space for buttons in Edge */
display: inline-block;
padding: 0;
padding-left: 3px;
@ -1466,6 +1511,8 @@ a:only-of-type > .remove {
}
.textarea {
position: relative;
display: -webkit-flex;
display: flex;
}
:root.webkit .textarea {
margin-bottom: -2px;
@ -1505,7 +1552,7 @@ a:only-of-type > .remove {
height: 15px;
text-align: center;
}
.menu-button + .container:not(:empty) {
.menu-button + .container :first-child {
margin-left: -5px;
}
#menu {
@ -1752,12 +1799,7 @@ grunt.file.expand('src/General/img/links/*.png').map(function(file) {
}
.gal-fit-height .gal-image img,
.gal-fit-height .gal-image video {
/*
Chrome doesn't support viewpoint units in calc()
http://bugs.chromium.org/168840
"It looks like the original author of viewport units in WebKit is not coming back to fix this stuff."
Well, fuck.
*/
/* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
max-height: 95vh;
max-height: calc(100vh - 25px);
}
@ -1860,6 +1902,6 @@ grunt.file.expand('src/General/img/links/*.png').map(function(file) {
width: 4em;
}
:root.gallery-open.fixed #header-bar:not(.autohide),
:root.gallery-open.fixed #header-bar:not(.autohide) .fa::before {
:root.gallery-open.fixed #header-bar:not(.autohide) #shortcuts .fa::before {
visibility: hidden;
}

3
src/General/css/www.css Normal file
View File

@ -0,0 +1,3 @@
#captcha-cnt {
height: auto;
}

View File

@ -12,6 +12,12 @@
</table>
</fieldset>
<fieldset>
<legend>Captcha Language</legend>
<div>Choose from <a href="https://developers.google.com/recaptcha/docs/language" target="_blank">list of language codes</a>. Leave blank to autoselect.</div>
<div><input name="captchaLanguage" class="field" spellcheck="false"></div>
</fieldset>
<fieldset>
<legend>Custom Board Navigation</legend>
<div><textarea name="boardnav" class="field" spellcheck="false"></textarea></div>
@ -41,7 +47,7 @@
<fieldset>
<legend>Time Formatting <span class="warning" data-feature="Time Formatting">is disabled.</span></legend>
<div><input name="time" class="field" spellcheck="false">: <span class="time-preview"></span></div>
<div>Supported <a href="//en.wikipedia.org/wiki/Date_%28Unix%29#Formatting">format specifiers</a>:</div>
<div>Supported <a href="http://man7.org/linux/man-pages/man1/date.1.html" target="_blank">format specifiers</a>:</div>
<div>Day: <code>%a</code>, <code>%A</code>, <code>%d</code>, <code>%e</code></div>
<div>Month: <code>%m</code>, <code>%b</code>, <code>%B</code></div>
<div>Year: <code>%y</code>, <code>%Y</code></div>

View File

@ -1,6 +1,6 @@
<div class="warning"><code>Filter</code> is disabled.</div>
<p>
Use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions">regular expressions</a>, one per line.<br>
Use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions" target="_blank">regular expressions</a>, one per line.<br>
Lines starting with a <code>#</code> will be ignored.<br>
For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br>
MD5 filtering uses exact string matching, not regular expressions.

View File

@ -7,8 +7,8 @@
<a class="reset">Reset Settings</a>&nbsp|&nbsp
<input type="file" hidden>
<a href="<%= meta.page %>" target="_blank"><%= meta.name %></a>&nbsp|&nbsp
<a href="<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md" target="_blank">${g.VERSION}</a>&nbsp|&nbsp
<a href="<%= meta.repo %>issues" target="_blank">Issues</a>&nbsp|&nbsp
<a href="<%= meta.changelog %>" target="_blank">${g.VERSION}</a>&nbsp|&nbsp
<a href="<%= meta.issues %>" target="_blank">Issues</a>&nbsp|&nbsp
<a href="javascript:;" class="close fa fa-times" title="Close"></a>
</div>
</nav>

View File

@ -117,6 +117,11 @@ $.asap = (test, cb) ->
$.onExists = (root, selector, subtree, cb) ->
if el = $ selector, root
return cb el
# XXX Edge doesn't notify MutationObservers of nodes added as document loads.
if $.engine is 'edge' and d.readyState is 'loading'
$.asap (-> d.readyState isnt 'loading' or $ selector, root), ->
$.onExists root, selector, subtree, cb
return
observer = new MutationObserver ->
if el = $ selector, root
observer.disconnect()
@ -156,7 +161,7 @@ $.hasClass = (el, className) ->
className in el.classList
$.rm = (el) ->
el.remove()
el?.remove()
$.rmAll = (root) ->
# https://gist.github.com/MayhemYDG/8646194
@ -242,9 +247,12 @@ unless typeof cloneInto is 'function' or /^[01]\./.test(GM_info.version) or GM_i
$.open =
<% if (type === 'userscript') { %>
GM_openInTab
if GM_openInTab?
GM_openInTab
else
(url) -> window.open url, '_blank'
<% } else { %>
(URL) -> window.open URL, '_blank'
(url) -> window.open url, '_blank'
<% } %>
$.debounce = (wait, fn) ->
@ -320,6 +328,12 @@ $.minmax = (value, min, max) ->
$.hasAudio = (video) ->
video.mozHasAudio or !!video.webkitAudioDecodedByteCount
$.engine = do ->
return 'edge' if /Edge\//.test navigator.userAgent
return 'blink' if /Chrome\//.test navigator.userAgent
return 'webkit' if /WebKit\//.test navigator.userAgent
return 'gecko' if /Gecko\/|Goanna/.test navigator.userAgent # Goanna = Pale Moon 26+
$.item = (key, val) ->
item = {}
item[key] = val
@ -434,42 +448,76 @@ do ->
<% } else { %>
# http://wiki.greasespot.net/Main_Page
$.oldValue = {}
# https://tampermonkey.net/documentation.php
if GM_deleteValue?
$.getValue = GM_getValue
$.listValues = -> GM_listValues() # error when called if missing
else
$.getValue = (key) -> localStorage[key]
$.listValues = ->
key for key of localStorage when key[...g.NAMESPACE.length] is g.NAMESPACE
$.sync = (key, cb) ->
key = g.NAMESPACE + key
$.syncing[key] = cb
$.oldValue[key] = GM_getValue key
do ->
onChange = (key) ->
return unless cb = $.syncing[key]
newValue = GM_getValue key
return if newValue is $.oldValue[key]
if newValue?
$.oldValue[key] = newValue
cb JSON.parse(newValue), key
else
if GM_addValueChangeListener?
$.setValue = GM_setValue
$.deleteValue = GM_deleteValue
else if GM_deleteValue?
$.oldValue = {}
$.setValue = (key, val) ->
GM_setValue key, val
if key of $.syncing
$.oldValue[key] = val
localStorage[key] = val # for `storage` events
$.deleteValue = (key) ->
GM_deleteValue key
if key of $.syncing
delete $.oldValue[key]
cb undefined, key
$.on window, 'storage', ({key}) -> onChange key
delete localStorage[key] # for `storage` events
else
$.oldValue = {}
$.setValue = (key, val) ->
$.oldValue[key] = val if key of $.syncing
localStorage[key] = val
$.deleteValue = (key) ->
delete $.oldValue[key] if key of $.syncing
delete localStorage[key]
$.forceSync = (key) ->
# Storage events don't work across origins
# e.g. http://boards.4chan.org and https://boards.4chan.org
# so force a check for changes to avoid lost data.
onChange g.NAMESPACE + key
if GM_addValueChangeListener?
$.sync = (key, cb) ->
$.syncing[key] = GM_addValueChangeListener g.NAMESPACE + key, (key2, oldValue, newValue, remote) ->
if remote
newValue = JSON.parse newValue unless newValue is undefined
cb newValue, key
$.forceSync = ->
else
$.sync = (key, cb) ->
key = g.NAMESPACE + key
$.syncing[key] = cb
$.oldValue[key] = $.getValue key
do ->
onChange = (key) ->
return unless cb = $.syncing[key]
newValue = $.getValue key
return if newValue is $.oldValue[key]
if newValue?
$.oldValue[key] = newValue
cb JSON.parse(newValue), key[g.NAMESPACE.length..]
else
delete $.oldValue[key]
cb undefined, key[g.NAMESPACE.length..]
$.on window, 'storage', ({key}) -> onChange key
$.forceSync = (key) ->
# Storage events don't work across origins
# e.g. http://boards.4chan.org and https://boards.4chan.org
# so force a check for changes to avoid lost data.
onChange g.NAMESPACE + key
$.delete = (keys) ->
unless keys instanceof Array
keys = [keys]
for key in keys
key = g.NAMESPACE + key
GM_deleteValue key
if key of $.syncing
delete $.oldValue[key]
# for `storage` events
localStorage.removeItem key
$.deleteValue g.NAMESPACE + key
return
$.get = (key, val, cb) ->
@ -480,38 +528,27 @@ $.get = (key, val, cb) ->
cb = val
$.queueTask ->
for key of items
if val = GM_getValue g.NAMESPACE + key
if val = $.getValue g.NAMESPACE + key
items[key] = JSON.parse val
cb items
$.set = do ->
set = (key, val) ->
key = g.NAMESPACE + key
val = JSON.stringify val
GM_setValue key, val
if key of $.syncing
$.oldValue[key] = val
# for `storage` events
localStorage.setItem key, val
(keys, val, cb) ->
if typeof keys is 'string'
set keys, val
else
set key, value for key, value of keys
cb = val
cb?()
$.set = (keys, val, cb) ->
if typeof keys is 'string'
$.setValue(g.NAMESPACE + keys, JSON.stringify val)
else
for key, value of keys
$.setValue(g.NAMESPACE + key, JSON.stringify value)
cb = val
cb?()
$.clear = (cb) ->
# XXX https://github.com/greasemonkey/greasemonkey/issues/2033
# Also support case where GM_listValues is not defined.
$.delete Object.keys(Conf)
$.delete ['previousversion', 'AutoWatch', 'cooldown.global', 'QR Size', 'captchas', 'QR.persona', 'hiddenPSA']
$.delete ['previousversion', 'AutoWatch', 'QR Size', 'captchas', 'QR.persona', 'hiddenPSA']
$.delete ("#{id}.position" for id in ['embedding', 'updater', 'thread-stats', 'thread-watcher', 'qr'])
boards = (a.textContent for a in $$ '#boardNavDesktop > .boardList > a')
boards.push 'qa'
$.delete ("cooldown.#{board}" for board in boards)
try
$.delete GM_listValues().map (key) -> key.replace g.NAMESPACE, ''
$.delete $.listValues().map (key) -> key.replace g.NAMESPACE, ''
cb?()
<% } %>

View File

@ -6,10 +6,10 @@ class Callbacks
@keys.push name unless @[name]
@[name] = cb
execute: (node) ->
for name in @keys
execute: (node, keys=@keys) ->
for name in keys
try
@[name].call node
@[name]?.call node
catch err
errors = [] unless errors
errors.push

View File

@ -19,7 +19,16 @@ class Clone extends Post
quote: $ '.postNum > a:nth-of-type(2)', info
comment: $ '.postMessage', post
quotelinks: []
backlinks: info.getElementsByClassName 'backlink'
# XXX Edge invalidates HTMLCollections when an ancestor node is inserted into another node.
# https://connect.microsoft.com/IE/feedback/details/1198967/ie11-appendchild-provoke-an-error-on-an-htmlcollection
if $.engine is 'edge'
Object.defineProperty @nodes, 'backlinks',
configurable: true
enumerable: true
get: -> info.getElementsByClassName 'backlink'
else
@nodes.backlinks = info.getElementsByClassName 'backlink'
# Remove inlined posts inside of this post.
for inline in $$ '.inline', post

View File

@ -1,12 +1,18 @@
class Connection
constructor: (@target, @origin, @cb) ->
constructor: (@target, @origin, @cb={}) ->
$.on window, 'message', @onMessage
targetWindow: ->
if @target instanceof window.HTMLIFrameElement
@target.contentWindow
else
@target
send: (data) =>
@target.postMessage "#{g.NAMESPACE}#{JSON.stringify data}", @origin
@targetWindow().postMessage "#{g.NAMESPACE}#{JSON.stringify data}", @origin
onMessage: (e) =>
return unless e.source is @target and
return unless e.source is @targetWindow() and
e.origin is @origin and
typeof e.data is 'string' and
e.data[...g.NAMESPACE.length] is g.NAMESPACE

View File

@ -81,7 +81,7 @@ class Fetcher
return false unless url = Redirect.to 'post', {@boardID, @postID}
if /^https:\/\//.test(url) or location.protocol is 'http:'
$.cache url,
do (self = @) -> -> self.parseArchivedPost @response
do (self = @) -> -> self.parseArchivedPost @response, url
,
responseType: 'json'
withCredentials: url.archive.withCredentials
@ -93,11 +93,11 @@ class Fetcher
# Image/thumbnail URLs loaded over HTTP can be modified in transit.
# Require them to be from a known HTTP host so that no referrer is sent to them from an HTTPS page.
delete media[key] unless media[key]? and media[key].match(/^(http:\/\/[^\/]+\/)?/)[0] in url.archive.imagehosts
@parseArchivedPost response
@parseArchivedPost response, url
return true
return false
parseArchivedPost: (data) ->
parseArchivedPost: (data, url) ->
# In case of multiple callbacks for the same request,
# don't parse the same original post more than once.
if post = g.posts["#{@boardID}.#{@postID}"]
@ -147,6 +147,9 @@ class Fetcher
commentHTML: comment
delete o.info.uniqueID if o.info.capcode
if data.media?.media_filename
# Fix URLs missing origin
for key, val of data.media when /_link$/.test(key) and val?[0] is '/'
data.media[key] = url.split('/', 3).join('/') + val
o.file =
name: data.media.media_filename
url: data.media.media_link or data.media.remote_media_link or

View File

@ -1,19 +1,6 @@
Polyfill =
init: ->
@notificationPermission()
@toBlob()
@visibility()
notificationPermission: ->
return if !window.Notification or 'permission' of Notification or !window.webkitNotifications
Object.defineProperty Notification, 'permission',
get: ->
switch webkitNotifications.checkPermission()
when 0
'granted'
when 1
'default'
when 2
'denied'
toBlob: ->
HTMLCanvasElement::toBlob or= (cb) ->
data = atob @toDataURL()[22..]
@ -23,12 +10,3 @@ Polyfill =
for i in [0...l] by 1
ui8a[i] = data.charCodeAt i
cb new Blob [ui8a], type: 'image/png'
visibility: ->
# page visibility API
return if 'visibilityState' of d
Object.defineProperties HTMLDocument.prototype,
visibilityState:
get: -> @webkitVisibilityState
hidden:
get: -> @webkitHidden
$.on d, 'webkitvisibilitychange', -> $.event 'visibilitychange'

View File

@ -40,7 +40,16 @@ class Post
comment: $ '.postMessage', post
links: []
quotelinks: []
backlinks: info.getElementsByClassName 'backlink'
# XXX Edge invalidates HTMLCollections when an ancestor node is inserted into another node.
# https://connect.microsoft.com/IE/feedback/details/1198967/ie11-appendchild-provoke-an-error-on-an-htmlcollection
if $.engine is 'edge'
Object.defineProperty @nodes, 'backlinks',
configurable: true
enumerable: true
get: -> info.getElementsByClassName 'backlink'
else
@nodes.backlinks = info.getElementsByClassName 'backlink'
unless @isReply = $.hasClass post, 'reply'
@thread.OP = @
@ -226,7 +235,7 @@ class Post
# XXX Workaround for 4chan's racing condition
# giving us false-positive dead posts.
resurrect: ->
delete @isDead
@isDead = false
$.rmClass @nodes.root, 'deleted-post'
strong = $ 'strong.warning', @nodes.info
# no false-positive files
@ -245,7 +254,6 @@ class Post
return
collect: ->
@kill()
g.posts.rm @fullID
@thread.posts.rm @
@board.posts.rm @

View File

@ -5,20 +5,28 @@
// @minFFVer <%= meta.min.firefox %>
// @namespace <%= name %>
// @description <%= description %>
// @license MIT; <%= meta.repo %>blob/<%= meta.mainBranch %>/LICENSE
// @license MIT; <%= meta.license %>
<%=
meta.matches.map(function(match) {
return '// @match ' + match;
if (/^\*/.test(match)) {
return (
'// @include ' + match.replace(/^\*/, 'http') + '\n' +
'// @include ' + match.replace(/^\*/, 'https')
);
} else {
return '// @include ' + match;
}
}).join('\n')
%>
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addValueChangeListener
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @run-at document-start
<% if (channel !== 'dev') { %><% if (channel !== 'noupdate') { %>// @updateURL <%= meta.downloads %><%= name %><%= meta.suffix[channel] %>.meta.js
<% } %>// @downloadURL <%= meta.downloads %><%= name %><%= meta.suffix[channel] %>.user.js
<% if (channel !== 'dev') { %>// @updateURL <%= (channel !== 'noupdate') ? (meta.downloads + name + meta.suffix[channel] + '.meta.js') : 'https://noupdate.invalid/' %>
// @downloadURL <%= (channel !== 'noupdate') ? (meta.downloads + name + meta.suffix[channel] + '.user.js') : 'https://noupdate.invalid/' %>
<% } %>// @icon data:image/png;base64,<%= grunt.file.read('src/General/img/icon48.png', {encoding: 'base64'}) %>
// ==/UserScript==

View File

@ -32,8 +32,7 @@ FappeTyme =
cb: @catalogNode
node: ->
return if @file
$.addClass @nodes.root, "noFile"
@nodes.root.classList.toggle 'noFile', !@file
catalogNode: ->
{file} = @thread.OP

View File

@ -128,53 +128,66 @@ Gallery =
Gallery.images.push thumb
$.add Gallery.nodes.thumbs, thumb
load: (thumb, errorCB) ->
ext = thumb.href.match /\w*$/
elType = {'webm': 'video', 'pdf': 'iframe'}[ext] or 'img'
file = $.el elType,
title: thumb.title
$.extend file.dataset, thumb.dataset
$.on file, 'error', errorCB
file.src = thumb.href
file
open: (thumb) ->
{nodes} = Gallery
{name} = nodes
oldID = +nodes.current.dataset.id
newID = +thumb.dataset.id
slideshow = Gallery.slideshow and (newID > oldID or (oldID is Gallery.images.length-1 and newID is 0))
$.rmClass el, 'gal-highlight' if el = $ '.gal-highlight', nodes.thumbs
# Highlight, center selected thumbnail
$.rmClass el, 'gal-highlight' if el = Gallery.images[oldID]
$.addClass thumb, 'gal-highlight'
nodes.thumbs.scrollTop = thumb.offsetTop + thumb.offsetHeight/2 - nodes.thumbs.clientHeight/2
elType = if /\.webm$/.test(thumb.href)
'video'
else if /\.pdf$/.test(thumb.href)
'iframe'
# Load image or use preloaded image
if Gallery.cache?.dataset.id is ''+newID
file = Gallery.cache
$.off file, 'error', Gallery.cacheError
$.on file, 'error', Gallery.error
else
'img'
$[if elType is 'iframe' then 'addClass' else 'rmClass'] doc, 'gal-pdf'
file = $.el elType,
title: name.download = name.textContent = thumb.title
$.extend file.dataset, thumb.dataset
$.on file, 'error', Gallery.error
file.src = name.href = thumb.href
file = Gallery.load thumb, Gallery.error
# Replace old image with new one
$.off nodes.current, 'error', Gallery.error
ImageCommon.pause nodes.current
$.replace nodes.current, file
if elType is 'video'
nodes.current = file
if file.nodeName is 'VIDEO'
file.loop = true
Volume.setup file
file.play() if Conf['Autoplay']
ImageCommon.addControls file if Conf['Show Controls']
doc.classList.toggle 'gal-pdf', file.nodeName is 'IFRAME'
Gallery.cb.setHeight()
nodes.count.textContent = +thumb.dataset.id + 1
nodes.current = file
nodes.name.download = nodes.name.textContent = thumb.title
nodes.name.href = thumb.href
nodes.frame.scrollTop = 0
nodes.next.focus()
if slideshow
# Continue slideshow if moving forward, stop otherwise
if Gallery.slideshow and (newID > oldID or (oldID is Gallery.images.length-1 and newID is 0))
Gallery.setupTimer()
else
Gallery.cb.stop()
# Scroll to post
if Conf['Scroll to Post'] and post = (post = g.posts[file.dataset.post])?.nodes.root
Header.scrollTo post
if Conf['Scroll to Post'] and (post = g.posts[file.dataset.post])
Header.scrollTo post.nodes.root
# Center selected thumbnail
nodes.thumbs.scrollTop = thumb.offsetTop + thumb.offsetHeight/2 - nodes.thumbs.clientHeight/2
# Preload next image
Gallery.cache = Gallery.load Gallery.images[(newID + 1) % Gallery.images.length], Gallery.cacheError
error: ->
if @error?.code is MediaError.MEDIA_ERR_DECODE
@ -185,6 +198,9 @@ Gallery =
Gallery.images[@dataset.id].href = url
@src = url if Gallery.nodes.current is @
cacheError: ->
delete Gallery.cache
cleanupTimer: ->
clearTimeout Gallery.timeoutID
{current} = Gallery.nodes
@ -301,6 +317,14 @@ Gallery =
setFitness: ->
(if @checked then $.addClass else $.rmClass) doc, "gal-#{@name.toLowerCase().replace /\s+/g, '-'}"
setHeight: ->
{current, frame} = Gallery.nodes
current.style.minHeight = if Conf['Stretch to Fit'] and (dim = g.posts[current.dataset.post]?.file.dimensions)
[width, height] = dim.split 'x'
Math.min(doc.clientHeight - 25, height / width * frame.clientWidth) + 'px'
else
null
setDelay: -> Gallery.delay = +@value
menu:
@ -319,14 +343,14 @@ Gallery =
createSubEntry: (name) ->
label = UI.checkbox name, name
input = label.firstElementChild
if name in ['Fit Width', 'Fit Height', 'Hide Thumbnails']
$.on input, 'change', Gallery.cb.setFitness
$.on input, 'change', Gallery.cb.setFitness if name in ['Hide Thumbnails', 'Fit Width', 'Fit Height']
$.event 'change', null, input
$.on input, 'change', $.cb.checked
$.on input, 'change', Gallery.cb.setHeight if name in ['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit']
el: label
createSubEntries: ->
subEntries = (Gallery.menu.createSubEntry item for item in ['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Scroll to Post'])
subEntries = (Gallery.menu.createSubEntry item for item in ['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit', 'Scroll to Post'])
delayLabel = $.el 'label', <%= html('Slide Delay: <input type="number" name="Slide Delay" min="0" step="any" class="field">') %>
delayInput = delayLabel.firstElementChild

View File

@ -83,7 +83,7 @@ ImageCommon =
$.off video, 'mouseover', handler
# Hacky workaround for Firefox forever-loading bug for very short videos
t = new Date().getTime()
$.asap (-> chrome? or (video.readyState >= 3 and video.currentTime <= Math.max 0.1, (video.duration - 0.5)) or new Date().getTime() >= t + 1000), ->
$.asap (-> $.engine isnt 'gecko' or (video.readyState >= 3 and video.currentTime <= Math.max 0.1, (video.duration - 0.5)) or new Date().getTime() >= t + 1000), ->
video.controls = true
$.on video, 'mouseover', handler

View File

@ -121,7 +121,7 @@ ImageExpand =
$.rmClass post.nodes.root, 'expanded-image'
$.rmClass file.thumb, 'expanding'
$.rm file.videoControls if file.videoControls
$.rm file.videoControls
file.thumb.parentNode.href = file.url
file.thumb.parentNode.target = '_blank'
for x in ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']
@ -221,7 +221,7 @@ ImageExpand =
# Scroll to display full image.
if file.scrollIntoView
delete file.scrollIntoView
imageBottom = Header.getBottomOf(file.fullImage) - 25
imageBottom = Math.min(doc.clientHeight - file.fullImage.getBoundingClientRect().bottom - 25, Header.getBottomOf file.fullImage)
if imageBottom < 0
window.scrollBy 0, Math.min(-imageBottom, Header.getTopOf file.fullImage)

View File

@ -46,7 +46,7 @@ ImageHover =
el.play() if Conf['Autoplay']
[width, height] = (+x for x in file.dimensions.split 'x')
{left, right} = @getBoundingClientRect()
padding = 16
padding = 25
maxWidth = Math.max left, doc.clientWidth - right
maxHeight = doc.clientHeight - padding
scale = Math.min 1, maxWidth / width, maxHeight / height

View File

@ -63,7 +63,7 @@ ImageLoader =
clone.file.thumb.preload = 'auto' for clone in post.clones
thumb.preload = 'auto'
# XXX Cloned video elements with poster in Firefox cause momentary display of image loading icon.
if !chrome?
if $.engine is 'gecko'
$.on thumb, 'loadeddata', -> @removeAttribute 'poster'
return

View File

@ -70,6 +70,7 @@ Volume =
$.on @nodes.thumb, 'wheel', Volume.wheel.bind(Header.hover)
wheel: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey
return unless el = $ 'video:not([data-md5])', @
return if el.muted or not $.hasAudio el
volume = el.volume + 0.1

View File

@ -253,7 +253,7 @@ Embedding =
src: "//strawpoll.me/embed_1/#{a.dataset.uid}"
,
key: 'TwitchTV'
regExp: /^\w+:\/\/(?:www\.)?twitch\.tv\/([^#\&\?]*)/
regExp: /^\w+:\/\/(?:www\.)?twitch\.tv\/(\w[^#\&\?]*)/
httpOnly: true
style: "border: none; width: 640px; height: 360px;"
el: (a) ->
@ -277,10 +277,12 @@ Embedding =
regExp: /^\w+:\/\/(?:www\.)?vocaroo\.com\/i\/(\w+)/
style: ''
el: (a) ->
$.el 'audio',
el = $.el 'audio',
controls: true
preload: 'auto'
src: "http://vocaroo.com/media_command.php?media=#{a.dataset.uid}&command=download_ogg"
type = if el.canPlayType 'audio/ogg' then 'ogg' else 'mp3'
el.src = "http://vocaroo.com/media_command.php?media=#{a.dataset.uid}&command=download_#{type}"
el
,
key: 'Vimeo'
regExp: /^\w+:\/\/(?:www\.)?vimeo\.com\/(\d+)/
@ -360,10 +362,12 @@ Embedding =
regExp: /^\w+:\/\/(?:www\.)?clyp\.it\/(\w+)/
style: ''
el: (a) ->
$.el 'audio',
el = $.el 'audio',
controls: true
preload: 'auto'
src: "http://clyp.it/#{a.dataset.uid}.ogg"
type = if el.canPlayType 'audio/ogg' then 'ogg' else 'mp3'
el.src = "http://clyp.it/#{a.dataset.uid}.#{type}"
el
,
# dummy entries: not implemented but included to prevent them being wrongly embedded as a subsequent type
key: 'Loopvid-dummy'

View File

@ -79,7 +79,7 @@ Linkify =
)
| # This should account for virtually all links posted without http:
([-a-z\d]+[.])+(
aero|asia|biz|cat|com|coop|dance|info|int|jobs|mobi|moe|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2}
aero|asia|biz|cat|com|coop|dance|info|int|jobs|mobi|moe|museum|name|net|org|post|pro|tel|travel|xxx|xyz|edu|gov|mil|[a-z]{2}
)([:/]|(?![^\s'"]))
| # IPv4 Addresses
[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}

View File

@ -1,4 +1,6 @@
DeleteLink =
auto: [{}, {}]
init: ->
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Delete Link']
@ -11,19 +13,22 @@ DeleteLink =
fileEl = $.el 'a',
className: 'delete-file'
href: 'javascript:;'
@nodes =
menu: div.firstChild
links: [postEl, fileEl]
postEntry =
el: postEl
open: ->
postEl.textContent = 'Post'
$.on postEl, 'click', DeleteLink.delete
postEl.textContent = DeleteLink.linkText false
$.on postEl, 'click', DeleteLink.toggle
true
fileEntry =
el: fileEl
open: ({file}) ->
return false if !file or file.isDead
fileEl.textContent = 'File'
$.on fileEl, 'click', DeleteLink.delete
fileEl.textContent = DeleteLink.linkText true
$.on fileEl, 'click', DeleteLink.toggle
true
Menu.menu.addEntry
@ -32,19 +37,41 @@ DeleteLink =
open: (post) ->
return false if post.isDead
DeleteLink.post = post
node = div.firstChild
node.textContent = 'Delete'
DeleteLink.cooldown.start post, node
DeleteLink.nodes.menu.textContent = DeleteLink.menuText()
DeleteLink.cooldown.start post
true
subEntries: [postEntry, fileEntry]
delete: ->
{post} = DeleteLink
return if DeleteLink.cooldown.counting is post
menuText: ->
if seconds = DeleteLink.cooldown.seconds[DeleteLink.post.fullID]
"Delete (#{seconds})"
else
'Delete'
$.off @, 'click', DeleteLink.delete
linkText: (fileOnly) ->
text = if fileOnly then 'File' else 'Post'
if DeleteLink.auto[+fileOnly][DeleteLink.post.fullID]
text = "Deleting #{text.toLowerCase()}..."
text
toggle: ->
{post} = DeleteLink
fileOnly = $.hasClass @, 'delete-file'
@textContent = "Deleting #{if fileOnly then 'file' else 'post'}..."
auto = DeleteLink.auto[+fileOnly]
if auto[post.fullID]
delete auto[post.fullID]
else
auto[post.fullID] = true
@textContent = DeleteLink.linkText fileOnly
unless DeleteLink.cooldown.seconds[post.fullID]
DeleteLink.delete post, fileOnly
delete: (post, fileOnly) ->
link = DeleteLink.nodes.links[+fileOnly]
delete DeleteLink.auto[+fileOnly][post.fullID]
$.off link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
form =
mode: 'usrdel'
@ -52,47 +79,56 @@ DeleteLink =
pwd: QR.persona.getPassword()
form[post.ID] = 'delete'
link = @
$.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"),
responseType: 'document'
withCredentials: true
onload: -> DeleteLink.load link, post, fileOnly, @response
onerror: -> DeleteLink.error link
onerror: -> DeleteLink.error link, post
,
form: $.formData form
load: (link, post, fileOnly, resDoc) ->
link.textContent = DeleteLink.linkText fileOnly
if resDoc.title is '4chan - Banned' # Ban/warn check
s = 'Banned!'
el = $.el 'span', <%= html('You can&#039;t delete posts because you are <a href="//www.4chan.org/banned" target="_blank">banned</a>.') %>
new Notice 'warning', el, 20
else if msg = resDoc.getElementById 'errmsg' # error!
s = msg.textContent
$.on link, 'click', DeleteLink.delete
new Notice 'warning', msg.textContent, 20
$.on link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
if /\bwait\b/i.test msg.textContent
DeleteLink.cooldown.start post, 5
DeleteLink.auto[+fileOnly][post.fullID] = true
DeleteLink.nodes.links[+fileOnly].textContent = DeleteLink.linkText fileOnly
else
QR.cooldown.delete post unless fileOnly
if resDoc.title is 'Updating index...'
# We're 100% sure.
QR.cooldown.delete post
(post.origin or post).kill fileOnly
s = 'Deleted'
link.textContent = s
error: (link) ->
link.textContent = 'Connection error, please retry.'
$.on link, 'click', DeleteLink.delete
link.textContent = 'Deleted' if post.fullID is DeleteLink.post.fullID
error: (link, post) ->
new Notice 'warning', 'Connection error, please retry.', 20
$.on link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
cooldown:
start: (post, node) ->
unless QR.db?.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}
# Only start counting on our posts.
delete DeleteLink.cooldown.counting
return
DeleteLink.cooldown.counting = post
length = 60
seconds = Math.ceil (length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND
DeleteLink.cooldown.count post, seconds, length, node
count: (post, seconds, length, node) ->
return if DeleteLink.cooldown.counting isnt post
unless 0 <= seconds <= length
if DeleteLink.cooldown.counting is post
node.textContent = 'Delete'
delete DeleteLink.cooldown.counting
return
setTimeout DeleteLink.cooldown.count, 1000, post, seconds - 1, length, node
node.textContent = "Delete (#{seconds})"
seconds: {}
start: (post, seconds) ->
# Already counting.
return if DeleteLink.cooldown.seconds[post.fullID]?
seconds ?= QR.cooldown.secondsDeletion post
if seconds > 0
DeleteLink.cooldown.seconds[post.fullID] = seconds
DeleteLink.cooldown.count post
count: (post) ->
DeleteLink.nodes.menu.textContent = DeleteLink.menuText() if post.fullID is DeleteLink.post.fullID
if DeleteLink.cooldown.seconds[post.fullID] > 0
DeleteLink.cooldown.seconds[post.fullID]--
setTimeout DeleteLink.cooldown.count, 1000, post
else
delete DeleteLink.cooldown.seconds[post.fullID]
for fileOnly in [false, true] when DeleteLink.auto[+fileOnly][post.fullID]
DeleteLink.delete post, fileOnly
return

View File

@ -14,7 +14,7 @@ ReportLink =
unless post.isDead or (post.thread.isDead and not post.thread.isArchived)
a.textContent = 'Report this post'
ReportLink.url = "//sys.4chan.org/#{post.board}/imgboard.php?mode=report&no=#{post}"
ReportLink.height = 200
ReportLink.height = 180
else if Conf['Archive Report']
a.textContent = 'Report to archive'
ReportLink.url = Redirect.to 'report', {boardID: post.board.ID, postID: post.ID}

View File

@ -26,6 +26,8 @@ AntiAutoplay =
process: (root) ->
for iframe in $$ 'iframe[src*="youtube"][src*="autoplay=1"]', root
iframe.src = iframe.src.replace(/\?autoplay=1&?/, '?').replace('&autoplay=1', '')
$.addClass iframe, 'autoplay-removed'
for object in $$ 'object[data*="youtube"][data*="autoplay=1"]', root
object.data = object.data.replace(/\?autoplay=1&?/, '?').replace('&autoplay=1', '')
$.addClass object, 'autoplay-removed'
return

View File

@ -2,7 +2,7 @@ Keybinds =
init: ->
return if !Conf['Keybinds']
for hotkey of Conf.hotkeys
for hotkey of Config.hotkeys
$.sync hotkey, Keybinds.sync
init = ->
@ -204,6 +204,8 @@ Keybinds =
'Enter'
when 27
'Esc'
when 32
'Space'
when 37
'Left'
when 38
@ -212,9 +214,15 @@ Keybinds =
'Right'
when 40
'Down'
when 188
'Comma'
when 190
'Period'
else
if 48 <= kc <= 57 or 65 <= kc <= 90 # 0-9, A-Z
String.fromCharCode(kc).toLowerCase()
else if 96 <= kc <= 105 # numpad 0-9
String.fromCharCode(kc - 48).toLowerCase()
else
null
if key

View File

@ -1,19 +1,16 @@
Report =
css: '''
:root:not(.js-enabled) #g-recaptcha {
height: auto;
}
'''
css: `<%= importCSS('report') %>`
init: ->
return unless /\bmode=report\b/.test(location.search) and match = location.search.match /\bno=(\d+)/
Captcha.replace.init()
@postID = +match[1]
$.ready @ready
ready: ->
$.addStyle Report.css
Report.archive() if Conf['Archive Report']
if $.hasClass doc, 'js-enabled'
if Conf['Use Recaptcha v2 in Reports'] and $.hasClass doc, 'js-enabled'
new MutationObserver(-> Report.fit '.gc-bubbleDefault').observe d.body,
childList: true
attributes: true
@ -47,4 +44,4 @@ Report =
if types = $.id('reportTypes')
$.on types, 'change', (e) ->
$('form').action = if e.target.value in ['illegal', 'spam'] then '#redirect' else ''
$('form').action = if e.target.value is 'illegal' then '#redirect' else ''

File diff suppressed because one or more lines are too long

View File

@ -76,12 +76,14 @@ ThreadUpdater =
ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
$.on window, 'online offline', ThreadUpdater.cb.online
$.on d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost
$.on d, 'visibilitychange', ThreadUpdater.cb.visibility
ThreadUpdater.setInterval()
# Update immediately on /f/ to add files to replies.
ThreadUpdater.update() if @board.ID is 'f'
###
http://freesound.org/people/pierrecartoons1979/sounds/90112/
cc-by-nc-3.0
@ -89,18 +91,6 @@ ThreadUpdater =
beep: 'data:audio/wav;base64,<%= grunt.file.read("src/General/audio/beep.wav", {encoding: "base64"}) %>'
cb:
online: ->
return if ThreadUpdater.thread.isDead
if navigator.onLine
ThreadUpdater.set 'status', ''
else
ThreadUpdater.set 'status', 'Offline', 'warning'
if Conf['Auto Update'] and not Conf['Ignore Offline Status']
ThreadUpdater.outdateCount = 0
ThreadUpdater.setInterval()
checkpost: (e) ->
return if e.detail.threadID isnt ThreadUpdater.thread.ID
ThreadUpdater.postID = e.detail.postID
@ -189,12 +179,6 @@ ThreadUpdater =
ThreadUpdater.set 'timer', 'Update'
return
unless navigator.onLine
ThreadUpdater.set 'status', 'Offline', 'warning'
unless Conf['Ignore Offline Status']
ThreadUpdater.set 'timer', ''
return
{interval} = ThreadUpdater
if Conf['Optional Increase']
# Lower the max refresh rate limit on visible tabs.
@ -289,6 +273,13 @@ ThreadUpdater =
index.push ID
files.push ID if postObject.fsize
# Add files to replies on /f/.
if board.ID is 'f' and postObject.fsize and (post = thread.posts[ID]) and not post.file
node = Build.postFromObject postObject, board.ID
$.after post.nodes.info, $('.file', node)
post.parseFile()
Post.callbacks.execute post, ['Filter', 'File Info Formatting', 'Fappe Tyme', 'Sauce']
# Insert new posts, not older ones.
continue if ID <= lastPost

View File

@ -127,16 +127,18 @@ Unread =
openNotification: (post) ->
return unless Header.areNotificationsEnabled
notif = new Notification "#{post.info.nameBlock} replied to you",
body: post.info.commentDisplay
icon: Favicon.logo
notif.onclick = ->
Header.scrollToIfNeeded post.nodes.root, true
window.focus()
notif.onshow = ->
setTimeout ->
notif.close()
, 7 * $.SECOND
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1130502 (SeaMonkey)
try
notif = new Notification "#{post.info.nameBlock} replied to you",
body: post.info.commentDisplay
icon: Favicon.logo
notif.onclick = ->
Header.scrollToIfNeeded post.nodes.root, true
window.focus()
notif.onshow = ->
setTimeout ->
notif.close()
, 7 * $.SECOND
onUpdate: (e) ->
if !e.detail[404]
@ -241,6 +243,5 @@ Unread =
Favicon[if isDead then 'unreadDead' else 'unread']
else
Favicon[if isDead then 'dead' else 'default']
unless chrome?
# `favicon.href = href` doesn't work on Firefox.
$.add d.head, Favicon.el
# `favicon.href = href` doesn't work on Firefox.
$.add d.head, Favicon.el

View File

@ -1,6 +1,8 @@
Captcha.fixes =
imageKeys: '789456123uiojklm'.split('').concat(['Comma', 'Period'])
css: '''
.rc-imageselect-target > .rc-imageselect-tile > img:focus {
.rc-imageselect-target > div:focus {
outline: 2px solid #4a90e2;
}
.rc-button-default:focus {
@ -8,10 +10,29 @@ Captcha.fixes =
}
'''
cssNoscript: '''
.fbc-payload-imageselect {
position: relative;
}
.fbc-payload-imageselect > label {
position: absolute;
display: block;
height: 93.3px;
width: 93.3px;
}
label[data-row="0"] {top: 0px;}
label[data-row="1"] {top: 93.3px;}
label[data-row="2"] {top: 186.6px;}
label[data-col="0"] {left: 0px;}
label[data-col="1"] {left: 93.3px;}
label[data-col="2"] {left: 186.6px;}
'''
init: ->
switch location.pathname.split('/')[3]
when 'anchor' then @initMain()
when 'frame' then @initPopup()
when 'anchor' then @initMain()
when 'frame' then @initPopup()
when 'fallback' then @initNoscript()
initMain: ->
$.onExists d.body, '#recaptcha-anchor', true, (checkbox) ->
@ -28,35 +49,66 @@ Captcha.fixes =
new MutationObserver(=> @fixImages()).observe d.body, {childList: true, subtree: true}
$.on d, 'keydown', @keybinds.bind(@)
initNoscript: ->
@noscript = true
@images = $$ '.fbc-payload-imageselect > input'
return unless @images.length
$.addStyle @cssNoscript
@addLabels()
$.on d, 'keydown', @keybinds.bind(@)
$.on $('.fbc-imageselect-challenge > form'), 'submit', @checkForm.bind(@)
fixImages: ->
return unless (@images = $$ '.rc-imageselect-target > .rc-imageselect-tile > img').length
focus = @images[0].tabIndex isnt 0
@images = $$ '.rc-imageselect-target > div'
for img in @images
img.tabIndex = 0
if focus
$.queueTask => @focusImage()
@addTooltips @images if @images.length
focusImage: ->
# XXX Image is not focusable at first in Firefox; to be refactored when I figure out why.
img = @images[0]
$.asap ->
return true unless doc.contains img
img.focus()
d.activeElement is img
, ->
addLabels: ->
imageSelect = $ '.fbc-payload-imageselect'
labels = for checkbox, i in @images
checkbox.id = "checkbox-#{i}"
label = $.el 'label',
htmlFor: checkbox.id
label.dataset.row = i // 3
label.dataset.col = i % 3
label
$.add imageSelect, labels
@addTooltips labels
addTooltips: (nodes) ->
for node, i in nodes
node.title = "#{@imageKeys[i]} or #{@imageKeys[i+9][0].toUpperCase()}#{@imageKeys[i+9][1..]}"
return
checkForm: (e) ->
n = 0
n++ for checkbox in @images when checkbox.checked
e.preventDefault() if n is 0
keybinds: (e) ->
return unless @images and doc.contains(@images[0]) and d.activeElement
reload = $.id 'recaptcha-reload-button'
verify = $.id 'recaptcha-verify-button'
return unless @images and doc.contains(@images[0])
reload = $ '#recaptcha-reload-button, .fbc-button-reload'
verify = $ '#recaptcha-verify-button, .fbc-button-verify > input'
x = @images.indexOf d.activeElement
if x < 0
return unless $('.rc-controls').contains d.activeElement
x = if d.activeElement is verify then 11 else 9
return unless dx = {38: 9, 40: 3, 37: 11, 39: 1, 73: 9, 75: 3, 74: 11, 76: 1}[e.keyCode] # Up, Down, Left, Right, I, K, J, L
x = (x + dx) % 12
if x is 10
x = if dx is 11 then 9 else 11
(@images[x] or {9: reload, 11: verify}[x]).focus()
key = Keybinds.keyCode e
if !@noscript and key is 'Space' and x < 9
@images[x].click()
else if (i = @imageKeys.indexOf key) >= 0
@images[i % 9].click()
verify.focus()
else if dx = {'Up': 9, 'Down': 3, 'Left': 11, 'Right': 1}[key]
x = (x + dx) % 12
if x is 10
x = if dx is 11 then 9 else 11
(@images[x] or {9: reload, 11: verify}[x]).focus()
else
return
e.preventDefault()
e.stopPropagation()

View File

@ -1,10 +1,9 @@
Captcha.noscript =
lifetime: 2 * $.MINUTE
iframeURL: '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
lifetime: 30 * $.MINUTE
init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0
return unless @isEnabled = !!$.id 'g-recaptcha'
return unless @isEnabled = !!$ '#g-recaptcha, #captchaContainerAlt'
container = $.el 'div',
className: 'captcha-img'
@ -27,7 +26,7 @@ Captcha.noscript =
token: @save.bind @
error: @error.bind @
$.addClass QR.nodes.el, 'has-captcha'
$.addClass QR.nodes.el, 'has-captcha', 'captcha-v1', 'noscript-captcha'
$.after QR.nodes.com.parentNode, [container, input]
@captchas = []
@ -42,12 +41,15 @@ Captcha.noscript =
initFrame: ->
conn = new Connection window.parent, "#{location.protocol}//boards.4chan.org",
response: (response) ->
$.id('response').value = response
$('.fbc-challenge > form').submit()
conn.send
token: $('.fbc-verification-token > textarea')?.value
error: $('.fbc-error')?.textContent
return unless img = $ '.fbc-payload > img'
$.id('recaptcha_response_field').value = response
# The form has a field named 'submit'
HTMLFormElement.prototype.submit.call $('form')
if location.hash is '#response'
conn.send
token: $('textarea')?.value
error: $('.recaptcha_input_area')?.textContent.replace(/:$/, '')
return unless img = $ 'img'
$('form').action = '#response'
cb = ->
canvas = $.el 'canvas'
canvas.width = img.width
@ -61,6 +63,12 @@ Captcha.noscript =
timers: {}
iframeURL: ->
url = '//www.google.com/recaptcha/api/noscript?k=<%= meta.recaptchaKey %>'
if lang = Conf['captchaLanguage'].trim()
url += "&hl=#{encodeURIComponent lang}"
url
cb:
focus: -> QR.captcha.setup false, true
@ -88,11 +96,11 @@ Captcha.noscript =
if !@nodes.iframe
@nodes.iframe = $.el 'iframe',
id: 'qr-captcha-iframe'
src: @iframeURL
$.add d.body, @nodes.iframe
@conn.target = @nodes.iframe.contentWindow
src: @iframeURL()
$.add QR.nodes.el, @nodes.iframe
@conn.target = @nodes.iframe
else if !@occupied or force
@nodes.iframe.src = @iframeURL
@nodes.iframe.src = @iframeURL()
@occupied = true
@nodes.input.focus() if focus
@ -109,9 +117,9 @@ Captcha.noscript =
destroy: ->
return unless @isEnabled
$.rm @nodes.img if @nodes.img
$.rm @nodes.img
delete @nodes.img
$.rm @nodes.iframe if @nodes.iframe
$.rm @nodes.iframe
delete @nodes.iframe
delete @occupied
@beforeSetup()
@ -125,7 +133,7 @@ Captcha.noscript =
if captcha = @captchas.shift()
@count()
$.set 'captchas', @captchas
captcha.response
captcha
else if /\S/.test @nodes.input.value
(cb) =>
@submitCB = cb
@ -141,15 +149,17 @@ Captcha.noscript =
save: (token) ->
delete @occupied
@nodes.input.value = ''
captcha =
challenge: token
response: 'manual_challenge'
timeout: @timeout
if @submitCB
@submitCB token
@submitCB captcha
delete @submitCB
if @needed() then @reload() else @destroy()
else
$.forceSync 'captchas'
@captchas.push
response: token
timeout: @timeout
@captchas.push captcha
@count()
$.set 'captchas', @captchas
@reload()
@ -212,7 +222,7 @@ Captcha.noscript =
@destroy()
reload: ->
@nodes.iframe.src = @iframeURL
@nodes.iframe.src = @iframeURL()
@occupied = true
@nodes.img?.hidden = true

View File

@ -0,0 +1,54 @@
Captcha.replace =
init: ->
return unless d.cookie.indexOf('pass_enabled=1') < 0
return if location.hostname is 'boards.4chan.org' and Conf['Hide Original Post Form']
jsEnabled = $.hasClass doc, 'js-enabled'
if location.hostname is 'sys.4chan.org' and Conf['Use Recaptcha v2 in Reports'] and jsEnabled
$.ready Captcha.replace.v2
return
if Conf['Use Recaptcha v1'] and jsEnabled and location.hostname isnt 'www.4chan.org'
$.ready Captcha.replace.v1
return
if Conf['captchaLanguage'].trim()
if location.hostname is 'boards.4chan.org'
$.onExists doc, '#captchaFormPart', true, (node) -> $.onExists node, 'iframe', true, Captcha.replace.iframe
else
$.onExists doc, 'iframe', true, Captcha.replace.iframe
v1: ->
return unless $.id 'g-recaptcha'
Captcha.v1.replace()
if link = $.id 'form-link'
$.on link, 'click', -> Captcha.v1.create()
else if location.hostname is 'boards.4chan.org'
form = $.id 'postForm'
form.addEventListener 'focus', (-> Captcha.v1.create()), true
else
Captcha.v1.create()
v2: ->
return unless old = $.id 'captchaContainerAlt'
container = $.el 'div',
className: 'g-recaptcha'
$.extend container.dataset,
sitekey: '<%= meta.recaptchaKey %>'
tabindex: 3
$.replace old, container
url = 'https://www.google.com/recaptcha/api.js'
if lang = Conf['captchaLanguage'].trim()
url += "?hl=#{encodeURIComponent lang}"
script = $.el 'script',
src: url
$.add d.head, script
iframe: (el) ->
return unless lang = Conf['captchaLanguage'].trim()
src = if /[?&]hl=/.test el.src
el.src.replace(/([?&]hl=)[^&]*/, '$1' + encodeURIComponent lang)
else
el.src + "&hl=#{encodeURIComponent lang}"
el.src = src unless el.src is src

View File

@ -0,0 +1,219 @@
Captcha.v1 =
blank: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='57'/>"
init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0
return unless @isEnabled = !!$ '#g-recaptcha, #captchaContainerAlt'
imgContainer = $.el 'div',
className: 'captcha-img'
title: 'Reload reCAPTCHA'
$.extend imgContainer, <%= html('<img>') %>
input = $.el 'input',
className: 'captcha-input field'
title: 'Verification'
autocomplete: 'off'
spellcheck: false
@nodes =
img: imgContainer.firstChild
input: input
$.on input, 'blur', QR.focusout
$.on input, 'focus', QR.focusin
$.on input, 'keydown', QR.captcha.keydown.bind QR.captcha
$.on @nodes.img.parentNode, 'click', QR.captcha.reload.bind QR.captcha
$.addClass QR.nodes.el, 'has-captcha', 'captcha-v1'
$.after QR.nodes.com.parentNode, [imgContainer, input]
@captchas = []
$.get 'captchas', [], ({captchas}) ->
QR.captcha.sync captchas
QR.captcha.clear()
$.sync 'captchas', @sync
@replace()
@beforeSetup()
@setup() if Conf['Auto-load captcha']
new MutationObserver(@afterSetup).observe $.id('captchaContainerAlt'), childList: true
@afterSetup() # reCAPTCHA might have loaded before the QR.
replace: ->
return if @script
unless @script = $ 'script[src="//www.google.com/recaptcha/api/js/recaptcha_ajax.js"]', d.head
@script = $.el 'script',
src: '//www.google.com/recaptcha/api/js/recaptcha_ajax.js'
$.add d.head, @script
if old = $.id 'g-recaptcha'
container = $.el 'div',
id: 'captchaContainerAlt'
$.replace old, container
create: ->
$.globalEval '''
(function() {
var container = document.getElementById("captchaContainerAlt");
if (container.firstChild) return;
var options = {
theme: "clean",
tabindex: {"boards.4chan.org": 5, "sys.4chan.org": 3}[location.hostname]
};
if (window.Recaptcha) {
window.Recaptcha.create("<%= meta.recaptchaKey %>", container, options);
} else {
var script = document.head.querySelector('script[src="//www.google.com/recaptcha/api/js/recaptcha_ajax.js"]');
script.addEventListener('load', function() {
window.Recaptcha.create("<%= meta.recaptchaKey %>", container, options);
}, false);
}
})();
'''
cb:
focus: -> QR.captcha.setup false, true
beforeSetup: ->
{img, input} = @nodes
img.parentNode.hidden = true
img.src = @blank
input.value = ''
input.placeholder = 'Focus to load reCAPTCHA'
@count()
$.on input, 'focus click', @cb.focus
needed: ->
captchaCount = @captchas.length
captchaCount++ if QR.req
postsCount = QR.posts.length
postsCount = 0 if postsCount is 1 and !Conf['Auto-load captcha'] and !QR.posts[0].com and !QR.posts[0].file
captchaCount < postsCount
onNewPost: ->
onPostChange: ->
setup: (focus, force) ->
return unless @isEnabled and (force or @needed())
@create()
@nodes.input.focus() if focus
afterSetup: ->
return unless challenge = $.id 'recaptcha_challenge_field_holder'
return if challenge is QR.captcha.nodes.challenge
setLifetime = (e) -> QR.captcha.lifetime = e.detail
$.on window, 'captcha:timeout', setLifetime
$.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
$.off window, 'captcha:timeout', setLifetime
{img, input} = QR.captcha.nodes
img.parentNode.hidden = false
input.placeholder = 'Verification'
QR.captcha.count()
$.off input, 'focus click', QR.captcha.cb.focus
QR.captcha.nodes.challenge = challenge
new MutationObserver(QR.captcha.load.bind QR.captcha).observe challenge,
childList: true
subtree: true
attributes: true
QR.captcha.load()
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
QR.nodes.el.style.top = null
QR.nodes.el.style.bottom = '0px'
destroy: ->
return unless @script
$.globalEval 'window.Recaptcha.destroy();'
@beforeSetup() if @nodes
sync: (captchas=[]) ->
QR.captcha.captchas = captchas
QR.captcha.count()
getOne: ->
@clear()
if captcha = @captchas.shift()
@count()
$.set 'captchas', @captchas
captcha
else
challenge = @nodes.img.alt
timeout = @timeout
if /\S/.test(response = @nodes.input.value)
@destroy()
{challenge, response, timeout}
else
null
save: ->
return unless /\S/.test(response = @nodes.input.value)
@nodes.input.value = ''
@captchas.push
challenge: @nodes.img.alt
response: response
timeout: @timeout
@count()
@destroy()
@setup false, true
$.set 'captchas', @captchas
clear: ->
return unless @captchas.length
$.forceSync 'captchas'
now = Date.now()
for captcha, i in @captchas
break if captcha.timeout > now
return unless i
@captchas = @captchas[i..]
@count()
$.set 'captchas', @captchas
load: ->
if $('#captchaContainerAlt[class~="recaptcha_is_showing_audio"]')
@nodes.img.src = @blank
return
return unless @nodes.challenge.firstChild
return unless challenge_image = $.id 'recaptcha_challenge_image'
# -1 minute to give upload some time.
@timeout = Date.now() + @lifetime * $.SECOND - $.MINUTE
challenge = @nodes.challenge.firstChild.value
@nodes.img.alt = challenge
@nodes.img.src = challenge_image.src
@nodes.input.value = ''
@clear()
count: ->
count = if @captchas then @captchas.length else 0
placeholder = @nodes.input.placeholder.replace /\ \(.*\)$/, ''
placeholder += switch count
when 0
if placeholder is 'Verification' then ' (Shift + Enter to cache)' else ''
when 1
' (1 cached captcha)'
else
" (#{count} cached captchas)"
@nodes.input.placeholder = placeholder
@nodes.input.alt = count # For XTRM RICE.
reload: (focus) ->
# Recaptcha.should_focus = false: Hack to prevent the input from being focused
$.globalEval '''
if (window.Recaptcha.type === "image") {
window.Recaptcha.reload();
} else {
window.Recaptcha.switch_type("image");
}
window.Recaptcha.should_focus = false;
'''
@nodes.input.focus() if focus
keydown: (e) ->
if e.keyCode is 8 and not @nodes.input.value
@reload()
else if e.keyCode is 13 and e.shiftKey
@save()
else
return
e.preventDefault()

View File

@ -3,7 +3,12 @@ Captcha.v2 =
init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0
return unless @isEnabled = !!$.id 'g-recaptcha'
return unless @isEnabled = !!$ '#g-recaptcha, #captchaContainerAlt'
if @noscript = Conf['Force Noscript Captcha'] or not $.hasClass doc, 'js-enabled'
@conn = new Connection null, "#{location.protocol}//www.google.com",
token: (token) => @save true, token
$.addClass QR.nodes.el, 'noscript-captcha'
@captchas = []
$.get 'captchas', [], ({captchas}) ->
@ -17,18 +22,34 @@ Captcha.v2 =
counter = $ '.captcha-counter > a', root
@nodes = {root, counter}
@count()
$.addClass QR.nodes.el, 'has-captcha'
$.addClass QR.nodes.el, 'has-captcha', 'captcha-v2'
$.after QR.nodes.com.parentNode, root
$.on counter, 'click', @toggle.bind @
$.on counter, 'keydown', (e) =>
return unless Keybinds.keyCode(e) is 'Space'
@toggle()
e.preventDefault()
e.stopPropagation()
$.on window, 'captcha:success', =>
# XXX Greasemonkey 1.x workaround to gain access to GM_* functions.
$.queueTask => @save false
initFrame: ->
if token = $('.fbc-verification-token > textarea')?.value
conn = new Connection window.parent, "#{location.protocol}//boards.4chan.org"
conn.send {token}
shouldFocus: false
timeouts: {}
postsCount: 0
noscriptURL: ->
url = '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
if lang = Conf['captchaLanguage'].trim()
url += "&hl=#{encodeURIComponent lang}"
url
needed: ->
captchaCount = @captchas.length
captchaCount++ if QR.req
@ -60,6 +81,7 @@ Captcha.v2 =
if @nodes.container
if @shouldFocus and iframe = $ 'iframe', @nodes.container
iframe.focus()
QR.focus() # Event handler not fired in Firefox
delete @shouldFocus
return
@ -69,6 +91,19 @@ Captcha.v2 =
childList: true
subtree: true
if @noscript
@setupNoscript()
else
@setupJS()
setupNoscript: ->
iframe = $.el 'iframe',
id: 'qr-captcha-iframe'
src: @noscriptURL()
$.add @nodes.container, iframe
@conn.target = iframe
setupJS: ->
$.globalEval '''
(function() {
function render() {
@ -101,7 +136,7 @@ Captcha.v2 =
return
setupIFrame: (iframe) ->
@setupTime = Date.now()
Captcha.replace.iframe iframe
$.addClass QR.nodes.el, 'captcha-open'
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
QR.nodes.el.style.top = null
@ -134,19 +169,19 @@ Captcha.v2 =
if captcha = @captchas.shift()
$.set 'captchas', @captchas
@count()
captcha.response
captcha
else
null
save: (pasted) ->
save: (pasted, token) ->
$.forceSync 'captchas'
@captchas.push
response: $('textarea', @nodes.container).value
timeout: (if pasted then @setupTime else Date.now()) + @lifetime
response: token or $('textarea', @nodes.container).value
timeout: Date.now() + @lifetime
$.set 'captchas', @captchas
@count()
focus = d.activeElement?.nodeName is 'IFRAME' and d.activeElement.src?[...38] is 'https://www.google.com/recaptcha/api2/'
focus = d.activeElement?.nodeName is 'IFRAME' and /https?:\/\/www\.google\.com\/recaptcha\//.test(d.activeElement.src)
if @needed()
if focus
if QR.cooldown.auto or Conf['Post on Captcha Completion']
@ -182,9 +217,12 @@ Captcha.v2 =
@timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now()
reload: ->
$.globalEval '''
(function() {
var container = document.querySelector("#qr .captcha-container");
window.grecaptcha.reset(container.dataset.widgetID);
})();
'''
if @noscript
$('iframe', @nodes.container).src = @noscriptURL()
else
$.globalEval '''
(function() {
var container = document.querySelector("#qr .captcha-container");
window.grecaptcha.reset(container.dataset.widgetID);
})();
'''

View File

@ -1,6 +1,17 @@
QR =
mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm']
validExtension: /\.(jpe?g|png|gif|pdf|swf|webm)$/i
typeFromExtension:
'jpg': 'image/jpeg'
'jpeg': 'image/jpeg'
'png': 'image/png'
'gif': 'image/gif'
'pdf': 'application/pdf'
'swf': 'application/vnd.adobe.flash.movie'
'webm': 'video/webm'
init: ->
return unless Conf['Quick Reply']
@ -9,8 +20,12 @@ QR =
return if g.VIEW is 'archive'
noscript = Conf['Force Noscript Captcha'] or not $.hasClass doc, 'js-enabled'
@captcha = Captcha[if noscript then 'noscript' else 'v2']
version = if Conf['Use Recaptcha v1']
noscript = Conf['Force Noscript Captcha'] or not $.hasClass doc, 'js-enabled'
if noscript then 'noscript' else 'v1'
else
'v2'
@captcha = Captcha[version]
$.on d, '4chanXInitFinished', @initReady
@ -134,14 +149,16 @@ QR =
QR.hasFocus = d.activeElement and QR.nodes.el.contains(d.activeElement)
QR.nodes.el.classList.toggle 'focus', QR.hasFocus
# XXX Stop unwanted scrolling due to captcha.
if QR.inCaptcha()
QR.scrollY = window.scrollY
$.on d, 'scroll', QR.scrollLock
else
$.off d, 'scroll', QR.scrollLock
if QR.captcha.isEnabled and QR.captcha is Captcha.v2 and !QR.captcha.noscript
if QR.inCaptcha()
QR.scrollY = window.scrollY
$.on d, 'scroll', QR.scrollLock
else
$.off d, 'scroll', QR.scrollLock
inBubble: ->
$$('.goog-bubble-content > iframe').some((el) -> el.getBoundingClientRect().bottom > 0)
bubbles = $$ '.goog-bubble-content > iframe'
d.activeElement in bubbles or bubbles.some((el) -> el.getBoundingClientRect().bottom > 0)
inCaptcha: ->
(d.activeElement?.nodeName is 'IFRAME' and QR.nodes.el.contains(d.activeElement)) or (QR.hasFocus and QR.inBubble())
@ -191,19 +208,21 @@ QR =
unless Header.areNotificationsEnabled
alert el.textContent if d.hidden and not QR.cooldown.auto
else if d.hidden or not (focusOverride or d.hasFocus())
notif = new Notification el.textContent,
body: el.textContent
icon: Favicon.logo
notif.onclick = -> window.focus()
if chrome?
# Firefox automatically closes notifications
# so we can't control the onclose properly.
notif.onclose = -> notice.close()
notif.onshow = ->
setTimeout ->
notif.onclose = null
notif.close()
, 7 * $.SECOND
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1130502 (SeaMonkey)
try
notif = new Notification el.textContent,
body: el.textContent
icon: Favicon.logo
notif.onclick = -> window.focus()
if $.engine isnt 'gecko'
# Firefox automatically closes notifications
# so we can't control the onclose properly.
notif.onclose = -> notice.close()
notif.onshow = ->
setTimeout ->
notif.onclose = null
notif.close()
, 7 * $.SECOND
notifications: []
@ -346,7 +365,7 @@ QR =
for i in [0...bstr.length]
arr[i] = bstr.charCodeAt(i)
blob = new Blob [arr], {type: m[1]}
blob.name = "image.#{m[2]}"
blob.name = "file.#{m[2]}"
QR.handleFiles [blob]
else if /^https?:\/\//.test src
QR.handleUrl src
@ -523,7 +542,7 @@ QR =
# We don't receive blur events from captcha iframe.
$.on d, 'click', QR.focus
unless chrome?
if $.engine is 'gecko'
nodes.pasteArea.hidden = false
new MutationObserver(QR.pasteFF).observe nodes.pasteArea, {childList: true}
@ -536,20 +555,19 @@ QR =
event = if node.nodeName is 'SELECT' then 'change' else 'input'
$.on nodes[name], event, save
<% if (type === 'userscript') { %>
if Conf['Remember QR Size']
# XXX Blink and WebKit treat width and height of <textarea>s as min-width and min-height
if $.engine is 'gecko' and Conf['Remember QR Size']
$.get 'QR Size', '', (item) ->
nodes.com.style.cssText = item['QR Size']
$.on nodes.com, 'mouseup', (e) ->
return if e.button isnt 0
$.set 'QR Size', @style.cssText
<% } %>
QR.generatePostableThreadsList()
QR.persona.init()
new QR.post true
QR.status()
QR.cooldown.init()
QR.cooldown.setup()
QR.captcha.init()
$.add d.body, dialog
@ -649,7 +667,6 @@ QR =
onload: ->
# Upload done, waiting for server response.
QR.req.isUploadFinished = true
QR.req.uploadEndTime = Date.now()
QR.req.progress = '...'
QR.status()
onprogress: (e) ->
@ -658,7 +675,12 @@ QR =
QR.status()
cb = (response) ->
extra.form.append 'g-recaptcha-response', response if response?
if response?
if response.challenge?
extra.form.append 'recaptcha_challenge_field', response.challenge
extra.form.append 'recaptcha_response_field', response.response
else
extra.form.append 'g-recaptcha-response', response.response
QR.req = $.ajax "https://sys.4chan.org/#{g.BOARD}/post", options, extra
QR.req.progress = '...'
@ -772,7 +794,7 @@ QR =
post.rm()
QR.captcha.setup(d.activeElement is QR.nodes.status)
QR.cooldown.add req.uploadEndTime, threadID, postID
QR.cooldown.add threadID, postID
URL = if threadID is postID # new thread
"#{window.location.origin}/#{g.BOARD}/thread/#{threadID}"

View File

@ -1,47 +1,55 @@
QR.cooldown =
seconds: 0
delays:
thread: 0
reply: 0
image: 0
reply_intra: 0
image_intra: 0
deletion: 60 # cooldown for deleting posts/files
thread_global: 300 # inter-board thread cooldown
# Called from Main
init: ->
return unless Conf['Quick Reply'] and Conf['Cooldown']
@data = Conf['cooldowns']
$.sync 'cooldowns', @sync
# Called from QR
setup: ->
return unless Conf['Cooldown']
# Read cooldown times
QR.cooldown.delays = if m = Get.scriptData().match /\bcooldowns *= *({[^}]+})/
JSON.parse m[1]
else
{thread: 0, reply: 0, image: 0, reply_intra: 0, image_intra: 0}
if m = Get.scriptData().match /\bcooldowns *= *({[^}]+})/
$.extend QR.cooldown.delays, JSON.parse m[1]
# The longest reply cooldown, for use in pruning old reply data
QR.cooldown.maxDelay = 0
for type, delay of QR.cooldown.delays when type isnt 'thread'
for type, delay of QR.cooldown.delays when type not in ['thread', 'thread_global']
QR.cooldown.maxDelay = Math.max QR.cooldown.maxDelay, delay
# There is a 300 second inter-board thread cooldown.
QR.cooldown.delays['thread_global'] = 300
# Retrieve recent posts and delays.
keys = QR.cooldown.keys =
local: "cooldown.#{g.BOARD}"
global: 'cooldown.global'
items = {}
items[key] = {} for scope, key of keys
$.get items, (items) ->
QR.cooldown[scope] = items[key] for scope, key of keys
QR.cooldown.start()
$.sync key, QR.cooldown.sync scope for scope, key of keys
QR.cooldown.isSetup = true
QR.cooldown.start()
start: ->
return if QR.cooldown.isCounting or Object.keys(QR.cooldown.local).length + Object.keys(QR.cooldown.global).length is 0
{data} = QR.cooldown
return unless (
QR.cooldown.isSetup and
!QR.cooldown.isCounting and
Object.keys(data[g.BOARD.ID] or {}).length + Object.keys(data.global or {}).length > 0
)
QR.cooldown.isCounting = true
QR.cooldown.count()
sync: (scope) -> (cooldowns) ->
QR.cooldown[scope] = cooldowns or {}
sync: (data) ->
QR.cooldown.data = data or {}
QR.cooldown.start()
add: (start, threadID, postID) ->
add: (threadID, postID) ->
return unless Conf['Cooldown']
start = Date.now()
boardID = g.BOARD.ID
QR.cooldown.set 'local', start, {threadID, postID}
QR.cooldown.set boardID, start, {threadID, postID}
QR.cooldown.set 'global', start, {boardID, threadID, postID} if threadID is postID
QR.cooldown.start()
@ -49,16 +57,25 @@ QR.cooldown =
return unless Conf['Cooldown']
cooldown = QR.cooldown.categorize post
cooldown.delay = delay
QR.cooldown.set 'local', Date.now(), cooldown
QR.cooldown.set g.BOARD.ID, Date.now(), cooldown
QR.cooldown.start()
delete: (post) ->
return unless Conf['Cooldown'] and g.BOARD.ID is post.board.ID
$.forceSync QR.cooldown.keys.local
for id, cooldown of QR.cooldown.local
return unless Conf['Cooldown']
$.forceSync 'cooldowns'
cooldowns = (QR.cooldown.data[post.board.ID] or= {})
for id, cooldown of cooldowns
if !cooldown.delay? and cooldown.threadID is post.thread.ID and cooldown.postID is post.ID
delete QR.cooldown.local[id]
QR.cooldown.save 'local'
delete cooldowns[id]
QR.cooldown.save [post.board.ID]
secondsDeletion: (post) ->
cooldowns = QR.cooldown.data[post.board.ID] or {}
for start, cooldown of cooldowns
if !cooldown.delay? and cooldown.threadID is post.thread.ID and cooldown.postID is post.ID
seconds = QR.cooldown.delays.deletion - (Date.now() - start) // $.SECOND
return Math.max seconds, 0
0
categorize: (post) ->
if post.thread is 'new'
@ -68,38 +85,41 @@ QR.cooldown =
threadID: +post.thread
set: (scope, id, value) ->
$.forceSync QR.cooldown.keys[scope]
QR.cooldown[scope][id] = value
$.set QR.cooldown.keys[scope], QR.cooldown[scope]
$.forceSync 'cooldowns'
cooldowns = (QR.cooldown.data[scope] or= {})
cooldowns[id] = value
$.set 'cooldowns', QR.cooldown.data
save: (scope) ->
if Object.keys(QR.cooldown[scope]).length
$.set QR.cooldown.keys[scope], QR.cooldown[scope]
else
$.delete QR.cooldown.keys[scope]
save: (scopes) ->
{data} = QR.cooldown
for scope in scopes when scope of data and !Object.keys(data[scope]).length
delete data[scope]
$.set 'cooldowns', data
count: ->
$.forceSync 'cooldowns'
save = []
nCooldowns = 0
now = Date.now()
{type, threadID} = QR.cooldown.categorize QR.posts[0]
seconds = 0
for scope, key of QR.cooldown.keys
$.forceSync key
save = false
for scope in [g.BOARD.ID, 'global']
cooldowns = (QR.cooldown.data[scope] or= {})
for start, cooldown of QR.cooldown[scope]
for start, cooldown of cooldowns
start = +start
elapsed = (now - start) // $.SECOND
if elapsed < 0 # clock changed since then?
delete QR.cooldown[scope][start]
save = true
delete cooldowns[start]
save.push scope
continue
# Explicit delays from error messages
if cooldown.delay?
if cooldown.delay <= elapsed
delete QR.cooldown[scope][start]
save = true
delete cooldowns[start]
save.push scope
else if cooldown.type is type and cooldown.threadID is threadID
# Delays only apply to the given post type and thread.
seconds = Math.max seconds, cooldown.delay - elapsed
@ -113,8 +133,8 @@ QR.cooldown =
if QR.cooldown.customCooldown
maxDelay = Math.max maxDelay, parseInt(Conf['customCooldown'], 10)
if maxDelay <= elapsed
delete QR.cooldown[scope][start]
save = true
delete cooldowns[start]
save.push scope
continue
if (type is 'thread') is (cooldown.threadID is cooldown.postID) and cooldown.boardID isnt g.BOARD.ID
@ -133,9 +153,11 @@ QR.cooldown =
if QR.cooldown.customCooldown
seconds = Math.max seconds, parseInt(Conf['customCooldown'], 10) - elapsed
QR.cooldown.save scope if save
nCooldowns += Object.keys(cooldowns).length
if Object.keys(QR.cooldown.local).length + Object.keys(QR.cooldown.global).length
QR.cooldown.save save if save.length
if nCooldowns
clearTimeout QR.cooldown.timeout
QR.cooldown.timeout = setTimeout QR.cooldown.count, $.SECOND
else

View File

@ -105,7 +105,7 @@ QR.post = class
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename']
continue unless node = QR.nodes[name]
node.value = @[name] or node.dataset.default or null
node.value = @[name] or node.dataset.default or ''
(if @thread isnt 'new' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread'
@ -122,6 +122,7 @@ QR.post = class
when 'thread'
(if @thread isnt 'new' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread'
QR.status()
@updateFlashURL()
when 'com'
@nodes.span.textContent = @com
QR.captcha.onPostChange()
@ -132,13 +133,9 @@ QR.post = class
QR.cooldown.auto = false
when 'filename'
return unless @file
@file.newName = @filename.replace /[/\\]/g, '-'
unless /\.(jpe?g|png|gif|pdf|swf|webm)$/i.test @filename
# 4chan will truncate the filename if it has no extension,
# but it will always replace the extension by the correct one,
# so we suffix it with '.jpg' when needed.
@file.newName += '.jpg'
@saveFilename()
@updateFilename()
@updateFlashURL()
when 'name'
QR.persona.set @
@ -182,16 +179,22 @@ QR.post = class
return
setFile: (@file) ->
@filename = @file.name
if Conf['Randomize Filename'] and g.BOARD.ID isnt 'f'
@filename = "#{Date.now() - Math.floor(Math.random() * 365 * $.DAY)}"
@filename += ext[0] if ext = @file.name.match QR.validExtension
else
@filename = @file.name
@filesize = $.bytesToString @file.size
@checkSize()
@nodes.label.hidden = false if QR.spoiler
QR.captcha.onPostChange()
URL.revokeObjectURL @URL
@saveFilename()
if @ is QR.selected
@showFileData()
else
@updateFilename()
@updateFlashURL()
@nodes.el.style.backgroundImage = null
unless @file.type in QR.mimeTypes
@fileError 'Unsupported file type.'
@ -207,6 +210,8 @@ QR.post = class
readFile: ->
isVideo = /^video\//.test @file.type
el = $.el(if isVideo then 'video' else 'img')
return if isVideo and !el.canPlayType @file.type
event = if isVideo then 'loadeddata' else 'load'
onload = =>
$.off el, event, onload
@ -289,9 +294,18 @@ QR.post = class
@nodes.el.style.backgroundImage = null
@nodes.label.hidden = true if QR.spoiler
@showFileData()
@updateFlashURL()
URL.revokeObjectURL @URL
@dismissErrors (error) -> $.hasClass error, 'file-error'
saveFilename: ->
@file.newName = (@filename or '').replace /[/\\]/g, '-'
unless QR.validExtension.test @filename
# 4chan will truncate the filename if it has no extension,
# but it will always replace the extension by the correct one,
# so we suffix it with '.jpg' when needed.
@file.newName += '.jpg'
updateFilename: ->
long = "#{@filename} (#{@filesize})"
@nodes.el.title = long
@ -307,6 +321,28 @@ QR.post = class
else
$.rmClass QR.nodes.fileSubmit, 'has-file'
updateFlashURL: ->
return unless g.BOARD.ID is 'f'
if @thread is 'new' or !@file
url = ''
else
url = @file.newName
url = url.replace(/"/g, '%22') if $.engine in ['blink', 'webkit']
url = url
.replace(/[\t\n\f\r \xa0\u200B\u2029\u3000]+/g, ' ')
.replace(/(^ | $)/g, '')
.replace(/\.[0-9A-Za-z]+$/, '')
url = "https://i.4cdn.org/f/#{encodeURIComponent E url}.swf\n"
oldURL = @flashURL or ''
if url isnt oldURL
@com or= ''
@com = @com[oldURL.length..] if @com[...oldURL.length] is oldURL
@com = (url + @com) or null
if @ is QR.selected
QR.nodes.com.value = @com
QR.characterCount()
@flashURL = url
pasteText: (file) ->
@pasting = true
reader = new FileReader()

View File

@ -3,7 +3,7 @@
<meta charset="utf-8">
<title>4chan X</title>
<link rel="stylesheet" href="web.css">
<link rel="icon" href="src/General/img/icon.gif">
<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">
@ -12,11 +12,18 @@
<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/issues">Report Bugs</a>
<a href="https://gitreports.com/issue/ccd0/4chan-x">Report Bugs</a>
</div>
</div>
<a class="screenshot" href="img/screenshot.png"><img src="img/screenshot.png" alt="Screenshot"></a>
<%= content.match(/<\/h1>([^]*)<h2 id="more-information"/)[1] %>
<%=
content
.match(/<\/h1>([^]*)<h2 id="more-information"/)[1]
.replace(
/(<h3 id="(.*?)">.*?<\/h3>)([^]*?)(?=<h)/g,
'<input hidden type="checkbox" id="$2-hide"><div><label for="$2-hide">$1</label>$3</div>'
)
%>
<script>
function imagePreview() {
this.removeEventListener('mouseover', imagePreview, false);
@ -30,7 +37,9 @@ function imagePreview() {
}
function storeInstall(e) {
if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.button === 0) {
chrome.webstore.install();
chrome.webstore.install(this.href, function(){}, function(){
location.href = this.href;
});
e.preventDefault();
}
}
@ -42,5 +51,18 @@ for (var i = 0; i < document.links.length; i++) {
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';
})();
if (engine) {
var engines = {'firefox': 'gecko', 'chromium': 'blink presto', 'safari': 'webkit', 'webkitgtk-': 'webkit', 'other-browsers': 'edge'};
for (browser in engines) {
document.getElementById(browser + '-hide').checked = (engines[browser].indexOf(engine) < 0);
}
}
</script>
</body></html>

18
web.css
View File

@ -70,6 +70,11 @@ span.hover > img {
max-height: 100%;
box-shadow: 5px 5px 20px rgba(0,0,0,0.4);
}
@media (max-width: 960px) {
a.screenshot:hover + span.hover {
display: none;
}
}
@supports not (pointer-events: auto) {
a[href$=".png"] {
position: relative;
@ -84,6 +89,17 @@ span.hover > img {
z-index: 1;
}
}
h2 ~ p, h2 ~ p + ul {
h2 ~ p, h2 ~ p + ul, input + div, div > label ~ * {
margin-left: 1em;
}
input + div {
margin-top: 1em;
margin-bottom: 1em;
margin-left: 1em;
}
h3 {
display: inline;
}
input:checked + div > :not(label) {
display: none;
}