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). 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
**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")] **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. - 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
**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")] **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. - Remove guard against dropping files into captcha due to continued reports of disabled captchas.

View File

@ -1,6 +1,10 @@
## Reporting bugs ## 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. 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. 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 ### Get started
- Install [node.js](http://nodejs.org/). - Install [node.js](http://nodejs.org/).
- Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`. - Install [Grunt's CLI](http://gruntjs.com/): `npm install -g grunt-cli`
- Clone 4chan X. - Clone 4chan X: `git clone https://github.com/ccd0/4chan-x.git`
- `cd` into it. - Open the directory: `cd 4chan-x`
- Install/Update 4chan X dependencies with `npm install`. - Install/Update 4chan X dependencies: `npm install`
### Build ### 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). - Edit the sources (not the compiled scripts in the builds/ directory).
- Compile the script with `grunt`. - Compile the script with `grunt`.
- Install the compiled script (found in the testbuilds/ directory), and test your changes. - 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. Archive list updates should go to https://github.com/MayhemYDG/archives.json.

View File

@ -8,6 +8,12 @@ module.exports = (grunt) ->
json = (data) -> json = (data) ->
"`#{JSON.stringify(data).replace(/`/g, '\\`')}`" "`#{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) -> importHTML = (filename) ->
html grunt.template.process(grunt.file.read("src/General/html/#{filename}.html").replace(/^ +/gm, '').replace(/\r?\n/g, ''), data: grunt.config('pkg')) 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: options: process: Object.create(null, data:
get: -> get: ->
pkg = grunt.config 'pkg' pkg = grunt.config 'pkg'
pkg.importCSS = importCSS
pkg.importHTML = importHTML pkg.importHTML = importHTML
pkg.html = html pkg.html = html
pkg.assert = assert pkg.assert = assert
@ -166,8 +173,6 @@ module.exports = (grunt) ->
stdout: true stdout: true
stderr: true stderr: true
failOnError: true failOnError: true
checkout:
command: 'git checkout <%= pkg.meta.mainBranch %>'
commit: commit:
command: """ command: """
git commit -am "Release <%= pkg.meta.name %> v<%= pkg.meta.version %>." git commit -am "Release <%= pkg.meta.name %> v<%= pkg.meta.version %>."
@ -185,12 +190,12 @@ module.exports = (grunt) ->
""".split('\n').join('&&') """.split('\n').join('&&')
stable: stable:
command: """ command: """
git push . HEAD:bstable
git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.meta.version %>." git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.meta.version %>."
git checkout gh-pages git checkout gh-pages
git pull git pull
git merge --no-commit -s ours stable git merge --no-commit -s ours stable
git checkout stable builds git checkout stable "builds/<%= pkg.name %>.*" builds/updates.xml
git checkout HEAD "builds/*<%= pkg.meta.suffix.beta %>.*"
git commit -am "Move <%= pkg.meta.name %> v<%= pkg.meta.version %> to stable channel." git commit -am "Move <%= pkg.meta.name %> v<%= pkg.meta.version %> to stable channel."
git checkout - git checkout -
""".split('\n').join('&&') """.split('\n').join('&&')
@ -201,12 +206,24 @@ module.exports = (grunt) ->
git checkout gh-pages git checkout gh-pages
git pull git pull
git merge --no-commit -s ours - 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 commit -am "Update web page."
git checkout - git checkout -
""".split('\n').join('&&') """.split('\n').join('&&')
push: push:
command: 'git push origin --tags -f && git push origin --all' 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: npm:
command: 'npm install' command: 'npm install'
update: update:
@ -282,9 +299,10 @@ module.exports = (grunt) ->
GM_setValue: true GM_setValue: true
GM_deleteValue: true GM_deleteValue: true
GM_listValues: true GM_listValues: true
GM_addValueChangeListener: true
GM_openInTab: true GM_openInTab: true
GM_info: true
GM_xmlhttpRequest: true GM_xmlhttpRequest: true
GM_info: true
cloneInto: true cloneInto: true
unsafeWindow: true unsafeWindow: true
chrome: true chrome: true
@ -432,6 +450,10 @@ module.exports = (grunt) ->
'shell:push' 'shell:push'
] ]
grunt.registerTask 'aws', [
'shell:aws'
]
grunt.registerTask 'store', [ grunt.registerTask 'store', [
'webstore_upload' '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 # 4chan X
Adds various features to 4chan. 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. 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 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. https://github.com/Nebukazar/OneeChan.
## Firefox version: [Click to Install](https://ccd0.github.io/4chan-x/builds/4chan-X.user.js) **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.
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.
**WARNING**: ## Install
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.
**Known issues**: ### Firefox
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. 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) - **Pale Moon** users should use [Greasemonkey 1.15](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/versions/1.15.1-signed).
Download the file from the link above and add drag it to your `chrome://extensions` page. - **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).
This should also work for non-Windows/dev/canary Chrome and Chromium-based versions of Opera.
**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) - **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)**.
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. - **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.
Only the latest stable version of 4chan X is available. - **Chrome**, **Vivaldi**: Install 4chan X from the **[Chrome store](https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam)**.
## Other browsers 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/).
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.
- Some people have reported success in Safari using [JS Blocker](http://jsblocker.toggleable.com/) to install the Firefox/Greasemonkey version. ### Safari
- Instructions are available for [installing 4chan X in dwb](https://github.com/ccd0/4chan-x/wiki/Installing-4chan-X-in-dwb). 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 ## 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. 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 ## 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). 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 ## Troubleshooting
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. 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 ## More information
- [Changelog](https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md) - [Changelog](https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md)
- [Frequently Asked Questions](https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions) - [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) - [Contributing](https://github.com/ccd0/4chan-x/blob/master/CONTRIBUTING.md)

Binary file not shown.

View File

@ -1,26 +1,38 @@
// ==UserScript== // ==UserScript==
// @name 4chan X beta // @name 4chan X beta
// @version 1.10.13.1 // @version 1.11.9.2
// @minGMVer 1.14 // @minGMVer 1.14
// @minFFVer 26 // @minFFVer 26
// @namespace 4chan-X // @namespace 4chan-X
// @description Cross-browser userscript for maximum lurking on 4chan. // @description Cross-browser userscript for maximum lurking on 4chan.
// @license MIT; https://github.com/ccd0/4chan-x/blob/master/LICENSE // @license MIT; https://github.com/ccd0/4chan-x/blob/master/LICENSE
// @match *://boards.4chan.org/* // @include http://boards.4chan.org/*
// @match *://sys.4chan.org/* // @include https://boards.4chan.org/*
// @match *://a.4cdn.org/* // @include http://sys.4chan.org/*
// @match *://i.4cdn.org/* // @include https://sys.4chan.org/*
// @match https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc* // @include http://a.4cdn.org/*
// @match https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc* // @include https://a.4cdn.org/*
// @match *://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc // @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_getValue
// @grant GM_setValue // @grant GM_setValue
// @grant GM_deleteValue // @grant GM_deleteValue
// @grant GM_listValues // @grant GM_listValues
// @grant GM_addValueChangeListener
// @grant GM_openInTab // @grant GM_openInTab
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @run-at document-start // @run-at document-start
// @updateURL https://ccd0.github.io/4chan-x/builds/4chan-X-beta.meta.js // @updateURL https://www.4chan-x.net/builds/4chan-X-beta.meta.js
// @downloadURL https://ccd0.github.io/4chan-x/builds/4chan-X-beta.user.js // @downloadURL https://www.4chan-x.net/builds/4chan-X-beta.user.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII= // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
// ==/UserScript== // ==/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== // ==UserScript==
// @name 4chan X // @name 4chan X
// @version 1.10.13.1 // @version 1.11.9.2
// @minGMVer 1.14 // @minGMVer 1.14
// @minFFVer 26 // @minFFVer 26
// @namespace 4chan-X // @namespace 4chan-X
// @description Cross-browser userscript for maximum lurking on 4chan. // @description Cross-browser userscript for maximum lurking on 4chan.
// @license MIT; https://github.com/ccd0/4chan-x/blob/master/LICENSE // @license MIT; https://github.com/ccd0/4chan-x/blob/master/LICENSE
// @match *://boards.4chan.org/* // @include http://boards.4chan.org/*
// @match *://sys.4chan.org/* // @include https://boards.4chan.org/*
// @match *://a.4cdn.org/* // @include http://sys.4chan.org/*
// @match *://i.4cdn.org/* // @include https://sys.4chan.org/*
// @match https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc* // @include http://a.4cdn.org/*
// @match https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc* // @include https://a.4cdn.org/*
// @match *://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc // @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_getValue
// @grant GM_setValue // @grant GM_setValue
// @grant GM_deleteValue // @grant GM_deleteValue
// @grant GM_listValues // @grant GM_listValues
// @grant GM_addValueChangeListener
// @grant GM_openInTab // @grant GM_openInTab
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @run-at document-start // @run-at document-start
// @updateURL https://ccd0.github.io/4chan-x/builds/4chan-X.meta.js // @updateURL https://www.4chan-x.net/builds/4chan-X.meta.js
// @downloadURL https://ccd0.github.io/4chan-x/builds/4chan-X.user.js // @downloadURL https://www.4chan-x.net/builds/4chan-X.user.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII= // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
// ==/UserScript== // ==/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'?> <?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'> <gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'> <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> </app>
</gupdate> </gupdate>

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'> <gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'> <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> </app>
</gupdate> </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"> <meta charset="utf-8">
<title>4chan X</title> <title>4chan X</title>
<link rel="stylesheet" href="web.css"> <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"> <link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/ohnjgmpcibpbafdlkimncjhflgedgpam">
</head><body> </head><body>
<div id="header"> <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">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/blob/master/CHANGELOG.md">Changelog</a>
<a href="https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions">FAQ</a> <a href="https://github.com/ccd0/4chan-x/wiki/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>
</div> </div>
<a class="screenshot" href="img/screenshot.png"><img src="img/screenshot.png" alt="Screenshot"></a> <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> 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 <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> <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><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>
<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> <h2 id="install">Install</h2>
<p><strong>WARNING</strong>: <input hidden type="checkbox" id="firefox-hide"><div><label for="firefox-hide"><h3 id="firefox">Firefox</h3></label>
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>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>
<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>
<ul> <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><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>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>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> </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>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> <ul>
<li><a href="https://ccd0.github.io/4chan-x/builds/4chan-X-beta.user.js">Firefox 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://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.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> </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> <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> <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> <h2 id="troubleshooting">Troubleshooting</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> <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> <script>
function imagePreview() { function imagePreview() {
@ -65,7 +90,9 @@ function imagePreview() {
} }
function storeInstall(e) { function storeInstall(e) {
if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.button === 0) { 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(); e.preventDefault();
} }
} }
@ -77,5 +104,18 @@ for (var i = 0; i < document.links.length; i++) {
link.addEventListener('click', storeInstall, false); 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> </script>
</body></html> </body></html>

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

View File

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

View File

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

View File

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

View File

@ -31,18 +31,28 @@ CrossOrigin = do ->
cb new Uint8Array(response), contentType, contentDisposition cb new Uint8Array(response), contentType, contentDisposition
<% } %> <% } %>
<% if (type === 'userscript') { %> <% 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" method: "GET"
url: url url: url
headers: headers headers: headers
overrideMimeType: "text/plain; charset=x-user-defined"
onload: (xhr) -> onload: (xhr) ->
if workaround
r = xhr.responseText r = xhr.responseText
data = new Uint8Array r.length data = new Uint8Array r.length
i = 0 i = 0
while i < r.length while i < r.length
data[i] = r.charCodeAt i data[i] = r.charCodeAt i
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] contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1]
contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1] contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1]
cb data, contentType, contentDisposition cb data, contentType, contentDisposition
@ -50,6 +60,12 @@ CrossOrigin = do ->
cb null cb null
onabort: -> onabort: ->
cb null 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) -> file: (url, cb) ->
@ -62,6 +78,9 @@ CrossOrigin = do ->
contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1] contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1]
if match if match
name = match.replace /\\"/g, '"' 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 = new Blob([data], {type: mime})
blob.name = name blob.name = name
cb blob cb blob

