Merge branch 'master' into palemoon
This commit is contained in:
commit
50bbb9010f
251
CHANGELOG.md
251
CHANGELOG.md
@ -1,9 +1,251 @@
|
||||
**Note**: Installing the script from one of the links below will disable automatic updates. If you want automatic updates, install the script from the links on the [main page](https://www.4chan-x.net/).
|
||||
|
||||
Sometimes the changelog has notes (not comprehensive) acknowledging people's work. This does not mean the changes are their fault, only that their code was used. All changes to the script are chosen by and the fault of the maintainer (ccd0).
|
||||
|
||||
The links to individual versions below are to copies of the script with the update URL removed. If you want automatic updates, install the script from the links on the [main page](https://github.com/ccd0/4chan-x).
|
||||
### v1.11.9
|
||||
|
||||
**v1.11.9.2** *(2015-08-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.11.8.9: Fix regression from v1.11.8.0 that caused the cooldown to stop working in certain circumstances.
|
||||
|
||||
**v1.11.9.1** *(2015-08-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add more info and a reporting link to error messages.
|
||||
|
||||
**v1.11.9.0** *(2015-08-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.9.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.11.8.8.
|
||||
- Add `Randomize Filename` option: Replaces filenames with a random timestamp from the past year.
|
||||
- Fix bugs with cached captchas when you change captcha settings.
|
||||
|
||||
### v1.11.8
|
||||
|
||||
**v1.11.8.9** *(2015-08-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.9/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.9/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix regression from v1.11.8.0 that caused the cooldown to stop working in certain circumstances.
|
||||
|
||||
**v1.11.8.8** *(2015-08-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.8/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.8/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add link to new anonymous bug reporting form: https://gitreports.com/issue/ccd0/4chan-x
|
||||
|
||||
**v1.11.8.7** *(2015-08-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.7/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.7/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix captcha issues in Pale Moon in some cases where it was still not working.
|
||||
|
||||
**v1.11.8.6** *(2015-08-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.6/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.6/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add .xyz to TLDs recognized by linkifier without http://.
|
||||
|
||||
**v1.11.8.5** *(2015-08-12)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.5/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Change script home page and update URLs to the new https://www.4chan-x.net/ site.
|
||||
|
||||
**v1.11.8.4** *(2015-08-10)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.11.7.5: Fix captcha issues in Pale Moon.
|
||||
|
||||
**v1.11.8.3** *(2015-08-09)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix `Show Updated Notifications` setting.
|
||||
|
||||
**v1.11.8.2** *(2015-08-08)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix bug in infinite scrolling causing skipped pages.
|
||||
|
||||
**v1.11.8.1** *(2015-08-08)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.11.7.4: Fix cooldown bug.
|
||||
|
||||
**v1.11.8.0** *(2015-08-08)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.8.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.11.7.3.
|
||||
- Posts selected for deletion will now be auto-deleted when 4chan's 60-second deletion cooldown expires.
|
||||
- The setting `Except Archives from Encryption` has been changed to `Exempt Archives from Encryption`.
|
||||
- Bug fixes.
|
||||
|
||||
### v1.11.7
|
||||
|
||||
**v1.11.7.5** *(2015-08-10)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.5/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix captcha issues in Pale Moon.
|
||||
|
||||
**v1.11.7.4** *(2015-08-08)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix cooldown bug.
|
||||
|
||||
**v1.11.7.3** *(2015-08-06)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Turn `Custom Board Navigation` back on by default for now until we support better migration of old settings.
|
||||
|
||||
**v1.11.7.2** *(2015-08-05)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.11.6.2: Fix bug where clicking on scrollbar of captcha image selection bubble could mess up the captcha.
|
||||
- Redirect threads deleted but with stubs left behind to archive.
|
||||
|
||||
**v1.11.7.1** *(2015-08-02)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.11.6.1: Update banner list.
|
||||
|
||||
**v1.11.7.0** *(2015-08-02)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.7.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.11.6.0.
|
||||
- Turn `Custom Board Navigation` off by default. To turn it back on, check `Header` > `Custom board navigation` in the header menu.
|
||||
- Experimental MS Edge support via https://github.com/ccd0/4chan-x-proxy.
|
||||
|
||||
### v1.11.6
|
||||
|
||||
**v1.11.6.2** *(2015-08-05)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix bug where clicking on scrollbar of captcha image selection bubble could mess up the captcha.
|
||||
|
||||
**v1.11.6.1** *(2015-08-02)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Update banner list.
|
||||
|
||||
**v1.11.6.0** *(2015-07-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.6.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.11.5.2.
|
||||
- Implement 404 Redirect for `sys.4chan.org/board/imgboard.php?res=` URLs.
|
||||
- Index navigation bugfixes.
|
||||
|
||||
### v1.11.5
|
||||
|
||||
**v1.11.5.2** *(2015-07-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Update `Disable Autoplaying Sounds` so the video in https://boards.4chan.org/g/thread/49036627 is visible.
|
||||
- Tweak position of expanded images.
|
||||
|
||||
**v1.11.5.1** *(2015-07-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Bugfixes.
|
||||
|
||||
**v1.11.5.0** *(2015-07-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.5.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.11.4.1.
|
||||
- When posting a reply with a file on /f/, add a link to it in the comment.
|
||||
|
||||
### v1.11.4
|
||||
|
||||
**v1.11.4.1** *(2015-07-13)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.4.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.4.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.11.3.5: Fix video not being displayed when 'Disable Autoplaying Sounds' is enabled.
|
||||
|
||||
**v1.11.4.0** *(2015-07-12)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.4.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.4.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.11.3.4.
|
||||
- Show files in replies on /f/.
|
||||
- Remove code that disabled the updater if you were offline since detection was too unreliable.
|
||||
|
||||
### v1.11.3
|
||||
|
||||
**v1.11.3.5** *(2015-07-13)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.5/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix video not being displayed when 'Disable Autoplaying Sounds' is enabled.
|
||||
|
||||
**v1.11.3.4** *(2015-07-12)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix captcha in original post form obscuring file field for some users.
|
||||
- Turn `Ignore Offline Status` on by default.
|
||||
|
||||
**v1.11.3.3** *(2015-07-07)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.11.2.5: Add fireden.net archive.
|
||||
|
||||
**v1.11.3.2** *(2015-07-05)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Get posting from URLs, WebM titles, and Vocaroo/Clyp embedding working in Safari.
|
||||
|
||||
**v1.11.3.1** *(2015-07-04)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Make posting from URL more efficient, and make it work in Tampermonkey.
|
||||
|
||||
**v1.11.3.0** *(2015-07-04)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.3.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.11.2.4.
|
||||
- Improved Tampermonkey and WebKit support.
|
||||
- Minor bugfixes.
|
||||
|
||||
### v1.11.2
|
||||
|
||||
**v1.11.2.5** *(2015-07-07)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.5/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add fireden.net archive.
|
||||
|
||||
**v1.11.2.4** *(2015-07-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Minor bugfixes.
|
||||
|
||||
**v1.11.2.3** *(2015-06-30)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add 'webkit' CSS class to document when WebKit engine is detected.
|
||||
- Various CSS-related bugfixes.
|
||||
|
||||
**v1.11.2.2** *(2015-06-30)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix bug from v1.11.2.0 that broke `Use Recaptcha v1` when the original post form was hidden.
|
||||
|
||||
**v1.11.2.1** *(2015-06-30)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- CSS bugfixes for thread watcher and header.
|
||||
|
||||
**v1.11.2.0** *(2015-06-29)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.2.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.11.1.3.
|
||||
- `Use Recaptcha v1` option now works on the original post form.
|
||||
- Captcha section of QR can now be opened with the space bar.
|
||||
- Minor bugfixes.
|
||||
|
||||
### v1.11.1
|
||||
|
||||
**v1.11.1.3** *(2015-06-26)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.11.0.10: (Hasumi) Update archive list.
|
||||
|
||||
**v1.11.1.2** *(2015-06-23)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Improved compatibility: Fix some issues in dwb and Midori; add partial support for Luakit.
|
||||
|
||||
**v1.11.1.1** *(2015-06-22)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add `Use Recaptcha v2 in Reports` option to use the image selection captcha in the report window.
|
||||
|
||||
**v1.11.1.0** *(2015-06-22)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.1.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.11.0.9.
|
||||
- Add partial support for Midori and other browsers whose userscript engines don't implement the Greasemonkey API. Some features will not work in these browsers.
|
||||
|
||||
## v1.11.0
|
||||
|
||||
**v1.11.0.10** *(2015-06-25)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.10/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.10/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- (Hasumi) Update archive list.
|
||||
|
||||
**v1.11.0.9** *(2015-06-21)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.9/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.9/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix caching of v1 captchas.
|
||||
- Other minor bugfixes.
|
||||
|
||||
**v1.11.0.8** *(2015-06-21)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.8/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.8/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Support noscript version of Recaptcha v1.
|
||||
- Captcha-related bugfixes/improvements.
|
||||
|
||||
**v1.11.0.7** *(2015-06-21)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.7/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.7/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add `Use Recaptcha v1` option to use the old text-based captchas in the Quick Reply.
|
||||
|
||||
**v1.11.0.6** *(2015-06-20)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.6/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.6/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Support toggling images in the captcha with the number keys (as arranged in the numpad) and the UIOJKLM,. keys.
|
||||
- Arrow key navigation now works in noscript captcha.
|
||||
- Various captcha-related improvements/bugfixes.
|
||||
- Support space bar, numbers in numpad, comma, and space in keybinds.
|
||||
|
||||
**v1.11.0.5** *(2015-06-20)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.5/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add `Captcha Language` setting in the `Advanced` panel.
|
||||
- Minor bugfixes.
|
||||
|
||||
**v1.11.0.4** *(2015-06-20)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Noscript captcha improvements, including the ability to click the image rather than the little checkbox.
|
||||
|
||||
**v1.11.0.3** *(2015-06-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.10.14.4: Update script for new non-Javascript captcha using image selection.
|
||||
|
||||
**v1.11.0.2** *(2015-06-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Remove code that focused on first image of the captcha as Google now focuses on the refresh button.
|
||||
|
||||
**v1.11.0.1** *(2015-06-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.10.14.3: Fix words being cut off in non-Javascript captcha.
|
||||
|
||||
**v1.11.0.0** *(2015-06-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.0.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.10.14.2.
|
||||
- Gallery now preloads the next image in sequence.
|
||||
- `Stretch to Fit` option added to gallery.
|
||||
- Various bug fixes.
|
||||
- Drop workarounds for old versions of Chromium (< v34).
|
||||
|
||||
### v1.10.14
|
||||
|
||||
**v1.10.14.4** *(2015-06-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Update script for new non-Javascript captcha using image selection.
|
||||
|
||||
**v1.10.14.3** *(2015-06-16)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix words being cut off in non-Javascript captcha.
|
||||
|
||||
**v1.10.14.2** *(2015-06-11)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Bring back workaround for scrolling to top caused by captcha as some users are still reporting it happening.
|
||||
|
||||
**v1.10.14.1** *(2015-06-11)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add ImgOps to default sauce examples: `//imgops.com/%URL;types:gif,jpg,png`
|
||||
|
||||
**v1.10.14.0** *(2015-06-07)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.14.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.10.13.4.
|
||||
- Fix board-specific filters in thread watcher (filters apply to unread posts quoting you).
|
||||
|
||||
### v1.10.13
|
||||
|
||||
**v1.10.13.4** *(2015-06-05)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Support selecting images in the image captcha with the space bar in addition to the enter key.
|
||||
|
||||
**v1.10.13.3** *(2015-06-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.10.12.10: Revert workaround for scrolling to top as it seems to have been fixed on Google's end.
|
||||
|
||||
**v1.10.13.2** *(2015-06-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.10.12.9: Update for captcha changes.
|
||||
- Merge v1.10.12.9: Work around issue where the captcha causes scrolling to the top of the page.
|
||||
|
||||
**v1.10.13.1** *(2015-05-27)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix manual page number updating not working when `Updater and Stats in Header` is off.
|
||||
|
||||
@ -20,6 +262,13 @@ The links to individual versions below are to copies of the script with the upda
|
||||
|
||||
### v1.10.12
|
||||
|
||||
**v1.10.12.10** *(2015-06-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.10/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.10/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Revert workaround for scrolling to top as it seems to have been fixed on Google's end.
|
||||
|
||||
**v1.10.12.9** *(2015-06-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.9/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.9/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Update for captcha changes.
|
||||
- Work around issue where the captcha causes scrolling to the top of the page.
|
||||
|
||||
**v1.10.12.8** *(2015-05-22)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.8/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.8/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Remove guard against dropping files into captcha due to continued reports of disabled captchas.
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
## Reporting bugs
|
||||
|
||||
If I can't reproduce a bug, I probably won't be able to fix it. You can help by doing the following when you [report a bug](https://github.com/ccd0/4chan-x/issues):
|
||||
Bug reports and feature requests for 4chan X are tracked at **https://github.com/ccd0/4chan-x/issues**.
|
||||
|
||||
You can submit a bug report / feature request either via your Github account or the [anonymous report form](https://gitreports.com/issue/ccd0/4chan-x).
|
||||
|
||||
If you're reporting a bug, the more detail you can give, the better. If I can't reproduce your bug, I probably won't be able to fix it. You can help by doing the following:
|
||||
|
||||
1. Include precise steps to reproduce the problem, with the expected and actual results.
|
||||
2. Make sure your **browser**, **4chan X**, and (if applicable) **Greasemonkey** are up to date. Include the versions you're using in bug reports.
|
||||
@ -14,10 +18,10 @@ If I can't reproduce a bug, I probably won't be able to fix it. You can help by
|
||||
### Get started
|
||||
|
||||
- Install [node.js](http://nodejs.org/).
|
||||
- Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`.
|
||||
- Clone 4chan X.
|
||||
- `cd` into it.
|
||||
- Install/Update 4chan X dependencies with `npm install`.
|
||||
- Install [Grunt's CLI](http://gruntjs.com/): `npm install -g grunt-cli`
|
||||
- Clone 4chan X: `git clone https://github.com/ccd0/4chan-x.git`
|
||||
- Open the directory: `cd 4chan-x`
|
||||
- Install/Update 4chan X dependencies: `npm install`
|
||||
|
||||
### Build
|
||||
|
||||
@ -29,6 +33,9 @@ If I can't reproduce a bug, I probably won't be able to fix it. You can help by
|
||||
- Edit the sources (not the compiled scripts in the builds/ directory).
|
||||
- Compile the script with `grunt`.
|
||||
- Install the compiled script (found in the testbuilds/ directory), and test your changes.
|
||||
- Open a pull request.
|
||||
- Open a pull request by doing any of the following:
|
||||
- Fork this repository on Github, push your changes to your fork, and make a pull request via Github's mechanism.
|
||||
- Push your changes to any online Git repository, and [open an issue](https://gitreports.com/issue/ccd0/4chan-x) with an explanation of your changes and the URL, branch, and commit you want me to pull from.
|
||||
- Export your changes via `git bundle` (e.g. `git bundle create file.bundle master..your-branch`), and upload them to a file host like https://jii.moe/. Then [open an issue](https://gitreports.com/issue/ccd0/4chan-x) with an explanation of your changes and the URL of the file.
|
||||
|
||||
Archive list updates should go to https://github.com/MayhemYDG/archives.json.
|
||||
|
||||
@ -8,6 +8,12 @@ module.exports = (grunt) ->
|
||||
json = (data) ->
|
||||
"`#{JSON.stringify(data).replace(/`/g, '\\`')}`"
|
||||
|
||||
importCSS = (filenames...) ->
|
||||
grunt.template.process(
|
||||
filenames.map((name) -> grunt.file.read "src/General/css/#{name}.css").join(''),
|
||||
{data: grunt.config 'pkg'}
|
||||
).trim().replace(/\n+/g, '\n').split(/^/m).map(JSON.stringify).join(' +\n').replace(/`/g, '\\`')
|
||||
|
||||
importHTML = (filename) ->
|
||||
html grunt.template.process(grunt.file.read("src/General/html/#{filename}.html").replace(/^ +/gm, '').replace(/\r?\n/g, ''), data: grunt.config('pkg'))
|
||||
|
||||
@ -69,6 +75,7 @@ module.exports = (grunt) ->
|
||||
options: process: Object.create(null, data:
|
||||
get: ->
|
||||
pkg = grunt.config 'pkg'
|
||||
pkg.importCSS = importCSS
|
||||
pkg.importHTML = importHTML
|
||||
pkg.html = html
|
||||
pkg.assert = assert
|
||||
@ -166,8 +173,6 @@ module.exports = (grunt) ->
|
||||
stdout: true
|
||||
stderr: true
|
||||
failOnError: true
|
||||
checkout:
|
||||
command: 'git checkout <%= pkg.meta.mainBranch %>'
|
||||
commit:
|
||||
command: """
|
||||
git commit -am "Release <%= pkg.meta.name %> v<%= pkg.meta.version %>."
|
||||
@ -185,12 +190,12 @@ module.exports = (grunt) ->
|
||||
""".split('\n').join('&&')
|
||||
stable:
|
||||
command: """
|
||||
git push . HEAD:bstable
|
||||
git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.meta.version %>."
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
git merge --no-commit -s ours stable
|
||||
git checkout stable builds
|
||||
git checkout HEAD "builds/*<%= pkg.meta.suffix.beta %>.*"
|
||||
git checkout stable "builds/<%= pkg.name %>.*" builds/updates.xml
|
||||
git commit -am "Move <%= pkg.meta.name %> v<%= pkg.meta.version %> to stable channel."
|
||||
git checkout -
|
||||
""".split('\n').join('&&')
|
||||
@ -201,12 +206,24 @@ module.exports = (grunt) ->
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
git merge --no-commit -s ours -
|
||||
git checkout - README.md index.html web.css img src/General/img/icon.gif
|
||||
git checkout - README.md index.html web.css img
|
||||
git commit -am "Update web page."
|
||||
git checkout -
|
||||
""".split('\n').join('&&')
|
||||
push:
|
||||
command: 'git push origin --tags -f && git push origin --all'
|
||||
aws:
|
||||
command: """
|
||||
git checkout gh-pages
|
||||
aws s3 cp builds/ s3://<%= pkg.meta.awsBucket %>/builds/ --recursive --exclude "*" --include "*.js" --cache-control "max-age=600" --content-type "application/javascript; charset=utf-8"
|
||||
aws s3 cp builds/ s3://<%= pkg.meta.awsBucket %>/builds/ --recursive --exclude "*" --include "*.crx" --cache-control "max-age=600" --content-type "application/x-chrome-extension"
|
||||
aws s3 cp builds/ s3://<%= pkg.meta.awsBucket %>/builds/ --recursive --exclude "*" --include "*.xml" --cache-control "max-age=600" --content-type "text/xml; charset=utf-8"
|
||||
aws s3 cp builds/ s3://<%= pkg.meta.awsBucket %>/builds/ --recursive --exclude "*" --include "*.zip" --cache-control "max-age=600" --content-type "application/zip"
|
||||
aws s3 cp img/ s3://<%= pkg.meta.awsBucket %>/img/ --recursive --cache-control "max-age=600"
|
||||
aws s3 cp index.html s3://<%= pkg.meta.awsBucket %> --cache-control "max-age=600" --content-type "text/html; charset=utf-8"
|
||||
aws s3 cp web.css s3://<%= pkg.meta.awsBucket %> --cache-control "max-age=600" --content-type "text/css; charset=utf-8"
|
||||
git checkout -
|
||||
""".split('\n').join('&&')
|
||||
npm:
|
||||
command: 'npm install'
|
||||
update:
|
||||
@ -282,9 +299,10 @@ module.exports = (grunt) ->
|
||||
GM_setValue: true
|
||||
GM_deleteValue: true
|
||||
GM_listValues: true
|
||||
GM_addValueChangeListener: true
|
||||
GM_openInTab: true
|
||||
GM_info: true
|
||||
GM_xmlhttpRequest: true
|
||||
GM_info: true
|
||||
cloneInto: true
|
||||
unsafeWindow: true
|
||||
chrome: true
|
||||
@ -432,6 +450,10 @@ module.exports = (grunt) ->
|
||||
'shell:push'
|
||||
]
|
||||
|
||||
grunt.registerTask 'aws', [
|
||||
'shell:aws'
|
||||
]
|
||||
|
||||
grunt.registerTask 'store', [
|
||||
'webstore_upload'
|
||||
]
|
||||
|
||||
72
README.md
72
README.md
@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
# 4chan X
|
||||
Adds various features to 4chan.
|
||||
Previously developed by [aeosynth](https://github.com/aeosynth/4chan-x), [Mayhem](https://github.com/MayhemYDG/4chan-x), [ihavenoface](https://github.com/ihavenoface/4chan-x), [Zixaphir](https://github.com/zixaphir/appchan-x), [Seaweed](https://github.com/seaweedchan/4chan-x), and [Spittie](https://github.com/Spittie/4chan-x), with contributions from many others.
|
||||
@ -6,47 +6,71 @@ Previously developed by [aeosynth](https://github.com/aeosynth/4chan-x), [Mayhem
|
||||
If you're looking for a maintained fork of OneeChan (a style script used in addition to 4chan X), try
|
||||
https://github.com/Nebukazar/OneeChan.
|
||||
|
||||
## Firefox version: [Click to Install](https://ccd0.github.io/4chan-x/builds/4chan-X.user.js)
|
||||
Install [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/), then click the link above to install 4chan X. If you're using a fork of Firefox (e.g. Pale Moon), you may need to use [Greasemonkey 1.15](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/versions/#version-1.15) instead of the most recent version.
|
||||
**Note**: 4chan X disables the native extension, so if you uninstall 4chan X, you'll need to re-enable it. To do this, click the `[Settings]` link in the top right corner and uncheck "`Disable the native extension`" in the panel that appears.
|
||||
|
||||
**WARNING**:
|
||||
If you're switching to this fork from someone else's fork of 4chan X, back up your old script before installing this one as the old one may be overwritten.
|
||||
## Install
|
||||
|
||||
**Known issues**:
|
||||
Greasemonkey 3.0 has a [bug](https://github.com/greasemonkey/greasemonkey/issues/2094) causing 4chan X to open multiple tabs when you open a new tab (for example, when starting a thread). If you're having this problem, [updating](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/) to Greasemonkey 3.1 or later should fix it.
|
||||
### Firefox
|
||||
Install [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
|
||||
|
||||
## Chromium version: [Click to Install](https://ccd0.github.io/4chan-x/builds/4chan-X.crx)
|
||||
Download the file from the link above and add drag it to your `chrome://extensions` page.
|
||||
This should also work for non-Windows/dev/canary Chrome and Chromium-based versions of Opera.
|
||||
- **Pale Moon** users should use [Greasemonkey 1.15](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/versions/1.15.1-signed).
|
||||
- **SeaMonkey** users should use Greasemonkey 2.3 converted with [this tool](http://addonconverter.fotokraina.com/?url=https://addons.mozilla.org/firefox/downloads/file/282084/greasemonkey-2.3-fx.xpi).
|
||||
|
||||
**The above will not work in Chrome (stable or beta) users on Windows; you must install from the [Chrome store](https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam).**
|
||||
### Chromium
|
||||
4chan X is available as a Chrome extension. The Chrome extension has the additional feature of being able to sync your settings and data with other devices via Chrome Sync.
|
||||
|
||||
## Chromium version (Chrome store): [Click to Install](https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam)
|
||||
The stable and beta releases of Chrome on Windows will disable extensions not installed from the Chrome store, so users will need to install 4chan X from the link above.
|
||||
Only the latest stable version of 4chan X is available.
|
||||
- **Chromium**: **[Download 4chan X](https://www.4chan-x.net/builds/4chan-X.crx)**, then open `chrome://extensions` and drag the downloaded file onto the page. Alternatively, you can install 4chan X from the **[Chrome store](https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam)**.
|
||||
- **Opera**: **[Click to install 4chan X](https://www.4chan-x.net/builds/4chan-X.crx)**, then follow the prompts to activate it in your extension manager. Note: This version does not work with Opera 12; try [loadletter's fork](https://github.com/loadletter/4chan-x) instead.
|
||||
- **Chrome**, **Vivaldi**: Install 4chan X from the **[Chrome store](https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam)**.
|
||||
|
||||
## Other browsers
|
||||
This fork of 4chan X is not guaranteed to work correctly in other browsers, but you are welcome to try your luck. Pull requests to fix the bugs you will likely find are always welcome. You may fare better with [loadletter's fork](https://github.com/loadletter/4chan-x), which has fewer features but less dependence on browser-specific APIs.
|
||||
You can also use the [userscript version of 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js) with [Tampermonkey](https://tampermonkey.net/).
|
||||
|
||||
- Some people have reported success in Safari using [JS Blocker](http://jsblocker.toggleable.com/) to install the Firefox/Greasemonkey version.
|
||||
- Instructions are available for [installing 4chan X in dwb](https://github.com/ccd0/4chan-x/wiki/Installing-4chan-X-in-dwb).
|
||||
### Safari
|
||||
Install [JS Blocker](http://jsblocker.toggleable.com/), then **[click here to install 4chan X](https://www.4chan-x.net/builds/4chan-X.user.js)**.
|
||||
|
||||
### WebKitGTK+
|
||||
Several WebKitGTK+ based browsers have support for userscripts and can run 4chan X. Due to the lack of the cross-site GM_* API, and lack of support for userscripts in iframes, not all features will work. You may experience crashes when repeatedly solving the default image-based captchas. You can avoid this problem by enabling `Use Recaptcha v1` in your settings.
|
||||
|
||||
- **dwb**: Install the userscripts extension, then save the [script](https://www.4chan-x.net/builds/4chan-X.user.js) to the `$XDG_CONFIG_HOME/dwb/greasemonkey` or `$HOME/.config/dwb/greasemonkey` directory (creating it if necessary):
|
||||
|
||||
dwbem -N -i userscripts
|
||||
wget -P ${XDG_CONFIG_HOME:-$HOME/.config}/dwb/greasemonkey https://www.4chan-x.net/builds/4chan-X.user.js
|
||||
|
||||
- **Midori**: Enable `User addons` in your preferences, under the Extensions tab. In the Privacy tab, check `Enable HTML5 local storage support`. Optionally, if you want 4chan X to be able to open new tabs when you start or reply to a thread, you will need to check `Allow scripts to open popups` under the Behavior tab. Then click the link to the [script](https://www.4chan-x.net/builds/4chan-X.user.js) to install it.
|
||||
|
||||
- **Luakit**: Navigate to the [script](https://www.4chan-x.net/builds/4chan-X.user.js), then type the command `:usi` to install it.
|
||||
|
||||
- **uzbl**: Install the script from https://github.com/singpolyma/singpolyma/blob/master/uzbl/data/scripts/userscript.sh, enable it in your config file, and then save [4chan X](https://www.4chan-x.net/builds/4chan-X.user.js) to `$XDG_DATA_HOME/uzbl/userscripts` (or `$HOME/.local/share/uzbl/userscripts`).
|
||||
|
||||
wget -P ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts https://raw.githubusercontent.com/singpolyma/singpolyma/master/uzbl/data/scripts/userscript.sh
|
||||
chmod +x ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/scripts/userscript.sh
|
||||
echo '@on_event LOAD_COMMIT spawn @scripts_dir/userscript.sh document-start' >> ${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config
|
||||
echo '@on_event LOAD_FINISH spawn @scripts_dir/userscript.sh document-end' >> ${XDG_CONFIG_HOME:-$HOME/.config}/uzbl/config
|
||||
wget -P ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/userscripts https://www.4chan-x.net/builds/4chan-X.user.js
|
||||
|
||||
### Other browsers
|
||||
4chan X can be used in some browsers that do not support userscripts, such as **Microsoft Edge**, using [a local proxy](https://github.com/ccd0/4chan-x-proxy). Not all features will work.
|
||||
|
||||
## Beta version
|
||||
New features and non-urgent bugfixes are released on the beta channel for further testing before they are moved the stable version. Please [report](https://github.com/ccd0/4chan-x/issues) any issues you find, and be sure to mention which version you're using. You should back up your settings regularly to prevent them from being lost due to bugs.
|
||||
- [Firefox version](https://ccd0.github.io/4chan-x/builds/4chan-X-beta.user.js)
|
||||
- [Chromium version](https://ccd0.github.io/4chan-x/builds/4chan-X-beta.crx)
|
||||
|
||||
If you want to install the current beta version but get updates from the stable channel after that, install it from [here](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js) for Firefox or [here](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx) for Chromium.
|
||||
To install the current **beta** version but get updates from the **stable** channel (recommended if you want a particular recent feature):
|
||||
- [Install userscript](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js) (use with Greasemonkey / Tampermonkey / JS Blocker / etc.)
|
||||
- [Download Chrome extension](https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx) (download and drag to `chrome://extensions`)
|
||||
|
||||
To install the **beta** version and get updates whenever there's a new **beta** version:
|
||||
- [Install userscript](https://www.4chan-x.net/builds/4chan-X-beta.user.js)
|
||||
- [Download Chrome extension](https://www.4chan-x.net/builds/4chan-X-beta.crx)
|
||||
|
||||
## Security note
|
||||
4chan X currently shares your settings and post history between the HTTP and HTTPS versions of 4chan. If you are concerned about protecting your privacy against a man-in-the-middle attack, you should disable 4chan X on the HTTP version of 4chan and/or install [HTTPS Everywhere](https://www.eff.org/https-everywhere).
|
||||
|
||||
## Uninstalling
|
||||
4chan X disables the native extension, so if you uninstall 4chan X, you'll need to re-enable it. To do this, click the `[Settings]` link in the top right corner and uncheck "`Disable the native extension`" in the panel that appears.
|
||||
## Troubleshooting
|
||||
If you encounter a bug, try the steps [here](https://github.com/ccd0/4chan-x/blob/master/CONTRIBUTING.md#reporting-bugs), then report it to the [issue tracker](https://github.com/ccd0/4chan-x/issues). You can report bugs without a Github account via [this form](https://gitreports.com/issue/ccd0/4chan-x). If the bug seems to be caused by a script update, you can install a old version from the [changelog](https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md).
|
||||
|
||||
## More information
|
||||
- [Changelog](https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md)
|
||||
- [Frequently Asked Questions](https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions)
|
||||
- [Report Bugs](https://github.com/ccd0/4chan-x/issues)
|
||||
- [Report Bugs](https://gitreports.com/issue/ccd0/4chan-x)
|
||||
- [Contributing](https://github.com/ccd0/4chan-x/blob/master/CONTRIBUTING.md)
|
||||
|
||||
|
||||
Binary file not shown.
@ -1,26 +1,38 @@
|
||||
// ==UserScript==
|
||||
// @name 4chan X beta
|
||||
// @version 1.10.13.1
|
||||
// @version 1.11.9.2
|
||||
// @minGMVer 1.14
|
||||
// @minFFVer 26
|
||||
// @namespace 4chan-X
|
||||
// @description Cross-browser userscript for maximum lurking on 4chan.
|
||||
// @license MIT; https://github.com/ccd0/4chan-x/blob/master/LICENSE
|
||||
// @match *://boards.4chan.org/*
|
||||
// @match *://sys.4chan.org/*
|
||||
// @match *://a.4cdn.org/*
|
||||
// @match *://i.4cdn.org/*
|
||||
// @match https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @match https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @match *://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc
|
||||
// @include http://boards.4chan.org/*
|
||||
// @include https://boards.4chan.org/*
|
||||
// @include http://sys.4chan.org/*
|
||||
// @include https://sys.4chan.org/*
|
||||
// @include http://a.4cdn.org/*
|
||||
// @include https://a.4cdn.org/*
|
||||
// @include http://i.4cdn.org/*
|
||||
// @include https://i.4cdn.org/*
|
||||
// @include http://www.4chan.org/banned
|
||||
// @include https://www.4chan.org/banned
|
||||
// @include http://www.4chan.org/feedback
|
||||
// @include https://www.4chan.org/feedback
|
||||
// @include https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include http://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include http://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include https://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @grant GM_getValue
|
||||
// @grant GM_setValue
|
||||
// @grant GM_deleteValue
|
||||
// @grant GM_listValues
|
||||
// @grant GM_addValueChangeListener
|
||||
// @grant GM_openInTab
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @run-at document-start
|
||||
// @updateURL https://ccd0.github.io/4chan-x/builds/4chan-X-beta.meta.js
|
||||
// @downloadURL https://ccd0.github.io/4chan-x/builds/4chan-X-beta.user.js
|
||||
// @updateURL https://www.4chan-x.net/builds/4chan-X-beta.meta.js
|
||||
// @downloadURL https://www.4chan-x.net/builds/4chan-X-beta.user.js
|
||||
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
|
||||
// ==/UserScript==
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1,26 +1,38 @@
|
||||
// ==UserScript==
|
||||
// @name 4chan X
|
||||
// @version 1.10.13.1
|
||||
// @version 1.11.9.2
|
||||
// @minGMVer 1.14
|
||||
// @minFFVer 26
|
||||
// @namespace 4chan-X
|
||||
// @description Cross-browser userscript for maximum lurking on 4chan.
|
||||
// @license MIT; https://github.com/ccd0/4chan-x/blob/master/LICENSE
|
||||
// @match *://boards.4chan.org/*
|
||||
// @match *://sys.4chan.org/*
|
||||
// @match *://a.4cdn.org/*
|
||||
// @match *://i.4cdn.org/*
|
||||
// @match https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @match https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @match *://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc
|
||||
// @include http://boards.4chan.org/*
|
||||
// @include https://boards.4chan.org/*
|
||||
// @include http://sys.4chan.org/*
|
||||
// @include https://sys.4chan.org/*
|
||||
// @include http://a.4cdn.org/*
|
||||
// @include https://a.4cdn.org/*
|
||||
// @include http://i.4cdn.org/*
|
||||
// @include https://i.4cdn.org/*
|
||||
// @include http://www.4chan.org/banned
|
||||
// @include https://www.4chan.org/banned
|
||||
// @include http://www.4chan.org/feedback
|
||||
// @include https://www.4chan.org/feedback
|
||||
// @include https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include http://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include http://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @include https://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @grant GM_getValue
|
||||
// @grant GM_setValue
|
||||
// @grant GM_deleteValue
|
||||
// @grant GM_listValues
|
||||
// @grant GM_addValueChangeListener
|
||||
// @grant GM_openInTab
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @run-at document-start
|
||||
// @updateURL https://ccd0.github.io/4chan-x/builds/4chan-X.meta.js
|
||||
// @downloadURL https://ccd0.github.io/4chan-x/builds/4chan-X.user.js
|
||||
// @updateURL https://www.4chan-x.net/builds/4chan-X.meta.js
|
||||
// @downloadURL https://www.4chan-x.net/builds/4chan-X.user.js
|
||||
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAACVBMVEUAAGcAAABmzDNZt9VtAAAAAXRSTlMAQObYZgAAAF5JREFUeNrtkTESABAQxPD/R6tsE2dUGYUtFJvLDKf93KevHJAjpBorAQWSBIKqFASC4G0pCAkm4GfaEvgYXl0T6HBaE97f0vmnfYHbZOMLZCx9ISdKWwjOWZSC8GYm4SUGwfYgqI4AAAAASUVORK5CYII=
|
||||
// ==/UserScript==
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
|
||||
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X-beta.crx' version='1.10.13.1' />
|
||||
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.11.9.2' />
|
||||
</app>
|
||||
</gupdate>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
|
||||
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X.crx' version='1.10.13.1' />
|
||||
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.11.9.2' />
|
||||
</app>
|
||||
</gupdate>
|
||||
|
||||
|
||||
BIN
img/icon.gif
Normal file
BIN
img/icon.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 B |
92
index.html
92
index.html
@ -3,7 +3,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>4chan X</title>
|
||||
<link rel="stylesheet" href="web.css">
|
||||
<link rel="icon" href="src/General/img/icon.gif">
|
||||
<link rel="icon" href="img/icon.gif">
|
||||
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/ohnjgmpcibpbafdlkimncjhflgedgpam">
|
||||
</head><body>
|
||||
<div id="header">
|
||||
@ -12,7 +12,7 @@
|
||||
<a href="https://github.com/ccd0/4chan-x">Source Code</a>
|
||||
<a href="https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md">Changelog</a>
|
||||
<a href="https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions">FAQ</a>
|
||||
<a href="https://github.com/ccd0/4chan-x/issues">Report Bugs</a>
|
||||
<a href="https://gitreports.com/issue/ccd0/4chan-x">Report Bugs</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="screenshot" href="img/screenshot.png"><img src="img/screenshot.png" alt="Screenshot"></a>
|
||||
@ -21,36 +21,61 @@
|
||||
Previously developed by <a href="https://github.com/aeosynth/4chan-x">aeosynth</a>, <a href="https://github.com/MayhemYDG/4chan-x">Mayhem</a>, <a href="https://github.com/ihavenoface/4chan-x">ihavenoface</a>, <a href="https://github.com/zixaphir/appchan-x">Zixaphir</a>, <a href="https://github.com/seaweedchan/4chan-x">Seaweed</a>, and <a href="https://github.com/Spittie/4chan-x">Spittie</a>, with contributions from many others.</p>
|
||||
<p>If you're looking for a maintained fork of OneeChan (a style script used in addition to 4chan X), try
|
||||
<a href="https://github.com/Nebukazar/OneeChan">https://github.com/Nebukazar/OneeChan</a>.</p>
|
||||
<h2 id="firefox-version-click-to-install-https-ccd0-github-io-4chan-x-builds-4chan-x-user-js-">Firefox version: <a href="https://ccd0.github.io/4chan-x/builds/4chan-X.user.js">Click to Install</a></h2>
|
||||
<p>Install <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">Greasemonkey</a>, then click the link above to install 4chan X. If you're using a fork of Firefox (e.g. Pale Moon), you may need to use <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/versions/#version-1.15">Greasemonkey 1.15</a> instead of the most recent version.</p>
|
||||
<p><strong>WARNING</strong>:
|
||||
If you'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.</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'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's fork</a>, which has fewer features but less dependence on browser-specific APIs.</p>
|
||||
<p><strong>Note</strong>: 4chan X disables the native extension, so if you uninstall 4chan X, you'll need to re-enable it. To do this, click the <code>[Settings]</code> link in the top right corner and uncheck "<code>Disable the native extension</code>" in the panel that appears.</p>
|
||||
<h2 id="install">Install</h2>
|
||||
<input hidden type="checkbox" id="firefox-hide"><div><label for="firefox-hide"><h3 id="firefox">Firefox</h3></label>
|
||||
<p>Install <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/">Greasemonkey</a>, then <strong><a href="https://www.4chan-x.net/builds/4chan-X.user.js">click here to install 4chan X</a></strong>.</p>
|
||||
<ul>
|
||||
<li>Some people have reported success in Safari using <a href="http://jsblocker.toggleable.com/">JS Blocker</a> to install the Firefox/Greasemonkey version.</li>
|
||||
<li>Instructions are available for <a href="https://github.com/ccd0/4chan-x/wiki/Installing-4chan-X-in-dwb">installing 4chan X in dwb</a>.</li>
|
||||
<li><strong>Pale Moon</strong> users should use <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/versions/1.15.1-signed">Greasemonkey 1.15</a>.</li>
|
||||
<li><strong>SeaMonkey</strong> users should use Greasemonkey 2.3 converted with <a href="http://addonconverter.fotokraina.com/?url=https://addons.mozilla.org/firefox/downloads/file/282084/greasemonkey-2.3-fx.xpi">this tool</a>.</li>
|
||||
</ul>
|
||||
<h2 id="beta-version">Beta version</h2>
|
||||
</div><input hidden type="checkbox" id="chromium-hide"><div><label for="chromium-hide"><h3 id="chromium">Chromium</h3></label>
|
||||
<p>4chan X is available as a Chrome extension. The Chrome extension has the additional feature of being able to sync your settings and data with other devices via Chrome Sync.</p>
|
||||
<ul>
|
||||
<li><strong>Chromium</strong>: <strong><a href="https://www.4chan-x.net/builds/4chan-X.crx">Download 4chan X</a></strong>, then open <code>chrome://extensions</code> and drag the downloaded file onto the page. Alternatively, you can install 4chan X from the <strong><a href="https://chrome.google.com/webstore/detail/4chan-x/ohnjgmpcibpbafdlkimncjhflgedgpam">Chrome store</a></strong>.</li>
|
||||
<li><strong>Opera</strong>: <strong><a href="https://www.4chan-x.net/builds/4chan-X.crx">Click to install 4chan X</a></strong>, then follow the prompts to activate it in your extension manager. Note: This version does not work with Opera 12; try <a href="https://github.com/loadletter/4chan-x">loadletter'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 '@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
|
||||
</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're using. You should back up your settings regularly to prevent them from being lost due to bugs.</p>
|
||||
<p>To install the current <strong>beta</strong> version but get updates from the <strong>stable</strong> channel (recommended if you want a particular recent feature):</p>
|
||||
<ul>
|
||||
<li><a href="https://ccd0.github.io/4chan-x/builds/4chan-X-beta.user.js">Firefox version</a></li>
|
||||
<li><a href="https://ccd0.github.io/4chan-x/builds/4chan-X-beta.crx">Chromium version</a></li>
|
||||
<li><a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js">Install userscript</a> (use with Greasemonkey / Tampermonkey / JS Blocker / etc.)</li>
|
||||
<li><a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx">Download Chrome extension</a> (download and drag to <code>chrome://extensions</code>)</li>
|
||||
</ul>
|
||||
<p>To install the <strong>beta</strong> version and get updates whenever there's a new <strong>beta</strong> version:</p>
|
||||
<ul>
|
||||
<li><a href="https://www.4chan-x.net/builds/4chan-X-beta.user.js">Install userscript</a></li>
|
||||
<li><a href="https://www.4chan-x.net/builds/4chan-X-beta.crx">Download Chrome extension</a></li>
|
||||
</ul>
|
||||
<p>If you want to install the current beta version but get updates from the stable channel after that, install it from <a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.user.js">here</a> for Firefox or <a href="https://github.com/ccd0/4chan-x/raw/beta/builds/4chan-X.crx">here</a> for Chromium.</p>
|
||||
<h2 id="security-note">Security note</h2>
|
||||
<p>4chan X currently shares your settings and post history between the HTTP and HTTPS versions of 4chan. If you are concerned about protecting your privacy against a man-in-the-middle attack, you should disable 4chan X on the HTTP version of 4chan and/or install <a href="https://www.eff.org/https-everywhere">HTTPS Everywhere</a>.</p>
|
||||
<h2 id="uninstalling">Uninstalling</h2>
|
||||
<p>4chan X disables the native extension, so if you uninstall 4chan X, you'll need to re-enable it. To do this, click the <code>[Settings]</code> link in the top right corner and uncheck "<code>Disable the native extension</code>" in the panel that appears.</p>
|
||||
<h2 id="troubleshooting">Troubleshooting</h2>
|
||||
<p>If you encounter a bug, try the steps <a href="https://github.com/ccd0/4chan-x/blob/master/CONTRIBUTING.md#reporting-bugs">here</a>, then report it to the <a href="https://github.com/ccd0/4chan-x/issues">issue tracker</a>. You can report bugs without a Github account via <a href="https://gitreports.com/issue/ccd0/4chan-x">this form</a>. If the bug seems to be caused by a script update, you can install a old version from the <a href="https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md">changelog</a>.</p>
|
||||
|
||||
<script>
|
||||
function imagePreview() {
|
||||
@ -65,7 +90,9 @@ function imagePreview() {
|
||||
}
|
||||
function storeInstall(e) {
|
||||
if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.button === 0) {
|
||||
chrome.webstore.install();
|
||||
chrome.webstore.install(this.href, function(){}, function(){
|
||||
location.href = this.href;
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
@ -77,5 +104,18 @@ for (var i = 0; i < document.links.length; i++) {
|
||||
link.addEventListener('click', storeInstall, false);
|
||||
}
|
||||
}
|
||||
var engine = (function() {
|
||||
if (/Edge\//.test(navigator.userAgent)) return 'edge';
|
||||
if (/Chrome\//.test(navigator.userAgent)) return 'blink';
|
||||
if (/WebKit\//.test(navigator.userAgent)) return 'webkit';
|
||||
if (/Gecko\/|Goanna/.test(navigator.userAgent)) return 'gecko';
|
||||
if (/Presto\//.test(navigator.userAgent)) return 'presto';
|
||||
})();
|
||||
if (engine) {
|
||||
var engines = {'firefox': 'gecko', 'chromium': 'blink presto', 'safari': 'webkit', 'webkitgtk-': 'webkit', 'other-browsers': 'edge'};
|
||||
for (browser in engines) {
|
||||
document.getElementById(browser + '-hide').checked = (engines[browser].indexOf(engine) < 0);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body></html>
|
||||
|
||||
3080
npm-shrinkwrap.json
generated
3080
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
Executable file → Normal file
32
package.json
Executable file → Normal file
@ -3,27 +3,33 @@
|
||||
"description": "Cross-browser userscript for maximum lurking on 4chan.",
|
||||
"meta": {
|
||||
"name": "4chan X",
|
||||
"version": "1.10.13.1",
|
||||
"date": "2015-05-27T18:54:32.769Z",
|
||||
"repo": "https://github.com/ccd0/4chan-x/",
|
||||
"page": "https://github.com/ccd0/4chan-x",
|
||||
"downloads": "https://ccd0.github.io/4chan-x/builds/",
|
||||
"fork": "ccd0",
|
||||
"version": "1.11.9.2",
|
||||
"date": "2015-08-17T01:13:28.335Z",
|
||||
"page": "https://www.4chan-x.net/",
|
||||
"downloads": "https://www.4chan-x.net/builds/",
|
||||
"oldVersions": "https://raw.githubusercontent.com/ccd0/4chan-x/",
|
||||
"faq": "https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions",
|
||||
"license": "https://github.com/ccd0/4chan-x/blob/master/LICENSE",
|
||||
"changelog": "https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md",
|
||||
"issues": "https://gitreports.com/issue/ccd0/4chan-x",
|
||||
"newIssue": "https://gitreports.com/issue/ccd0/4chan-x?issue_title=%title&details=%details",
|
||||
"appid": "lacclbnghgdicfifcamcmcnilckjamag",
|
||||
"chromeStoreID": "ohnjgmpcibpbafdlkimncjhflgedgpam",
|
||||
"recaptchaKey": "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc",
|
||||
"youtubeAPIKey": "AIzaSyB5_zaen_-46Uhz1xGR-lz1YoUMHqCD6CE",
|
||||
"buildsPath": "builds/",
|
||||
"mainBranch": "master",
|
||||
"awsBucket": "4chan-x",
|
||||
"matches": [
|
||||
"*://boards.4chan.org/*",
|
||||
"*://sys.4chan.org/*",
|
||||
"*://a.4cdn.org/*",
|
||||
"*://i.4cdn.org/*",
|
||||
"*://www.4chan.org/banned",
|
||||
"*://www.4chan.org/feedback",
|
||||
"https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*",
|
||||
"https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*",
|
||||
"*://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"
|
||||
"*://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*",
|
||||
"*://www.google.com/recaptcha/api/noscript?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*"
|
||||
],
|
||||
"suffix": {
|
||||
"stable": "",
|
||||
@ -38,16 +44,16 @@
|
||||
"dev": " dev"
|
||||
},
|
||||
"min": {
|
||||
"chrome": "32",
|
||||
"chrome": "33",
|
||||
"firefox": "26",
|
||||
"greasemonkey": "1.14"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"crx": "^3.0.2",
|
||||
"font-awesome": "4.3.0",
|
||||
"crx": "^3.0.3",
|
||||
"font-awesome": "^4.4.0",
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-concurrent": "^1.0.0",
|
||||
"grunt-concurrent": "^2.0.1",
|
||||
"grunt-contrib-clean": "^0.6.0",
|
||||
"grunt-contrib-coffee": "^0.13.0",
|
||||
"grunt-contrib-concat": "^0.5.1",
|
||||
@ -56,7 +62,7 @@
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-markdown": "^0.7.0",
|
||||
"grunt-shell": "^1.1.2",
|
||||
"grunt-webstore-upload": "^0.8.2",
|
||||
"grunt-webstore-upload": "^0.8.5",
|
||||
"jszip": "^2.5.0",
|
||||
"load-grunt-tasks": "^3.2.0",
|
||||
"npm-shrinkwrap": "^5.4.0"
|
||||
|
||||
@ -95,11 +95,13 @@ Redirect =
|
||||
location.protocol is 'http:' or
|
||||
Conf['Except Archives from Encryption']
|
||||
|
||||
navigate: (URL, alternative) ->
|
||||
if URL and (
|
||||
Redirect.securityCheck(URL) or
|
||||
confirm "Redirect to #{URL}?\n\nYour connection will not be encrypted."
|
||||
navigate: (dest, data, alternative) ->
|
||||
Redirect.init() unless Redirect.data
|
||||
url = Redirect.to dest, data
|
||||
if url and (
|
||||
Redirect.securityCheck(url) or
|
||||
confirm "Redirect to #{url}?\n\nYour connection will not be encrypted."
|
||||
)
|
||||
location.replace URL
|
||||
location.replace url
|
||||
else if alternative
|
||||
location.replace alternative
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"https": true,
|
||||
"software": "foolfuuka",
|
||||
"boards": ["a", "an", "biz", "c", "co", "diy", "fit", "gd", "gif", "h", "i", "int", "jp", "k", "m", "mlp", "out", "po", "qa", "r9k", "s4s", "sci", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
|
||||
"files": ["a", "biz", "c", "co", "diy", "fit", "gd", "h", "i", "jp", "k", "m", "mlp", "po", "qa", "r9k", "s4s", "sci", "tg", "u", "v", "vg", "vp", "vr", "wsg"]
|
||||
"files": ["a", "an", "biz", "c", "co", "diy", "fit", "gd", "gif", "h", "i", "int", "jp", "k", "m", "mlp", "out", "po", "qa", "r9k", "s4s", "sci", "tg", "u", "v", "vg", "vp", "vr", "wsg"]
|
||||
}, {
|
||||
"uid": 3,
|
||||
"name": "4plebs Archive",
|
||||
@ -79,4 +79,13 @@
|
||||
"software": "foolfuuka",
|
||||
"boards": ["mlp", "qa"],
|
||||
"files": ["mlp", "qa"]
|
||||
}, {
|
||||
"uid": 24,
|
||||
"name": "fireden.net",
|
||||
"domain": "boards.fireden.net",
|
||||
"http": false,
|
||||
"https": true,
|
||||
"software": "foolfuuka",
|
||||
"boards": ["cm", "ic", "vg", "y"],
|
||||
"files": ["cm", "ic", "vg", "y"]
|
||||
}]
|
||||
|
||||
@ -17,12 +17,10 @@ Filter =
|
||||
# Don't mix up filter flags with the regular expression.
|
||||
filter = line.replace regexp[0], ''
|
||||
|
||||
# Do not add this filter to the list if it's not a global one
|
||||
# and it's not specifically applicable to the current board.
|
||||
# Comma-separated list of the boards this filter applies to.
|
||||
# Defaults to global.
|
||||
boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global'
|
||||
if boards isnt 'global' and g.BOARD.ID not in boards.split ','
|
||||
continue
|
||||
boards = if boards is 'global' then null else boards.split(',')
|
||||
|
||||
if key in ['uniqueID', 'MD5']
|
||||
# MD5 filter will use strings instead of regular expressions.
|
||||
@ -66,7 +64,7 @@ Filter =
|
||||
top = filter.match(/top:(yes|no)/)?[1] or 'yes'
|
||||
top = top is 'yes' # Turn it into a boolean
|
||||
|
||||
@filters[key].push @createFilter regexp, op, stub, hl, top
|
||||
@filters[key].push @createFilter regexp, boards, op, stub, hl, top
|
||||
|
||||
# Only execute filter types that contain valid filters.
|
||||
unless @filters[key].length
|
||||
@ -77,7 +75,7 @@ Filter =
|
||||
name: 'Filter'
|
||||
cb: @node
|
||||
|
||||
createFilter: (regexp, op, stub, hl, top) ->
|
||||
createFilter: (regexp, boards, op, stub, hl, top) ->
|
||||
test =
|
||||
if typeof regexp is 'string'
|
||||
# MD5 checking
|
||||
@ -91,7 +89,9 @@ Filter =
|
||||
class: hl
|
||||
top: top
|
||||
|
||||
(value, isReply) ->
|
||||
(value, boardID, isReply) ->
|
||||
if boards and boardID not in boards
|
||||
return false
|
||||
if isReply and op is 'only' or !isReply and op is 'no'
|
||||
return false
|
||||
unless test value
|
||||
@ -103,7 +103,7 @@ Filter =
|
||||
for key of Filter.filters when (value = Filter[key] @)?
|
||||
# Continue if there's nothing to filter (no tripcode for example).
|
||||
|
||||
for filter in Filter.filters[key] when result = filter value, @isReply
|
||||
for filter in Filter.filters[key] when result = filter value, @board.ID, @isReply
|
||||
# Hide
|
||||
if result.hide and not @isFetchedQuote
|
||||
if @isReply
|
||||
@ -123,7 +123,7 @@ Filter =
|
||||
|
||||
isHidden: (post) ->
|
||||
for key of Filter.filters when (value = Filter[key] post)?
|
||||
for filter in Filter.filters[key] when result = filter value, post.isReply
|
||||
for filter in Filter.filters[key] when result = filter value, post.boardID, post.isReply
|
||||
return true if result.hide
|
||||
false
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ Config =
|
||||
true
|
||||
'Enable reporting posts to supported archives.'
|
||||
]
|
||||
'Except Archives from Encryption': [
|
||||
'Exempt Archives from Encryption': [
|
||||
false
|
||||
'Permit loading content from, and warningless redirects to, HTTP-only archives from HTTPS pages.'
|
||||
]
|
||||
@ -382,7 +382,7 @@ Config =
|
||||
'Bookmark threads.'
|
||||
]
|
||||
'Fixed Thread Watcher': [
|
||||
null # XXX temporarily set in Main.coffee so old versions update to correct setting
|
||||
true
|
||||
'Makes the thread watcher scroll with the page.'
|
||||
1
|
||||
]
|
||||
@ -396,7 +396,7 @@ Config =
|
||||
'Label each post from a new IP with the thread\'s current IP count.'
|
||||
]
|
||||
|
||||
'Posting':
|
||||
'Posting and Captchas':
|
||||
'Quick Reply': [
|
||||
true
|
||||
'All-in-one form to reply, create threads, automate dumping and more.'
|
||||
@ -421,23 +421,27 @@ Config =
|
||||
'Open new threads or replies to a thread from the index in a new tab.'
|
||||
1
|
||||
]
|
||||
<% if (type === 'userscript') { %>
|
||||
'Remember QR Size': [
|
||||
false
|
||||
'Remember the size of the Quick reply.'
|
||||
1
|
||||
]
|
||||
<% } %>
|
||||
'Remember Spoiler': [
|
||||
false
|
||||
'Remember the spoiler state, instead of resetting after posting.'
|
||||
1
|
||||
]
|
||||
'Randomize Filename': [
|
||||
false
|
||||
'Set the filename to a random timestamp within the past year. Disabled on /f/.'
|
||||
1
|
||||
]
|
||||
'Show New Thread Option in Threads': [
|
||||
false
|
||||
'Show the option to post a new / different thread from inside a thread.'
|
||||
1
|
||||
]
|
||||
# XXX This has been migrated to Name Sync and will be removed from 4chan X in a future version.
|
||||
'Show Name and Subject': [
|
||||
false
|
||||
'Show the classic name, email, and subject fields in the QR, even when 4chan doesn\'t use them all.'
|
||||
@ -480,7 +484,15 @@ Config =
|
||||
]
|
||||
'Captcha Fixes': [
|
||||
true
|
||||
'Make captcha more keyboard-navigable.'
|
||||
'Make captcha easier to use, especially with the keyboard.'
|
||||
]
|
||||
'Use Recaptcha v1': [
|
||||
false
|
||||
'Use the old text version of Recaptcha.'
|
||||
]
|
||||
'Use Recaptcha v2 in Reports': [
|
||||
false
|
||||
'Use the image selection captcha in the report window.'
|
||||
]
|
||||
|
||||
'Quote Links':
|
||||
@ -587,6 +599,9 @@ Config =
|
||||
'Fit Height': [
|
||||
true
|
||||
]
|
||||
'Stretch to Fit': [
|
||||
false
|
||||
]
|
||||
'Scroll to Post': [
|
||||
true
|
||||
]
|
||||
@ -653,6 +668,10 @@ Config =
|
||||
comment: """
|
||||
# Filter Stallman copypasta on /g/:
|
||||
#/what you\'re refer+ing to as linux/i;boards:g
|
||||
# Filter posts with 20 or more quote links:
|
||||
#/(?:>>\\d(?:(?!>>\\d)[^])*){20}/
|
||||
# Filter posts like T H I S / H / I / S:
|
||||
#/^>?\\s?\\w\\s?(\\w)\\s?(\\w)\\s?(\\w).*$[\\s>]+\\1[\\s>]+\\2[\\s>]+\\3/im
|
||||
"""
|
||||
|
||||
flag: ''
|
||||
@ -674,7 +693,9 @@ Config =
|
||||
#https://www.yandex.com/images/search?rpt=imageview&img_url=%IMG
|
||||
#//saucenao.com/search.php?url=%IMG
|
||||
#http://3d.iqdb.org/?url=%IMG
|
||||
# tools:
|
||||
#http://regex.info/exif.cgi?imgurl=%URL
|
||||
#//imgops.com/%URL;types:gif,jpg,png
|
||||
# uploaders:
|
||||
#//imgur.com/upload?url=%URL;types:gif,jpg,png,pdf;text:Upload to imgur
|
||||
# "View Same" in archives:
|
||||
@ -738,6 +759,8 @@ Config =
|
||||
#options:"sage";boards:jp;always
|
||||
"""
|
||||
|
||||
captchaLanguage: ''
|
||||
|
||||
time: '%m/%d/%y(%a)%H:%M:%S'
|
||||
|
||||
backlink: '>>%id'
|
||||
@ -946,10 +969,6 @@ Config =
|
||||
true
|
||||
'Automatically fetch new posts.'
|
||||
]
|
||||
'Ignore Offline Status': [
|
||||
false
|
||||
'Update even if your browser reports you are offline.'
|
||||
]
|
||||
'Optional Increase': [
|
||||
false
|
||||
'Increase the intervals between updates on threads without new posts.'
|
||||
|
||||
@ -31,25 +31,41 @@ CrossOrigin = do ->
|
||||
cb new Uint8Array(response), contentType, contentDisposition
|
||||
<% } %>
|
||||
<% if (type === 'userscript') { %>
|
||||
GM_xmlhttpRequest
|
||||
# Use workaround for binary data in Greasemonkey versions < 3.2 and in JS Blocker (Safari)
|
||||
workaround = $.engine is 'gecko' and GM_info? and /^[0-2]\.|^3\.[01](?!\d)/.test(GM_info.version)
|
||||
workaround or= GM_info?.script?.includeJSB?
|
||||
options =
|
||||
method: "GET"
|
||||
url: url
|
||||
headers: headers
|
||||
overrideMimeType: "text/plain; charset=x-user-defined"
|
||||
onload: (xhr) ->
|
||||
r = xhr.responseText
|
||||
data = new Uint8Array r.length
|
||||
i = 0
|
||||
while i < r.length
|
||||
data[i] = r.charCodeAt i
|
||||
i++
|
||||
contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1]
|
||||
contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1]
|
||||
if workaround
|
||||
r = xhr.responseText
|
||||
data = new Uint8Array r.length
|
||||
i = 0
|
||||
while i < r.length
|
||||
data[i] = r.charCodeAt i
|
||||
i++
|
||||
else
|
||||
data = new Uint8Array xhr.response
|
||||
if typeof xhr.responseHeaders is 'object'
|
||||
# XXX https://github.com/infernoboy/JavaScript-Blocker/issues/35
|
||||
contentType = xhr.responseHeaders['Content-Type']
|
||||
contentDisposition = xhr.responseHeaders['Content-Disposition']
|
||||
else
|
||||
contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1]
|
||||
contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1]
|
||||
cb data, contentType, contentDisposition
|
||||
onerror: ->
|
||||
cb null
|
||||
onabort: ->
|
||||
cb null
|
||||
if workaround
|
||||
# XXX https://github.com/infernoboy/JavaScript-Blocker/issues/35
|
||||
options.overrideMimeType = options.mimeType = 'text/plain; charset=x-user-defined'
|
||||
else
|
||||
options.responseType = 'arraybuffer'
|
||||
GM_xmlhttpRequest options
|
||||
<% } %>
|
||||
|
||||
file: (url, cb) ->
|
||||
@ -62,6 +78,9 @@ CrossOrigin = do ->
|
||||
contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1]
|
||||
if match
|
||||
name = match.replace /\\"/g, '"'
|
||||
if GM_info?.script?.includeJSB?
|
||||
# Content type comes back as 'text/plain; charset=x-user-defined'; guess from filename instead.
|
||||
mime = QR.typeFromExtension[name.match(/[^.]*$/)[0].toLowerCase()] or 'application/octet-stream'
|
||||
blob = new Blob([data], {type: mime})
|
||||
blob.name = name
|
||||
cb blob
|
||||
|
||||
@ -105,7 +105,7 @@ Index =
|
||||
d.implementation.createDocument(null, null, null).appendChild board
|
||||
|
||||
$.rm el for el in $$ '.navLinks'
|
||||
$.id('search-box')?.parentNode.remove()
|
||||
$.rm $.id('ctrl-top')
|
||||
topNavPos = $.id('delform').previousElementSibling
|
||||
$.before topNavPos, $.el 'hr'
|
||||
$.before topNavPos, Index.navLinks
|
||||
@ -227,13 +227,13 @@ Index =
|
||||
popstate: (e) ->
|
||||
if e?.state
|
||||
{search, mode} = e.state
|
||||
page = Index.getCurrentPage()
|
||||
state = {}
|
||||
if Index.search isnt search
|
||||
state.search = Index.search = search
|
||||
if Conf['Index Mode'] isnt mode
|
||||
state.mode = mode
|
||||
Index.saveMode mode
|
||||
page = Index.getCurrentPage()
|
||||
if Index.currentPage isnt page
|
||||
state.page = Index.currentPage = page
|
||||
if state.search? or state.mode? or state.page?
|
||||
@ -412,7 +412,6 @@ Index =
|
||||
"#{hiddenCount} hidden threads"
|
||||
|
||||
update: (state) ->
|
||||
delete Index.pageNum
|
||||
Index.req?.abort()
|
||||
Index.notice?.close()
|
||||
|
||||
@ -631,8 +630,11 @@ Index =
|
||||
i = 0
|
||||
i++ while Index.followedThreadID isnt Get.threadFromRoot(Index.sortedNodes[i]).ID
|
||||
page = i // Index.threadsNumPerPage + 1
|
||||
Index.pushState {page} if page isnt Index.getCurrentPage()
|
||||
if page isnt Index.getCurrentPage()
|
||||
Index.pushState {page}
|
||||
Index.setPage()
|
||||
nodes = Index.buildSinglePage Index.getCurrentPage()
|
||||
delete Index.pageNum
|
||||
$.rmAll Index.root
|
||||
$.rmAll Header.hover
|
||||
if Conf['Index Mode'] is 'catalog'
|
||||
|
||||
@ -1,12 +1,21 @@
|
||||
Main =
|
||||
init: ->
|
||||
# XXX Work around Pale Moon / old Firefox + GM 1.15 bug where script runs in iframe with wrong window.location.
|
||||
return if d.body and not $ 'title', d.head
|
||||
|
||||
# XXX dwb userscripts extension reloads scripts run at document-start when replaceState/pushState is called.
|
||||
return if window['<%= meta.name %> antidup']
|
||||
window['<%= meta.name %> antidup'] = true
|
||||
|
||||
if location.hostname is 'www.google.com'
|
||||
if location.pathname is '/recaptcha/api/fallback'
|
||||
if location.pathname is '/recaptcha/api/noscript'
|
||||
$.ready -> Captcha.noscript.initFrame()
|
||||
else
|
||||
$.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) ->
|
||||
if enabled
|
||||
$.ready -> Captcha.fixes.init()
|
||||
return
|
||||
if location.pathname is '/recaptcha/api/fallback'
|
||||
$.ready -> Captcha.v2.initFrame()
|
||||
$.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) ->
|
||||
if enabled
|
||||
$.ready -> Captcha.fixes.init()
|
||||
return
|
||||
|
||||
g.threads = new SimpleDict()
|
||||
@ -28,8 +37,7 @@ Main =
|
||||
if g.VIEW is 'thread'
|
||||
g.THREADID = +pathname[3]
|
||||
|
||||
# flatten Config into Conf
|
||||
# and get saved or default values
|
||||
# Flatten default values from Config into Conf
|
||||
flatten = (parent, obj) ->
|
||||
if obj instanceof Array
|
||||
Conf[parent] = obj[0]
|
||||
@ -45,37 +53,83 @@ Main =
|
||||
for db in DataBoard.keys
|
||||
Conf[db] = boards: {}
|
||||
Conf['selectedArchives'] = {}
|
||||
Conf['cooldowns'] = {}
|
||||
|
||||
$.get Conf, (items) ->
|
||||
$.extend Conf, items
|
||||
# XXX temporarily set here so old versions update to correct setting
|
||||
Conf['Fixed Thread Watcher'] ?= Conf['Toggleable Thread Watcher']
|
||||
$.asap (-> doc = d.documentElement), Main.initFeatures
|
||||
# XXX old key names
|
||||
Conf['Except Archives from Encryption'] = false
|
||||
|
||||
# Get saved values as items
|
||||
items = {}
|
||||
items[key] = undefined for key of Conf
|
||||
items['previousversion'] = undefined
|
||||
$.get items, (items) ->
|
||||
$.asap (-> doc = d.documentElement), ->
|
||||
|
||||
# Fresh install
|
||||
if !items.previousversion?
|
||||
Main.ready ->
|
||||
$.set 'previousversion', g.VERSION
|
||||
Settings.open()
|
||||
|
||||
# Migrate old settings
|
||||
else if items.previousversion isnt g.VERSION
|
||||
Main.upgrade items
|
||||
|
||||
# Combine default values with saved values
|
||||
for key, val of Conf
|
||||
Conf[key] = items[key] ? val
|
||||
|
||||
Main.initFeatures()
|
||||
|
||||
# set up CSS when <head> is completely loaded
|
||||
$.asap (-> doc = d.documentElement), ->
|
||||
$.onExists doc, 'body', false, Main.initStyle
|
||||
|
||||
upgrade: (items) ->
|
||||
{previousversion} = items
|
||||
items2 = {previousversion: g.VERSION}
|
||||
compareString = previousversion.replace(/\d+/g, (x) -> ('0000'+x)[-5..])
|
||||
|
||||
if compareString < '00001.00011.00008.00000'
|
||||
unless items['Fixed Thread Watcher']?
|
||||
items2['Fixed Thread Watcher'] = items['Toggleable Thread Watcher'] ? true
|
||||
unless items['Exempt Archives from Encryption']?
|
||||
items2['Exempt Archives from Encryption'] = items['Except Archives from Encryption'] ? false
|
||||
|
||||
$.extend items, items2
|
||||
$.set items2, ->
|
||||
if items['Show Updated Notifications'] ? true
|
||||
el = $.el 'span',
|
||||
<%= html(meta.name + ' has been updated to <a href="' + meta.changelog + '" target="_blank">version ${g.VERSION}</a>.') %>
|
||||
new Notice 'info', el, 15
|
||||
|
||||
initFeatures: ->
|
||||
if location.hostname in ['boards.4chan.org', 'sys.4chan.org']
|
||||
if location.hostname in ['boards.4chan.org', 'sys.4chan.org', 'www.4chan.org']
|
||||
$.globalEval 'document.documentElement.classList.add("js-enabled");'
|
||||
|
||||
switch location.hostname
|
||||
when 'www.4chan.org'
|
||||
Captcha.replace.init()
|
||||
return
|
||||
when 'a.4cdn.org'
|
||||
return
|
||||
when 'sys.4chan.org'
|
||||
Report.init()
|
||||
PostSuccessful.init() if g.VIEW is 'post'
|
||||
if Conf['404 Redirect'] and /\/imgboard\.php$/.test(location.pathname) and (match = location.search.match /\bres=(\d+)/)
|
||||
$.ready ->
|
||||
if $.id('errmsg')?.textContent is 'Error: Specified thread does not exist.'
|
||||
Redirect.navigate 'thread',
|
||||
boardID: g.BOARD.ID
|
||||
postID: +match[1]
|
||||
return
|
||||
when 'i.4cdn.org'
|
||||
$.asap (-> d.readyState isnt 'loading'), ->
|
||||
if Conf['404 Redirect'] and d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
|
||||
Redirect.init()
|
||||
pathname = location.pathname.split '/'
|
||||
URL = Redirect.to 'file',
|
||||
Redirect.navigate 'file',
|
||||
boardID: g.BOARD.ID
|
||||
filename: pathname[pathname.length - 1]
|
||||
Redirect.navigate URL
|
||||
else if video = $ 'video'
|
||||
if Conf['Volume in New Tab']
|
||||
Volume.setup video
|
||||
@ -109,12 +163,15 @@ Main =
|
||||
$.ready Main.initReady
|
||||
|
||||
initStyle: ->
|
||||
$.addStyle Main.cssWWW if location.hostname is 'www.4chan.org'
|
||||
|
||||
return if !Main.isThisPageLegit() or $.hasClass doc, 'fourchan-x'
|
||||
|
||||
# disable the mobile layout
|
||||
$('link[href*=mobile]', d.head)?.disabled = true
|
||||
$.addClass doc, 'fourchan-x', 'seaweedchan'
|
||||
$.addClass doc, if g.VIEW is 'thread' then 'thread-view' else g.VIEW
|
||||
$.addClass doc, if chrome? then 'blink' else 'gecko'
|
||||
$.addClass doc, $.engine if $.engine
|
||||
$.addStyle Main.css, 'fourchanx-css'
|
||||
|
||||
keyboard = false
|
||||
@ -146,17 +203,19 @@ Main =
|
||||
attributeFilter: ['href']
|
||||
|
||||
initReady: ->
|
||||
if d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
|
||||
if g.VIEW is 'thread'
|
||||
ThreadWatcher.set404 g.BOARD.ID, g.THREADID, ->
|
||||
if Conf['404 Redirect']
|
||||
href = Redirect.to 'thread',
|
||||
boardID: g.BOARD.ID
|
||||
threadID: g.THREADID
|
||||
postID: +location.hash.match /\d+/ # post number or 0
|
||||
Redirect.navigate href, "/#{g.BOARD}/"
|
||||
# XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post.
|
||||
if g.VIEW is 'thread' and (d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] or ($('.board') and not $('.opContainer')))
|
||||
ThreadWatcher.set404 g.BOARD.ID, g.THREADID, ->
|
||||
if Conf['404 Redirect']
|
||||
Redirect.navigate 'thread',
|
||||
boardID: g.BOARD.ID
|
||||
threadID: g.THREADID
|
||||
postID: +location.hash.match /\d+/ # post number or 0
|
||||
, "/#{g.BOARD}/"
|
||||
return
|
||||
|
||||
return if d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
|
||||
|
||||
# 4chan Pass Link
|
||||
if styleSelector = $.id 'styleSelector'
|
||||
passLink = $.el 'a',
|
||||
@ -174,25 +233,7 @@ Main =
|
||||
else
|
||||
$.event '4chanXInitFinished'
|
||||
|
||||
$.get 'previousversion', null, ({previousversion}) ->
|
||||
return if previousversion is g.VERSION
|
||||
if previousversion
|
||||
el = $.el 'span',
|
||||
<%= html(meta.name + ' has been updated to <a href="' + meta.repo + 'blob/' + meta.mainBranch + '/CHANGELOG.md" target="_blank">version ${g.VERSION}</a>.') %>
|
||||
new Notice 'info', el, 15
|
||||
else
|
||||
Settings.open()
|
||||
$.set 'previousversion', g.VERSION
|
||||
|
||||
if Conf['Show Support Message']
|
||||
<% if (type === 'userscript') { %>
|
||||
GMver = GM_info.version.split '.'
|
||||
for v, i in "<%= meta.min.greasemonkey %>".split '.'
|
||||
continue if v is GMver[i]
|
||||
(v < GMver[i]) or new Notice 'warning', "Your version of Greasemonkey is outdated (v#{GM_info.version} instead of v<%= meta.min.greasemonkey %> minimum) and <%= meta.name %> may not operate correctly.", 30
|
||||
break
|
||||
<% } %>
|
||||
|
||||
try
|
||||
localStorage.getItem '4chan-settings'
|
||||
catch err
|
||||
@ -263,11 +304,11 @@ Main =
|
||||
else if errors.length is 1
|
||||
error = errors[0]
|
||||
if error
|
||||
new Notice 'error', Main.parseError(error), 15
|
||||
new Notice 'error', Main.parseError(error, Main.reportLink([error])), 15
|
||||
return
|
||||
|
||||
div = $.el 'div',
|
||||
<%= html('${errors.length} errors occurred. [<a href="javascript:;">show</a>]') %>
|
||||
<%= html('${errors.length} errors occurred.&{Main.reportLink(errors)} [<a href="javascript:;">show</a>]') %>
|
||||
$.on div.lastElementChild, 'click', ->
|
||||
[@textContent, logs.hidden] = if @textContent is 'show'
|
||||
['hide', false]
|
||||
@ -281,13 +322,34 @@ Main =
|
||||
|
||||
new Notice 'error', [div, logs], 30
|
||||
|
||||
parseError: (data) ->
|
||||
parseError: (data, reportLink) ->
|
||||
c.error data.message, data.error.stack
|
||||
message = $.el 'div',
|
||||
textContent: data.message
|
||||
<%= html('${data.message}?{reportLink}{&{reportLink}}') %>
|
||||
error = $.el 'div',
|
||||
textContent: "#{data.error.name or 'Error'}: #{data.error.message or 'see console for details'}"
|
||||
[message, error]
|
||||
lines = data.error.stack?.match(/\d+(?=:\d+\)?$)/mg)?.join().replace(/^/, ' at ') or ''
|
||||
context = $.el 'div',
|
||||
textContent: "(<%= meta.name %> <%= meta.fork %> v#{g.VERSION} <%= type %> on #{$.engine}#{lines})"
|
||||
[message, error, context]
|
||||
|
||||
reportLink: (errors) ->
|
||||
data = errors[0]
|
||||
title = data.message
|
||||
title += " (+#{errors.length - 1} other errors)" if errors.length > 1
|
||||
details = """
|
||||
[Please describe the steps needed to reproduce this error.]
|
||||
|
||||
Script: <%= meta.name %> <%= meta.fork %> v#{g.VERSION} <%= type %>
|
||||
User agent: #{navigator.userAgent}
|
||||
URL: #{location.href}
|
||||
|
||||
#{data.error}
|
||||
#{data.error.stack?.replace(data.error.toString(), '').trim() or ''}
|
||||
"""
|
||||
details = details.replace /file:\/{3}.+\//g, '' # Remove local file paths
|
||||
url = "<%= meta.newIssue.replace('%title', '#{encodeURIComponent title}').replace('%details', '#{encodeURIComponent details}') %>"
|
||||
<%= html(' [<a href="${url}" target="_blank">report</a>]') %>
|
||||
|
||||
isThisPageLegit: ->
|
||||
# 404 error page or similar.
|
||||
@ -301,17 +363,13 @@ Main =
|
||||
$.ready ->
|
||||
cb() if Main.isThisPageLegit()
|
||||
|
||||
css: `<%=
|
||||
grunt.template.process(
|
||||
['font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'].map(function(name) {
|
||||
return grunt.file.read('src/General/css/'+name+'.css');
|
||||
}).join(''),
|
||||
{data: {type: type}}
|
||||
).trim().replace(/\n+/g, '\n').split(/^/m).map(JSON.stringify).join(' +\n').replace(/`/g, '\\`')
|
||||
%>`
|
||||
css: `<%= importCSS('font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon') %>`
|
||||
|
||||
cssWWW: `<%= importCSS('www') %>`
|
||||
|
||||
features: [
|
||||
['Polyfill', Polyfill]
|
||||
['Captcha Replacement', Captcha.replace]
|
||||
['Redirect', Redirect]
|
||||
['Header', Header]
|
||||
['Catalog Links', CatalogLinks]
|
||||
@ -332,6 +390,7 @@ Main =
|
||||
['Recursive', Recursive]
|
||||
['Strike-through Quotes', QuoteStrikeThrough]
|
||||
['Quick Reply', QR]
|
||||
['Cooldown', QR.cooldown]
|
||||
['Menu', Menu]
|
||||
['Index Generator (Menu)', Index.menu]
|
||||
['Report Link', ReportLink]
|
||||
|
||||
@ -103,6 +103,7 @@ Settings =
|
||||
description = arr[1]
|
||||
div = $.el 'div',
|
||||
<%= html('<label><input type="checkbox" name="${key}">${key}</label><span class="description">: ${description}</span>') %>
|
||||
div.hidden = true if $.engine isnt 'gecko' and key is 'Remember QR Size' # XXX not supported
|
||||
input = $ 'input', div
|
||||
$.on input, 'change', ->
|
||||
@parentNode.parentNode.dataset.checked = @checked
|
||||
@ -155,11 +156,9 @@ Settings =
|
||||
a = $.el 'a',
|
||||
download: "<%= meta.name %> v#{g.VERSION}-#{data.date}.json"
|
||||
href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}"
|
||||
<% if (type === 'userscript') { %>
|
||||
p = $ '.imp-exp-result', Settings.dialog
|
||||
$.rmAll p
|
||||
$.add p, a
|
||||
<% } %>
|
||||
a.click()
|
||||
import: ->
|
||||
$('input[type=file]', @parentNode).click()
|
||||
@ -314,7 +313,7 @@ Settings =
|
||||
|
||||
items = {}
|
||||
inputs = {}
|
||||
for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss', 'customCooldown']
|
||||
for name in ['captchaLanguage', 'boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss', 'customCooldown']
|
||||
input = $ "[name='#{name}']", section
|
||||
items[name] = Conf[name]
|
||||
inputs[name] = input
|
||||
@ -323,9 +322,9 @@ Settings =
|
||||
else if name is 'favicon'
|
||||
$.on input, 'change', $.cb.value
|
||||
$.on input, 'change', Settings[name]
|
||||
else
|
||||
else
|
||||
$.on input, 'input', $.cb.value
|
||||
$.on input, 'input', Settings[name]
|
||||
$.on input, 'input', Settings[name] if name of Settings
|
||||
|
||||
# Quick Reply Personas
|
||||
ta = $ '.personafield', section
|
||||
@ -338,8 +337,8 @@ Settings =
|
||||
for key, val of items
|
||||
input = inputs[key]
|
||||
input.value = val
|
||||
continue if key in ['usercss', 'customCooldown']
|
||||
Settings[key].call input
|
||||
if key of Settings and key isnt 'usercss'
|
||||
Settings[key].call input
|
||||
return
|
||||
|
||||
interval = $ 'input[name="Interval"]', section
|
||||
|
||||
@ -102,7 +102,13 @@ UI = do ->
|
||||
|
||||
insertEntry: (entry, parent, data) ->
|
||||
if typeof entry.open is 'function'
|
||||
return unless entry.open data
|
||||
try
|
||||
return unless entry.open data
|
||||
catch err
|
||||
Main.handleErrors
|
||||
message: "Error in building the #{@type} menu."
|
||||
error: err
|
||||
return
|
||||
$.add parent, entry.el
|
||||
|
||||
return unless entry.subEntries
|
||||
@ -331,11 +337,9 @@ UI = do ->
|
||||
$.on d, 'keydown', o.hoverend
|
||||
$.on root, 'mousemove', o.hover
|
||||
|
||||
<% if (type === 'userscript') { %>
|
||||
# Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955
|
||||
o.workaround = (e) -> o.hoverend(e) unless root.contains e.target
|
||||
$.on doc, 'mousemove', o.workaround
|
||||
<% } %>
|
||||
|
||||
hover = (e) ->
|
||||
@latestEvent = e
|
||||
@ -365,10 +369,8 @@ UI = do ->
|
||||
$.off @root, @endEvents, @hoverend
|
||||
$.off d, 'keydown', @hoverend
|
||||
$.off @root, 'mousemove', @hover
|
||||
<% if (type === 'userscript') { %>
|
||||
# Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955
|
||||
$.off doc, 'mousemove', @workaround
|
||||
<% } %>
|
||||
@cb.call @ if @cb
|
||||
|
||||
checkbox = (name, text, checked) ->
|
||||
|
||||
6
src/General/css/report.css
Normal file
6
src/General/css/report.css
Normal file
@ -0,0 +1,6 @@
|
||||
:root:not(.js-enabled) #captchaContainerAlt {
|
||||
height: auto;
|
||||
}
|
||||
noscript > iframe, #recaptcha_challenge_field {
|
||||
width: 500px;
|
||||
}
|
||||
@ -105,9 +105,16 @@ hr + div.center:not(.ad-cnt):not(.topad):not(.middlead):not(.bottomad) {
|
||||
/* party hats */
|
||||
pointer-events: none;
|
||||
}
|
||||
#g-recaptcha {
|
||||
min-height: 78px;
|
||||
height: auto;
|
||||
}
|
||||
:root:not(.js-enabled) #postForm {
|
||||
display: table;
|
||||
}
|
||||
#captchaContainerAlt td:nth-child(2) {
|
||||
display: table-cell !important;
|
||||
}
|
||||
|
||||
/* Anti-autoplay */
|
||||
audio.controls-added {
|
||||
@ -120,6 +127,11 @@ audio.controls-added {
|
||||
height: auto;
|
||||
text-align: center;
|
||||
}
|
||||
:root.anti-autoplay .autoplay-removed {
|
||||
display: block !important;
|
||||
min-width: 640px;
|
||||
min-height: 390px;
|
||||
}
|
||||
|
||||
/* fixed, z-index */
|
||||
#overlay,
|
||||
@ -161,9 +173,15 @@ audio.controls-added {
|
||||
#embedding {
|
||||
z-index: 11;
|
||||
}
|
||||
#thread-watcher {
|
||||
:root.fixed-watcher #thread-watcher {
|
||||
z-index: 10;
|
||||
}
|
||||
:root.fixed:not(.gallery-open) #header-bar:not(:hover) {
|
||||
z-index: 8;
|
||||
}
|
||||
#thread-watcher {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.fixed.top-header body {
|
||||
@ -239,7 +257,7 @@ audio.controls-added {
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
}
|
||||
:root:not(.autohide) #scroll-marker {
|
||||
#header-bar:not(.autohide) #scroll-marker {
|
||||
pointer-events: none;
|
||||
}
|
||||
#header-bar #scroll-marker {
|
||||
@ -318,6 +336,7 @@ audio.controls-added {
|
||||
flex: auto;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
width: 0px; /* XXX Fixes Edge not shrinking the board list below default size when needed */
|
||||
}
|
||||
:root.fixed:not(.centered-links) #full-board-list > .boardList > a,
|
||||
:root.fixed:not(.centered-links) #full-board-list > .boardList > span:not(.space):not(.spacer) {
|
||||
@ -530,7 +549,7 @@ div[data-checked="false"] > .suboption-list {
|
||||
.section-filter textarea {
|
||||
height: 500px;
|
||||
}
|
||||
.section-filter a {
|
||||
.section-filter a, .section-advanced a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.section-sauce textarea {
|
||||
@ -621,10 +640,10 @@ div[data-checked="false"] > .suboption-list {
|
||||
left: -1em;
|
||||
width: 0;
|
||||
}
|
||||
<% if (type === 'crx') { %>
|
||||
/* ``::-webkit-*'' selectors break selector lists on Firefox. */
|
||||
#index-search::-webkit-search-cancel-button,
|
||||
<% } %>
|
||||
#index-search::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
#index-search:not([data-searching]) + #index-search-clear {
|
||||
display: none;
|
||||
}
|
||||
@ -792,10 +811,11 @@ span.hide-announcement {
|
||||
#thread-watcher {
|
||||
padding-bottom: 3px;
|
||||
padding-left: 3px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
min-width: 146px;
|
||||
max-height: 92%;
|
||||
}
|
||||
#watched-threads {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#thread-watcher .refresh {
|
||||
@ -804,7 +824,12 @@ span.hide-announcement {
|
||||
:root.fixed-watcher #thread-watcher {
|
||||
position: fixed;
|
||||
}
|
||||
:root:not(.fixed-watcher) #thread-watcher:not(:hover) {
|
||||
:root.fixed-watcher #watched-threads {
|
||||
/* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
|
||||
max-height: 85vh;
|
||||
max-height: calc(100vh - 75px);
|
||||
}
|
||||
:root:not(.fixed-watcher) #watched-threads:not(:hover) {
|
||||
max-height: 210px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
@ -976,6 +1001,11 @@ span.hide-announcement {
|
||||
:root.fit-height .full-image {
|
||||
max-height: 100vh;
|
||||
}
|
||||
:root.fit-height.fixed .full-image {
|
||||
/* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
|
||||
max-height: 93vh;
|
||||
max-height: calc(100vh - 35px);
|
||||
}
|
||||
:root.fit-width .full-image {
|
||||
max-width: 100%;
|
||||
}
|
||||
@ -1150,6 +1180,13 @@ input[name="Default Volume"] {
|
||||
min-width: 300px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
#qr > form {
|
||||
/* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
|
||||
max-height: 85vh;
|
||||
max-height: calc(100vh - 75px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
#qrtab {
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
@ -1222,7 +1259,7 @@ input.field.tripped:not(:hover):not(:focus) {
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
/* Noscript Recaptcha */
|
||||
/* Recaptcha v1 */
|
||||
.captcha-img {
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
@ -1231,11 +1268,11 @@ input.field.tripped:not(:hover):not(:focus) {
|
||||
min-height: 59px;
|
||||
min-width: 302px;
|
||||
}
|
||||
.captcha-input{
|
||||
.captcha-input {
|
||||
width: 100%;
|
||||
margin: 1px 0 0;
|
||||
}
|
||||
#qr-captcha-iframe {
|
||||
#qr.captcha-v1 #qr-captcha-iframe {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -1263,6 +1300,13 @@ input.field.tripped:not(:hover):not(:focus) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
#qr.captcha-v2 #qr-captcha-iframe {
|
||||
width: 302px;
|
||||
height: 423px;
|
||||
border: 0;
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
.goog-bubble-content {
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
@ -1317,6 +1361,7 @@ input#qr-filename {
|
||||
.has-file #qr-filename {
|
||||
-webkit-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
width: 0px; /* XXX Fixes filename not shrinking to allow space for buttons in Edge */
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
padding-left: 3px;
|
||||
@ -1466,6 +1511,8 @@ a:only-of-type > .remove {
|
||||
}
|
||||
.textarea {
|
||||
position: relative;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
}
|
||||
:root.webkit .textarea {
|
||||
margin-bottom: -2px;
|
||||
@ -1505,7 +1552,7 @@ a:only-of-type > .remove {
|
||||
height: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.menu-button + .container:not(:empty) {
|
||||
.menu-button + .container :first-child {
|
||||
margin-left: -5px;
|
||||
}
|
||||
#menu {
|
||||
@ -1752,12 +1799,7 @@ grunt.file.expand('src/General/img/links/*.png').map(function(file) {
|
||||
}
|
||||
.gal-fit-height .gal-image img,
|
||||
.gal-fit-height .gal-image video {
|
||||
/*
|
||||
Chrome doesn't support viewpoint units in calc()
|
||||
http://bugs.chromium.org/168840
|
||||
"It looks like the original author of viewport units in WebKit is not coming back to fix this stuff."
|
||||
Well, fuck.
|
||||
*/
|
||||
/* XXX https://code.google.com/p/chromium/issues/detail?id=168840, https://bugs.webkit.org/show_bug.cgi?id=94158 */
|
||||
max-height: 95vh;
|
||||
max-height: calc(100vh - 25px);
|
||||
}
|
||||
@ -1860,6 +1902,6 @@ grunt.file.expand('src/General/img/links/*.png').map(function(file) {
|
||||
width: 4em;
|
||||
}
|
||||
:root.gallery-open.fixed #header-bar:not(.autohide),
|
||||
:root.gallery-open.fixed #header-bar:not(.autohide) .fa::before {
|
||||
:root.gallery-open.fixed #header-bar:not(.autohide) #shortcuts .fa::before {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
3
src/General/css/www.css
Normal file
3
src/General/css/www.css
Normal file
@ -0,0 +1,3 @@
|
||||
#captcha-cnt {
|
||||
height: auto;
|
||||
}
|
||||
@ -12,6 +12,12 @@
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Captcha Language</legend>
|
||||
<div>Choose from <a href="https://developers.google.com/recaptcha/docs/language" target="_blank">list of language codes</a>. Leave blank to autoselect.</div>
|
||||
<div><input name="captchaLanguage" class="field" spellcheck="false"></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Custom Board Navigation</legend>
|
||||
<div><textarea name="boardnav" class="field" spellcheck="false"></textarea></div>
|
||||
@ -41,7 +47,7 @@
|
||||
<fieldset>
|
||||
<legend>Time Formatting <span class="warning" data-feature="Time Formatting">is disabled.</span></legend>
|
||||
<div><input name="time" class="field" spellcheck="false">: <span class="time-preview"></span></div>
|
||||
<div>Supported <a href="//en.wikipedia.org/wiki/Date_%28Unix%29#Formatting">format specifiers</a>:</div>
|
||||
<div>Supported <a href="http://man7.org/linux/man-pages/man1/date.1.html" target="_blank">format specifiers</a>:</div>
|
||||
<div>Day: <code>%a</code>, <code>%A</code>, <code>%d</code>, <code>%e</code></div>
|
||||
<div>Month: <code>%m</code>, <code>%b</code>, <code>%B</code></div>
|
||||
<div>Year: <code>%y</code>, <code>%Y</code></div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="warning"><code>Filter</code> is disabled.</div>
|
||||
<p>
|
||||
Use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions">regular expressions</a>, one per line.<br>
|
||||
Use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions" target="_blank">regular expressions</a>, one per line.<br>
|
||||
Lines starting with a <code>#</code> will be ignored.<br>
|
||||
For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br>
|
||||
MD5 filtering uses exact string matching, not regular expressions.
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
<a class="reset">Reset Settings</a> | 
|
||||
<input type="file" hidden>
|
||||
<a href="<%= meta.page %>" target="_blank"><%= meta.name %></a> | 
|
||||
<a href="<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md" target="_blank">${g.VERSION}</a> | 
|
||||
<a href="<%= meta.repo %>issues" target="_blank">Issues</a> | 
|
||||
<a href="<%= meta.changelog %>" target="_blank">${g.VERSION}</a> | 
|
||||
<a href="<%= meta.issues %>" target="_blank">Issues</a> | 
|
||||
<a href="javascript:;" class="close fa fa-times" title="Close"></a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@ -117,6 +117,11 @@ $.asap = (test, cb) ->
|
||||
$.onExists = (root, selector, subtree, cb) ->
|
||||
if el = $ selector, root
|
||||
return cb el
|
||||
# XXX Edge doesn't notify MutationObservers of nodes added as document loads.
|
||||
if $.engine is 'edge' and d.readyState is 'loading'
|
||||
$.asap (-> d.readyState isnt 'loading' or $ selector, root), ->
|
||||
$.onExists root, selector, subtree, cb
|
||||
return
|
||||
observer = new MutationObserver ->
|
||||
if el = $ selector, root
|
||||
observer.disconnect()
|
||||
@ -156,7 +161,7 @@ $.hasClass = (el, className) ->
|
||||
className in el.classList
|
||||
|
||||
$.rm = (el) ->
|
||||
el.remove()
|
||||
el?.remove()
|
||||
|
||||
$.rmAll = (root) ->
|
||||
# https://gist.github.com/MayhemYDG/8646194
|
||||
@ -242,9 +247,12 @@ unless typeof cloneInto is 'function' or /^[01]\./.test(GM_info.version) or GM_i
|
||||
|
||||
$.open =
|
||||
<% if (type === 'userscript') { %>
|
||||
GM_openInTab
|
||||
if GM_openInTab?
|
||||
GM_openInTab
|
||||
else
|
||||
(url) -> window.open url, '_blank'
|
||||
<% } else { %>
|
||||
(URL) -> window.open URL, '_blank'
|
||||
(url) -> window.open url, '_blank'
|
||||
<% } %>
|
||||
|
||||
$.debounce = (wait, fn) ->
|
||||
@ -320,6 +328,12 @@ $.minmax = (value, min, max) ->
|
||||
$.hasAudio = (video) ->
|
||||
video.mozHasAudio or !!video.webkitAudioDecodedByteCount
|
||||
|
||||
$.engine = do ->
|
||||
return 'edge' if /Edge\//.test navigator.userAgent
|
||||
return 'blink' if /Chrome\//.test navigator.userAgent
|
||||
return 'webkit' if /WebKit\//.test navigator.userAgent
|
||||
return 'gecko' if /Gecko\/|Goanna/.test navigator.userAgent # Goanna = Pale Moon 26+
|
||||
|
||||
$.item = (key, val) ->
|
||||
item = {}
|
||||
item[key] = val
|
||||
@ -434,42 +448,76 @@ do ->
|
||||
<% } else { %>
|
||||
|
||||
# http://wiki.greasespot.net/Main_Page
|
||||
$.oldValue = {}
|
||||
# https://tampermonkey.net/documentation.php
|
||||
if GM_deleteValue?
|
||||
$.getValue = GM_getValue
|
||||
$.listValues = -> GM_listValues() # error when called if missing
|
||||
else
|
||||
$.getValue = (key) -> localStorage[key]
|
||||
$.listValues = ->
|
||||
key for key of localStorage when key[...g.NAMESPACE.length] is g.NAMESPACE
|
||||
|
||||
$.sync = (key, cb) ->
|
||||
key = g.NAMESPACE + key
|
||||
$.syncing[key] = cb
|
||||
$.oldValue[key] = GM_getValue key
|
||||
|
||||
do ->
|
||||
onChange = (key) ->
|
||||
return unless cb = $.syncing[key]
|
||||
newValue = GM_getValue key
|
||||
return if newValue is $.oldValue[key]
|
||||
if newValue?
|
||||
$.oldValue[key] = newValue
|
||||
cb JSON.parse(newValue), key
|
||||
else
|
||||
if GM_addValueChangeListener?
|
||||
$.setValue = GM_setValue
|
||||
$.deleteValue = GM_deleteValue
|
||||
else if GM_deleteValue?
|
||||
$.oldValue = {}
|
||||
$.setValue = (key, val) ->
|
||||
GM_setValue key, val
|
||||
if key of $.syncing
|
||||
$.oldValue[key] = val
|
||||
localStorage[key] = val # for `storage` events
|
||||
$.deleteValue = (key) ->
|
||||
GM_deleteValue key
|
||||
if key of $.syncing
|
||||
delete $.oldValue[key]
|
||||
cb undefined, key
|
||||
$.on window, 'storage', ({key}) -> onChange key
|
||||
delete localStorage[key] # for `storage` events
|
||||
else
|
||||
$.oldValue = {}
|
||||
$.setValue = (key, val) ->
|
||||
$.oldValue[key] = val if key of $.syncing
|
||||
localStorage[key] = val
|
||||
$.deleteValue = (key) ->
|
||||
delete $.oldValue[key] if key of $.syncing
|
||||
delete localStorage[key]
|
||||
|
||||
$.forceSync = (key) ->
|
||||
# Storage events don't work across origins
|
||||
# e.g. http://boards.4chan.org and https://boards.4chan.org
|
||||
# so force a check for changes to avoid lost data.
|
||||
onChange g.NAMESPACE + key
|
||||
if GM_addValueChangeListener?
|
||||
$.sync = (key, cb) ->
|
||||
$.syncing[key] = GM_addValueChangeListener g.NAMESPACE + key, (key2, oldValue, newValue, remote) ->
|
||||
if remote
|
||||
newValue = JSON.parse newValue unless newValue is undefined
|
||||
cb newValue, key
|
||||
$.forceSync = ->
|
||||
else
|
||||
$.sync = (key, cb) ->
|
||||
key = g.NAMESPACE + key
|
||||
$.syncing[key] = cb
|
||||
$.oldValue[key] = $.getValue key
|
||||
|
||||
do ->
|
||||
onChange = (key) ->
|
||||
return unless cb = $.syncing[key]
|
||||
newValue = $.getValue key
|
||||
return if newValue is $.oldValue[key]
|
||||
if newValue?
|
||||
$.oldValue[key] = newValue
|
||||
cb JSON.parse(newValue), key[g.NAMESPACE.length..]
|
||||
else
|
||||
delete $.oldValue[key]
|
||||
cb undefined, key[g.NAMESPACE.length..]
|
||||
$.on window, 'storage', ({key}) -> onChange key
|
||||
|
||||
$.forceSync = (key) ->
|
||||
# Storage events don't work across origins
|
||||
# e.g. http://boards.4chan.org and https://boards.4chan.org
|
||||
# so force a check for changes to avoid lost data.
|
||||
onChange g.NAMESPACE + key
|
||||
|
||||
$.delete = (keys) ->
|
||||
unless keys instanceof Array
|
||||
keys = [keys]
|
||||
for key in keys
|
||||
key = g.NAMESPACE + key
|
||||
GM_deleteValue key
|
||||
if key of $.syncing
|
||||
delete $.oldValue[key]
|
||||
# for `storage` events
|
||||
localStorage.removeItem key
|
||||
$.deleteValue g.NAMESPACE + key
|
||||
return
|
||||
|
||||
$.get = (key, val, cb) ->
|
||||
@ -480,38 +528,27 @@ $.get = (key, val, cb) ->
|
||||
cb = val
|
||||
$.queueTask ->
|
||||
for key of items
|
||||
if val = GM_getValue g.NAMESPACE + key
|
||||
if val = $.getValue g.NAMESPACE + key
|
||||
items[key] = JSON.parse val
|
||||
cb items
|
||||
|
||||
$.set = do ->
|
||||
set = (key, val) ->
|
||||
key = g.NAMESPACE + key
|
||||
val = JSON.stringify val
|
||||
GM_setValue key, val
|
||||
if key of $.syncing
|
||||
$.oldValue[key] = val
|
||||
# for `storage` events
|
||||
localStorage.setItem key, val
|
||||
|
||||
(keys, val, cb) ->
|
||||
if typeof keys is 'string'
|
||||
set keys, val
|
||||
else
|
||||
set key, value for key, value of keys
|
||||
cb = val
|
||||
cb?()
|
||||
$.set = (keys, val, cb) ->
|
||||
if typeof keys is 'string'
|
||||
$.setValue(g.NAMESPACE + keys, JSON.stringify val)
|
||||
else
|
||||
for key, value of keys
|
||||
$.setValue(g.NAMESPACE + key, JSON.stringify value)
|
||||
cb = val
|
||||
cb?()
|
||||
|
||||
$.clear = (cb) ->
|
||||
# XXX https://github.com/greasemonkey/greasemonkey/issues/2033
|
||||
# Also support case where GM_listValues is not defined.
|
||||
$.delete Object.keys(Conf)
|
||||
$.delete ['previousversion', 'AutoWatch', 'cooldown.global', 'QR Size', 'captchas', 'QR.persona', 'hiddenPSA']
|
||||
$.delete ['previousversion', 'AutoWatch', 'QR Size', 'captchas', 'QR.persona', 'hiddenPSA']
|
||||
$.delete ("#{id}.position" for id in ['embedding', 'updater', 'thread-stats', 'thread-watcher', 'qr'])
|
||||
boards = (a.textContent for a in $$ '#boardNavDesktop > .boardList > a')
|
||||
boards.push 'qa'
|
||||
$.delete ("cooldown.#{board}" for board in boards)
|
||||
try
|
||||
$.delete GM_listValues().map (key) -> key.replace g.NAMESPACE, ''
|
||||
$.delete $.listValues().map (key) -> key.replace g.NAMESPACE, ''
|
||||
cb?()
|
||||
<% } %>
|
||||
|
||||
|
||||
@ -6,10 +6,10 @@ class Callbacks
|
||||
@keys.push name unless @[name]
|
||||
@[name] = cb
|
||||
|
||||
execute: (node) ->
|
||||
for name in @keys
|
||||
execute: (node, keys=@keys) ->
|
||||
for name in keys
|
||||
try
|
||||
@[name].call node
|
||||
@[name]?.call node
|
||||
catch err
|
||||
errors = [] unless errors
|
||||
errors.push
|
||||
|
||||
@ -19,7 +19,16 @@ class Clone extends Post
|
||||
quote: $ '.postNum > a:nth-of-type(2)', info
|
||||
comment: $ '.postMessage', post
|
||||
quotelinks: []
|
||||
backlinks: info.getElementsByClassName 'backlink'
|
||||
|
||||
# XXX Edge invalidates HTMLCollections when an ancestor node is inserted into another node.
|
||||
# https://connect.microsoft.com/IE/feedback/details/1198967/ie11-appendchild-provoke-an-error-on-an-htmlcollection
|
||||
if $.engine is 'edge'
|
||||
Object.defineProperty @nodes, 'backlinks',
|
||||
configurable: true
|
||||
enumerable: true
|
||||
get: -> info.getElementsByClassName 'backlink'
|
||||
else
|
||||
@nodes.backlinks = info.getElementsByClassName 'backlink'
|
||||
|
||||
# Remove inlined posts inside of this post.
|
||||
for inline in $$ '.inline', post
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
class Connection
|
||||
constructor: (@target, @origin, @cb) ->
|
||||
constructor: (@target, @origin, @cb={}) ->
|
||||
$.on window, 'message', @onMessage
|
||||
|
||||
targetWindow: ->
|
||||
if @target instanceof window.HTMLIFrameElement
|
||||
@target.contentWindow
|
||||
else
|
||||
@target
|
||||
|
||||
send: (data) =>
|
||||
@target.postMessage "#{g.NAMESPACE}#{JSON.stringify data}", @origin
|
||||
@targetWindow().postMessage "#{g.NAMESPACE}#{JSON.stringify data}", @origin
|
||||
|
||||
onMessage: (e) =>
|
||||
return unless e.source is @target and
|
||||
return unless e.source is @targetWindow() and
|
||||
e.origin is @origin and
|
||||
typeof e.data is 'string' and
|
||||
e.data[...g.NAMESPACE.length] is g.NAMESPACE
|
||||
|
||||
@ -81,7 +81,7 @@ class Fetcher
|
||||
return false unless url = Redirect.to 'post', {@boardID, @postID}
|
||||
if /^https:\/\//.test(url) or location.protocol is 'http:'
|
||||
$.cache url,
|
||||
do (self = @) -> -> self.parseArchivedPost @response
|
||||
do (self = @) -> -> self.parseArchivedPost @response, url
|
||||
,
|
||||
responseType: 'json'
|
||||
withCredentials: url.archive.withCredentials
|
||||
@ -93,11 +93,11 @@ class Fetcher
|
||||
# Image/thumbnail URLs loaded over HTTP can be modified in transit.
|
||||
# Require them to be from a known HTTP host so that no referrer is sent to them from an HTTPS page.
|
||||
delete media[key] unless media[key]? and media[key].match(/^(http:\/\/[^\/]+\/)?/)[0] in url.archive.imagehosts
|
||||
@parseArchivedPost response
|
||||
@parseArchivedPost response, url
|
||||
return true
|
||||
return false
|
||||
|
||||
parseArchivedPost: (data) ->
|
||||
parseArchivedPost: (data, url) ->
|
||||
# In case of multiple callbacks for the same request,
|
||||
# don't parse the same original post more than once.
|
||||
if post = g.posts["#{@boardID}.#{@postID}"]
|
||||
@ -147,6 +147,9 @@ class Fetcher
|
||||
commentHTML: comment
|
||||
delete o.info.uniqueID if o.info.capcode
|
||||
if data.media?.media_filename
|
||||
# Fix URLs missing origin
|
||||
for key, val of data.media when /_link$/.test(key) and val?[0] is '/'
|
||||
data.media[key] = url.split('/', 3).join('/') + val
|
||||
o.file =
|
||||
name: data.media.media_filename
|
||||
url: data.media.media_link or data.media.remote_media_link or
|
||||
|
||||
@ -1,19 +1,6 @@
|
||||
Polyfill =
|
||||
init: ->
|
||||
@notificationPermission()
|
||||
@toBlob()
|
||||
@visibility()
|
||||
notificationPermission: ->
|
||||
return if !window.Notification or 'permission' of Notification or !window.webkitNotifications
|
||||
Object.defineProperty Notification, 'permission',
|
||||
get: ->
|
||||
switch webkitNotifications.checkPermission()
|
||||
when 0
|
||||
'granted'
|
||||
when 1
|
||||
'default'
|
||||
when 2
|
||||
'denied'
|
||||
toBlob: ->
|
||||
HTMLCanvasElement::toBlob or= (cb) ->
|
||||
data = atob @toDataURL()[22..]
|
||||
@ -23,12 +10,3 @@ Polyfill =
|
||||
for i in [0...l] by 1
|
||||
ui8a[i] = data.charCodeAt i
|
||||
cb new Blob [ui8a], type: 'image/png'
|
||||
visibility: ->
|
||||
# page visibility API
|
||||
return if 'visibilityState' of d
|
||||
Object.defineProperties HTMLDocument.prototype,
|
||||
visibilityState:
|
||||
get: -> @webkitVisibilityState
|
||||
hidden:
|
||||
get: -> @webkitHidden
|
||||
$.on d, 'webkitvisibilitychange', -> $.event 'visibilitychange'
|
||||
|
||||
@ -40,7 +40,16 @@ class Post
|
||||
comment: $ '.postMessage', post
|
||||
links: []
|
||||
quotelinks: []
|
||||
backlinks: info.getElementsByClassName 'backlink'
|
||||
|
||||
# XXX Edge invalidates HTMLCollections when an ancestor node is inserted into another node.
|
||||
# https://connect.microsoft.com/IE/feedback/details/1198967/ie11-appendchild-provoke-an-error-on-an-htmlcollection
|
||||
if $.engine is 'edge'
|
||||
Object.defineProperty @nodes, 'backlinks',
|
||||
configurable: true
|
||||
enumerable: true
|
||||
get: -> info.getElementsByClassName 'backlink'
|
||||
else
|
||||
@nodes.backlinks = info.getElementsByClassName 'backlink'
|
||||
|
||||
unless @isReply = $.hasClass post, 'reply'
|
||||
@thread.OP = @
|
||||
@ -226,7 +235,7 @@ class Post
|
||||
# XXX Workaround for 4chan's racing condition
|
||||
# giving us false-positive dead posts.
|
||||
resurrect: ->
|
||||
delete @isDead
|
||||
@isDead = false
|
||||
$.rmClass @nodes.root, 'deleted-post'
|
||||
strong = $ 'strong.warning', @nodes.info
|
||||
# no false-positive files
|
||||
@ -245,7 +254,6 @@ class Post
|
||||
return
|
||||
|
||||
collect: ->
|
||||
@kill()
|
||||
g.posts.rm @fullID
|
||||
@thread.posts.rm @
|
||||
@board.posts.rm @
|
||||
|
||||
@ -5,20 +5,28 @@
|
||||
// @minFFVer <%= meta.min.firefox %>
|
||||
// @namespace <%= name %>
|
||||
// @description <%= description %>
|
||||
// @license MIT; <%= meta.repo %>blob/<%= meta.mainBranch %>/LICENSE
|
||||
// @license MIT; <%= meta.license %>
|
||||
<%=
|
||||
meta.matches.map(function(match) {
|
||||
return '// @match ' + match;
|
||||
if (/^\*/.test(match)) {
|
||||
return (
|
||||
'// @include ' + match.replace(/^\*/, 'http') + '\n' +
|
||||
'// @include ' + match.replace(/^\*/, 'https')
|
||||
);
|
||||
} else {
|
||||
return '// @include ' + match;
|
||||
}
|
||||
}).join('\n')
|
||||
%>
|
||||
// @grant GM_getValue
|
||||
// @grant GM_setValue
|
||||
// @grant GM_deleteValue
|
||||
// @grant GM_listValues
|
||||
// @grant GM_addValueChangeListener
|
||||
// @grant GM_openInTab
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @run-at document-start
|
||||
<% if (channel !== 'dev') { %><% if (channel !== 'noupdate') { %>// @updateURL <%= meta.downloads %><%= name %><%= meta.suffix[channel] %>.meta.js
|
||||
<% } %>// @downloadURL <%= meta.downloads %><%= name %><%= meta.suffix[channel] %>.user.js
|
||||
<% if (channel !== 'dev') { %>// @updateURL <%= (channel !== 'noupdate') ? (meta.downloads + name + meta.suffix[channel] + '.meta.js') : 'https://noupdate.invalid/' %>
|
||||
// @downloadURL <%= (channel !== 'noupdate') ? (meta.downloads + name + meta.suffix[channel] + '.user.js') : 'https://noupdate.invalid/' %>
|
||||
<% } %>// @icon data:image/png;base64,<%= grunt.file.read('src/General/img/icon48.png', {encoding: 'base64'}) %>
|
||||
// ==/UserScript==
|
||||
|
||||
@ -32,8 +32,7 @@ FappeTyme =
|
||||
cb: @catalogNode
|
||||
|
||||
node: ->
|
||||
return if @file
|
||||
$.addClass @nodes.root, "noFile"
|
||||
@nodes.root.classList.toggle 'noFile', !@file
|
||||
|
||||
catalogNode: ->
|
||||
{file} = @thread.OP
|
||||
|
||||
@ -128,53 +128,66 @@ Gallery =
|
||||
Gallery.images.push thumb
|
||||
$.add Gallery.nodes.thumbs, thumb
|
||||
|
||||
load: (thumb, errorCB) ->
|
||||
ext = thumb.href.match /\w*$/
|
||||
elType = {'webm': 'video', 'pdf': 'iframe'}[ext] or 'img'
|
||||
file = $.el elType,
|
||||
title: thumb.title
|
||||
$.extend file.dataset, thumb.dataset
|
||||
$.on file, 'error', errorCB
|
||||
file.src = thumb.href
|
||||
file
|
||||
|
||||
open: (thumb) ->
|
||||
{nodes} = Gallery
|
||||
{name} = nodes
|
||||
oldID = +nodes.current.dataset.id
|
||||
newID = +thumb.dataset.id
|
||||
slideshow = Gallery.slideshow and (newID > oldID or (oldID is Gallery.images.length-1 and newID is 0))
|
||||
|
||||
$.rmClass el, 'gal-highlight' if el = $ '.gal-highlight', nodes.thumbs
|
||||
# Highlight, center selected thumbnail
|
||||
$.rmClass el, 'gal-highlight' if el = Gallery.images[oldID]
|
||||
$.addClass thumb, 'gal-highlight'
|
||||
nodes.thumbs.scrollTop = thumb.offsetTop + thumb.offsetHeight/2 - nodes.thumbs.clientHeight/2
|
||||
|
||||
elType = if /\.webm$/.test(thumb.href)
|
||||
'video'
|
||||
else if /\.pdf$/.test(thumb.href)
|
||||
'iframe'
|
||||
# Load image or use preloaded image
|
||||
if Gallery.cache?.dataset.id is ''+newID
|
||||
file = Gallery.cache
|
||||
$.off file, 'error', Gallery.cacheError
|
||||
$.on file, 'error', Gallery.error
|
||||
else
|
||||
'img'
|
||||
|
||||
$[if elType is 'iframe' then 'addClass' else 'rmClass'] doc, 'gal-pdf'
|
||||
file = $.el elType,
|
||||
title: name.download = name.textContent = thumb.title
|
||||
$.extend file.dataset, thumb.dataset
|
||||
$.on file, 'error', Gallery.error
|
||||
file.src = name.href = thumb.href
|
||||
file = Gallery.load thumb, Gallery.error
|
||||
|
||||
# Replace old image with new one
|
||||
$.off nodes.current, 'error', Gallery.error
|
||||
ImageCommon.pause nodes.current
|
||||
$.replace nodes.current, file
|
||||
if elType is 'video'
|
||||
nodes.current = file
|
||||
|
||||
if file.nodeName is 'VIDEO'
|
||||
file.loop = true
|
||||
Volume.setup file
|
||||
file.play() if Conf['Autoplay']
|
||||
ImageCommon.addControls file if Conf['Show Controls']
|
||||
|
||||
doc.classList.toggle 'gal-pdf', file.nodeName is 'IFRAME'
|
||||
Gallery.cb.setHeight()
|
||||
nodes.count.textContent = +thumb.dataset.id + 1
|
||||
nodes.current = file
|
||||
nodes.name.download = nodes.name.textContent = thumb.title
|
||||
nodes.name.href = thumb.href
|
||||
nodes.frame.scrollTop = 0
|
||||
nodes.next.focus()
|
||||
if slideshow
|
||||
|
||||
# Continue slideshow if moving forward, stop otherwise
|
||||
if Gallery.slideshow and (newID > oldID or (oldID is Gallery.images.length-1 and newID is 0))
|
||||
Gallery.setupTimer()
|
||||
else
|
||||
Gallery.cb.stop()
|
||||
|
||||
# Scroll to post
|
||||
if Conf['Scroll to Post'] and post = (post = g.posts[file.dataset.post])?.nodes.root
|
||||
Header.scrollTo post
|
||||
if Conf['Scroll to Post'] and (post = g.posts[file.dataset.post])
|
||||
Header.scrollTo post.nodes.root
|
||||
|
||||
# Center selected thumbnail
|
||||
nodes.thumbs.scrollTop = thumb.offsetTop + thumb.offsetHeight/2 - nodes.thumbs.clientHeight/2
|
||||
# Preload next image
|
||||
Gallery.cache = Gallery.load Gallery.images[(newID + 1) % Gallery.images.length], Gallery.cacheError
|
||||
|
||||
error: ->
|
||||
if @error?.code is MediaError.MEDIA_ERR_DECODE
|
||||
@ -185,6 +198,9 @@ Gallery =
|
||||
Gallery.images[@dataset.id].href = url
|
||||
@src = url if Gallery.nodes.current is @
|
||||
|
||||
cacheError: ->
|
||||
delete Gallery.cache
|
||||
|
||||
cleanupTimer: ->
|
||||
clearTimeout Gallery.timeoutID
|
||||
{current} = Gallery.nodes
|
||||
@ -301,6 +317,14 @@ Gallery =
|
||||
setFitness: ->
|
||||
(if @checked then $.addClass else $.rmClass) doc, "gal-#{@name.toLowerCase().replace /\s+/g, '-'}"
|
||||
|
||||
setHeight: ->
|
||||
{current, frame} = Gallery.nodes
|
||||
current.style.minHeight = if Conf['Stretch to Fit'] and (dim = g.posts[current.dataset.post]?.file.dimensions)
|
||||
[width, height] = dim.split 'x'
|
||||
Math.min(doc.clientHeight - 25, height / width * frame.clientWidth) + 'px'
|
||||
else
|
||||
null
|
||||
|
||||
setDelay: -> Gallery.delay = +@value
|
||||
|
||||
menu:
|
||||
@ -319,14 +343,14 @@ Gallery =
|
||||
createSubEntry: (name) ->
|
||||
label = UI.checkbox name, name
|
||||
input = label.firstElementChild
|
||||
if name in ['Fit Width', 'Fit Height', 'Hide Thumbnails']
|
||||
$.on input, 'change', Gallery.cb.setFitness
|
||||
$.on input, 'change', Gallery.cb.setFitness if name in ['Hide Thumbnails', 'Fit Width', 'Fit Height']
|
||||
$.event 'change', null, input
|
||||
$.on input, 'change', $.cb.checked
|
||||
$.on input, 'change', Gallery.cb.setHeight if name in ['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit']
|
||||
el: label
|
||||
|
||||
createSubEntries: ->
|
||||
subEntries = (Gallery.menu.createSubEntry item for item in ['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Scroll to Post'])
|
||||
subEntries = (Gallery.menu.createSubEntry item for item in ['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Stretch to Fit', 'Scroll to Post'])
|
||||
|
||||
delayLabel = $.el 'label', <%= html('Slide Delay: <input type="number" name="Slide Delay" min="0" step="any" class="field">') %>
|
||||
delayInput = delayLabel.firstElementChild
|
||||
|
||||
@ -83,7 +83,7 @@ ImageCommon =
|
||||
$.off video, 'mouseover', handler
|
||||
# Hacky workaround for Firefox forever-loading bug for very short videos
|
||||
t = new Date().getTime()
|
||||
$.asap (-> chrome? or (video.readyState >= 3 and video.currentTime <= Math.max 0.1, (video.duration - 0.5)) or new Date().getTime() >= t + 1000), ->
|
||||
$.asap (-> $.engine isnt 'gecko' or (video.readyState >= 3 and video.currentTime <= Math.max 0.1, (video.duration - 0.5)) or new Date().getTime() >= t + 1000), ->
|
||||
video.controls = true
|
||||
$.on video, 'mouseover', handler
|
||||
|
||||
|
||||
@ -121,7 +121,7 @@ ImageExpand =
|
||||
|
||||
$.rmClass post.nodes.root, 'expanded-image'
|
||||
$.rmClass file.thumb, 'expanding'
|
||||
$.rm file.videoControls if file.videoControls
|
||||
$.rm file.videoControls
|
||||
file.thumb.parentNode.href = file.url
|
||||
file.thumb.parentNode.target = '_blank'
|
||||
for x in ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']
|
||||
@ -221,7 +221,7 @@ ImageExpand =
|
||||
# Scroll to display full image.
|
||||
if file.scrollIntoView
|
||||
delete file.scrollIntoView
|
||||
imageBottom = Header.getBottomOf(file.fullImage) - 25
|
||||
imageBottom = Math.min(doc.clientHeight - file.fullImage.getBoundingClientRect().bottom - 25, Header.getBottomOf file.fullImage)
|
||||
if imageBottom < 0
|
||||
window.scrollBy 0, Math.min(-imageBottom, Header.getTopOf file.fullImage)
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ ImageHover =
|
||||
el.play() if Conf['Autoplay']
|
||||
[width, height] = (+x for x in file.dimensions.split 'x')
|
||||
{left, right} = @getBoundingClientRect()
|
||||
padding = 16
|
||||
padding = 25
|
||||
maxWidth = Math.max left, doc.clientWidth - right
|
||||
maxHeight = doc.clientHeight - padding
|
||||
scale = Math.min 1, maxWidth / width, maxHeight / height
|
||||
|
||||
@ -63,7 +63,7 @@ ImageLoader =
|
||||
clone.file.thumb.preload = 'auto' for clone in post.clones
|
||||
thumb.preload = 'auto'
|
||||
# XXX Cloned video elements with poster in Firefox cause momentary display of image loading icon.
|
||||
if !chrome?
|
||||
if $.engine is 'gecko'
|
||||
$.on thumb, 'loadeddata', -> @removeAttribute 'poster'
|
||||
return
|
||||
|
||||
|
||||
@ -70,6 +70,7 @@ Volume =
|
||||
$.on @nodes.thumb, 'wheel', Volume.wheel.bind(Header.hover)
|
||||
|
||||
wheel: (e) ->
|
||||
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey
|
||||
return unless el = $ 'video:not([data-md5])', @
|
||||
return if el.muted or not $.hasAudio el
|
||||
volume = el.volume + 0.1
|
||||
|
||||
@ -253,7 +253,7 @@ Embedding =
|
||||
src: "//strawpoll.me/embed_1/#{a.dataset.uid}"
|
||||
,
|
||||
key: 'TwitchTV'
|
||||
regExp: /^\w+:\/\/(?:www\.)?twitch\.tv\/([^#\&\?]*)/
|
||||
regExp: /^\w+:\/\/(?:www\.)?twitch\.tv\/(\w[^#\&\?]*)/
|
||||
httpOnly: true
|
||||
style: "border: none; width: 640px; height: 360px;"
|
||||
el: (a) ->
|
||||
@ -277,10 +277,12 @@ Embedding =
|
||||
regExp: /^\w+:\/\/(?:www\.)?vocaroo\.com\/i\/(\w+)/
|
||||
style: ''
|
||||
el: (a) ->
|
||||
$.el 'audio',
|
||||
el = $.el 'audio',
|
||||
controls: true
|
||||
preload: 'auto'
|
||||
src: "http://vocaroo.com/media_command.php?media=#{a.dataset.uid}&command=download_ogg"
|
||||
type = if el.canPlayType 'audio/ogg' then 'ogg' else 'mp3'
|
||||
el.src = "http://vocaroo.com/media_command.php?media=#{a.dataset.uid}&command=download_#{type}"
|
||||
el
|
||||
,
|
||||
key: 'Vimeo'
|
||||
regExp: /^\w+:\/\/(?:www\.)?vimeo\.com\/(\d+)/
|
||||
@ -360,10 +362,12 @@ Embedding =
|
||||
regExp: /^\w+:\/\/(?:www\.)?clyp\.it\/(\w+)/
|
||||
style: ''
|
||||
el: (a) ->
|
||||
$.el 'audio',
|
||||
el = $.el 'audio',
|
||||
controls: true
|
||||
preload: 'auto'
|
||||
src: "http://clyp.it/#{a.dataset.uid}.ogg"
|
||||
type = if el.canPlayType 'audio/ogg' then 'ogg' else 'mp3'
|
||||
el.src = "http://clyp.it/#{a.dataset.uid}.#{type}"
|
||||
el
|
||||
,
|
||||
# dummy entries: not implemented but included to prevent them being wrongly embedded as a subsequent type
|
||||
key: 'Loopvid-dummy'
|
||||
|
||||
@ -79,7 +79,7 @@ Linkify =
|
||||
)
|
||||
| # This should account for virtually all links posted without http:
|
||||
([-a-z\d]+[.])+(
|
||||
aero|asia|biz|cat|com|coop|dance|info|int|jobs|mobi|moe|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2}
|
||||
aero|asia|biz|cat|com|coop|dance|info|int|jobs|mobi|moe|museum|name|net|org|post|pro|tel|travel|xxx|xyz|edu|gov|mil|[a-z]{2}
|
||||
)([:/]|(?![^\s'"]))
|
||||
| # IPv4 Addresses
|
||||
[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
DeleteLink =
|
||||
auto: [{}, {}]
|
||||
|
||||
init: ->
|
||||
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Delete Link']
|
||||
|
||||
@ -11,19 +13,22 @@ DeleteLink =
|
||||
fileEl = $.el 'a',
|
||||
className: 'delete-file'
|
||||
href: 'javascript:;'
|
||||
@nodes =
|
||||
menu: div.firstChild
|
||||
links: [postEl, fileEl]
|
||||
|
||||
postEntry =
|
||||
el: postEl
|
||||
open: ->
|
||||
postEl.textContent = 'Post'
|
||||
$.on postEl, 'click', DeleteLink.delete
|
||||
postEl.textContent = DeleteLink.linkText false
|
||||
$.on postEl, 'click', DeleteLink.toggle
|
||||
true
|
||||
fileEntry =
|
||||
el: fileEl
|
||||
open: ({file}) ->
|
||||
return false if !file or file.isDead
|
||||
fileEl.textContent = 'File'
|
||||
$.on fileEl, 'click', DeleteLink.delete
|
||||
fileEl.textContent = DeleteLink.linkText true
|
||||
$.on fileEl, 'click', DeleteLink.toggle
|
||||
true
|
||||
|
||||
Menu.menu.addEntry
|
||||
@ -32,19 +37,41 @@ DeleteLink =
|
||||
open: (post) ->
|
||||
return false if post.isDead
|
||||
DeleteLink.post = post
|
||||
node = div.firstChild
|
||||
node.textContent = 'Delete'
|
||||
DeleteLink.cooldown.start post, node
|
||||
DeleteLink.nodes.menu.textContent = DeleteLink.menuText()
|
||||
DeleteLink.cooldown.start post
|
||||
true
|
||||
subEntries: [postEntry, fileEntry]
|
||||
|
||||
delete: ->
|
||||
{post} = DeleteLink
|
||||
return if DeleteLink.cooldown.counting is post
|
||||
menuText: ->
|
||||
if seconds = DeleteLink.cooldown.seconds[DeleteLink.post.fullID]
|
||||
"Delete (#{seconds})"
|
||||
else
|
||||
'Delete'
|
||||
|
||||
$.off @, 'click', DeleteLink.delete
|
||||
linkText: (fileOnly) ->
|
||||
text = if fileOnly then 'File' else 'Post'
|
||||
if DeleteLink.auto[+fileOnly][DeleteLink.post.fullID]
|
||||
text = "Deleting #{text.toLowerCase()}..."
|
||||
text
|
||||
|
||||
toggle: ->
|
||||
{post} = DeleteLink
|
||||
fileOnly = $.hasClass @, 'delete-file'
|
||||
@textContent = "Deleting #{if fileOnly then 'file' else 'post'}..."
|
||||
auto = DeleteLink.auto[+fileOnly]
|
||||
|
||||
if auto[post.fullID]
|
||||
delete auto[post.fullID]
|
||||
else
|
||||
auto[post.fullID] = true
|
||||
@textContent = DeleteLink.linkText fileOnly
|
||||
|
||||
unless DeleteLink.cooldown.seconds[post.fullID]
|
||||
DeleteLink.delete post, fileOnly
|
||||
|
||||
delete: (post, fileOnly) ->
|
||||
link = DeleteLink.nodes.links[+fileOnly]
|
||||
delete DeleteLink.auto[+fileOnly][post.fullID]
|
||||
$.off link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
|
||||
|
||||
form =
|
||||
mode: 'usrdel'
|
||||
@ -52,47 +79,56 @@ DeleteLink =
|
||||
pwd: QR.persona.getPassword()
|
||||
form[post.ID] = 'delete'
|
||||
|
||||
link = @
|
||||
$.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"),
|
||||
responseType: 'document'
|
||||
withCredentials: true
|
||||
onload: -> DeleteLink.load link, post, fileOnly, @response
|
||||
onerror: -> DeleteLink.error link
|
||||
onerror: -> DeleteLink.error link, post
|
||||
,
|
||||
form: $.formData form
|
||||
|
||||
load: (link, post, fileOnly, resDoc) ->
|
||||
link.textContent = DeleteLink.linkText fileOnly
|
||||
if resDoc.title is '4chan - Banned' # Ban/warn check
|
||||
s = 'Banned!'
|
||||
el = $.el 'span', <%= html('You can't delete posts because you are <a href="//www.4chan.org/banned" target="_blank">banned</a>.') %>
|
||||
new Notice 'warning', el, 20
|
||||
else if msg = resDoc.getElementById 'errmsg' # error!
|
||||
s = msg.textContent
|
||||
$.on link, 'click', DeleteLink.delete
|
||||
new Notice 'warning', msg.textContent, 20
|
||||
$.on link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
|
||||
if /\bwait\b/i.test msg.textContent
|
||||
DeleteLink.cooldown.start post, 5
|
||||
DeleteLink.auto[+fileOnly][post.fullID] = true
|
||||
DeleteLink.nodes.links[+fileOnly].textContent = DeleteLink.linkText fileOnly
|
||||
else
|
||||
QR.cooldown.delete post unless fileOnly
|
||||
if resDoc.title is 'Updating index...'
|
||||
# We're 100% sure.
|
||||
QR.cooldown.delete post
|
||||
(post.origin or post).kill fileOnly
|
||||
s = 'Deleted'
|
||||
link.textContent = s
|
||||
error: (link) ->
|
||||
link.textContent = 'Connection error, please retry.'
|
||||
$.on link, 'click', DeleteLink.delete
|
||||
link.textContent = 'Deleted' if post.fullID is DeleteLink.post.fullID
|
||||
|
||||
error: (link, post) ->
|
||||
new Notice 'warning', 'Connection error, please retry.', 20
|
||||
$.on link, 'click', DeleteLink.toggle if post.fullID is DeleteLink.post.fullID
|
||||
|
||||
cooldown:
|
||||
start: (post, node) ->
|
||||
unless QR.db?.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}
|
||||
# Only start counting on our posts.
|
||||
delete DeleteLink.cooldown.counting
|
||||
return
|
||||
DeleteLink.cooldown.counting = post
|
||||
length = 60
|
||||
seconds = Math.ceil (length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND
|
||||
DeleteLink.cooldown.count post, seconds, length, node
|
||||
count: (post, seconds, length, node) ->
|
||||
return if DeleteLink.cooldown.counting isnt post
|
||||
unless 0 <= seconds <= length
|
||||
if DeleteLink.cooldown.counting is post
|
||||
node.textContent = 'Delete'
|
||||
delete DeleteLink.cooldown.counting
|
||||
return
|
||||
setTimeout DeleteLink.cooldown.count, 1000, post, seconds - 1, length, node
|
||||
node.textContent = "Delete (#{seconds})"
|
||||
seconds: {}
|
||||
|
||||
start: (post, seconds) ->
|
||||
# Already counting.
|
||||
return if DeleteLink.cooldown.seconds[post.fullID]?
|
||||
|
||||
seconds ?= QR.cooldown.secondsDeletion post
|
||||
if seconds > 0
|
||||
DeleteLink.cooldown.seconds[post.fullID] = seconds
|
||||
DeleteLink.cooldown.count post
|
||||
|
||||
count: (post) ->
|
||||
DeleteLink.nodes.menu.textContent = DeleteLink.menuText() if post.fullID is DeleteLink.post.fullID
|
||||
if DeleteLink.cooldown.seconds[post.fullID] > 0
|
||||
DeleteLink.cooldown.seconds[post.fullID]--
|
||||
setTimeout DeleteLink.cooldown.count, 1000, post
|
||||
else
|
||||
delete DeleteLink.cooldown.seconds[post.fullID]
|
||||
for fileOnly in [false, true] when DeleteLink.auto[+fileOnly][post.fullID]
|
||||
DeleteLink.delete post, fileOnly
|
||||
return
|
||||
|
||||
@ -14,7 +14,7 @@ ReportLink =
|
||||
unless post.isDead or (post.thread.isDead and not post.thread.isArchived)
|
||||
a.textContent = 'Report this post'
|
||||
ReportLink.url = "//sys.4chan.org/#{post.board}/imgboard.php?mode=report&no=#{post}"
|
||||
ReportLink.height = 200
|
||||
ReportLink.height = 180
|
||||
else if Conf['Archive Report']
|
||||
a.textContent = 'Report to archive'
|
||||
ReportLink.url = Redirect.to 'report', {boardID: post.board.ID, postID: post.ID}
|
||||
|
||||
@ -26,6 +26,8 @@ AntiAutoplay =
|
||||
process: (root) ->
|
||||
for iframe in $$ 'iframe[src*="youtube"][src*="autoplay=1"]', root
|
||||
iframe.src = iframe.src.replace(/\?autoplay=1&?/, '?').replace('&autoplay=1', '')
|
||||
$.addClass iframe, 'autoplay-removed'
|
||||
for object in $$ 'object[data*="youtube"][data*="autoplay=1"]', root
|
||||
object.data = object.data.replace(/\?autoplay=1&?/, '?').replace('&autoplay=1', '')
|
||||
$.addClass object, 'autoplay-removed'
|
||||
return
|
||||
@ -2,7 +2,7 @@ Keybinds =
|
||||
init: ->
|
||||
return if !Conf['Keybinds']
|
||||
|
||||
for hotkey of Conf.hotkeys
|
||||
for hotkey of Config.hotkeys
|
||||
$.sync hotkey, Keybinds.sync
|
||||
|
||||
init = ->
|
||||
@ -204,6 +204,8 @@ Keybinds =
|
||||
'Enter'
|
||||
when 27
|
||||
'Esc'
|
||||
when 32
|
||||
'Space'
|
||||
when 37
|
||||
'Left'
|
||||
when 38
|
||||
@ -212,9 +214,15 @@ Keybinds =
|
||||
'Right'
|
||||
when 40
|
||||
'Down'
|
||||
when 188
|
||||
'Comma'
|
||||
when 190
|
||||
'Period'
|
||||
else
|
||||
if 48 <= kc <= 57 or 65 <= kc <= 90 # 0-9, A-Z
|
||||
String.fromCharCode(kc).toLowerCase()
|
||||
else if 96 <= kc <= 105 # numpad 0-9
|
||||
String.fromCharCode(kc - 48).toLowerCase()
|
||||
else
|
||||
null
|
||||
if key
|
||||
|
||||
@ -1,19 +1,16 @@
|
||||
Report =
|
||||
css: '''
|
||||
:root:not(.js-enabled) #g-recaptcha {
|
||||
height: auto;
|
||||
}
|
||||
'''
|
||||
css: `<%= importCSS('report') %>`
|
||||
|
||||
init: ->
|
||||
return unless /\bmode=report\b/.test(location.search) and match = location.search.match /\bno=(\d+)/
|
||||
Captcha.replace.init()
|
||||
@postID = +match[1]
|
||||
$.ready @ready
|
||||
|
||||
ready: ->
|
||||
$.addStyle Report.css
|
||||
Report.archive() if Conf['Archive Report']
|
||||
if $.hasClass doc, 'js-enabled'
|
||||
if Conf['Use Recaptcha v2 in Reports'] and $.hasClass doc, 'js-enabled'
|
||||
new MutationObserver(-> Report.fit '.gc-bubbleDefault').observe d.body,
|
||||
childList: true
|
||||
attributes: true
|
||||
@ -47,4 +44,4 @@ Report =
|
||||
|
||||
if types = $.id('reportTypes')
|
||||
$.on types, 'change', (e) ->
|
||||
$('form').action = if e.target.value in ['illegal', 'spam'] then '#redirect' else ''
|
||||
$('form').action = if e.target.value is 'illegal' then '#redirect' else ''
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -76,12 +76,14 @@ ThreadUpdater =
|
||||
|
||||
ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
|
||||
|
||||
$.on window, 'online offline', ThreadUpdater.cb.online
|
||||
$.on d, 'QRPostSuccessful', ThreadUpdater.cb.checkpost
|
||||
$.on d, 'visibilitychange', ThreadUpdater.cb.visibility
|
||||
|
||||
ThreadUpdater.setInterval()
|
||||
|
||||
# Update immediately on /f/ to add files to replies.
|
||||
ThreadUpdater.update() if @board.ID is 'f'
|
||||
|
||||
###
|
||||
http://freesound.org/people/pierrecartoons1979/sounds/90112/
|
||||
cc-by-nc-3.0
|
||||
@ -89,18 +91,6 @@ ThreadUpdater =
|
||||
beep: 'data:audio/wav;base64,<%= grunt.file.read("src/General/audio/beep.wav", {encoding: "base64"}) %>'
|
||||
|
||||
cb:
|
||||
online: ->
|
||||
return if ThreadUpdater.thread.isDead
|
||||
|
||||
if navigator.onLine
|
||||
ThreadUpdater.set 'status', ''
|
||||
else
|
||||
ThreadUpdater.set 'status', 'Offline', 'warning'
|
||||
|
||||
if Conf['Auto Update'] and not Conf['Ignore Offline Status']
|
||||
ThreadUpdater.outdateCount = 0
|
||||
ThreadUpdater.setInterval()
|
||||
|
||||
checkpost: (e) ->
|
||||
return if e.detail.threadID isnt ThreadUpdater.thread.ID
|
||||
ThreadUpdater.postID = e.detail.postID
|
||||
@ -189,12 +179,6 @@ ThreadUpdater =
|
||||
ThreadUpdater.set 'timer', 'Update'
|
||||
return
|
||||
|
||||
unless navigator.onLine
|
||||
ThreadUpdater.set 'status', 'Offline', 'warning'
|
||||
unless Conf['Ignore Offline Status']
|
||||
ThreadUpdater.set 'timer', ''
|
||||
return
|
||||
|
||||
{interval} = ThreadUpdater
|
||||
if Conf['Optional Increase']
|
||||
# Lower the max refresh rate limit on visible tabs.
|
||||
@ -289,6 +273,13 @@ ThreadUpdater =
|
||||
index.push ID
|
||||
files.push ID if postObject.fsize
|
||||
|
||||
# Add files to replies on /f/.
|
||||
if board.ID is 'f' and postObject.fsize and (post = thread.posts[ID]) and not post.file
|
||||
node = Build.postFromObject postObject, board.ID
|
||||
$.after post.nodes.info, $('.file', node)
|
||||
post.parseFile()
|
||||
Post.callbacks.execute post, ['Filter', 'File Info Formatting', 'Fappe Tyme', 'Sauce']
|
||||
|
||||
# Insert new posts, not older ones.
|
||||
continue if ID <= lastPost
|
||||
|
||||
|
||||
@ -127,16 +127,18 @@ Unread =
|
||||
|
||||
openNotification: (post) ->
|
||||
return unless Header.areNotificationsEnabled
|
||||
notif = new Notification "#{post.info.nameBlock} replied to you",
|
||||
body: post.info.commentDisplay
|
||||
icon: Favicon.logo
|
||||
notif.onclick = ->
|
||||
Header.scrollToIfNeeded post.nodes.root, true
|
||||
window.focus()
|
||||
notif.onshow = ->
|
||||
setTimeout ->
|
||||
notif.close()
|
||||
, 7 * $.SECOND
|
||||
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1130502 (SeaMonkey)
|
||||
try
|
||||
notif = new Notification "#{post.info.nameBlock} replied to you",
|
||||
body: post.info.commentDisplay
|
||||
icon: Favicon.logo
|
||||
notif.onclick = ->
|
||||
Header.scrollToIfNeeded post.nodes.root, true
|
||||
window.focus()
|
||||
notif.onshow = ->
|
||||
setTimeout ->
|
||||
notif.close()
|
||||
, 7 * $.SECOND
|
||||
|
||||
onUpdate: (e) ->
|
||||
if !e.detail[404]
|
||||
@ -241,6 +243,5 @@ Unread =
|
||||
Favicon[if isDead then 'unreadDead' else 'unread']
|
||||
else
|
||||
Favicon[if isDead then 'dead' else 'default']
|
||||
unless chrome?
|
||||
# `favicon.href = href` doesn't work on Firefox.
|
||||
$.add d.head, Favicon.el
|
||||
# `favicon.href = href` doesn't work on Firefox.
|
||||
$.add d.head, Favicon.el
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
Captcha.fixes =
|
||||
imageKeys: '789456123uiojklm'.split('').concat(['Comma', 'Period'])
|
||||
|
||||
css: '''
|
||||
.rc-imageselect-target > .rc-imageselect-tile > img:focus {
|
||||
.rc-imageselect-target > div:focus {
|
||||
outline: 2px solid #4a90e2;
|
||||
}
|
||||
.rc-button-default:focus {
|
||||
@ -8,10 +10,29 @@ Captcha.fixes =
|
||||
}
|
||||
'''
|
||||
|
||||
cssNoscript: '''
|
||||
.fbc-payload-imageselect {
|
||||
position: relative;
|
||||
}
|
||||
.fbc-payload-imageselect > label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 93.3px;
|
||||
width: 93.3px;
|
||||
}
|
||||
label[data-row="0"] {top: 0px;}
|
||||
label[data-row="1"] {top: 93.3px;}
|
||||
label[data-row="2"] {top: 186.6px;}
|
||||
label[data-col="0"] {left: 0px;}
|
||||
label[data-col="1"] {left: 93.3px;}
|
||||
label[data-col="2"] {left: 186.6px;}
|
||||
'''
|
||||
|
||||
init: ->
|
||||
switch location.pathname.split('/')[3]
|
||||
when 'anchor' then @initMain()
|
||||
when 'frame' then @initPopup()
|
||||
when 'anchor' then @initMain()
|
||||
when 'frame' then @initPopup()
|
||||
when 'fallback' then @initNoscript()
|
||||
|
||||
initMain: ->
|
||||
$.onExists d.body, '#recaptcha-anchor', true, (checkbox) ->
|
||||
@ -28,35 +49,66 @@ Captcha.fixes =
|
||||
new MutationObserver(=> @fixImages()).observe d.body, {childList: true, subtree: true}
|
||||
$.on d, 'keydown', @keybinds.bind(@)
|
||||
|
||||
initNoscript: ->
|
||||
@noscript = true
|
||||
@images = $$ '.fbc-payload-imageselect > input'
|
||||
return unless @images.length
|
||||
|
||||
$.addStyle @cssNoscript
|
||||
@addLabels()
|
||||
$.on d, 'keydown', @keybinds.bind(@)
|
||||
$.on $('.fbc-imageselect-challenge > form'), 'submit', @checkForm.bind(@)
|
||||
|
||||
fixImages: ->
|
||||
return unless (@images = $$ '.rc-imageselect-target > .rc-imageselect-tile > img').length
|
||||
focus = @images[0].tabIndex isnt 0
|
||||
@images = $$ '.rc-imageselect-target > div'
|
||||
for img in @images
|
||||
img.tabIndex = 0
|
||||
if focus
|
||||
$.queueTask => @focusImage()
|
||||
@addTooltips @images if @images.length
|
||||
|
||||
focusImage: ->
|
||||
# XXX Image is not focusable at first in Firefox; to be refactored when I figure out why.
|
||||
img = @images[0]
|
||||
$.asap ->
|
||||
return true unless doc.contains img
|
||||
img.focus()
|
||||
d.activeElement is img
|
||||
, ->
|
||||
addLabels: ->
|
||||
imageSelect = $ '.fbc-payload-imageselect'
|
||||
labels = for checkbox, i in @images
|
||||
checkbox.id = "checkbox-#{i}"
|
||||
label = $.el 'label',
|
||||
htmlFor: checkbox.id
|
||||
label.dataset.row = i // 3
|
||||
label.dataset.col = i % 3
|
||||
label
|
||||
$.add imageSelect, labels
|
||||
@addTooltips labels
|
||||
|
||||
addTooltips: (nodes) ->
|
||||
for node, i in nodes
|
||||
node.title = "#{@imageKeys[i]} or #{@imageKeys[i+9][0].toUpperCase()}#{@imageKeys[i+9][1..]}"
|
||||
return
|
||||
|
||||
checkForm: (e) ->
|
||||
n = 0
|
||||
n++ for checkbox in @images when checkbox.checked
|
||||
e.preventDefault() if n is 0
|
||||
|
||||
keybinds: (e) ->
|
||||
return unless @images and doc.contains(@images[0]) and d.activeElement
|
||||
reload = $.id 'recaptcha-reload-button'
|
||||
verify = $.id 'recaptcha-verify-button'
|
||||
return unless @images and doc.contains(@images[0])
|
||||
|
||||
reload = $ '#recaptcha-reload-button, .fbc-button-reload'
|
||||
verify = $ '#recaptcha-verify-button, .fbc-button-verify > input'
|
||||
x = @images.indexOf d.activeElement
|
||||
if x < 0
|
||||
return unless $('.rc-controls').contains d.activeElement
|
||||
x = if d.activeElement is verify then 11 else 9
|
||||
return unless dx = {38: 9, 40: 3, 37: 11, 39: 1, 73: 9, 75: 3, 74: 11, 76: 1}[e.keyCode] # Up, Down, Left, Right, I, K, J, L
|
||||
x = (x + dx) % 12
|
||||
if x is 10
|
||||
x = if dx is 11 then 9 else 11
|
||||
(@images[x] or {9: reload, 11: verify}[x]).focus()
|
||||
key = Keybinds.keyCode e
|
||||
|
||||
if !@noscript and key is 'Space' and x < 9
|
||||
@images[x].click()
|
||||
else if (i = @imageKeys.indexOf key) >= 0
|
||||
@images[i % 9].click()
|
||||
verify.focus()
|
||||
else if dx = {'Up': 9, 'Down': 3, 'Left': 11, 'Right': 1}[key]
|
||||
x = (x + dx) % 12
|
||||
if x is 10
|
||||
x = if dx is 11 then 9 else 11
|
||||
(@images[x] or {9: reload, 11: verify}[x]).focus()
|
||||
else
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
Captcha.noscript =
|
||||
lifetime: 2 * $.MINUTE
|
||||
iframeURL: '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
|
||||
lifetime: 30 * $.MINUTE
|
||||
|
||||
init: ->
|
||||
return if d.cookie.indexOf('pass_enabled=1') >= 0
|
||||
return unless @isEnabled = !!$.id 'g-recaptcha'
|
||||
return unless @isEnabled = !!$ '#g-recaptcha, #captchaContainerAlt'
|
||||
|
||||
container = $.el 'div',
|
||||
className: 'captcha-img'
|
||||
@ -27,7 +26,7 @@ Captcha.noscript =
|
||||
token: @save.bind @
|
||||
error: @error.bind @
|
||||
|
||||
$.addClass QR.nodes.el, 'has-captcha'
|
||||
$.addClass QR.nodes.el, 'has-captcha', 'captcha-v1', 'noscript-captcha'
|
||||
$.after QR.nodes.com.parentNode, [container, input]
|
||||
|
||||
@captchas = []
|
||||
@ -42,12 +41,15 @@ Captcha.noscript =
|
||||
initFrame: ->
|
||||
conn = new Connection window.parent, "#{location.protocol}//boards.4chan.org",
|
||||
response: (response) ->
|
||||
$.id('response').value = response
|
||||
$('.fbc-challenge > form').submit()
|
||||
conn.send
|
||||
token: $('.fbc-verification-token > textarea')?.value
|
||||
error: $('.fbc-error')?.textContent
|
||||
return unless img = $ '.fbc-payload > img'
|
||||
$.id('recaptcha_response_field').value = response
|
||||
# The form has a field named 'submit'
|
||||
HTMLFormElement.prototype.submit.call $('form')
|
||||
if location.hash is '#response'
|
||||
conn.send
|
||||
token: $('textarea')?.value
|
||||
error: $('.recaptcha_input_area')?.textContent.replace(/:$/, '')
|
||||
return unless img = $ 'img'
|
||||
$('form').action = '#response'
|
||||
cb = ->
|
||||
canvas = $.el 'canvas'
|
||||
canvas.width = img.width
|
||||
@ -61,6 +63,12 @@ Captcha.noscript =
|
||||
|
||||
timers: {}
|
||||
|
||||
iframeURL: ->
|
||||
url = '//www.google.com/recaptcha/api/noscript?k=<%= meta.recaptchaKey %>'
|
||||
if lang = Conf['captchaLanguage'].trim()
|
||||
url += "&hl=#{encodeURIComponent lang}"
|
||||
url
|
||||
|
||||
cb:
|
||||
focus: -> QR.captcha.setup false, true
|
||||
|
||||
@ -88,11 +96,11 @@ Captcha.noscript =
|
||||
if !@nodes.iframe
|
||||
@nodes.iframe = $.el 'iframe',
|
||||
id: 'qr-captcha-iframe'
|
||||
src: @iframeURL
|
||||
$.add d.body, @nodes.iframe
|
||||
@conn.target = @nodes.iframe.contentWindow
|
||||
src: @iframeURL()
|
||||
$.add QR.nodes.el, @nodes.iframe
|
||||
@conn.target = @nodes.iframe
|
||||
else if !@occupied or force
|
||||
@nodes.iframe.src = @iframeURL
|
||||
@nodes.iframe.src = @iframeURL()
|
||||
@occupied = true
|
||||
@nodes.input.focus() if focus
|
||||
|
||||
@ -109,9 +117,9 @@ Captcha.noscript =
|
||||
|
||||
destroy: ->
|
||||
return unless @isEnabled
|
||||
$.rm @nodes.img if @nodes.img
|
||||
$.rm @nodes.img
|
||||
delete @nodes.img
|
||||
$.rm @nodes.iframe if @nodes.iframe
|
||||
$.rm @nodes.iframe
|
||||
delete @nodes.iframe
|
||||
delete @occupied
|
||||
@beforeSetup()
|
||||
@ -125,7 +133,7 @@ Captcha.noscript =
|
||||
if captcha = @captchas.shift()
|
||||
@count()
|
||||
$.set 'captchas', @captchas
|
||||
captcha.response
|
||||
captcha
|
||||
else if /\S/.test @nodes.input.value
|
||||
(cb) =>
|
||||
@submitCB = cb
|
||||
@ -141,15 +149,17 @@ Captcha.noscript =
|
||||
save: (token) ->
|
||||
delete @occupied
|
||||
@nodes.input.value = ''
|
||||
captcha =
|
||||
challenge: token
|
||||
response: 'manual_challenge'
|
||||
timeout: @timeout
|
||||
if @submitCB
|
||||
@submitCB token
|
||||
@submitCB captcha
|
||||
delete @submitCB
|
||||
if @needed() then @reload() else @destroy()
|
||||
else
|
||||
$.forceSync 'captchas'
|
||||
@captchas.push
|
||||
response: token
|
||||
timeout: @timeout
|
||||
@captchas.push captcha
|
||||
@count()
|
||||
$.set 'captchas', @captchas
|
||||
@reload()
|
||||
@ -212,7 +222,7 @@ Captcha.noscript =
|
||||
@destroy()
|
||||
|
||||
reload: ->
|
||||
@nodes.iframe.src = @iframeURL
|
||||
@nodes.iframe.src = @iframeURL()
|
||||
@occupied = true
|
||||
@nodes.img?.hidden = true
|
||||
|
||||
|
||||
54
src/Posting/Captcha.replace.coffee
Normal file
54
src/Posting/Captcha.replace.coffee
Normal 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
|
||||
219
src/Posting/Captcha.v1.coffee
Normal file
219
src/Posting/Captcha.v1.coffee
Normal 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()
|
||||
@ -3,7 +3,12 @@ Captcha.v2 =
|
||||
|
||||
init: ->
|
||||
return if d.cookie.indexOf('pass_enabled=1') >= 0
|
||||
return unless @isEnabled = !!$.id 'g-recaptcha'
|
||||
return unless @isEnabled = !!$ '#g-recaptcha, #captchaContainerAlt'
|
||||
|
||||
if @noscript = Conf['Force Noscript Captcha'] or not $.hasClass doc, 'js-enabled'
|
||||
@conn = new Connection null, "#{location.protocol}//www.google.com",
|
||||
token: (token) => @save true, token
|
||||
$.addClass QR.nodes.el, 'noscript-captcha'
|
||||
|
||||
@captchas = []
|
||||
$.get 'captchas', [], ({captchas}) ->
|
||||
@ -17,18 +22,34 @@ Captcha.v2 =
|
||||
counter = $ '.captcha-counter > a', root
|
||||
@nodes = {root, counter}
|
||||
@count()
|
||||
$.addClass QR.nodes.el, 'has-captcha'
|
||||
$.addClass QR.nodes.el, 'has-captcha', 'captcha-v2'
|
||||
$.after QR.nodes.com.parentNode, root
|
||||
|
||||
$.on counter, 'click', @toggle.bind @
|
||||
$.on counter, 'keydown', (e) =>
|
||||
return unless Keybinds.keyCode(e) is 'Space'
|
||||
@toggle()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
$.on window, 'captcha:success', =>
|
||||
# XXX Greasemonkey 1.x workaround to gain access to GM_* functions.
|
||||
$.queueTask => @save false
|
||||
|
||||
initFrame: ->
|
||||
if token = $('.fbc-verification-token > textarea')?.value
|
||||
conn = new Connection window.parent, "#{location.protocol}//boards.4chan.org"
|
||||
conn.send {token}
|
||||
|
||||
shouldFocus: false
|
||||
timeouts: {}
|
||||
postsCount: 0
|
||||
|
||||
noscriptURL: ->
|
||||
url = '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
|
||||
if lang = Conf['captchaLanguage'].trim()
|
||||
url += "&hl=#{encodeURIComponent lang}"
|
||||
url
|
||||
|
||||
needed: ->
|
||||
captchaCount = @captchas.length
|
||||
captchaCount++ if QR.req
|
||||
@ -60,6 +81,7 @@ Captcha.v2 =
|
||||
if @nodes.container
|
||||
if @shouldFocus and iframe = $ 'iframe', @nodes.container
|
||||
iframe.focus()
|
||||
QR.focus() # Event handler not fired in Firefox
|
||||
delete @shouldFocus
|
||||
return
|
||||
|
||||
@ -69,6 +91,19 @@ Captcha.v2 =
|
||||
childList: true
|
||||
subtree: true
|
||||
|
||||
if @noscript
|
||||
@setupNoscript()
|
||||
else
|
||||
@setupJS()
|
||||
|
||||
setupNoscript: ->
|
||||
iframe = $.el 'iframe',
|
||||
id: 'qr-captcha-iframe'
|
||||
src: @noscriptURL()
|
||||
$.add @nodes.container, iframe
|
||||
@conn.target = iframe
|
||||
|
||||
setupJS: ->
|
||||
$.globalEval '''
|
||||
(function() {
|
||||
function render() {
|
||||
@ -101,7 +136,7 @@ Captcha.v2 =
|
||||
return
|
||||
|
||||
setupIFrame: (iframe) ->
|
||||
@setupTime = Date.now()
|
||||
Captcha.replace.iframe iframe
|
||||
$.addClass QR.nodes.el, 'captcha-open'
|
||||
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
|
||||
QR.nodes.el.style.top = null
|
||||
@ -134,19 +169,19 @@ Captcha.v2 =
|
||||
if captcha = @captchas.shift()
|
||||
$.set 'captchas', @captchas
|
||||
@count()
|
||||
captcha.response
|
||||
captcha
|
||||
else
|
||||
null
|
||||
|
||||
save: (pasted) ->
|
||||
save: (pasted, token) ->
|
||||
$.forceSync 'captchas'
|
||||
@captchas.push
|
||||
response: $('textarea', @nodes.container).value
|
||||
timeout: (if pasted then @setupTime else Date.now()) + @lifetime
|
||||
response: token or $('textarea', @nodes.container).value
|
||||
timeout: Date.now() + @lifetime
|
||||
$.set 'captchas', @captchas
|
||||
@count()
|
||||
|
||||
focus = d.activeElement?.nodeName is 'IFRAME' and d.activeElement.src?[...38] is 'https://www.google.com/recaptcha/api2/'
|
||||
focus = d.activeElement?.nodeName is 'IFRAME' and /https?:\/\/www\.google\.com\/recaptcha\//.test(d.activeElement.src)
|
||||
if @needed()
|
||||
if focus
|
||||
if QR.cooldown.auto or Conf['Post on Captcha Completion']
|
||||
@ -182,9 +217,12 @@ Captcha.v2 =
|
||||
@timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now()
|
||||
|
||||
reload: ->
|
||||
$.globalEval '''
|
||||
(function() {
|
||||
var container = document.querySelector("#qr .captcha-container");
|
||||
window.grecaptcha.reset(container.dataset.widgetID);
|
||||
})();
|
||||
'''
|
||||
if @noscript
|
||||
$('iframe', @nodes.container).src = @noscriptURL()
|
||||
else
|
||||
$.globalEval '''
|
||||
(function() {
|
||||
var container = document.querySelector("#qr .captcha-container");
|
||||
window.grecaptcha.reset(container.dataset.widgetID);
|
||||
})();
|
||||
'''
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
QR =
|
||||
mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm']
|
||||
|
||||
validExtension: /\.(jpe?g|png|gif|pdf|swf|webm)$/i
|
||||
|
||||
typeFromExtension:
|
||||
'jpg': 'image/jpeg'
|
||||
'jpeg': 'image/jpeg'
|
||||
'png': 'image/png'
|
||||
'gif': 'image/gif'
|
||||
'pdf': 'application/pdf'
|
||||
'swf': 'application/vnd.adobe.flash.movie'
|
||||
'webm': 'video/webm'
|
||||
|
||||
init: ->
|
||||
return unless Conf['Quick Reply']
|
||||
|
||||
@ -9,8 +20,12 @@ QR =
|
||||
|
||||
return if g.VIEW is 'archive'
|
||||
|
||||
noscript = Conf['Force Noscript Captcha'] or not $.hasClass doc, 'js-enabled'
|
||||
@captcha = Captcha[if noscript then 'noscript' else 'v2']
|
||||
version = if Conf['Use Recaptcha v1']
|
||||
noscript = Conf['Force Noscript Captcha'] or not $.hasClass doc, 'js-enabled'
|
||||
if noscript then 'noscript' else 'v1'
|
||||
else
|
||||
'v2'
|
||||
@captcha = Captcha[version]
|
||||
|
||||
$.on d, '4chanXInitFinished', @initReady
|
||||
|
||||
@ -134,14 +149,16 @@ QR =
|
||||
QR.hasFocus = d.activeElement and QR.nodes.el.contains(d.activeElement)
|
||||
QR.nodes.el.classList.toggle 'focus', QR.hasFocus
|
||||
# XXX Stop unwanted scrolling due to captcha.
|
||||
if QR.inCaptcha()
|
||||
QR.scrollY = window.scrollY
|
||||
$.on d, 'scroll', QR.scrollLock
|
||||
else
|
||||
$.off d, 'scroll', QR.scrollLock
|
||||
if QR.captcha.isEnabled and QR.captcha is Captcha.v2 and !QR.captcha.noscript
|
||||
if QR.inCaptcha()
|
||||
QR.scrollY = window.scrollY
|
||||
$.on d, 'scroll', QR.scrollLock
|
||||
else
|
||||
$.off d, 'scroll', QR.scrollLock
|
||||
|
||||
inBubble: ->
|
||||
$$('.goog-bubble-content > iframe').some((el) -> el.getBoundingClientRect().bottom > 0)
|
||||
bubbles = $$ '.goog-bubble-content > iframe'
|
||||
d.activeElement in bubbles or bubbles.some((el) -> el.getBoundingClientRect().bottom > 0)
|
||||
|
||||
inCaptcha: ->
|
||||
(d.activeElement?.nodeName is 'IFRAME' and QR.nodes.el.contains(d.activeElement)) or (QR.hasFocus and QR.inBubble())
|
||||
@ -191,19 +208,21 @@ QR =
|
||||
unless Header.areNotificationsEnabled
|
||||
alert el.textContent if d.hidden and not QR.cooldown.auto
|
||||
else if d.hidden or not (focusOverride or d.hasFocus())
|
||||
notif = new Notification el.textContent,
|
||||
body: el.textContent
|
||||
icon: Favicon.logo
|
||||
notif.onclick = -> window.focus()
|
||||
if chrome?
|
||||
# Firefox automatically closes notifications
|
||||
# so we can't control the onclose properly.
|
||||
notif.onclose = -> notice.close()
|
||||
notif.onshow = ->
|
||||
setTimeout ->
|
||||
notif.onclose = null
|
||||
notif.close()
|
||||
, 7 * $.SECOND
|
||||
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1130502 (SeaMonkey)
|
||||
try
|
||||
notif = new Notification el.textContent,
|
||||
body: el.textContent
|
||||
icon: Favicon.logo
|
||||
notif.onclick = -> window.focus()
|
||||
if $.engine isnt 'gecko'
|
||||
# Firefox automatically closes notifications
|
||||
# so we can't control the onclose properly.
|
||||
notif.onclose = -> notice.close()
|
||||
notif.onshow = ->
|
||||
setTimeout ->
|
||||
notif.onclose = null
|
||||
notif.close()
|
||||
, 7 * $.SECOND
|
||||
|
||||
notifications: []
|
||||
|
||||
@ -346,7 +365,7 @@ QR =
|
||||
for i in [0...bstr.length]
|
||||
arr[i] = bstr.charCodeAt(i)
|
||||
blob = new Blob [arr], {type: m[1]}
|
||||
blob.name = "image.#{m[2]}"
|
||||
blob.name = "file.#{m[2]}"
|
||||
QR.handleFiles [blob]
|
||||
else if /^https?:\/\//.test src
|
||||
QR.handleUrl src
|
||||
@ -523,7 +542,7 @@ QR =
|
||||
# We don't receive blur events from captcha iframe.
|
||||
$.on d, 'click', QR.focus
|
||||
|
||||
unless chrome?
|
||||
if $.engine is 'gecko'
|
||||
nodes.pasteArea.hidden = false
|
||||
new MutationObserver(QR.pasteFF).observe nodes.pasteArea, {childList: true}
|
||||
|
||||
@ -536,20 +555,19 @@ QR =
|
||||
event = if node.nodeName is 'SELECT' then 'change' else 'input'
|
||||
$.on nodes[name], event, save
|
||||
|
||||
<% if (type === 'userscript') { %>
|
||||
if Conf['Remember QR Size']
|
||||
# XXX Blink and WebKit treat width and height of <textarea>s as min-width and min-height
|
||||
if $.engine is 'gecko' and Conf['Remember QR Size']
|
||||
$.get 'QR Size', '', (item) ->
|
||||
nodes.com.style.cssText = item['QR Size']
|
||||
$.on nodes.com, 'mouseup', (e) ->
|
||||
return if e.button isnt 0
|
||||
$.set 'QR Size', @style.cssText
|
||||
<% } %>
|
||||
|
||||
QR.generatePostableThreadsList()
|
||||
QR.persona.init()
|
||||
new QR.post true
|
||||
QR.status()
|
||||
QR.cooldown.init()
|
||||
QR.cooldown.setup()
|
||||
QR.captcha.init()
|
||||
|
||||
$.add d.body, dialog
|
||||
@ -649,7 +667,6 @@ QR =
|
||||
onload: ->
|
||||
# Upload done, waiting for server response.
|
||||
QR.req.isUploadFinished = true
|
||||
QR.req.uploadEndTime = Date.now()
|
||||
QR.req.progress = '...'
|
||||
QR.status()
|
||||
onprogress: (e) ->
|
||||
@ -658,7 +675,12 @@ QR =
|
||||
QR.status()
|
||||
|
||||
cb = (response) ->
|
||||
extra.form.append 'g-recaptcha-response', response if response?
|
||||
if response?
|
||||
if response.challenge?
|
||||
extra.form.append 'recaptcha_challenge_field', response.challenge
|
||||
extra.form.append 'recaptcha_response_field', response.response
|
||||
else
|
||||
extra.form.append 'g-recaptcha-response', response.response
|
||||
QR.req = $.ajax "https://sys.4chan.org/#{g.BOARD}/post", options, extra
|
||||
QR.req.progress = '...'
|
||||
|
||||
@ -772,7 +794,7 @@ QR =
|
||||
post.rm()
|
||||
QR.captcha.setup(d.activeElement is QR.nodes.status)
|
||||
|
||||
QR.cooldown.add req.uploadEndTime, threadID, postID
|
||||
QR.cooldown.add threadID, postID
|
||||
|
||||
URL = if threadID is postID # new thread
|
||||
"#{window.location.origin}/#{g.BOARD}/thread/#{threadID}"
|
||||
|
||||
@ -1,47 +1,55 @@
|
||||
QR.cooldown =
|
||||
seconds: 0
|
||||
delays:
|
||||
thread: 0
|
||||
reply: 0
|
||||
image: 0
|
||||
reply_intra: 0
|
||||
image_intra: 0
|
||||
deletion: 60 # cooldown for deleting posts/files
|
||||
thread_global: 300 # inter-board thread cooldown
|
||||
|
||||
# Called from Main
|
||||
init: ->
|
||||
return unless Conf['Quick Reply'] and Conf['Cooldown']
|
||||
@data = Conf['cooldowns']
|
||||
$.sync 'cooldowns', @sync
|
||||
|
||||
# Called from QR
|
||||
setup: ->
|
||||
return unless Conf['Cooldown']
|
||||
|
||||
# Read cooldown times
|
||||
QR.cooldown.delays = if m = Get.scriptData().match /\bcooldowns *= *({[^}]+})/
|
||||
JSON.parse m[1]
|
||||
else
|
||||
{thread: 0, reply: 0, image: 0, reply_intra: 0, image_intra: 0}
|
||||
if m = Get.scriptData().match /\bcooldowns *= *({[^}]+})/
|
||||
$.extend QR.cooldown.delays, JSON.parse m[1]
|
||||
|
||||
# The longest reply cooldown, for use in pruning old reply data
|
||||
QR.cooldown.maxDelay = 0
|
||||
for type, delay of QR.cooldown.delays when type isnt 'thread'
|
||||
for type, delay of QR.cooldown.delays when type not in ['thread', 'thread_global']
|
||||
QR.cooldown.maxDelay = Math.max QR.cooldown.maxDelay, delay
|
||||
|
||||
# There is a 300 second inter-board thread cooldown.
|
||||
QR.cooldown.delays['thread_global'] = 300
|
||||
|
||||
# Retrieve recent posts and delays.
|
||||
keys = QR.cooldown.keys =
|
||||
local: "cooldown.#{g.BOARD}"
|
||||
global: 'cooldown.global'
|
||||
items = {}
|
||||
items[key] = {} for scope, key of keys
|
||||
$.get items, (items) ->
|
||||
QR.cooldown[scope] = items[key] for scope, key of keys
|
||||
QR.cooldown.start()
|
||||
$.sync key, QR.cooldown.sync scope for scope, key of keys
|
||||
QR.cooldown.isSetup = true
|
||||
QR.cooldown.start()
|
||||
|
||||
start: ->
|
||||
return if QR.cooldown.isCounting or Object.keys(QR.cooldown.local).length + Object.keys(QR.cooldown.global).length is 0
|
||||
{data} = QR.cooldown
|
||||
return unless (
|
||||
QR.cooldown.isSetup and
|
||||
!QR.cooldown.isCounting and
|
||||
Object.keys(data[g.BOARD.ID] or {}).length + Object.keys(data.global or {}).length > 0
|
||||
)
|
||||
QR.cooldown.isCounting = true
|
||||
QR.cooldown.count()
|
||||
|
||||
sync: (scope) -> (cooldowns) ->
|
||||
QR.cooldown[scope] = cooldowns or {}
|
||||
sync: (data) ->
|
||||
QR.cooldown.data = data or {}
|
||||
QR.cooldown.start()
|
||||
|
||||
add: (start, threadID, postID) ->
|
||||
add: (threadID, postID) ->
|
||||
return unless Conf['Cooldown']
|
||||
start = Date.now()
|
||||
boardID = g.BOARD.ID
|
||||
QR.cooldown.set 'local', start, {threadID, postID}
|
||||
QR.cooldown.set boardID, start, {threadID, postID}
|
||||
QR.cooldown.set 'global', start, {boardID, threadID, postID} if threadID is postID
|
||||
QR.cooldown.start()
|
||||
|
||||
@ -49,16 +57,25 @@ QR.cooldown =
|
||||
return unless Conf['Cooldown']
|
||||
cooldown = QR.cooldown.categorize post
|
||||
cooldown.delay = delay
|
||||
QR.cooldown.set 'local', Date.now(), cooldown
|
||||
QR.cooldown.set g.BOARD.ID, Date.now(), cooldown
|
||||
QR.cooldown.start()
|
||||
|
||||
delete: (post) ->
|
||||
return unless Conf['Cooldown'] and g.BOARD.ID is post.board.ID
|
||||
$.forceSync QR.cooldown.keys.local
|
||||
for id, cooldown of QR.cooldown.local
|
||||
return unless Conf['Cooldown']
|
||||
$.forceSync 'cooldowns'
|
||||
cooldowns = (QR.cooldown.data[post.board.ID] or= {})
|
||||
for id, cooldown of cooldowns
|
||||
if !cooldown.delay? and cooldown.threadID is post.thread.ID and cooldown.postID is post.ID
|
||||
delete QR.cooldown.local[id]
|
||||
QR.cooldown.save 'local'
|
||||
delete cooldowns[id]
|
||||
QR.cooldown.save [post.board.ID]
|
||||
|
||||
secondsDeletion: (post) ->
|
||||
cooldowns = QR.cooldown.data[post.board.ID] or {}
|
||||
for start, cooldown of cooldowns
|
||||
if !cooldown.delay? and cooldown.threadID is post.thread.ID and cooldown.postID is post.ID
|
||||
seconds = QR.cooldown.delays.deletion - (Date.now() - start) // $.SECOND
|
||||
return Math.max seconds, 0
|
||||
0
|
||||
|
||||
categorize: (post) ->
|
||||
if post.thread is 'new'
|
||||
@ -68,38 +85,41 @@ QR.cooldown =
|
||||
threadID: +post.thread
|
||||
|
||||
set: (scope, id, value) ->
|
||||
$.forceSync QR.cooldown.keys[scope]
|
||||
QR.cooldown[scope][id] = value
|
||||
$.set QR.cooldown.keys[scope], QR.cooldown[scope]
|
||||
$.forceSync 'cooldowns'
|
||||
cooldowns = (QR.cooldown.data[scope] or= {})
|
||||
cooldowns[id] = value
|
||||
$.set 'cooldowns', QR.cooldown.data
|
||||
|
||||
save: (scope) ->
|
||||
if Object.keys(QR.cooldown[scope]).length
|
||||
$.set QR.cooldown.keys[scope], QR.cooldown[scope]
|
||||
else
|
||||
$.delete QR.cooldown.keys[scope]
|
||||
save: (scopes) ->
|
||||
{data} = QR.cooldown
|
||||
for scope in scopes when scope of data and !Object.keys(data[scope]).length
|
||||
delete data[scope]
|
||||
$.set 'cooldowns', data
|
||||
|
||||
count: ->
|
||||
$.forceSync 'cooldowns'
|
||||
save = []
|
||||
nCooldowns = 0
|
||||
now = Date.now()
|
||||
{type, threadID} = QR.cooldown.categorize QR.posts[0]
|
||||
seconds = 0
|
||||
|
||||
for scope, key of QR.cooldown.keys
|
||||
$.forceSync key
|
||||
save = false
|
||||
for scope in [g.BOARD.ID, 'global']
|
||||
cooldowns = (QR.cooldown.data[scope] or= {})
|
||||
|
||||
for start, cooldown of QR.cooldown[scope]
|
||||
for start, cooldown of cooldowns
|
||||
start = +start
|
||||
elapsed = (now - start) // $.SECOND
|
||||
if elapsed < 0 # clock changed since then?
|
||||
delete QR.cooldown[scope][start]
|
||||
save = true
|
||||
delete cooldowns[start]
|
||||
save.push scope
|
||||
continue
|
||||
|
||||
# Explicit delays from error messages
|
||||
if cooldown.delay?
|
||||
if cooldown.delay <= elapsed
|
||||
delete QR.cooldown[scope][start]
|
||||
save = true
|
||||
delete cooldowns[start]
|
||||
save.push scope
|
||||
else if cooldown.type is type and cooldown.threadID is threadID
|
||||
# Delays only apply to the given post type and thread.
|
||||
seconds = Math.max seconds, cooldown.delay - elapsed
|
||||
@ -113,8 +133,8 @@ QR.cooldown =
|
||||
if QR.cooldown.customCooldown
|
||||
maxDelay = Math.max maxDelay, parseInt(Conf['customCooldown'], 10)
|
||||
if maxDelay <= elapsed
|
||||
delete QR.cooldown[scope][start]
|
||||
save = true
|
||||
delete cooldowns[start]
|
||||
save.push scope
|
||||
continue
|
||||
|
||||
if (type is 'thread') is (cooldown.threadID is cooldown.postID) and cooldown.boardID isnt g.BOARD.ID
|
||||
@ -133,9 +153,11 @@ QR.cooldown =
|
||||
if QR.cooldown.customCooldown
|
||||
seconds = Math.max seconds, parseInt(Conf['customCooldown'], 10) - elapsed
|
||||
|
||||
QR.cooldown.save scope if save
|
||||
nCooldowns += Object.keys(cooldowns).length
|
||||
|
||||
if Object.keys(QR.cooldown.local).length + Object.keys(QR.cooldown.global).length
|
||||
QR.cooldown.save save if save.length
|
||||
|
||||
if nCooldowns
|
||||
clearTimeout QR.cooldown.timeout
|
||||
QR.cooldown.timeout = setTimeout QR.cooldown.count, $.SECOND
|
||||
else
|
||||
|
||||
@ -105,7 +105,7 @@ QR.post = class
|
||||
|
||||
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename']
|
||||
continue unless node = QR.nodes[name]
|
||||
node.value = @[name] or node.dataset.default or null
|
||||
node.value = @[name] or node.dataset.default or ''
|
||||
|
||||
(if @thread isnt 'new' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread'
|
||||
|
||||
@ -122,6 +122,7 @@ QR.post = class
|
||||
when 'thread'
|
||||
(if @thread isnt 'new' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread'
|
||||
QR.status()
|
||||
@updateFlashURL()
|
||||
when 'com'
|
||||
@nodes.span.textContent = @com
|
||||
QR.captcha.onPostChange()
|
||||
@ -132,13 +133,9 @@ QR.post = class
|
||||
QR.cooldown.auto = false
|
||||
when 'filename'
|
||||
return unless @file
|
||||
@file.newName = @filename.replace /[/\\]/g, '-'
|
||||
unless /\.(jpe?g|png|gif|pdf|swf|webm)$/i.test @filename
|
||||
# 4chan will truncate the filename if it has no extension,
|
||||
# but it will always replace the extension by the correct one,
|
||||
# so we suffix it with '.jpg' when needed.
|
||||
@file.newName += '.jpg'
|
||||
@saveFilename()
|
||||
@updateFilename()
|
||||
@updateFlashURL()
|
||||
when 'name'
|
||||
QR.persona.set @
|
||||
|
||||
@ -182,16 +179,22 @@ QR.post = class
|
||||
return
|
||||
|
||||
setFile: (@file) ->
|
||||
@filename = @file.name
|
||||
if Conf['Randomize Filename'] and g.BOARD.ID isnt 'f'
|
||||
@filename = "#{Date.now() - Math.floor(Math.random() * 365 * $.DAY)}"
|
||||
@filename += ext[0] if ext = @file.name.match QR.validExtension
|
||||
else
|
||||
@filename = @file.name
|
||||
@filesize = $.bytesToString @file.size
|
||||
@checkSize()
|
||||
@nodes.label.hidden = false if QR.spoiler
|
||||
QR.captcha.onPostChange()
|
||||
URL.revokeObjectURL @URL
|
||||
@saveFilename()
|
||||
if @ is QR.selected
|
||||
@showFileData()
|
||||
else
|
||||
@updateFilename()
|
||||
@updateFlashURL()
|
||||
@nodes.el.style.backgroundImage = null
|
||||
unless @file.type in QR.mimeTypes
|
||||
@fileError 'Unsupported file type.'
|
||||
@ -207,6 +210,8 @@ QR.post = class
|
||||
readFile: ->
|
||||
isVideo = /^video\//.test @file.type
|
||||
el = $.el(if isVideo then 'video' else 'img')
|
||||
return if isVideo and !el.canPlayType @file.type
|
||||
|
||||
event = if isVideo then 'loadeddata' else 'load'
|
||||
onload = =>
|
||||
$.off el, event, onload
|
||||
@ -289,9 +294,18 @@ QR.post = class
|
||||
@nodes.el.style.backgroundImage = null
|
||||
@nodes.label.hidden = true if QR.spoiler
|
||||
@showFileData()
|
||||
@updateFlashURL()
|
||||
URL.revokeObjectURL @URL
|
||||
@dismissErrors (error) -> $.hasClass error, 'file-error'
|
||||
|
||||
saveFilename: ->
|
||||
@file.newName = (@filename or '').replace /[/\\]/g, '-'
|
||||
unless QR.validExtension.test @filename
|
||||
# 4chan will truncate the filename if it has no extension,
|
||||
# but it will always replace the extension by the correct one,
|
||||
# so we suffix it with '.jpg' when needed.
|
||||
@file.newName += '.jpg'
|
||||
|
||||
updateFilename: ->
|
||||
long = "#{@filename} (#{@filesize})"
|
||||
@nodes.el.title = long
|
||||
@ -307,6 +321,28 @@ QR.post = class
|
||||
else
|
||||
$.rmClass QR.nodes.fileSubmit, 'has-file'
|
||||
|
||||
updateFlashURL: ->
|
||||
return unless g.BOARD.ID is 'f'
|
||||
if @thread is 'new' or !@file
|
||||
url = ''
|
||||
else
|
||||
url = @file.newName
|
||||
url = url.replace(/"/g, '%22') if $.engine in ['blink', 'webkit']
|
||||
url = url
|
||||
.replace(/[\t\n\f\r \xa0\u200B\u2029\u3000]+/g, ' ')
|
||||
.replace(/(^ | $)/g, '')
|
||||
.replace(/\.[0-9A-Za-z]+$/, '')
|
||||
url = "https://i.4cdn.org/f/#{encodeURIComponent E url}.swf\n"
|
||||
oldURL = @flashURL or ''
|
||||
if url isnt oldURL
|
||||
@com or= ''
|
||||
@com = @com[oldURL.length..] if @com[...oldURL.length] is oldURL
|
||||
@com = (url + @com) or null
|
||||
if @ is QR.selected
|
||||
QR.nodes.com.value = @com
|
||||
QR.characterCount()
|
||||
@flashURL = url
|
||||
|
||||
pasteText: (file) ->
|
||||
@pasting = true
|
||||
reader = new FileReader()
|
||||
|
||||
30
template.jst
30
template.jst
@ -3,7 +3,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>4chan X</title>
|
||||
<link rel="stylesheet" href="web.css">
|
||||
<link rel="icon" href="src/General/img/icon.gif">
|
||||
<link rel="icon" href="img/icon.gif">
|
||||
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/ohnjgmpcibpbafdlkimncjhflgedgpam">
|
||||
</head><body>
|
||||
<div id="header">
|
||||
@ -12,11 +12,18 @@
|
||||
<a href="https://github.com/ccd0/4chan-x">Source Code</a>
|
||||
<a href="https://github.com/ccd0/4chan-x/blob/master/CHANGELOG.md">Changelog</a>
|
||||
<a href="https://github.com/ccd0/4chan-x/wiki/Frequently-Asked-Questions">FAQ</a>
|
||||
<a href="https://github.com/ccd0/4chan-x/issues">Report Bugs</a>
|
||||
<a href="https://gitreports.com/issue/ccd0/4chan-x">Report Bugs</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="screenshot" href="img/screenshot.png"><img src="img/screenshot.png" alt="Screenshot"></a>
|
||||
<%= content.match(/<\/h1>([^]*)<h2 id="more-information"/)[1] %>
|
||||
<%=
|
||||
content
|
||||
.match(/<\/h1>([^]*)<h2 id="more-information"/)[1]
|
||||
.replace(
|
||||
/(<h3 id="(.*?)">.*?<\/h3>)([^]*?)(?=<h)/g,
|
||||
'<input hidden type="checkbox" id="$2-hide"><div><label for="$2-hide">$1</label>$3</div>'
|
||||
)
|
||||
%>
|
||||
<script>
|
||||
function imagePreview() {
|
||||
this.removeEventListener('mouseover', imagePreview, false);
|
||||
@ -30,7 +37,9 @@ function imagePreview() {
|
||||
}
|
||||
function storeInstall(e) {
|
||||
if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.button === 0) {
|
||||
chrome.webstore.install();
|
||||
chrome.webstore.install(this.href, function(){}, function(){
|
||||
location.href = this.href;
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
@ -42,5 +51,18 @@ for (var i = 0; i < document.links.length; i++) {
|
||||
link.addEventListener('click', storeInstall, false);
|
||||
}
|
||||
}
|
||||
var engine = (function() {
|
||||
if (/Edge\//.test(navigator.userAgent)) return 'edge';
|
||||
if (/Chrome\//.test(navigator.userAgent)) return 'blink';
|
||||
if (/WebKit\//.test(navigator.userAgent)) return 'webkit';
|
||||
if (/Gecko\/|Goanna/.test(navigator.userAgent)) return 'gecko';
|
||||
if (/Presto\//.test(navigator.userAgent)) return 'presto';
|
||||
})();
|
||||
if (engine) {
|
||||
var engines = {'firefox': 'gecko', 'chromium': 'blink presto', 'safari': 'webkit', 'webkitgtk-': 'webkit', 'other-browsers': 'edge'};
|
||||
for (browser in engines) {
|
||||
document.getElementById(browser + '-hide').checked = (engines[browser].indexOf(engine) < 0);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body></html>
|
||||
|
||||
18
web.css
18
web.css
@ -70,6 +70,11 @@ span.hover > img {
|
||||
max-height: 100%;
|
||||
box-shadow: 5px 5px 20px rgba(0,0,0,0.4);
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
a.screenshot:hover + span.hover {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@supports not (pointer-events: auto) {
|
||||
a[href$=".png"] {
|
||||
position: relative;
|
||||
@ -84,6 +89,17 @@ span.hover > img {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
h2 ~ p, h2 ~ p + ul {
|
||||
h2 ~ p, h2 ~ p + ul, input + div, div > label ~ * {
|
||||
margin-left: 1em;
|
||||
}
|
||||
input + div {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
h3 {
|
||||
display: inline;
|
||||
}
|
||||
input:checked + div > :not(label) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user