View File

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

View File

@ -1,9 +1,18 @@
Main = Main =
init: -> 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.hostname is 'www.google.com'
if location.pathname is '/recaptcha/api/fallback' if location.pathname is '/recaptcha/api/noscript'
$.ready -> Captcha.noscript.initFrame() $.ready -> Captcha.noscript.initFrame()
else return
if location.pathname is '/recaptcha/api/fallback'
$.ready -> Captcha.v2.initFrame()
$.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) -> $.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) ->
if enabled if enabled
$.ready -> Captcha.fixes.init() $.ready -> Captcha.fixes.init()
@ -28,8 +37,7 @@ Main =
if g.VIEW is 'thread' if g.VIEW is 'thread'
g.THREADID = +pathname[3] g.THREADID = +pathname[3]
# flatten Config into Conf # Flatten default values from Config into Conf
# and get saved or default values
flatten = (parent, obj) -> flatten = (parent, obj) ->
if obj instanceof Array if obj instanceof Array
Conf[parent] = obj[0] Conf[parent] = obj[0]
@ -45,37 +53,83 @@ Main =
for db in DataBoard.keys for db in DataBoard.keys
Conf[db] = boards: {} Conf[db] = boards: {}
Conf['selectedArchives'] = {} Conf['selectedArchives'] = {}
Conf['cooldowns'] = {}
$.get Conf, (items) -> # XXX old key names
$.extend Conf, items Conf['Except Archives from Encryption'] = false
# XXX temporarily set here so old versions update to correct setting
Conf['Fixed Thread Watcher'] ?= Conf['Toggleable Thread Watcher'] # Get saved values as items
$.asap (-> doc = d.documentElement), Main.initFeatures 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 # set up CSS when <head> is completely loaded
$.asap (-> doc = d.documentElement), -> $.asap (-> doc = d.documentElement), ->
$.onExists doc, 'body', false, Main.initStyle $.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: -> 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");' $.globalEval 'document.documentElement.classList.add("js-enabled");'
switch location.hostname switch location.hostname
when 'www.4chan.org'
Captcha.replace.init()
return
when 'a.4cdn.org' when 'a.4cdn.org'
return return
when 'sys.4chan.org' when 'sys.4chan.org'
Report.init() Report.init()
PostSuccessful.init() if g.VIEW is 'post' 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 return
when 'i.4cdn.org' when 'i.4cdn.org'
$.asap (-> d.readyState isnt 'loading'), -> $.asap (-> d.readyState isnt 'loading'), ->
if Conf['404 Redirect'] and d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] if Conf['404 Redirect'] and d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
Redirect.init()
pathname = location.pathname.split '/' pathname = location.pathname.split '/'
URL = Redirect.to 'file', Redirect.navigate 'file',
boardID: g.BOARD.ID boardID: g.BOARD.ID
filename: pathname[pathname.length - 1] filename: pathname[pathname.length - 1]
Redirect.navigate URL
else if video = $ 'video' else if video = $ 'video'
if Conf['Volume in New Tab'] if Conf['Volume in New Tab']
Volume.setup video Volume.setup video
@ -109,12 +163,15 @@ Main =
$.ready Main.initReady $.ready Main.initReady
initStyle: -> initStyle: ->
$.addStyle Main.cssWWW if location.hostname is 'www.4chan.org'
return if !Main.isThisPageLegit() or $.hasClass doc, 'fourchan-x' return if !Main.isThisPageLegit() or $.hasClass doc, 'fourchan-x'
# disable the mobile layout # disable the mobile layout
$('link[href*=mobile]', d.head)?.disabled = true $('link[href*=mobile]', d.head)?.disabled = true
$.addClass doc, 'fourchan-x', 'seaweedchan' $.addClass doc, 'fourchan-x', 'seaweedchan'
$.addClass doc, if g.VIEW is 'thread' then 'thread-view' else g.VIEW $.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' $.addStyle Main.css, 'fourchanx-css'
keyboard = false keyboard = false
@ -146,17 +203,19 @@ Main =
attributeFilter: ['href'] attributeFilter: ['href']
initReady: -> initReady: ->
if d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] # XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post.
if g.VIEW is 'thread' 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, -> ThreadWatcher.set404 g.BOARD.ID, g.THREADID, ->
if Conf['404 Redirect'] if Conf['404 Redirect']
href = Redirect.to 'thread', Redirect.navigate 'thread',
boardID: g.BOARD.ID boardID: g.BOARD.ID
threadID: g.THREADID threadID: g.THREADID
postID: +location.hash.match /\d+/ # post number or 0 postID: +location.hash.match /\d+/ # post number or 0
Redirect.navigate href, "/#{g.BOARD}/" , "/#{g.BOARD}/"
return return
return if d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
# 4chan Pass Link # 4chan Pass Link
if styleSelector = $.id 'styleSelector' if styleSelector = $.id 'styleSelector'
passLink = $.el 'a', passLink = $.el 'a',
@ -174,25 +233,7 @@ Main =
else else
$.event '4chanXInitFinished' $.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 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 try
localStorage.getItem '4chan-settings' localStorage.getItem '4chan-settings'
catch err catch err
@ -263,11 +304,11 @@ Main =
else if errors.length is 1 else if errors.length is 1
error = errors[0] error = errors[0]
if error if error
new Notice 'error', Main.parseError(error), 15 new Notice 'error', Main.parseError(error, Main.reportLink([error])), 15
return return
div = $.el 'div', 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', -> $.on div.lastElementChild, 'click', ->
[@textContent, logs.hidden] = if @textContent is 'show' [@textContent, logs.hidden] = if @textContent is 'show'
['hide', false] ['hide', false]
@ -281,13 +322,34 @@ Main =
new Notice 'error', [div, logs], 30 new Notice 'error', [div, logs], 30
parseError: (data) -> parseError: (data, reportLink) ->
c.error data.message, data.error.stack c.error data.message, data.error.stack
message = $.el 'div', message = $.el 'div',
textContent: data.message <%= html('${data.message}?{reportLink}{&{reportLink}}') %>
error = $.el 'div', error = $.el 'div',
textContent: "#{data.error.name or 'Error'}: #{data.error.message or 'see console for details'}" 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: -> isThisPageLegit: ->
# 404 error page or similar. # 404 error page or similar.
@ -301,17 +363,13 @@ Main =
$.ready -> $.ready ->
cb() if Main.isThisPageLegit() cb() if Main.isThisPageLegit()
css: `<%= css: `<%= importCSS('font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon') %>`
grunt.template.process(
['font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'].map(function(name) { cssWWW: `<%= importCSS('www') %>`
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, '\\`')
%>`
features: [ features: [
['Polyfill', Polyfill] ['Polyfill', Polyfill]
['Captcha Replacement', Captcha.replace]
['Redirect', Redirect] ['Redirect', Redirect]
['Header', Header] ['Header', Header]
['Catalog Links', CatalogLinks] ['Catalog Links', CatalogLinks]
@ -332,6 +390,7 @@ Main =
['Recursive', Recursive] ['Recursive', Recursive]
['Strike-through Quotes', QuoteStrikeThrough] ['Strike-through Quotes', QuoteStrikeThrough]
['Quick Reply', QR] ['Quick Reply', QR]
['Cooldown', QR.cooldown]
['Menu', Menu] ['Menu', Menu]
['Index Generator (Menu)', Index.menu] ['Index Generator (Menu)', Index.menu]
['Report Link', ReportLink] ['Report Link', ReportLink]

View File

@ -103,6 +103,7 @@ Settings =
description = arr[1] description = arr[1]
div = $.el 'div', div = $.el 'div',
<%= html('<label><input type="checkbox" name="${key}">${key}</label><span class="description">: ${description}</span>') %> <%= 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 input = $ 'input', div
$.on input, 'change', -> $.on input, 'change', ->
@parentNode.parentNode.dataset.checked = @checked @parentNode.parentNode.dataset.checked = @checked
@ -155,11 +156,9 @@ Settings =
a = $.el 'a', a = $.el 'a',
download: "<%= meta.name %> v#{g.VERSION}-#{data.date}.json" download: "<%= meta.name %> v#{g.VERSION}-#{data.date}.json"
href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}" href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}"
<% if (type === 'userscript') { %>
p = $ '.imp-exp-result', Settings.dialog p = $ '.imp-exp-result', Settings.dialog
$.rmAll p $.rmAll p
$.add p, a $.add p, a
<% } %>
a.click() a.click()
import: -> import: ->
$('input[type=file]', @parentNode).click() $('input[type=file]', @parentNode).click()
@ -314,7 +313,7 @@ Settings =
items = {} items = {}
inputs = {} 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 input = $ "[name='#{name}']", section
items[name] = Conf[name] items[name] = Conf[name]
inputs[name] = input inputs[name] = input
@ -325,7 +324,7 @@ Settings =
$.on input, 'change', Settings[name] $.on input, 'change', Settings[name]
else else
$.on input, 'input', $.cb.value $.on input, 'input', $.cb.value
$.on input, 'input', Settings[name] $.on input, 'input', Settings[name] if name of Settings
# Quick Reply Personas # Quick Reply Personas
ta = $ '.personafield', section ta = $ '.personafield', section
@ -338,7 +337,7 @@ Settings =
for key, val of items for key, val of items
input = inputs[key] input = inputs[key]
input.value = val input.value = val
continue if key in ['usercss', 'customCooldown'] if key of Settings and key isnt 'usercss'
Settings[key].call input Settings[key].call input
return return

View File

@ -102,7 +102,13 @@ UI = do ->
insertEntry: (entry, parent, data) -> insertEntry: (entry, parent, data) ->
if typeof entry.open is 'function' if typeof entry.open is 'function'
try
return unless entry.open data return unless entry.open data
catch err
Main.handleErrors
message: "Error in building the #{@type} menu."
error: err
return
$.add parent, entry.el $.add parent, entry.el
return unless entry.subEntries return unless entry.subEntries
@ -331,11 +337,9 @@ UI = do ->
$.on d, 'keydown', o.hoverend $.on d, 'keydown', o.hoverend
$.on root, 'mousemove', o.hover $.on root, 'mousemove', o.hover
<% if (type === 'userscript') { %>
# Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955 # Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955
o.workaround = (e) -> o.hoverend(e) unless root.contains e.target o.workaround = (e) -> o.hoverend(e) unless root.contains e.target
$.on doc, 'mousemove', o.workaround $.on doc, 'mousemove', o.workaround
<% } %>
hover = (e) -> hover = (e) ->
@latestEvent = e @latestEvent = e
@ -365,10 +369,8 @@ UI = do ->
$.off @root, @endEvents, @hoverend $.off @root, @endEvents, @hoverend
$.off d, 'keydown', @hoverend $.off d, 'keydown', @hoverend
$.off @root, 'mousemove', @hover $.off @root, 'mousemove', @hover
<% if (type === 'userscript') { %>
# Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955 # Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955
$.off doc, 'mousemove', @workaround $.off doc, 'mousemove', @workaround
<% } %>
@cb.call @ if @cb @cb.call @ if @cb
checkbox = (name, text, checked) -> 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 */ /* party hats */
pointer-events: none; pointer-events: none;
} }
#g-recaptcha {
min-height: 78px;
height: auto;
}
:root:not(.js-enabled) #postForm { :root:not(.js-enabled) #postForm {
display: table; display: table;
} }
#captchaContainerAlt td:nth-child(2) {
display: table-cell !important;
}
/* Anti-autoplay */ /* Anti-autoplay */
audio.controls-added { audio.controls-added {
@ -120,6 +127,11 @@ audio.controls-added {
height: auto; height: auto;
text-align: center; text-align: center;
} }
:root.anti-autoplay .autoplay-removed {
display: block !important;
min-width: 640px;
min-height: 390px;
}
/* fixed, z-index */ /* fixed, z-index */
#overlay, #overlay,
@ -161,9 +173,15 @@ audio.controls-added {
#embedding { #embedding {
z-index: 11; z-index: 11;
} }
#thread-watcher { :root.fixed-watcher #thread-watcher {
z-index: 10; z-index: 10;
} }
:root.fixed:not(.gallery-open) #header-bar:not(:hover) {
z-index: 8;
}
#thread-watcher {
z-index: 5;
}
/* Header */ /* Header */
.fixed.top-header body { .fixed.top-header body {
@ -239,7 +257,7 @@ audio.controls-added {
height: 10px; height: 10px;
position: absolute; position: absolute;
} }
:root:not(.autohide) #scroll-marker { #header-bar:not(.autohide) #scroll-marker {
pointer-events: none; pointer-events: none;
} }
#header-bar #scroll-marker { #header-bar #scroll-marker {
@ -318,6 +336,7 @@ audio.controls-added {
flex: auto; flex: auto;
display: -webkit-flex; display: -webkit-flex;
display: 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 > a,
:root.fixed:not(.centered-links) #full-board-list > .boardList > span:not(.space):not(.spacer) { :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 { .section-filter textarea {
height: 500px; height: 500px;
} }
.section-filter a { .section-filter a, .section-advanced a {
text-decoration: underline; text-decoration: underline;
} }
.section-sauce textarea { .section-sauce textarea {
@ -621,10 +640,10 @@ div[data-checked="false"] > .suboption-list {
left: -1em; left: -1em;
width: 0; width: 0;
} }
<% if (type === 'crx') { %>
/* ``::-webkit-*'' selectors break selector lists on Firefox. */ /* ``::-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 { #index-search:not([data-searching]) + #index-search-clear {
display: none; display: none;
} }
@ -792,10 +811,11 @@ span.hide-announcement {
#thread-watcher { #thread-watcher {
padding-bottom: 3px; padding-bottom: 3px;
padding-left: 3px; padding-left: 3px;
overflow: hidden;
white-space: nowrap; white-space: nowrap;
min-width: 146px; min-width: 146px;
max-height: 92%; }
#watched-threads {
overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
} }
#thread-watcher .refresh { #thread-watcher .refresh {
@ -804,7 +824,12 @@ span.hide-announcement {
:root.fixed-watcher #thread-watcher { :root.fixed-watcher #thread-watcher {
position: fixed; 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; max-height: 210px;
overflow-y: hidden; overflow-y: hidden;
} }
@ -976,6 +1001,11 @@ span.hide-announcement {
:root.fit-height .full-image { :root.fit-height .full-image {
max-height: 100vh; 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 { :root.fit-width .full-image {
max-width: 100%; max-width: 100%;
} }
@ -1150,6 +1180,13 @@ input[name="Default Volume"] {
min-width: 300px; min-width: 300px;
border-radius: 3px 3px 0 0; 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 { #qrtab {
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
} }
@ -1222,7 +1259,7 @@ input.field.tripped:not(:hover):not(:focus) {
top: 2px; top: 2px;
} }
/* Noscript Recaptcha */ /* Recaptcha v1 */
.captcha-img { .captcha-img {
margin: 0px; margin: 0px;
text-align: center; text-align: center;
@ -1235,7 +1272,7 @@ input.field.tripped:not(:hover):not(:focus) {
width: 100%; width: 100%;
margin: 1px 0 0; margin: 1px 0 0;
} }
#qr-captcha-iframe { #qr.captcha-v1 #qr-captcha-iframe {
display: none; display: none;
} }
@ -1263,6 +1300,13 @@ input.field.tripped:not(:hover):not(:focus) {
display: block; display: block;
width: 100%; width: 100%;
} }
#qr.captcha-v2 #qr-captcha-iframe {
width: 302px;
height: 423px;
border: 0;
display: block;
margin: auto;
}
.goog-bubble-content { .goog-bubble-content {
max-width: 100vw; max-width: 100vw;
max-height: 100vh; max-height: 100vh;
@ -1317,6 +1361,7 @@ input#qr-filename {
.has-file #qr-filename { .has-file #qr-filename {
-webkit-flex: 1 1 auto; -webkit-flex: 1 1 auto;
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; display: inline-block;
padding: 0; padding: 0;
padding-left: 3px; padding-left: 3px;
@ -1466,6 +1511,8 @@ a:only-of-type > .remove {
} }
.textarea { .textarea {
position: relative; position: relative;
display: -webkit-flex;
display: flex;
} }
:root.webkit .textarea { :root.webkit .textarea {
margin-bottom: -2px; margin-bottom: -2px;
@ -1505,7 +1552,7 @@ a:only-of-type > .remove {
height: 15px; height: 15px;
text-align: center; text-align: center;
} }
.menu-button + .container:not(:empty) { .menu-button + .container :first-child {
margin-left: -5px; margin-left: -5px;
} }
#menu { #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 img,
.gal-fit-height .gal-image video { .gal-fit-height .gal-image video {
/* /* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
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.
*/
max-height: 95vh; max-height: 95vh;
max-height: calc(100vh - 25px); max-height: calc(100vh - 25px);
} }
@ -1860,6 +1902,6 @@ grunt.file.expand('src/General/img/links/*.png').map(function(file) {
width: 4em; width: 4em;
} }
:root.gallery-open.fixed #header-bar:not(.autohide), :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; 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> </table>
</fieldset> </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> <fieldset>
<legend>Custom Board Navigation</legend> <legend>Custom Board Navigation</legend>
<div><textarea name="boardnav" class="field" spellcheck="false"></textarea></div> <div><textarea name="boardnav" class="field" spellcheck="false"></textarea></div>
@ -41,7 +47,7 @@
<fieldset> <fieldset>
<legend>Time Formatting <span class="warning" data-feature="Time Formatting">is disabled.</span></legend> <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><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>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>Month: <code>%m</code>, <code>%b</code>, <code>%B</code></div>
<div>Year: <code>%y</code>, <code>%Y</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> <div class="warning"><code>Filter</code> is disabled.</div>
<p> <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> 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> 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. MD5 filtering uses exact string matching, not regular expressions.

View File

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

View File

@ -117,6 +117,11 @@ $.asap = (test, cb) ->
$.onExists = (root, selector, subtree, cb) -> $.onExists = (root, selector, subtree, cb) ->
if el = $ selector, root if el = $ selector, root
return cb el 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 -> observer = new MutationObserver ->
if el = $ selector, root if el = $ selector, root
observer.disconnect() observer.disconnect()
@ -156,7 +161,7 @@ $.hasClass = (el, className) ->
className in el.classList className in el.classList
$.rm = (el) -> $.rm = (el) ->
el.remove() el?.remove()
$.rmAll = (root) -> $.rmAll = (root) ->
# https://gist.github.com/MayhemYDG/8646194 # 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 = $.open =
<% if (type === 'userscript') { %> <% if (type === 'userscript') { %>
if GM_openInTab?
GM_openInTab GM_openInTab
else
(url) -> window.open url, '_blank'
<% } else { %> <% } else { %>
(URL) -> window.open URL, '_blank' (url) -> window.open url, '_blank'
<% } %> <% } %>
$.debounce = (wait, fn) -> $.debounce = (wait, fn) ->
@ -320,6 +328,12 @@ $.minmax = (value, min, max) ->
$.hasAudio = (video) -> $.hasAudio = (video) ->
video.mozHasAudio or !!video.webkitAudioDecodedByteCount 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 = (key, val) ->
item = {} item = {}
item[key] = val item[key] = val
@ -434,24 +448,63 @@ do ->
<% } else { %> <% } else { %>
# http://wiki.greasespot.net/Main_Page # 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
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]
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]
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) -> $.sync = (key, cb) ->
key = g.NAMESPACE + key key = g.NAMESPACE + key
$.syncing[key] = cb $.syncing[key] = cb
$.oldValue[key] = GM_getValue key $.oldValue[key] = $.getValue key
do -> do ->
onChange = (key) -> onChange = (key) ->
return unless cb = $.syncing[key] return unless cb = $.syncing[key]
newValue = GM_getValue key newValue = $.getValue key
return if newValue is $.oldValue[key] return if newValue is $.oldValue[key]
if newValue? if newValue?
$.oldValue[key] = newValue $.oldValue[key] = newValue
cb JSON.parse(newValue), key cb JSON.parse(newValue), key[g.NAMESPACE.length..]
else else
delete $.oldValue[key] delete $.oldValue[key]
cb undefined, key cb undefined, key[g.NAMESPACE.length..]
$.on window, 'storage', ({key}) -> onChange key $.on window, 'storage', ({key}) -> onChange key
$.forceSync = (key) -> $.forceSync = (key) ->
@ -464,12 +517,7 @@ $.delete = (keys) ->
unless keys instanceof Array unless keys instanceof Array
keys = [keys] keys = [keys]
for key in keys for key in keys
key = g.NAMESPACE + key $.deleteValue g.NAMESPACE + key
GM_deleteValue key
if key of $.syncing
delete $.oldValue[key]
# for `storage` events
localStorage.removeItem key
return return
$.get = (key, val, cb) -> $.get = (key, val, cb) ->
@ -480,38 +528,27 @@ $.get = (key, val, cb) ->
cb = val cb = val
$.queueTask -> $.queueTask ->
for key of items for key of items
if val = GM_getValue g.NAMESPACE + key if val = $.getValue g.NAMESPACE + key
items[key] = JSON.parse val items[key] = JSON.parse val
cb items cb items
$.set = do -> $.set = (keys, val, cb) ->
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' if typeof keys is 'string'
set keys, val $.setValue(g.NAMESPACE + keys, JSON.stringify val)
else else
set key, value for key, value of keys for key, value of keys
$.setValue(g.NAMESPACE + key, JSON.stringify value)
cb = val cb = val
cb?() cb?()
$.clear = (cb) -> $.clear = (cb) ->
# XXX https://github.com/greasemonkey/greasemonkey/issues/2033 # XXX https://github.com/greasemonkey/greasemonkey/issues/2033
# Also support case where GM_listValues is not defined.
$.delete Object.keys(Conf) $.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']) $.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 try
$.delete GM_listValues().map (key) -> key.replace g.NAMESPACE, '' $.delete $.listValues().map (key) -> key.replace g.NAMESPACE, ''
cb?() cb?()
<% } %> <% } %>

View File

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

View File

@ -19,7 +19,16 @@ class Clone extends Post
quote: $ '.postNum > a:nth-of-type(2)', info quote: $ '.postNum > a:nth-of-type(2)', info
comment: $ '.postMessage', post comment: $ '.postMessage', post
quotelinks: [] 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. # Remove inlined posts inside of this post.
for inline in $$ '.inline', post for inline in $$ '.inline', post

View File

@ -1,12 +1,18 @@
class Connection class Connection
constructor: (@target, @origin, @cb) -> constructor: (@target, @origin, @cb={}) ->
$.on window, 'message', @onMessage $.on window, 'message', @onMessage
targetWindow: ->
if @target instanceof window.HTMLIFrameElement
@target.contentWindow
else
@target
send: (data) => send: (data) =>
@target.postMessage "#{g.NAMESPACE}#{JSON.stringify data}", @origin @targetWindow().postMessage "#{g.NAMESPACE}#{JSON.stringify data}", @origin
onMessage: (e) => onMessage: (e) =>
return unless e.source is @target and return unless e.source is @targetWindow() and
e.origin is @origin and e.origin is @origin and
typeof e.data is 'string' and typeof e.data is 'string' and
e.data[...g.NAMESPACE.length] is g.NAMESPACE 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} return false unless url = Redirect.to 'post', {@boardID, @postID}
if /^https:\/\//.test(url) or location.protocol is 'http:' if /^https:\/\//.test(url) or location.protocol is 'http:'
$.cache url, $.cache url,
do (self = @) -> -> self.parseArchivedPost @response do (self = @) -> -> self.parseArchivedPost @response, url
, ,
responseType: 'json' responseType: 'json'
withCredentials: url.archive.withCredentials withCredentials: url.archive.withCredentials
@ -93,11 +93,11 @@ class Fetcher
# Image/thumbnail URLs loaded over HTTP can be modified in transit. # 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. # 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 delete media[key] unless media[key]? and media[key].match(/^(http:\/\/[^\/]+\/)?/)[0] in url.archive.imagehosts
@parseArchivedPost response @parseArchivedPost response, url
return true return true
return false return false
parseArchivedPost: (data) -> parseArchivedPost: (data, url) ->
# In case of multiple callbacks for the same request, # In case of multiple callbacks for the same request,
# don't parse the same original post more than once. # don't parse the same original post more than once.
if post = g.posts["#{@boardID}.#{@postID}"] if post = g.posts["#{@boardID}.#{@postID}"]
@ -147,6 +147,9 @@ class Fetcher
commentHTML: comment commentHTML: comment
delete o.info.uniqueID if o.info.capcode delete o.info.uniqueID if o.info.capcode
if data.media?.media_filename 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 = o.file =
name: data.media.media_filename name: data.media.media_filename
url: data.media.media_link or data.media.remote_media_link or url: data.media.media_link or data.media.remote_media_link or

View File

@ -1,19 +1,6 @@
Polyfill = Polyfill =
init: -> init: ->
@notificationPermission()
@toBlob() @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: -> toBlob: ->
HTMLCanvasElement::toBlob or= (cb) -> HTMLCanvasElement::toBlob or= (cb) ->
data = atob @toDataURL()[22..] data = atob @toDataURL()[22..]
@ -23,12 +10,3 @@ Polyfill =
for i in [0...l] by 1 for i in [0...l] by 1
ui8a[i] = data.charCodeAt i ui8a[i] = data.charCodeAt i
cb new Blob [ui8a], type: 'image/png' 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 comment: $ '.postMessage', post
links: [] links: []
quotelinks: [] 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' unless @isReply = $.hasClass post, 'reply'
@thread.OP = @ @thread.OP = @
@ -226,7 +235,7 @@ class Post
# XXX Workaround for 4chan's racing condition # XXX Workaround for 4chan's racing condition
# giving us false-positive dead posts. # giving us false-positive dead posts.
resurrect: -> resurrect: ->
delete @isDead @isDead = false
$.rmClass @nodes.root, 'deleted-post' $.rmClass @nodes.root, 'deleted-post'
strong = $ 'strong.warning', @nodes.info strong = $ 'strong.warning', @nodes.info
# no false-positive files # no false-positive files
@ -245,7 +254,6 @@ class Post
return return
collect: -> collect: ->
@kill()
g.posts.rm @fullID g.posts.rm @fullID
@thread.posts.rm @ @thread.posts.rm @
@board.posts.rm @ @board.posts.rm @

View File

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

View File

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

View File

@ -128,53 +128,66 @@ Gallery =
Gallery.images.push thumb Gallery.images.push thumb
$.add Gallery.nodes.thumbs, 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) -> open: (thumb) ->
{nodes} = Gallery {nodes} = Gallery
{name} = nodes
oldID = +nodes.current.dataset.id oldID = +nodes.current.dataset.id
newID = +thumb.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' $.addClass thumb, 'gal-highlight'
nodes.thumbs.scrollTop = thumb.offsetTop + thumb.offsetHeight/2 - nodes.thumbs.clientHeight/2
elType = if /\.webm$/.test(thumb.href) # Load image or use preloaded image
'video' if Gallery.cache?.dataset.id is ''+newID
else if /\.pdf$/.test(thumb.href) file = Gallery.cache
'iframe' $.off file, 'error', Gallery.cacheError
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 $.on file, 'error', Gallery.error
file.src = name.href = thumb.href else
file = Gallery.load thumb, Gallery.error
# Replace old image with new one
$.off nodes.current, 'error', Gallery.error $.off nodes.current, 'error', Gallery.error
ImageCommon.pause nodes.current ImageCommon.pause nodes.current
$.replace nodes.current, file $.replace nodes.current, file
if elType is 'video' nodes.current = file
if file.nodeName is 'VIDEO'
file.loop = true file.loop = true
Volume.setup file Volume.setup file
file.play() if Conf['Autoplay'] file.play() if Conf['Autoplay']
ImageCommon.addControls file if Conf['Show Controls'] 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.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.frame.scrollTop = 0
nodes.next.focus() 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() Gallery.setupTimer()
else else
Gallery.cb.stop() Gallery.cb.stop()
# Scroll to post # Scroll to post
if Conf['Scroll to Post'] and post = (post = g.posts[file.dataset.post])?.nodes.root if Conf['Scroll to Post'] and (post = g.posts[file.dataset.post])
Header.scrollTo post Header.scrollTo post.nodes.root
# Center selected thumbnail # Preload next image
nodes.thumbs.scrollTop = thumb.offsetTop + thumb.offsetHeight/2 - nodes.thumbs.clientHeight/2 Gallery.cache = Gallery.load Gallery.images[(newID + 1) % Gallery.images.length], Gallery.cacheError
error: -> error: ->
if @error?.code is MediaError.MEDIA_ERR_DECODE if @error?.code is MediaError.MEDIA_ERR_DECODE
@ -185,6 +198,9 @@ Gallery =
Gallery.images[@dataset.id].href = url Gallery.images[@dataset.id].href = url
@src = url if Gallery.nodes.current is @ @src = url if Gallery.nodes.current is @
cacheError: ->
delete Gallery.cache
cleanupTimer: -> cleanupTimer: ->
clearTimeout Gallery.timeoutID clearTimeout Gallery.timeoutID
{current} = Gallery.nodes {current} = Gallery.nodes
@ -301,6 +317,14 @@ Gallery =
setFitness: -> setFitness: ->
(if @checked then $.addClass else $.rmClass) doc, "gal-#{@name.toLowerCase().replace /\s+/g, '-'}" (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 setDelay: -> Gallery.delay = +@value
menu: menu:
@ -319,14 +343,14 @@ Gallery =
createSubEntry: (name) -> createSubEntry: (name) ->
label = UI.checkbox name, name label = UI.checkbox name, name
input = label.firstElementChild input = label.firstElementChild
if name in ['Fit Width', 'Fit Height', 'Hide Thumbnails'] $.on input, 'change', Gallery.cb.setFitness if name in ['Hide Thumbnails', 'Fit Width', 'Fit Height']
$.on input, 'change', Gallery.cb.setFitness
$.event 'change', null, input $.event 'change', null, input
$.on input, 'change', $.cb.checked $.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 el: label
createSubEntries: -> 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">') %> delayLabel = $.el 'label', <%= html('Slide Delay: <input type="number" name="Slide Delay" min="0" step="any" class="field">') %>
delayInput = delayLabel.firstElementChild delayInput = delayLabel.firstElementChild

View File

@ -83,7 +83,7 @@ ImageCommon =
$.off video, 'mouseover', handler $.off video, 'mouseover', handler
# Hacky workaround for Firefox forever-loading bug for very short videos # Hacky workaround for Firefox forever-loading bug for very short videos
t = new Date().getTime() 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 video.controls = true
$.on video, 'mouseover', handler $.on video, 'mouseover', handler

View File

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

View File

@ -46,7 +46,7 @@ ImageHover =
el.play() if Conf['Autoplay'] el.play() if Conf['Autoplay']
[width, height] = (+x for x in file.dimensions.split 'x') [width, height] = (+x for x in file.dimensions.split 'x')
{left, right} = @getBoundingClientRect() {left, right} = @getBoundingClientRect()
padding = 16 padding = 25
maxWidth = Math.max left, doc.clientWidth - right maxWidth = Math.max left, doc.clientWidth - right
maxHeight = doc.clientHeight - padding maxHeight = doc.clientHeight - padding
scale = Math.min 1, maxWidth / width, maxHeight / height 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 clone.file.thumb.preload = 'auto' for clone in post.clones
thumb.preload = 'auto' thumb.preload = 'auto'
# XXX Cloned video elements with poster in Firefox cause momentary display of image loading icon. # 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' $.on thumb, 'loadeddata', -> @removeAttribute 'poster'
return return

View File

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

View File

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

View File

@ -79,7 +79,7 @@ Linkify =
) )
| # This should account for virtually all links posted without http: | # This should account for virtually all links posted without http:
([-a-z\d]+[.])+( ([-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'"])) )([:/]|(?![^\s'"]))
| # IPv4 Addresses | # IPv4 Addresses
[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3} [\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}

View File

@ -1,4 +1,6 @@
DeleteLink = DeleteLink =
auto: [{}, {}]
init: -> init: ->
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Delete Link'] return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Delete Link']
@ -11,19 +13,22 @@ DeleteLink =
fileEl = $.el 'a', fileEl = $.el 'a',
className: 'delete-file' className: 'delete-file'
href: 'javascript:;' href: 'javascript:;'
@nodes =
menu: div.firstChild
links: [postEl, fileEl]
postEntry = postEntry =
el: postEl el: postEl
open: -> open: ->
postEl.textContent = 'Post' postEl.textContent = DeleteLink.linkText false
$.on postEl, 'click', DeleteLink.delete $.on postEl, 'click', DeleteLink.toggle
true true
fileEntry = fileEntry =
el: fileEl el: fileEl
open: ({file}) -> open: ({file}) ->
return false if !file or file.isDead return false if !file or file.isDead
fileEl.textContent = 'File' fileEl.textContent = DeleteLink.linkText true
$.on fileEl, 'click', DeleteLink.delete $.on fileEl, 'click', DeleteLink.toggle
true true
Menu.menu.addEntry Menu.menu.addEntry
@ -32,19 +37,41 @@ DeleteLink =
open: (post) -> open: (post) ->
return false if post.isDead return false if post.isDead
DeleteLink.post = post DeleteLink.post = post
node = div.firstChild DeleteLink.nodes.menu.textContent = DeleteLink.menuText()
node.textContent = 'Delete' DeleteLink.cooldown.start post
DeleteLink.cooldown.start post, node
true true
subEntries: [postEntry, fileEntry] subEntries: [postEntry, fileEntry]
delete: -> menuText: ->
{post} = DeleteLink if seconds = DeleteLink.cooldown.seconds[DeleteLink.post.fullID]
return if DeleteLink.cooldown.counting is post "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' 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 = form =
mode: 'usrdel' mode: 'usrdel'
@ -52,47 +79,56 @@ DeleteLink =
pwd: QR.persona.getPassword() pwd: QR.persona.getPassword()
form[post.ID] = 'delete' form[post.ID] = 'delete'
link = @
$.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"), $.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"),
responseType: 'document' responseType: 'document'
withCredentials: true withCredentials: true
onload: -> DeleteLink.load link, post, fileOnly, @response onload: -> DeleteLink.load link, post, fileOnly, @response
onerror: -> DeleteLink.error link onerror: -> DeleteLink.error link, post
, ,
form: $.formData form form: $.formData form
load: (link, post, fileOnly, resDoc) -> load: (link, post, fileOnly, resDoc) ->
link.textContent = DeleteLink.linkText fileOnly
if resDoc.title is '4chan - Banned' # Ban/warn check 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! else if msg = resDoc.getElementById 'errmsg' # error!
s = msg.textContent new Notice 'warning', msg.textContent, 20
$.on link, 'click', DeleteLink.delete $.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 else
QR.cooldown.delete post unless fileOnly
if resDoc.title is 'Updating index...' if resDoc.title is 'Updating index...'
# We're 100% sure. # We're 100% sure.
QR.cooldown.delete post
(post.origin or post).kill fileOnly (post.origin or post).kill fileOnly
s = 'Deleted' link.textContent = 'Deleted' if post.fullID is DeleteLink.post.fullID
link.textContent = s
error: (link) -> error: (link, post) ->
link.textContent = 'Connection error, please retry.' new Notice 'warning', 'Connection error, please retry.', 20
$.on link, 'click', DeleteLink.delete $.on link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
cooldown: cooldown:
start: (post, node) -> seconds: {}
unless QR.db?.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}
# Only start counting on our posts. start: (post, seconds) ->
delete DeleteLink.cooldown.counting # 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 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})"

View File

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

View File

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

View File

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

View File

@ -1,19 +1,16 @@
Report = Report =
css: ''' css: `<%= importCSS('report') %>`
:root:not(.js-enabled) #g-recaptcha {
height: auto;
}
'''
init: -> init: ->
return unless /\bmode=report\b/.test(location.search) and match = location.search.match /\bno=(\d+)/ return unless /\bmode=report\b/.test(location.search) and match = location.search.match /\bno=(\d+)/
Captcha.replace.init()
@postID = +match[1] @postID = +match[1]
$.ready @ready $.ready @ready
ready: -> ready: ->
$.addStyle Report.css $.addStyle Report.css
Report.archive() if Conf['Archive Report'] 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, new MutationObserver(-> Report.fit '.gc-bubbleDefault').observe d.body,
childList: true childList: true
attributes: true attributes: true
@ -47,4 +44,4 @@ Report =
if types = $.id('reportTypes') if types = $.id('reportTypes')
$.on types, 'change', (e) -> $.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'] ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
$.on window, 'online offline', ThreadUpdater.cb.online
$.on d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost $.on d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost
$.on d, 'visibilitychange', ThreadUpdater.cb.visibility $.on d, 'visibilitychange', ThreadUpdater.cb.visibility
ThreadUpdater.setInterval() 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/ http://freesound.org/people/pierrecartoons1979/sounds/90112/
cc-by-nc-3.0 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"}) %>' beep: 'data:audio/wav;base64,<%= grunt.file.read("src/General/audio/beep.wav", {encoding: "base64"}) %>'
cb: 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) -> checkpost: (e) ->
return if e.detail.threadID isnt ThreadUpdater.thread.ID return if e.detail.threadID isnt ThreadUpdater.thread.ID
ThreadUpdater.postID = e.detail.postID ThreadUpdater.postID = e.detail.postID
@ -189,12 +179,6 @@ ThreadUpdater =
ThreadUpdater.set 'timer', 'Update' ThreadUpdater.set 'timer', 'Update'
return return
unless navigator.onLine
ThreadUpdater.set 'status', 'Offline', 'warning'
unless Conf['Ignore Offline Status']
ThreadUpdater.set 'timer', ''
return
{interval} = ThreadUpdater {interval} = ThreadUpdater
if Conf['Optional Increase'] if Conf['Optional Increase']
# Lower the max refresh rate limit on visible tabs. # Lower the max refresh rate limit on visible tabs.
@ -289,6 +273,13 @@ ThreadUpdater =
index.push ID index.push ID
files.push ID if postObject.fsize 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. # Insert new posts, not older ones.
continue if ID <= lastPost continue if ID <= lastPost

View File

@ -127,6 +127,8 @@ Unread =
openNotification: (post) -> openNotification: (post) ->
return unless Header.areNotificationsEnabled return unless Header.areNotificationsEnabled
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1130502 (SeaMonkey)
try
notif = new Notification "#{post.info.nameBlock} replied to you", notif = new Notification "#{post.info.nameBlock} replied to you",
body: post.info.commentDisplay body: post.info.commentDisplay
icon: Favicon.logo icon: Favicon.logo
@ -241,6 +243,5 @@ Unread =
Favicon[if isDead then 'unreadDead' else 'unread'] Favicon[if isDead then 'unreadDead' else 'unread']
else else
Favicon[if isDead then 'dead' else 'default'] Favicon[if isDead then 'dead' else 'default']
unless chrome?
# `favicon.href = href` doesn't work on Firefox. # `favicon.href = href` doesn't work on Firefox.
$.add d.head, Favicon.el $.add d.head, Favicon.el

View File

@ -1,6 +1,8 @@
Captcha.fixes = Captcha.fixes =
imageKeys: '789456123uiojklm'.split('').concat(['Comma', 'Period'])
css: ''' css: '''
.rc-imageselect-target > .rc-imageselect-tile > img:focus { .rc-imageselect-target > div:focus {
outline: 2px solid #4a90e2; outline: 2px solid #4a90e2;
} }
.rc-button-default:focus { .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: -> init: ->
switch location.pathname.split('/')[3] switch location.pathname.split('/')[3]
when 'anchor' then @initMain() when 'anchor' then @initMain()
when 'frame' then @initPopup() when 'frame' then @initPopup()
when 'fallback' then @initNoscript()
initMain: -> initMain: ->
$.onExists d.body, '#recaptcha-anchor', true, (checkbox) -> $.onExists d.body, '#recaptcha-anchor', true, (checkbox) ->
@ -28,35 +49,66 @@ Captcha.fixes =
new MutationObserver(=> @fixImages()).observe d.body, {childList: true, subtree: true} new MutationObserver(=> @fixImages()).observe d.body, {childList: true, subtree: true}
$.on d, 'keydown', @keybinds.bind(@) $.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: -> fixImages: ->
return unless (@images = $$ '.rc-imageselect-target > .rc-imageselect-tile > img').length @images = $$ '.rc-imageselect-target > div'
focus = @images[0].tabIndex isnt 0
for img in @images for img in @images
img.tabIndex = 0 img.tabIndex = 0
if focus @addTooltips @images if @images.length
$.queueTask => @focusImage()
focusImage: -> addLabels: ->
# XXX Image is not focusable at first in Firefox; to be refactored when I figure out why. imageSelect = $ '.fbc-payload-imageselect'
img = @images[0] labels = for checkbox, i in @images
$.asap -> checkbox.id = "checkbox-#{i}"
return true unless doc.contains img label = $.el 'label',
img.focus() htmlFor: checkbox.id
d.activeElement is img 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) -> keybinds: (e) ->
return unless @images and doc.contains(@images[0]) and d.activeElement return unless @images and doc.contains(@images[0])
reload = $.id 'recaptcha-reload-button'
verify = $.id 'recaptcha-verify-button' reload = $ '#recaptcha-reload-button, .fbc-button-reload'
verify = $ '#recaptcha-verify-button, .fbc-button-verify > input'
x = @images.indexOf d.activeElement x = @images.indexOf d.activeElement
if x < 0 if x < 0
return unless $('.rc-controls').contains d.activeElement
x = if d.activeElement is verify then 11 else 9 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 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 x = (x + dx) % 12
if x is 10 if x is 10
x = if dx is 11 then 9 else 11 x = if dx is 11 then 9 else 11
(@images[x] or {9: reload, 11: verify}[x]).focus() (@images[x] or {9: reload, 11: verify}[x]).focus()
else
return
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()

View File

@ -1,10 +1,9 @@
Captcha.noscript = Captcha.noscript =
lifetime: 2 * $.MINUTE lifetime: 30 * $.MINUTE
iframeURL: '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
init: -> init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0 return if d.cookie.indexOf('pass_enabled=1') >= 0
return unless @isEnabled = !!$.id 'g-recaptcha' return unless @isEnabled = !!$ '#g-recaptcha, #captchaContainerAlt'
container = $.el 'div', container = $.el 'div',
className: 'captcha-img' className: 'captcha-img'
@ -27,7 +26,7 @@ Captcha.noscript =
token: @save.bind @ token: @save.bind @
error: @error.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] $.after QR.nodes.com.parentNode, [container, input]
@captchas = [] @captchas = []
@ -42,12 +41,15 @@ Captcha.noscript =
initFrame: -> initFrame: ->
conn = new Connection window.parent, "#{location.protocol}//boards.4chan.org", conn = new Connection window.parent, "#{location.protocol}//boards.4chan.org",
response: (response) -> response: (response) ->
$.id('response').value = response $.id('recaptcha_response_field').value = response
$('.fbc-challenge > form').submit() # The form has a field named 'submit'
HTMLFormElement.prototype.submit.call $('form')
if location.hash is '#response'
conn.send conn.send
token: $('.fbc-verification-token > textarea')?.value token: $('textarea')?.value
error: $('.fbc-error')?.textContent error: $('.recaptcha_input_area')?.textContent.replace(/:$/, '')
return unless img = $ '.fbc-payload > img' return unless img = $ 'img'
$('form').action = '#response'
cb = -> cb = ->
canvas = $.el 'canvas' canvas = $.el 'canvas'
canvas.width = img.width canvas.width = img.width
@ -61,6 +63,12 @@ Captcha.noscript =
timers: {} timers: {}
iframeURL: ->
url = '//www.google.com/recaptcha/api/noscript?k=<%= meta.recaptchaKey %>'
if lang = Conf['captchaLanguage'].trim()
url += "&hl=#{encodeURIComponent lang}"
url
cb: cb:
focus: -> QR.captcha.setup false, true focus: -> QR.captcha.setup false, true
@ -88,11 +96,11 @@ Captcha.noscript =
if !@nodes.iframe if !@nodes.iframe
@nodes.iframe = $.el 'iframe', @nodes.iframe = $.el 'iframe',
id: 'qr-captcha-iframe' id: 'qr-captcha-iframe'
src: @iframeURL src: @iframeURL()
$.add d.body, @nodes.iframe $.add QR.nodes.el, @nodes.iframe
@conn.target = @nodes.iframe.contentWindow @conn.target = @nodes.iframe
else if !@occupied or force else if !@occupied or force
@nodes.iframe.src = @iframeURL @nodes.iframe.src = @iframeURL()
@occupied = true @occupied = true
@nodes.input.focus() if focus @nodes.input.focus() if focus
@ -109,9 +117,9 @@ Captcha.noscript =
destroy: -> destroy: ->
return unless @isEnabled return unless @isEnabled
$.rm @nodes.img if @nodes.img $.rm @nodes.img
delete @nodes.img delete @nodes.img
$.rm @nodes.iframe if @nodes.iframe $.rm @nodes.iframe
delete @nodes.iframe delete @nodes.iframe
delete @occupied delete @occupied
@beforeSetup() @beforeSetup()
@ -125,7 +133,7 @@ Captcha.noscript =
if captcha = @captchas.shift() if captcha = @captchas.shift()
@count() @count()
$.set 'captchas', @captchas $.set 'captchas', @captchas
captcha.response captcha
else if /\S/.test @nodes.input.value else if /\S/.test @nodes.input.value
(cb) => (cb) =>
@submitCB = cb @submitCB = cb
@ -141,15 +149,17 @@ Captcha.noscript =
save: (token) -> save: (token) ->
delete @occupied delete @occupied
@nodes.input.value = '' @nodes.input.value = ''
captcha =
challenge: token
response: 'manual_challenge'
timeout: @timeout
if @submitCB if @submitCB
@submitCB token @submitCB captcha
delete @submitCB delete @submitCB
if @needed() then @reload() else @destroy() if @needed() then @reload() else @destroy()
else else
$.forceSync 'captchas' $.forceSync 'captchas'
@captchas.push @captchas.push captcha
response: token
timeout: @timeout
@count() @count()
$.set 'captchas', @captchas $.set 'captchas', @captchas
@reload() @reload()
@ -212,7 +222,7 @@ Captcha.noscript =
@destroy() @destroy()
reload: -> reload: ->
@nodes.iframe.src = @iframeURL @nodes.iframe.src = @iframeURL()
@occupied = true @occupied = true
@nodes.img?.hidden = 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: -> init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0 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 = [] @captchas = []
$.get 'captchas', [], ({captchas}) -> $.get 'captchas', [], ({captchas}) ->
@ -17,18 +22,34 @@ Captcha.v2 =
counter = $ '.captcha-counter > a', root counter = $ '.captcha-counter > a', root
@nodes = {root, counter} @nodes = {root, counter}
@count() @count()
$.addClass QR.nodes.el, 'has-captcha' $.addClass QR.nodes.el, 'has-captcha', 'captcha-v2'
$.after QR.nodes.com.parentNode, root $.after QR.nodes.com.parentNode, root
$.on counter, 'click', @toggle.bind @ $.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', => $.on window, 'captcha:success', =>
# XXX Greasemonkey 1.x workaround to gain access to GM_* functions. # XXX Greasemonkey 1.x workaround to gain access to GM_* functions.
$.queueTask => @save false $.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 shouldFocus: false
timeouts: {} timeouts: {}
postsCount: 0 postsCount: 0
noscriptURL: ->
url = '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
if lang = Conf['captchaLanguage'].trim()
url += "&hl=#{encodeURIComponent lang}"
url
needed: -> needed: ->
captchaCount = @captchas.length captchaCount = @captchas.length
captchaCount++ if QR.req captchaCount++ if QR.req
@ -60,6 +81,7 @@ Captcha.v2 =
if @nodes.container if @nodes.container
if @shouldFocus and iframe = $ 'iframe', @nodes.container if @shouldFocus and iframe = $ 'iframe', @nodes.container
iframe.focus() iframe.focus()
QR.focus() # Event handler not fired in Firefox
delete @shouldFocus delete @shouldFocus
return return
@ -69,6 +91,19 @@ Captcha.v2 =
childList: true childList: true
subtree: 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 ''' $.globalEval '''
(function() { (function() {
function render() { function render() {
@ -101,7 +136,7 @@ Captcha.v2 =
return return
setupIFrame: (iframe) -> setupIFrame: (iframe) ->
@setupTime = Date.now() Captcha.replace.iframe iframe
$.addClass QR.nodes.el, 'captcha-open' $.addClass QR.nodes.el, 'captcha-open'
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
QR.nodes.el.style.top = null QR.nodes.el.style.top = null
@ -134,19 +169,19 @@ Captcha.v2 =
if captcha = @captchas.shift() if captcha = @captchas.shift()
$.set 'captchas', @captchas $.set 'captchas', @captchas
@count() @count()
captcha.response captcha
else else
null null
save: (pasted) -> save: (pasted, token) ->
$.forceSync 'captchas' $.forceSync 'captchas'
@captchas.push @captchas.push
response: $('textarea', @nodes.container).value response: token or $('textarea', @nodes.container).value
timeout: (if pasted then @setupTime else Date.now()) + @lifetime timeout: Date.now() + @lifetime
$.set 'captchas', @captchas $.set 'captchas', @captchas
@count() @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 @needed()
if focus if focus
if QR.cooldown.auto or Conf['Post on Captcha Completion'] if QR.cooldown.auto or Conf['Post on Captcha Completion']
@ -182,6 +217,9 @@ Captcha.v2 =
@timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now() @timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now()
reload: -> reload: ->
if @noscript
$('iframe', @nodes.container).src = @noscriptURL()
else
$.globalEval ''' $.globalEval '''
(function() { (function() {
var container = document.querySelector("#qr .captcha-container"); var container = document.querySelector("#qr .captcha-container");

View File

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

View File

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

View File

@ -105,7 +105,7 @@ QR.post = class
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename'] for name in ['thread', 'name', 'email', 'sub', 'com', 'filename']
continue unless node = QR.nodes[name] 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' (if @thread isnt 'new' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread'
@ -122,6 +122,7 @@ QR.post = class
when 'thread' when 'thread'
(if @thread isnt 'new' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread' (if @thread isnt 'new' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread'
QR.status() QR.status()
@updateFlashURL()
when 'com' when 'com'
@nodes.span.textContent = @com @nodes.span.textContent = @com
QR.captcha.onPostChange() QR.captcha.onPostChange()
@ -132,13 +133,9 @@ QR.post = class
QR.cooldown.auto = false QR.cooldown.auto = false
when 'filename' when 'filename'
return unless @file return unless @file
@file.newName = @filename.replace /[/\\]/g, '-' @saveFilename()
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'
@updateFilename() @updateFilename()
@updateFlashURL()
when 'name' when 'name'
QR.persona.set @ QR.persona.set @
@ -182,16 +179,22 @@ QR.post = class
return return
setFile: (@file) -> setFile: (@file) ->
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 @filename = @file.name
@filesize = $.bytesToString @file.size @filesize = $.bytesToString @file.size
@checkSize() @checkSize()
@nodes.label.hidden = false if QR.spoiler @nodes.label.hidden = false if QR.spoiler
QR.captcha.onPostChange() QR.captcha.onPostChange()
URL.revokeObjectURL @URL URL.revokeObjectURL @URL
@saveFilename()
if @ is QR.selected if @ is QR.selected
@showFileData() @showFileData()
else else
@updateFilename() @updateFilename()
@updateFlashURL()
@nodes.el.style.backgroundImage = null @nodes.el.style.backgroundImage = null
unless @file.type in QR.mimeTypes unless @file.type in QR.mimeTypes
@fileError 'Unsupported file type.' @fileError 'Unsupported file type.'
@ -207,6 +210,8 @@ QR.post = class
readFile: -> readFile: ->
isVideo = /^video\//.test @file.type isVideo = /^video\//.test @file.type
el = $.el(if isVideo then 'video' else 'img') el = $.el(if isVideo then 'video' else 'img')
return if isVideo and !el.canPlayType @file.type
event = if isVideo then 'loadeddata' else 'load' event = if isVideo then 'loadeddata' else 'load'
onload = => onload = =>
$.off el, event, onload $.off el, event, onload
@ -289,9 +294,18 @@ QR.post = class
@nodes.el.style.backgroundImage = null @nodes.el.style.backgroundImage = null
@nodes.label.hidden = true if QR.spoiler @nodes.label.hidden = true if QR.spoiler
@showFileData() @showFileData()
@updateFlashURL()
URL.revokeObjectURL @URL URL.revokeObjectURL @URL
@dismissErrors (error) -> $.hasClass error, 'file-error' @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: -> updateFilename: ->
long = "#{@filename} (#{@filesize})" long = "#{@filename} (#{@filesize})"
@nodes.el.title = long @nodes.el.title = long
@ -307,6 +321,28 @@ QR.post = class
else else
$.rmClass QR.nodes.fileSubmit, 'has-file' $.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) -> pasteText: (file) ->
@pasting = true @pasting = true
reader = new FileReader() reader = new FileReader()

View File

@ -3,7 +3,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>4chan X</title> <title>4chan X</title>
<link rel="stylesheet" href="web.css"> <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"> <link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/ohnjgmpcibpbafdlkimncjhflgedgpam">
</head><body> </head><body>
<div id="header"> <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">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/blob/master/CHANGELOG.md">Changelog</a>
<a href="https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions">FAQ</a> <a href="https://github.com/ccd0/4chan-x/wiki/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>
</div> </div>
<a class="screenshot" href="img/screenshot.png"><img src="img/screenshot.png" alt="Screenshot"></a> <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> <script>
function imagePreview() { function imagePreview() {
this.removeEventListener('mouseover', imagePreview, false); this.removeEventListener('mouseover', imagePreview, false);
@ -30,7 +37,9 @@ function imagePreview() {
} }
function storeInstall(e) { function storeInstall(e) {
if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.button === 0) { 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(); e.preventDefault();
} }
} }
@ -42,5 +51,18 @@ for (var i = 0; i < document.links.length; i++) {
link.addEventListener('click', storeInstall, false); 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> </script>
</body></html> </body></html>

18
web.css
View File

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