Merge branch 'master' into v1
Conflicts: src/Posting/QR.coffee
This commit is contained in:
commit
2762363093
174
CHANGELOG.md
174
CHANGELOG.md
@ -2,8 +2,179 @@ Sometimes the changelog has notes (not comprehensive) acknowledging people's wor
|
||||
|
||||
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.0
|
||||
|
||||
**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.
|
||||
|
||||
**v1.10.13.0** *(2015-05-26)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.13.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.10.12.8.
|
||||
- You can now manually update the page number in thread stats by clicking on it.
|
||||
- Page number in thread stats does not show on /f/.
|
||||
- Change default gallery slideshow toggle keybind from `s` to `Ctrl+Right`.
|
||||
- Add IJKL as an alternative to the arrow keys for the image captcha.
|
||||
- Clicking on the thread watcher refresh button while it's loading threads aborts it.
|
||||
- Make thread watcher a bit more efficient by fixing some cases where it wasn't sending `If-Modified-Since`.
|
||||
- Restore clearing the unread count in the thread watcher when a thread 404's.
|
||||
- Various minor bugfixes.
|
||||
|
||||
### 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.
|
||||
|
||||
**v1.10.12.7** *(2015-05-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.7/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.7/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix bug where dragging and dropping disabled the captcha even after the drop was complete.
|
||||
|
||||
**v1.10.12.6** *(2015-05-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.6/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.6/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- (Hasumi) Update archive.moe: Add /an/.
|
||||
|
||||
**v1.10.12.5** *(2015-05-14)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.5/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix bug where submit button was still getting unwanted focus under certain conditions.
|
||||
|
||||
**v1.10.12.4** *(2015-05-10)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Improve reporting to archive functionality.
|
||||
|
||||
**v1.10.12.3** *(2015-05-09)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix some captcha-related bugs.
|
||||
|
||||
**v1.10.12.2** *(2015-05-08)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix sandboxed HTTP sauce links not loading from HTTPS pages.
|
||||
|
||||
**v1.10.12.1** *(2015-05-04)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Give paste area cursor pointer like the other QR buttons.
|
||||
|
||||
**v1.10.12.0** *(2015-05-04)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.12.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.10.11.12.
|
||||
- Fix files dropped on captcha causing navigation to the file.
|
||||
- Fix size of report window when Javascript is disabled.
|
||||
- QR character count now handles surrogate pairs correctly and turns red when the limit of 2000 is reached.
|
||||
- Add `;sandbox` option to sauce links to open links without scripts or popups. Re-add swfchan.
|
||||
|
||||
### v1.10.11
|
||||
|
||||
**v1.10.11.12** *(2015-05-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.12/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.12/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add scrollbars to captcha popup when larger than window.
|
||||
|
||||
**v1.10.11.11** *(2015-05-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.11/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.11/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Remove swfchan from default sauce list.
|
||||
|
||||
**v1.10.11.10** *(2015-05-03)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.10/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.10/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Make link to MDN in filter guide stand out better.
|
||||
|
||||
**v1.10.11.9** *(2015-05-02)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.9/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.9/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix loading archived /f/ posts from before tag was recorded.
|
||||
|
||||
**v1.10.11.8** *(2015-05-02)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.8/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.8/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix some /f/-specific quotelink bugs.
|
||||
|
||||
**v1.10.11.7** *(2015-05-02)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.7/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.7/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix "PDF" being parsed as a Flash tag.
|
||||
|
||||
**v1.10.11.6** *(2015-04-26)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.6/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.6/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix size of window for reporting to fgts archive.
|
||||
|
||||
**v1.10.11.5** *(2015-04-26)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.5/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Reduce unwanted scrolling from captcha.
|
||||
|
||||
**v1.10.11.4** *(2015-04-26)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Don't apply filters to the unread post count in the thread watcher, but do apply them to unread posts quoting you.
|
||||
|
||||
**v1.10.11.3** *(2015-04-26)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.10.10.4: Possible fix for bug causing scrolling to the top of the page upon loading image captcha.
|
||||
|
||||
**v1.10.11.2** *(2015-04-25)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Posts hidden by filtering are no longer counted as unread posts in the thread watcher.
|
||||
- Add Flash tag (`%g`) to File Info Formatting.
|
||||
- Fix subject not being displayed in old non-OP posts loaded from the archives.
|
||||
|
||||
**v1.10.11.1** *(2015-04-24)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.10.10.3: Fix original post form not showing when JS is disabled.
|
||||
|
||||
**v1.10.11.0** *(2015-04-24)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.11.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.10.10.2.
|
||||
- Fix whitespace being stripped from the comment before filtering. This makes it possible to filter whitespace spam.
|
||||
|
||||
### v1.10.10
|
||||
|
||||
**v1.10.10.4** *(2015-04-26)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Possible fix for bug causing scrolling to the top of the page upon loading image captcha.
|
||||
|
||||
**v1.10.10.3** *(2015-04-24)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.3/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.3/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Fix original post form not showing when JS is disabled.
|
||||
|
||||
**v1.10.10.2** *(2015-04-21)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Add focus indication to verify button in captcha popup.
|
||||
|
||||
**v1.10.10.1** *(2015-04-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Merge v1.10.9.5: (thebladeee) Archive list: Transferred /w/ and /wg/ back to Nyafuu.
|
||||
|
||||
**v1.10.10.0** *(2015-04-18)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.0/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.10.0/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- Based on v1.10.9.4.
|
||||
- Make images in image captcha selectable with arrow keys.
|
||||
@ -11,6 +182,9 @@ The links to individual versions below are to copies of the script with the upda
|
||||
|
||||
### v1.10.9
|
||||
|
||||
**v1.10.9.5** *(2015-04-19)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.9.5/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.9.5/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- (thebladeee) Archive list: Transferred /w/ and /wg/ back to Nyafuu.
|
||||
|
||||
**v1.10.9.4** *(2015-04-17)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.9.4/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.10.9.4/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||
- (Hasumi) Update archive.moe: Add /gif/.
|
||||
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -16,13 +22,14 @@ module.exports = (grunt) ->
|
||||
parts = []
|
||||
text = template
|
||||
while text
|
||||
if part = text.match /^[^{}]+(?!{)/
|
||||
if part = text.match /^(?:[^{}\\]|\\.)+(?!{)/
|
||||
text = text[part[0].length..]
|
||||
context = (context + part[0])
|
||||
unescaped = part[0].replace /\\(.)/g, '$1'
|
||||
context = (context + unescaped)
|
||||
.replace(/(=['"])[^'"<>]*/g, '$1')
|
||||
.replace(/(<\w+)( [\w-]+((?=[ >])|=''|=""))*/g, '$1')
|
||||
.replace(/^([^'"<>]+|<\/?\w+>)*/, '')
|
||||
parts.push json part[0]
|
||||
parts.push json unescaped
|
||||
else if part = text.match /^([^}]){([^}`]*)}/
|
||||
text = text[part[0].length..]
|
||||
unless context is '' or (part[1] is '$' and /\=['"]$/.test context) or part[1] is '?'
|
||||
@ -68,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
|
||||
@ -184,6 +192,7 @@ 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
|
||||
|
||||
7
LICENSE
7
LICENSE
@ -72,9 +72,6 @@
|
||||
* audio/beep.wav from http://freesound.org/people/pierrecartoons1979/sounds/90112/
|
||||
* cc-by-nc-3.0
|
||||
*
|
||||
* 4chan/4chan-JS (https://github.com/4chan/4chan-JS)
|
||||
* Copyright (c) 2012-2013, 4chan LLC
|
||||
* All rights reserved.
|
||||
*
|
||||
* license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
|
||||
* Font Awesome by Dave Gandy (http://fontawesome.io)
|
||||
* license: http://fontawesome.io/license/
|
||||
*/
|
||||
|
||||
@ -28,7 +28,8 @@ Only the latest stable version of 4chan X is available.
|
||||
## 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.
|
||||
|
||||
- [Installing 4chan X in dwb](https://github.com/ccd0/4chan-x/wiki/Installing-4chan-X-in-dwb)
|
||||
- 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).
|
||||
|
||||
## 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.
|
||||
|
||||
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
// ==UserScript==
|
||||
// @name 4chan X beta
|
||||
// @version 1.10.10.0
|
||||
// @version 1.11.0.6
|
||||
// @minGMVer 1.14
|
||||
// @minFFVer 26
|
||||
// @namespace 4chan-X
|
||||
@ -10,9 +10,11 @@
|
||||
// @match *://sys.4chan.org/*
|
||||
// @match *://a.4cdn.org/*
|
||||
// @match *://i.4cdn.org/*
|
||||
// @match *://www.4chan.org/banned
|
||||
// @match *://www.4chan.org/feedback
|
||||
// @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
|
||||
// @match *://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @grant GM_getValue
|
||||
// @grant GM_setValue
|
||||
// @grant GM_deleteValue
|
||||
|
||||
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,6 +1,6 @@
|
||||
// ==UserScript==
|
||||
// @name 4chan X
|
||||
// @version 1.10.10.0
|
||||
// @version 1.11.0.6
|
||||
// @minGMVer 1.14
|
||||
// @minFFVer 26
|
||||
// @namespace 4chan-X
|
||||
@ -10,9 +10,11 @@
|
||||
// @match *://sys.4chan.org/*
|
||||
// @match *://a.4cdn.org/*
|
||||
// @match *://i.4cdn.org/*
|
||||
// @match *://www.4chan.org/banned
|
||||
// @match *://www.4chan.org/feedback
|
||||
// @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
|
||||
// @match *://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
|
||||
// @grant GM_getValue
|
||||
// @grant GM_setValue
|
||||
// @grant GM_deleteValue
|
||||
|
||||
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.10.0' />
|
||||
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X-beta.crx' version='1.11.0.6' />
|
||||
</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.10.0' />
|
||||
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X.crx' version='1.11.0.6' />
|
||||
</app>
|
||||
</gupdate>
|
||||
|
||||
|
||||
@ -37,7 +37,8 @@ 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>
|
||||
<ul>
|
||||
<li><a href="https://github.com/ccd0/4chan-x/wiki/Installing-4chan-X-in-dwb">Installing 4chan X in dwb</a></li>
|
||||
<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>
|
||||
</ul>
|
||||
<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>
|
||||
|
||||
3513
npm-shrinkwrap.json
generated
3513
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -3,8 +3,8 @@
|
||||
"description": "Cross-browser userscript for maximum lurking on 4chan.",
|
||||
"meta": {
|
||||
"name": "4chan X",
|
||||
"version": "1.10.10.0",
|
||||
"date": "2015-04-18T07:45:01.778Z",
|
||||
"version": "1.11.0.6",
|
||||
"date": "2015-06-21T06:05:51.745Z",
|
||||
"repo": "https://github.com/ccd0/4chan-x/",
|
||||
"page": "https://github.com/ccd0/4chan-x",
|
||||
"downloads": "https://ccd0.github.io/4chan-x/builds/",
|
||||
@ -21,9 +21,11 @@
|
||||
"*://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*"
|
||||
],
|
||||
"suffix": {
|
||||
"stable": "",
|
||||
@ -38,7 +40,7 @@
|
||||
"dev": " dev"
|
||||
},
|
||||
"min": {
|
||||
"chrome": "32",
|
||||
"chrome": "34",
|
||||
"firefox": "26",
|
||||
"greasemonkey": "1.14"
|
||||
}
|
||||
@ -52,14 +54,14 @@
|
||||
"grunt-contrib-coffee": "^0.13.0",
|
||||
"grunt-contrib-concat": "^0.5.1",
|
||||
"grunt-contrib-copy": "^0.8.0",
|
||||
"grunt-contrib-jshint": "^0.11.1",
|
||||
"grunt-contrib-jshint": "^0.11.2",
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-markdown": "^0.7.0",
|
||||
"grunt-shell": "^1.1.2",
|
||||
"grunt-webstore-upload": "^0.8.2",
|
||||
"jszip": "^2.5.0",
|
||||
"load-grunt-tasks": "^3.1.0",
|
||||
"npm-shrinkwrap": "^5.3.0"
|
||||
"load-grunt-tasks": "^3.2.0",
|
||||
"npm-shrinkwrap": "^5.4.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"http": false,
|
||||
"https": true,
|
||||
"software": "foolfuuka",
|
||||
"boards": ["a", "biz", "c", "co", "diy", "fit", "gd", "gif", "h", "i", "int", "jp", "k", "m", "mlp", "out", "po", "qa", "r9k", "s4s", "sci", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
|
||||
"boards": ["a", "an", "biz", "c", "co", "diy", "fit", "gd", "gif", "h", "i", "int", "jp", "k", "m", "mlp", "out", "po", "qa", "r9k", "s4s", "sci", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
|
||||
"files": ["a", "biz", "c", "co", "diy", "fit", "gd", "h", "i", "jp", "k", "m", "mlp", "po", "qa", "r9k", "s4s", "sci", "tg", "u", "v", "vg", "vp", "vr", "wsg"]
|
||||
}, {
|
||||
"uid": 3,
|
||||
@ -32,8 +32,8 @@
|
||||
"http": true,
|
||||
"https": true,
|
||||
"software": "foolfuuka",
|
||||
"boards": ["c", "d", "e", "i", "lgbt", "t", "u", "w", "wg"],
|
||||
"files": ["c", "d", "e", "i", "lgbt", "t", "u", "w", "wg"]
|
||||
"boards": ["c", "d", "e", "i", "lgbt", "t", "u"],
|
||||
"files": ["c", "d", "e", "i", "lgbt", "t", "u"]
|
||||
}, {
|
||||
"uid": 8,
|
||||
"name": "Rebecca Black Tech",
|
||||
|
||||
@ -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
|
||||
@ -121,12 +121,18 @@ Filter =
|
||||
if !@isReply and result.top
|
||||
@thread.isOnTop = true
|
||||
|
||||
isHidden: (post) ->
|
||||
for key of Filter.filters when (value = Filter[key] post)?
|
||||
for filter in Filter.filters[key] when result = filter value, post.boardID, post.isReply
|
||||
return true if result.hide
|
||||
false
|
||||
|
||||
name: (post) -> post.info.name
|
||||
uniqueID: (post) -> post.info.uniqueID
|
||||
tripcode: (post) -> post.info.tripcode
|
||||
capcode: (post) -> post.info.capcode
|
||||
subject: (post) -> post.info.subject or undefined
|
||||
comment: (post) -> post.info.comment
|
||||
subject: (post) -> post.info.subject
|
||||
comment: (post) -> post.info.comment ? Build.parseComment(post)
|
||||
flag: (post) -> post.info.flag
|
||||
filename: (post) -> post.file?.name
|
||||
dimensions: (post) -> post.file?.dimensions
|
||||
|
||||
@ -2,10 +2,12 @@ Build =
|
||||
staticPath: '//s.4cdn.org/image/'
|
||||
gifIcon: if window.devicePixelRatio >= 2 then '@2x.gif' else '.gif'
|
||||
spoilerRange: {}
|
||||
|
||||
unescape: (text) ->
|
||||
return text unless text?
|
||||
text.replace(/<[^>]*>/g, '').replace /&(amp|#039|quot|lt|gt);/g, (c) ->
|
||||
{'&': '&', ''': "'", '"': '"', '<': '<', '>': '>'}[c]
|
||||
text.replace(/<[^>]*>/g, '').replace /&(amp|#039|quot|lt|gt|#44);/g, (c) ->
|
||||
{'&': '&', ''': "'", '"': '"', '<': '<', '>': '>', ',': ','}[c]
|
||||
|
||||
shortFilename: (filename) ->
|
||||
threshold = 30
|
||||
ext = filename.match(/\.?[^\.]*$/)[0]
|
||||
@ -13,93 +15,100 @@ Build =
|
||||
"#{filename[...threshold - 5]}(...)#{ext}"
|
||||
else
|
||||
filename
|
||||
|
||||
spoilerThumb: (boardID) ->
|
||||
if spoilerRange = Build.spoilerRange[boardID]
|
||||
# Randomize the spoiler image.
|
||||
"#{Build.staticPath}spoiler-#{boardID}#{Math.floor 1 + spoilerRange * Math.random()}.png"
|
||||
else
|
||||
"#{Build.staticPath}spoiler.png"
|
||||
|
||||
sameThread: (boardID, threadID) ->
|
||||
g.VIEW is 'thread' and g.BOARD.ID is boardID and g.THREADID is +threadID
|
||||
|
||||
postURL: (boardID, threadID, postID) ->
|
||||
if Build.sameThread boardID, threadID
|
||||
"#p#{postID}"
|
||||
else
|
||||
"/#{boardID}/thread/#{threadID}#p#{postID}"
|
||||
postFromObject: (data, boardID, suppressThumb) ->
|
||||
|
||||
parseJSON: (data, boardID) ->
|
||||
o =
|
||||
# id
|
||||
postID: data.no
|
||||
threadID: data.resto or data.no
|
||||
boardID: boardID
|
||||
# info
|
||||
name: Build.unescape data.name
|
||||
capcode: data.capcode
|
||||
tripcode: data.trip
|
||||
uniqueID: data.id
|
||||
email: Build.unescape data.email
|
||||
subject: Build.unescape data.sub
|
||||
flagCode: data.country
|
||||
flagName: Build.unescape data.country_name
|
||||
date: data.now
|
||||
dateUTC: data.time
|
||||
comment: {innerHTML: data.com or ''}
|
||||
isReply: !!data.resto
|
||||
# thread status
|
||||
isSticky: !!data.sticky
|
||||
isClosed: !!data.closed
|
||||
isArchived: !!data.archived
|
||||
# file
|
||||
if data.filedeleted
|
||||
o.file =
|
||||
isDeleted: true
|
||||
else if data.ext
|
||||
# file status
|
||||
fileDeleted: !!data.filedeleted
|
||||
o.info =
|
||||
subject: Build.unescape data.sub
|
||||
email: Build.unescape data.email
|
||||
name: Build.unescape(data.name) or ''
|
||||
tripcode: data.trip
|
||||
uniqueID: data.id
|
||||
flagCode: data.country
|
||||
flag: Build.unescape data.country_name
|
||||
dateUTC: data.time
|
||||
dateText: data.now
|
||||
commentHTML: {innerHTML: data.com or ''}
|
||||
if data.capcode
|
||||
o.info.capcode = data.capcode.replace(/_highlight$/, '').replace(/_/g, ' ').replace(/\b\w/g, (c) -> c.toUpperCase())
|
||||
o.capcodeHighlight = /_highlight$/.test data.capcode
|
||||
delete o.info.uniqueID
|
||||
if data.ext
|
||||
o.file =
|
||||
name: (Build.unescape data.filename) + data.ext
|
||||
timestamp: "#{data.tim}#{data.ext}"
|
||||
url: if boardID is 'f'
|
||||
"//i.4cdn.org/#{boardID}/#{encodeURIComponent data.filename}#{data.ext}"
|
||||
"#{location.protocol}//i.4cdn.org/#{boardID}/#{encodeURIComponent data.filename}#{data.ext}"
|
||||
else
|
||||
"//i.4cdn.org/#{boardID}/#{data.tim}#{data.ext}"
|
||||
"#{location.protocol}//i.4cdn.org/#{boardID}/#{data.tim}#{data.ext}"
|
||||
height: data.h
|
||||
width: data.w
|
||||
MD5: data.md5
|
||||
size: data.fsize
|
||||
turl: "//i.4cdn.org/#{boardID}/#{data.tim}s.jpg"
|
||||
size: $.bytesToString data.fsize
|
||||
thumbURL: "#{location.protocol}//i.4cdn.org/#{boardID}/#{data.tim}s.jpg"
|
||||
theight: data.tn_h
|
||||
twidth: data.tn_w
|
||||
isSpoiler: !!data.spoiler
|
||||
isDeleted: false
|
||||
tag: data.tag
|
||||
o.file.dimensions = "#{o.file.width}x#{o.file.height}" unless /\.pdf$/.test o.file.url
|
||||
o
|
||||
|
||||
parseComment: (o) ->
|
||||
html = o.info.commentHTML.innerHTML
|
||||
.replace(/<br\b[^<]*>/gi, '\n')
|
||||
.replace(/\n\n<span\b[^<]* class="abbr"[^]*$/i, '') # EXIF data (/p/)
|
||||
.replace(/^<b\b[^<]*>Rolled [^<]*<\/b>/i, '') # Rolls (/tg/)
|
||||
.replace(/<span\b[^<]* class="fortune"[^]*$/i, '') # Fortunes (/s4s/)
|
||||
.replace(/<[^>]*>/g, '')
|
||||
o.info.comment = Build.unescape html
|
||||
|
||||
postFromObject: (data, boardID, suppressThumb) ->
|
||||
o = Build.parseJSON data, boardID
|
||||
Build.post o, suppressThumb
|
||||
|
||||
post: (o, suppressThumb) ->
|
||||
###
|
||||
This function contains code from 4chan-JS (https://github.com/4chan/4chan-JS).
|
||||
@license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
|
||||
###
|
||||
{
|
||||
postID, threadID, boardID
|
||||
name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC
|
||||
comment
|
||||
file
|
||||
} = o
|
||||
name or= ''
|
||||
subject or= ''
|
||||
isOP = postID is threadID
|
||||
{postID, threadID, boardID, file} = o
|
||||
{subject, email, name, tripcode, capcode, uniqueID, flagCode, flag, dateUTC, dateText, commentHTML} = o.info
|
||||
{staticPath, gifIcon} = Build
|
||||
|
||||
### Post Info ###
|
||||
|
||||
if capcode
|
||||
capcodeLC = capcode.split('_')[0]
|
||||
capcodeUC = capcodeLC[0].toUpperCase() + capcodeLC[1..]
|
||||
capcodeText = capcodeUC
|
||||
capcodeLong = {'Admin': 'Administrator', 'Mod': 'Moderator'}[capcodeUC] or capcodeUC
|
||||
capcodePlural = "#{capcodeLong}s"
|
||||
capcodeDescription = "a 4chan #{capcodeLong}"
|
||||
if capcode is 'admin_emeritus'
|
||||
capcodeText = 'Admin Emeritus'
|
||||
capcodeUC = capcode.split(' ')[0]
|
||||
capcodeLC = capcodeUC.toLowerCase()
|
||||
if capcode is 'Admin Emeritus'
|
||||
capcodePlural = 'the Administrator Emeritus'
|
||||
capcodeDescription = "4chan's founding Administrator"
|
||||
else
|
||||
capcodeLong = {'Admin': 'Administrator', 'Mod': 'Moderator'}[capcode] or capcode
|
||||
capcodePlural = "#{capcodeLong}s"
|
||||
capcodeDescription = "a 4chan #{capcodeLong}"
|
||||
|
||||
postLink = Build.postURL boardID, threadID, postID
|
||||
quoteLink = if Build.sameThread boardID, threadID
|
||||
@ -111,17 +120,17 @@ Build =
|
||||
|
||||
### File Info ###
|
||||
|
||||
if file and not file.isDeleted
|
||||
if file
|
||||
protocol = /^https?:(?=\/\/i\.4cdn\.org\/)/
|
||||
fileURL = file.url.replace protocol, ''
|
||||
shortFilename = Build.shortFilename file.name
|
||||
fileSize = $.bytesToString file.size
|
||||
fileDims = if file.url[-4..] is '.pdf' then 'PDF' else "#{file.width}x#{file.height}"
|
||||
fileThumb = if file.isSpoiler then Build.spoilerThumb boardID else file.turl
|
||||
fileThumb = if file.isSpoiler then Build.spoilerThumb(boardID) else file.thumbURL.replace(protocol, '')
|
||||
|
||||
fileBlock = <%= importHTML('Build/File') %>
|
||||
|
||||
### Whole Post ###
|
||||
|
||||
postClass = if isOP then 'op' else 'reply'
|
||||
postClass = if o.isReply then 'reply' else 'op'
|
||||
|
||||
wholePost = <%= importHTML('Build/Post') %>
|
||||
|
||||
@ -130,13 +139,15 @@ Build =
|
||||
id: "pc#{postID}"
|
||||
$.extend container, wholePost
|
||||
|
||||
# Fix pathnames
|
||||
# Fix quotelinks
|
||||
for quote in $$ '.quotelink', container
|
||||
href = quote.getAttribute 'href'
|
||||
if (href[0] is '#') and !(Build.sameThread boardID, threadID)
|
||||
quote.href = "/#{boardID}/thread/#{threadID}" + href
|
||||
else if (match = href.match /^\/([^\/]+)\/thread\/(\d+)/) and (Build.sameThread match[1], match[2])
|
||||
quote.href = href.match(/(#[^#]*)?$/)[0] or '#'
|
||||
else if /^\d+(#|$)/.test(href) and not (g.VIEW is 'thread' and g.BOARD.ID is boardID) # used on /f/
|
||||
quote.href = "/#{boardID}/thread/#{href}"
|
||||
|
||||
container
|
||||
|
||||
|
||||
@ -38,22 +38,37 @@ BuildTest =
|
||||
for postData in posts
|
||||
if postData.no is post.ID
|
||||
t1 = new Date().getTime()
|
||||
root = Build.postFromObject postData, post.board.ID
|
||||
obj = Build.parseJSON postData, post.board.ID
|
||||
root = Build.post obj
|
||||
t2 = new Date().getTime()
|
||||
BuildTest.time += t2 - t1
|
||||
post2 = new Post root, post.thread, post.board
|
||||
fail = false
|
||||
|
||||
x = post.normalizedOriginal
|
||||
y = post2.normalizedOriginal
|
||||
if x.isEqualNode y
|
||||
c.log "#{post.fullID} correct"
|
||||
else
|
||||
unless x.isEqualNode y
|
||||
fail = true
|
||||
c.log "#{post.fullID} differs"
|
||||
BuildTest.postsFailed++
|
||||
[x2, y2] = BuildTest.firstDiff x, y
|
||||
c.log x2
|
||||
c.log y2
|
||||
c.log x.outerHTML
|
||||
c.log y.outerHTML
|
||||
|
||||
for key of Config.filter when not (key is 'MD5' and post.board.ID is 'f')
|
||||
val1 = Filter[key] obj
|
||||
val2 = Filter[key] post2
|
||||
if val1 isnt val2
|
||||
fail = true
|
||||
c.log "#{post.fullID} has filter bug in #{key}"
|
||||
c.log val1
|
||||
c.log val2
|
||||
|
||||
if fail
|
||||
BuildTest.postsFailed++
|
||||
else
|
||||
c.log "#{post.fullID} correct"
|
||||
BuildTest.postsRemaining--
|
||||
BuildTest.report() if BuildTest.postsRemaining is 0
|
||||
post2.isFetchedQuote = true
|
||||
@ -61,8 +76,9 @@ BuildTest =
|
||||
|
||||
testAll: ->
|
||||
g.posts.forEach (post) ->
|
||||
unless post.isClone or post.isFetchedQuote or $ '.abbr', post.nodes.comment
|
||||
BuildTest.testOne post
|
||||
unless post.isClone or post.isFetchedQuote
|
||||
unless (abbr = $ '.abbr', post.nodes.comment) and /Comment too long\./.test(abbr.textContent)
|
||||
BuildTest.testOne post
|
||||
return
|
||||
|
||||
postsRemaining: 0
|
||||
|
||||
@ -421,13 +421,11 @@ 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.'
|
||||
@ -485,7 +483,7 @@ Config =
|
||||
]
|
||||
'Captcha Fixes': [
|
||||
true
|
||||
'Make captcha more keyboard-navigable.'
|
||||
'Make captcha easier to use, especially with the keyboard.'
|
||||
]
|
||||
|
||||
'Quote Links':
|
||||
@ -592,6 +590,9 @@ Config =
|
||||
'Fit Height': [
|
||||
true
|
||||
]
|
||||
'Stretch to Fit': [
|
||||
false
|
||||
]
|
||||
'Scroll to Post': [
|
||||
true
|
||||
]
|
||||
@ -674,12 +675,14 @@ Config =
|
||||
sauces: """
|
||||
https://www.google.com/searchbyimage?image_url=%IMG
|
||||
http://iqdb.org/?url=%IMG
|
||||
http://eye.swfchan.com/search/?q=%name;types:swf
|
||||
http://eye.swfchan.com/search/?q=%name;types:swf;sandbox
|
||||
#//tineye.com/search?url=%IMG
|
||||
#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:
|
||||
@ -743,11 +746,13 @@ Config =
|
||||
#options:"sage";boards:jp;always
|
||||
"""
|
||||
|
||||
captchaLanguage: ''
|
||||
|
||||
time: '%m/%d/%y(%a)%H:%M:%S'
|
||||
|
||||
backlink: '>>%id'
|
||||
|
||||
fileInfo: '%l (%p%s, %r)'
|
||||
fileInfo: '%l (%p%s, %r%g)'
|
||||
|
||||
favicon: 'ferongr'
|
||||
|
||||
@ -830,7 +835,7 @@ Config =
|
||||
'Pause/play videos in the gallery.'
|
||||
]
|
||||
'Slideshow': [
|
||||
's'
|
||||
'Ctrl+Right'
|
||||
'Toggle the gallery slideshow mode.'
|
||||
]
|
||||
'fappeTyme': [
|
||||
|
||||
@ -3,7 +3,7 @@ Get =
|
||||
{OP} = thread
|
||||
excerpt = "/#{thread.board}/ - " + (
|
||||
OP.info.subject?.trim() or
|
||||
OP.info.comment.replace(/\n+/g, ' // ') or
|
||||
OP.info.commentDisplay.replace(/\n+/g, ' // ') or
|
||||
OP.info.nameBlock)
|
||||
return "#{excerpt[...70]}..." if excerpt.length > 73
|
||||
excerpt
|
||||
|
||||
@ -19,3 +19,6 @@ E.cat = (templates) ->
|
||||
html = ''
|
||||
html += x.innerHTML for x in templates
|
||||
html
|
||||
|
||||
E.url = (content) ->
|
||||
"data:text/html;charset=utf-8,<!doctype html>#{encodeURIComponent content.innerHTML}"
|
||||
|
||||
@ -431,7 +431,7 @@ Index =
|
||||
Index.req = $.ajax "//a.4cdn.org/#{g.BOARD}/catalog.json",
|
||||
onloadend: (e) -> Index.load e, state
|
||||
,
|
||||
whenModified: true
|
||||
whenModified: 'Index'
|
||||
$.addClass Index.button, 'fa-spin'
|
||||
|
||||
load: (e, state) ->
|
||||
|
||||
@ -2,11 +2,18 @@ Main =
|
||||
init: ->
|
||||
if location.hostname is 'www.google.com'
|
||||
if location.pathname is '/recaptcha/api/fallback'
|
||||
$.ready -> Captcha.noscript.initFrame()
|
||||
else
|
||||
$.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) ->
|
||||
if enabled
|
||||
$.ready -> Captcha.fixes.init()
|
||||
$.ready -> Captcha.v2.initFrame()
|
||||
$.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) ->
|
||||
if enabled
|
||||
$.ready -> Captcha.fixes.init()
|
||||
return
|
||||
|
||||
if location.hostname is 'www.4chan.org'
|
||||
$.onExists d.documentElement, 'body', false, -> $.addStyle Main.cssWWW
|
||||
Conf = {'captchaLanguage': Config.captchaLanguage}
|
||||
$.get Conf, (items) ->
|
||||
$.extend Conf, items
|
||||
Captcha.language.fixPage()
|
||||
return
|
||||
|
||||
g.threads = new SimpleDict()
|
||||
@ -57,6 +64,9 @@ Main =
|
||||
$.onExists doc, 'body', false, Main.initStyle
|
||||
|
||||
initFeatures: ->
|
||||
if location.hostname in ['boards.4chan.org', 'sys.4chan.org']
|
||||
$.globalEval 'document.documentElement.classList.add("js-enabled");'
|
||||
|
||||
switch location.hostname
|
||||
when 'a.4cdn.org'
|
||||
return
|
||||
@ -298,17 +308,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', 'noscript', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon') %>`
|
||||
|
||||
cssWWW: `<%= importCSS('noscript', 'www') %>`
|
||||
|
||||
features: [
|
||||
['Polyfill', Polyfill]
|
||||
['Captcha Language', Captcha.language]
|
||||
['Redirect', Redirect]
|
||||
['Header', Header]
|
||||
['Catalog Links', CatalogLinks]
|
||||
|
||||
@ -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 chrome? and key is 'Remember QR Size' # XXX not supported
|
||||
input = $ 'input', div
|
||||
$.on input, 'change', ->
|
||||
@parentNode.parentNode.dataset.checked = @checked
|
||||
@ -314,7 +315,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 +324,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 +339,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
|
||||
@ -454,13 +455,15 @@ Settings =
|
||||
data =
|
||||
isReply: true
|
||||
file:
|
||||
URL: '//i.4cdn.org/g/1334437723720.jpg'
|
||||
url: '//i.4cdn.org/g/1334437723720.jpg'
|
||||
name: 'd9bb2efc98dd0df141a94399ff5880b7.jpg'
|
||||
size: '276 KB'
|
||||
sizeInBytes: 276 * 1024
|
||||
dimensions: '1280x720'
|
||||
isImage: true
|
||||
isVideo: false
|
||||
isSpoiler: true
|
||||
tag: 'Loop'
|
||||
FileInfo.format @value, data, @nextElementSibling
|
||||
|
||||
favicon: ->
|
||||
|
||||
9
src/General/css/noscript.css
Normal file
9
src/General/css/noscript.css
Normal file
@ -0,0 +1,9 @@
|
||||
noscript > div, noscript > div > div {
|
||||
height: 545px !important;
|
||||
}
|
||||
noscript > div > div > div:first-child, noscript iframe {
|
||||
height: 423px !important;
|
||||
}
|
||||
:root:not(.js-enabled) #g-recaptcha {
|
||||
height: auto;
|
||||
}
|
||||
@ -105,10 +105,8 @@ hr + div.center:not(.ad-cnt):not(.topad):not(.middlead):not(.bottomad) {
|
||||
/* party hats */
|
||||
pointer-events: none;
|
||||
}
|
||||
marquee,
|
||||
.postMessage marquee + br,
|
||||
.postMessage marquee + br + br {
|
||||
display: none;
|
||||
:root:not(.js-enabled) #postForm {
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* Anti-autoplay */
|
||||
@ -166,6 +164,9 @@ audio.controls-added {
|
||||
#thread-watcher {
|
||||
z-index: 10;
|
||||
}
|
||||
:root.fixed:not(.gallery-open) #header-bar:not(.autohide) {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.fixed.top-header body {
|
||||
@ -532,6 +533,9 @@ div[data-checked="false"] > .suboption-list {
|
||||
.section-filter textarea {
|
||||
height: 500px;
|
||||
}
|
||||
.section-filter a, .section-advanced a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.section-sauce textarea {
|
||||
height: 350px;
|
||||
}
|
||||
@ -867,12 +871,15 @@ span.hide-announcement {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
:root.float #thread-stats > .move > span {
|
||||
:root.float #thread-stats > .move > :not(#page-count) {
|
||||
pointer-events: none;
|
||||
}
|
||||
:root.float #thread-stats {
|
||||
padding: 0px 3px;
|
||||
}
|
||||
#page-count {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Quote */
|
||||
.catalog-thread > .comment > span.quote, #arc-list span.quote {
|
||||
@ -1134,9 +1141,7 @@ input[name="Default Volume"] {
|
||||
display: none !important;
|
||||
}
|
||||
#qr select,
|
||||
#url-button,
|
||||
#custom-cooldown-button,
|
||||
#dump-button,
|
||||
#qr-filename-container > a,
|
||||
.remove,
|
||||
.captcha-img {
|
||||
cursor: pointer;
|
||||
@ -1148,6 +1153,10 @@ input[name="Default Volume"] {
|
||||
min-width: 300px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
#qr > form {
|
||||
max-height: calc(100vh - 75px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
#qrtab {
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
@ -1158,13 +1167,6 @@ input[name="Default Volume"] {
|
||||
float: right;
|
||||
padding: 0 3px;
|
||||
}
|
||||
#qr .warning {
|
||||
min-height: 1.6em;
|
||||
vertical-align: middle;
|
||||
padding: 0 1px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
.qr-link-container {
|
||||
text-align: center;
|
||||
}
|
||||
@ -1227,7 +1229,7 @@ input.field.tripped:not(:hover):not(:focus) {
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
/* Noscript Recaptcha */
|
||||
/* Recaptcha v1 */
|
||||
.captcha-img {
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
@ -1240,9 +1242,6 @@ input.field.tripped:not(:hover):not(:focus) {
|
||||
width: 100%;
|
||||
margin: 1px 0 0;
|
||||
}
|
||||
#qr-captcha-iframe {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Recaptcha v2 */
|
||||
#qr .captcha-root {
|
||||
@ -1268,6 +1267,21 @@ input.field.tripped:not(:hover):not(:focus) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
#qr-captcha-iframe {
|
||||
width: 302px;
|
||||
height: 423px;
|
||||
border: 0;
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
.goog-bubble-content {
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.goog-bubble-content iframe {
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
/* File Input, Submit Button */
|
||||
#file-n-submit {
|
||||
@ -1282,6 +1296,7 @@ input.field.tripped:not(:hover):not(:focus) {
|
||||
background: linear-gradient(to bottom, #F8F8F8, #DCDCDC) no-repeat;
|
||||
border: 1px solid #BBB;
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
#qr-file-button {
|
||||
width: 15%;
|
||||
@ -1462,6 +1477,8 @@ a:only-of-type > .remove {
|
||||
}
|
||||
.textarea {
|
||||
position: relative;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
}
|
||||
:root.webkit .textarea {
|
||||
margin-bottom: -2px;
|
||||
@ -1475,6 +1492,9 @@ a:only-of-type > .remove {
|
||||
right: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
#char-count.warning {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* Menu */
|
||||
.menu-button:not(.fa-bars) {
|
||||
@ -1498,7 +1518,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 {
|
||||
@ -1745,13 +1765,6 @@ 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.
|
||||
*/
|
||||
max-height: 95vh;
|
||||
max-height: calc(100vh - 25px);
|
||||
}
|
||||
.gal-image iframe {
|
||||
|
||||
3
src/General/css/www.css
Normal file
3
src/General/css/www.css
Normal file
@ -0,0 +1,3 @@
|
||||
#captcha-cnt {
|
||||
height: auto;
|
||||
}
|
||||
@ -1,29 +1,35 @@
|
||||
?{file}{<div class="file" id="f${postID}">
|
||||
?{file.isDeleted}{
|
||||
<span class="fileThumb">
|
||||
<img src="${staticPath}filedeleted-res${gifIcon}" alt="File deleted." class="fileDeletedRes retina">
|
||||
</span>
|
||||
}{?{boardID === "f"}{
|
||||
?{file}{
|
||||
<div class="file" id="f${postID}">
|
||||
?{boardID === "f"}{
|
||||
<div class="fileInfo"><span class="fileText" id="fT${postID}">
|
||||
File:
|
||||
<a data-width="${file.width}" data-height="${file.height}" href="${file.url}" target="_blank">${file.name}</a>
|
||||
-(${fileSize}, ${fileDims}, ${file.tag})
|
||||
<a data-width="${file.width}" data-height="${file.height}" href="${fileURL}" target="_blank">${file.name}</a>
|
||||
-(${file.size}, ${file.dimensions}?{file.tag}{, ${file.tag}})
|
||||
</span></div>
|
||||
}{
|
||||
<div class="fileText" id="fT${postID}"?{file.isSpoiler}{ title="${file.name}"}>
|
||||
File:
|
||||
<a?{file.name === shortFilename || file.isSpoiler}{}{ title="${file.name}"} href="${file.url}" target="_blank">
|
||||
<a?{file.name === shortFilename || file.isSpoiler}{}{ title="${file.name}"} href="${fileURL}" target="_blank">
|
||||
?{file.isSpoiler}{Spoiler Image}{${shortFilename}}
|
||||
</a>
|
||||
(${fileSize}, ${fileDims})
|
||||
(${file.size}, ${file.dimensions || "PDF"})
|
||||
</div>
|
||||
<a class="fileThumb?{file.isSpoiler}{ imgspoiler}{}" href="${file.url}" target="_blank">
|
||||
<a class="fileThumb?{file.isSpoiler}{ imgspoiler}{}" href="${fileURL}" target="_blank">
|
||||
<img
|
||||
?{suppressThumb}{ data-src="${fileThumb}"}{ src="${fileThumb}"}
|
||||
alt="${fileSize}"
|
||||
alt="${file.size}"
|
||||
data-md5="${file.MD5}"
|
||||
style="height: ${file.isSpoiler ? 100 : file.theight}px; width: ${file.isSpoiler ? 100 : file.twidth}px;"
|
||||
>
|
||||
</a>
|
||||
}}
|
||||
</div>}
|
||||
}
|
||||
</div>
|
||||
}{
|
||||
?{o.fileDeleted}{
|
||||
<div class="file" id="f${postID}">
|
||||
<span class="fileThumb">
|
||||
<img src="${staticPath}filedeleted-res${gifIcon}" alt="File deleted." class="fileDeletedRes retina">
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
?{!isOP}{<div class="sideArrows" id="sa${postID}">>></div>}
|
||||
<div id="p${postID}" class="post ${postClass}?{capcode === "admin_highlight"}{ highlightPost}">
|
||||
?{isOP}{&{fileBlock}&{postInfo}}{&{postInfo}&{fileBlock}}
|
||||
<blockquote class="postMessage" id="m${postID}">&{comment}</blockquote>
|
||||
?{o.isReply}{<div class="sideArrows" id="sa${postID}">>></div>}
|
||||
<div id="p${postID}" class="post ${postClass}?{o.capcodeHighlight}{ highlightPost}">
|
||||
?{o.isReply}{&{postInfo}&{fileBlock}}{&{fileBlock}&{postInfo}}
|
||||
<blockquote class="postMessage" id="m${postID}">&{commentHTML}</blockquote>
|
||||
</div>
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
<div class="postInfo desktop" id="pi${postID}">
|
||||
<input type="checkbox" name="${postID}" value="delete">
|
||||
?{isOP || boardID === "f"}{<span class="subject">${subject}</span> }
|
||||
?{!o.isReply || boardID === "f" || subject}{<span class="subject">${subject || ""}</span> }
|
||||
<span class="nameBlock?{capcode}{ capcode${capcodeUC}}">
|
||||
?{email}{<a href="mailto:${encodeURIComponent(email).replace(/%40/g, "@")}" class="useremail">}
|
||||
<span class="name?{capcode}{ capcode}">${name}</span>
|
||||
?{tripcode}{ <span class="postertrip">${tripcode}</span>}
|
||||
?{capcode}{ <strong class="capcode hand id_${capcodeLC}" title="Highlight posts by ${capcodePlural}">## ${capcodeText}</strong>}
|
||||
?{capcode}{ <strong class="capcode hand id_${capcodeLC}" title="Highlight posts by ${capcodePlural}">## ${capcode}</strong>}
|
||||
?{email}{</a>}
|
||||
?{boardID === "f" && isOP || capcode}{}{ }
|
||||
?{boardID === "f" && !o.isReply || capcode}{}{ }
|
||||
?{capcode}{ <img src="${staticPath}${capcodeLC}icon${gifIcon}" alt="${capcodeUC} Icon" title="This user is ${capcodeDescription}." class="identityIcon retina">}
|
||||
?{uniqueID && !capcode}{ <span class="posteruid id_${uniqueID}">(ID: <span class="hand" title="Highlight posts by this ID">${uniqueID}</span>)</span>}
|
||||
?{flagCode}{ <span title="${flagName}" class="flag flag-${flagCode.toLowerCase()}"></span>}
|
||||
?{flagCode}{ <span title="${flag}" class="flag flag-${flagCode.toLowerCase()}"></span>}
|
||||
</span>
|
||||
<span class="dateTime" data-utc="${dateUTC}">${date}</span>
|
||||
<span class="postNum?{!(boardID === "f" && isOP)}{ desktop}">
|
||||
<span class="dateTime" data-utc="${dateUTC}">${dateText}</span>
|
||||
<span class="postNum?{!(boardID === "f" && !o.isReply)}{ desktop}">
|
||||
<a href="${postLink}" title="Link to this post">No.</a>
|
||||
<a href="${quoteLink}" title="Reply to this post">${postID}</a>
|
||||
?{o.isSticky}{ <img src="${staticPath}sticky${gifIcon}" alt="Sticky" title="Sticky" class="stickyIcon retina">}
|
||||
?{o.isClosed && !o.isArchived}{ <img src="${staticPath}closed${gifIcon}" alt="Closed" title="Closed" class="closedIcon retina">}
|
||||
?{o.isArchived}{ <img src="${staticPath}archived${gifIcon}" alt="Archived" title="Archived" class="archivedIcon retina">}
|
||||
?{isOP && g.VIEW === "index"}{ <span>[<a href="/${boardID}/thread/${threadID}" class="replylink">Reply</a>]</span>}
|
||||
?{!o.isReply && g.VIEW === "index"}{ <span>[<a href="/${boardID}/thread/${threadID}" class="replylink">Reply</a>]</span>}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
16
src/General/html/Features/Sandbox.html
Normal file
16
src/General/html/Features/Sandbox.html
Normal file
@ -0,0 +1,16 @@
|
||||
<html><head>
|
||||
<title>[sb] ${url}</title>
|
||||
<style>
|
||||
iframe \{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border: 0;
|
||||
\}
|
||||
body \{
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
\}
|
||||
</style>
|
||||
</head><body>
|
||||
<iframe sandbox="allow-forms" src="${url}"></iframe>
|
||||
</body></html>
|
||||
@ -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>
|
||||
@ -64,6 +70,7 @@
|
||||
<div>Spoiler indicator: <code>%p</code></div>
|
||||
<div>Size: <code>%B</code> (Bytes), <code>%K</code> (KB), <code>%M</code> (MB), <code>%s</code> (4chan default)</div>
|
||||
<div>Resolution: <code>%r</code> (Displays 'PDF' for PDF files)</div>
|
||||
<div>Tag: <code>%g</code>
|
||||
<div>Literal <code>%</code>: <code>%%</code></div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
<div>You can specify a display text by appending <code>;text:[text]</code> to the URL.</div>
|
||||
<div>You can specify the applicable boards by appending <code>;boards:[board1],[board2]</code>.</div>
|
||||
<div>You can specify the applicable file types by appending <code>;types:[extension1],[extension2]</code>.</div>
|
||||
<div>You can open links with scripts and popups disabled by appending <code>;sandbox</code>.</div>
|
||||
<ul>These parameters will be replaced by their corresponding values:
|
||||
<li><code>%TURL</code>: Thumbnail URL.</li>
|
||||
<li><code>%URL</code>: Full image URL.</li>
|
||||
|
||||
@ -64,8 +64,8 @@ $.ajax = do ->
|
||||
options.onerror?()
|
||||
return
|
||||
if whenModified
|
||||
r.setRequestHeader 'If-Modified-Since', lastModified[url] if url of lastModified
|
||||
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
|
||||
r.setRequestHeader 'If-Modified-Since', lastModified[whenModified][url] if lastModified[whenModified]?[url]?
|
||||
$.on r, 'load', -> (lastModified[whenModified] or= {})[url] = r.getResponseHeader 'Last-Modified'
|
||||
if /\.json$/.test url
|
||||
r.responseType = 'json'
|
||||
$.extend r, options
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
class Connection
|
||||
constructor: (@target, @origin, @cb) ->
|
||||
constructor: (@target, @origin, @cb={}) ->
|
||||
$.on window, 'message', @onMessage
|
||||
|
||||
send: (data) =>
|
||||
|
||||
@ -126,41 +126,41 @@ class Fetcher
|
||||
|
||||
@threadID = +data.thread_num
|
||||
o =
|
||||
# id
|
||||
postID: @postID
|
||||
threadID: @threadID
|
||||
boardID: @boardID
|
||||
# info
|
||||
name: data.name
|
||||
capcode: switch data.capcode
|
||||
when 'M' then 'mod'
|
||||
when 'A' then 'admin'
|
||||
when 'D' then 'developer'
|
||||
tripcode: data.trip
|
||||
uniqueID: data.poster_hash
|
||||
email: data.email or ''
|
||||
isReply: @postID isnt @threadID
|
||||
o.info =
|
||||
subject: data.title
|
||||
email: data.email
|
||||
name: data.name or ''
|
||||
tripcode: data.trip
|
||||
capcode: switch data.capcode
|
||||
when 'M' then 'Mod'
|
||||
when 'A' then 'Admin'
|
||||
when 'D' then 'Developer'
|
||||
uniqueID: data.poster_hash
|
||||
flagCode: data.poster_country
|
||||
flagName: data.poster_country_name
|
||||
date: data.fourchan_date
|
||||
flag: data.poster_country_name
|
||||
dateUTC: data.timestamp
|
||||
comment: comment
|
||||
# file
|
||||
dateText: data.fourchan_date
|
||||
commentHTML: comment
|
||||
delete o.info.uniqueID if o.info.capcode
|
||||
if data.media?.media_filename
|
||||
o.file =
|
||||
name: data.media.media_filename
|
||||
timestamp: data.media.media_orig
|
||||
url: data.media.media_link or data.media.remote_media_link or
|
||||
"//i.4cdn.org/#{@boardID}/#{encodeURIComponent data.media[if @boardID is 'f' then 'media_filename' else 'media_orig']}"
|
||||
"#{location.protocol}//i.4cdn.org/#{@boardID}/#{encodeURIComponent data.media[if @boardID is 'f' then 'media_filename' else 'media_orig']}"
|
||||
height: data.media.media_h
|
||||
width: data.media.media_w
|
||||
MD5: data.media.media_hash
|
||||
size: data.media.media_size
|
||||
turl: data.media.thumb_link or "//i.4cdn.org/#{@boardID}/#{data.media.preview_orig}"
|
||||
size: $.bytesToString data.media.media_size
|
||||
thumbURL: data.media.thumb_link or "#{location.protocol}//i.4cdn.org/#{@boardID}/#{data.media.preview_orig}"
|
||||
theight: data.media.preview_h
|
||||
twidth: data.media.preview_w
|
||||
isSpoiler: data.media.spoiler is '1'
|
||||
o.file.tag = JSON.parse(data.media.exif).Tag if @boardID is 'f'
|
||||
o.file.dimensions = "#{o.file.width}x#{o.file.height}" unless /\.pdf$/.test o.file.url
|
||||
o.file.tag = JSON.parse(data.media.exif).Tag if @boardID is 'f' and data.media.exif
|
||||
|
||||
board = g.boards[@boardID] or
|
||||
new Board @boardID
|
||||
@ -168,7 +168,7 @@ class Fetcher
|
||||
new Thread @threadID, board
|
||||
post = new Post Build.post(o), thread, board
|
||||
post.kill()
|
||||
post.file.thumbURL = o.file.turl if post.file
|
||||
post.file.thumbURL = o.file.thumbURL if post.file
|
||||
post.isFetchedQuote = true
|
||||
Main.callbackNodes Post, [post]
|
||||
@insert post
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -56,7 +56,7 @@ class Post
|
||||
@nodes.nameBlock.textContent.trim()
|
||||
if subject = $ '.subject', info
|
||||
@nodes.subject = subject
|
||||
@info.subject = subject.textContent
|
||||
@info.subject = subject.textContent or undefined
|
||||
if name = $ '.name', info
|
||||
@nodes.name = name
|
||||
@info.name = name.textContent
|
||||
@ -106,22 +106,28 @@ class Post
|
||||
# 'Comment too long'...
|
||||
# EXIF data. (/p/)
|
||||
# Rolls. (/tg/)
|
||||
# Marquees. (/pol/)
|
||||
# Fortunes. (/s4s/)
|
||||
bq = @nodes.comment.cloneNode true
|
||||
for node in $$ '.abbr + br, .exif, b, .fortune', bq
|
||||
$.rm node
|
||||
if abbr = $ '.abbr', bq
|
||||
$.rm abbr
|
||||
@info.comment = @nodesToText bq
|
||||
if abbr
|
||||
@info.comment = @info.comment.replace /\n\n$/, ''
|
||||
|
||||
# Hide spoilers.
|
||||
# Remove:
|
||||
# Preceding and following new lines.
|
||||
# Trailing spaces.
|
||||
bq = @nodes.comment.cloneNode true
|
||||
for node in $$ '.abbr, .exif, b, marquee', bq
|
||||
$.rm node
|
||||
@info.comment = @nodesToText bq
|
||||
|
||||
# Get the comment's text with spoilers hidden.
|
||||
spoilers = $$ 's', bq
|
||||
@info.commentSpoilered = if spoilers.length
|
||||
for node in spoilers
|
||||
$.replace node, $.tn '[spoiler]'
|
||||
@nodesToText bq
|
||||
else
|
||||
@info.comment
|
||||
commentDisplay = @info.comment
|
||||
unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers']
|
||||
spoilers = $$ 's', bq
|
||||
if spoilers.length
|
||||
for node in spoilers
|
||||
$.replace node, $.tn '[spoiler]'
|
||||
commentDisplay = @nodesToText bq
|
||||
@info.commentDisplay = commentDisplay.trim().replace /\s+$/gm, ''
|
||||
|
||||
nodesToText: (bq) ->
|
||||
text = ""
|
||||
@ -129,7 +135,7 @@ class Post
|
||||
i = 0
|
||||
while node = nodes.snapshotItem i++
|
||||
text += node.data or '\n'
|
||||
text.trim().replace /\s+$/gm, ''
|
||||
text
|
||||
|
||||
parseQuotes: ->
|
||||
@quotes = []
|
||||
@ -170,12 +176,13 @@ class Post
|
||||
@file =
|
||||
text: fileText
|
||||
link: link
|
||||
URL: link.href
|
||||
url: link.href
|
||||
name: fileText.title or link.title or link.textContent
|
||||
size: info[1]
|
||||
isImage: /(jpg|png|gif)$/i.test link.href
|
||||
isVideo: /webm$/i.test link.href
|
||||
dimensions: info[0].match(/\d+x\d+/)?[0]
|
||||
tag: info[0].match(/,[^,]*, ([a-z]+)\)/i)?[1]
|
||||
size = +@file.size.match(/[\d.]+/)[0]
|
||||
unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0]
|
||||
size *= 1024 while unit-- > 0
|
||||
@ -183,7 +190,7 @@ class Post
|
||||
if (thumb = $ '.fileThumb > [data-md5]', fileEl)
|
||||
$.extend @file,
|
||||
thumb: thumb
|
||||
thumbURL: "#{location.protocol}//i.4cdn.org/#{@board}/#{link.href.match(/(\d+)\./)[1]}s.jpg"
|
||||
thumbURL: if m = link.href.match(/\d+(?=\.\w+$)/) then "#{location.protocol}//i.4cdn.org/#{@board}/#{m[0]}s.jpg"
|
||||
MD5: thumb.dataset.md5
|
||||
isSpoiler: $.hasClass thumb.parentNode, 'imgspoiler'
|
||||
|
||||
|
||||
@ -112,7 +112,7 @@ Gallery =
|
||||
|
||||
thumb = $.el 'a',
|
||||
className: 'gal-thumb'
|
||||
href: post.file.URL
|
||||
href: post.file.url
|
||||
target: '_blank'
|
||||
title: post.file.name
|
||||
|
||||
@ -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
|
||||
|
||||
@ -34,7 +34,7 @@ ImageCommon =
|
||||
return true
|
||||
|
||||
error: (file, post, delay, cb) ->
|
||||
src = post.file.URL.split '/'
|
||||
src = post.file.url.split '/'
|
||||
URL = Redirect.to 'file',
|
||||
boardID: post.board.ID
|
||||
filename: src[src.length - 1]
|
||||
@ -51,10 +51,10 @@ ImageCommon =
|
||||
cb URL
|
||||
|
||||
<% if (type === 'crx') { %>
|
||||
$.ajax post.file.URL,
|
||||
$.ajax post.file.url,
|
||||
onloadend: ->
|
||||
if @status is 200
|
||||
URL = post.file.URL
|
||||
URL = post.file.url
|
||||
else
|
||||
post.kill true if @status is 404
|
||||
redirect()
|
||||
@ -74,7 +74,7 @@ ImageCommon =
|
||||
post.kill true
|
||||
redirect()
|
||||
else
|
||||
URL = post.file.URL
|
||||
URL = post.file.url
|
||||
<% } %>
|
||||
|
||||
# Add controls, but not until the mouse is moved over the video.
|
||||
|
||||
@ -122,7 +122,7 @@ ImageExpand =
|
||||
$.rmClass post.nodes.root, 'expanded-image'
|
||||
$.rmClass file.thumb, 'expanding'
|
||||
$.rm file.videoControls if file.videoControls
|
||||
file.thumb.parentNode.href = file.URL
|
||||
file.thumb.parentNode.href = file.url
|
||||
file.thumb.parentNode.target = '_blank'
|
||||
for x in ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']
|
||||
delete file[x]
|
||||
@ -175,7 +175,7 @@ ImageExpand =
|
||||
el = file.fullImage = $.el (if isVideo then 'video' else 'img')
|
||||
el.dataset.fullID = post.fullID
|
||||
$.on el, 'error', ImageExpand.error
|
||||
el.src = src or file.URL
|
||||
el.src = src or file.url
|
||||
|
||||
el.className = 'full-image'
|
||||
$.after thumb, el
|
||||
|
||||
@ -32,7 +32,7 @@ ImageHover =
|
||||
el = $.el (if isVideo then 'video' else 'img')
|
||||
el.dataset.fullID = post.fullID
|
||||
$.on el, 'error', error
|
||||
el.src = file.URL
|
||||
el.src = file.url
|
||||
|
||||
if Conf['Restart when Opened']
|
||||
ImageCommon.rewind el
|
||||
|
||||
@ -44,7 +44,7 @@ ImageLoader =
|
||||
video.setAttribute 'muted', 'muted'
|
||||
video.dataset.md5 = thumb.dataset.md5
|
||||
video.style[attr] = thumb.style[attr] for attr in ['height', 'width', 'maxHeight', 'maxWidth']
|
||||
video.src = file.URL
|
||||
video.src = file.url
|
||||
$.replace thumb, video
|
||||
file.thumb = video
|
||||
file.videoThumb = true
|
||||
@ -52,9 +52,9 @@ ImageLoader =
|
||||
prefetch: (post) ->
|
||||
{file} = post
|
||||
return unless file
|
||||
{isImage, isVideo, thumb, URL} = file
|
||||
{isImage, isVideo, thumb, url} = file
|
||||
return if file.isPrefetched or !(isImage or isVideo) or post.isHidden or post.thread.isHidden
|
||||
type = if (match = URL.match(/\.([^.]+)$/)[1].toUpperCase()) is 'JPEG' then 'JPG' else match
|
||||
type = if (match = url.match(/\.([^.]+)$/)[1].toUpperCase()) is 'JPEG' then 'JPG' else match
|
||||
replace = Conf["Replace #{type}"] and !/spoiler/.test(thumb.src or thumb.dataset.src)
|
||||
return unless replace or Conf['prefetch']
|
||||
return unless [post, post.clones...].some (clone) -> doc.contains clone.nodes.root
|
||||
@ -70,11 +70,11 @@ ImageLoader =
|
||||
el = $.el if isImage then 'img' else 'video'
|
||||
if replace and isImage
|
||||
$.on el, 'load', ->
|
||||
clone.file.thumb.src = URL for clone in post.clones
|
||||
thumb.src = URL
|
||||
clone.file.thumb.src = url for clone in post.clones
|
||||
thumb.src = url
|
||||
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289
|
||||
thumb.removeAttribute 'data-src'
|
||||
el.src = URL
|
||||
el.src = url
|
||||
|
||||
toggle: ->
|
||||
if Conf['prefetch'] = @checked
|
||||
|
||||
@ -7,7 +7,7 @@ Metadata =
|
||||
cb: @node
|
||||
|
||||
node: ->
|
||||
return unless @file and /webm$/i.test @file.URL
|
||||
return unless @file and /webm$/i.test @file.url
|
||||
if @isClone
|
||||
el = $ '.webm-title', @file.text
|
||||
else
|
||||
@ -21,7 +21,7 @@ Metadata =
|
||||
load: ->
|
||||
$.rmClass @parentNode, 'error'
|
||||
$.addClass @parentNode, 'loading'
|
||||
CrossOrigin.binary Get.postFromNode(@).file.URL, (data) =>
|
||||
CrossOrigin.binary Get.postFromNode(@).file.url, (data) =>
|
||||
$.rmClass @parentNode, 'loading'
|
||||
if data?
|
||||
title = Metadata.parse data
|
||||
|
||||
@ -16,26 +16,35 @@ Sauce =
|
||||
name: 'Sauce'
|
||||
cb: @node
|
||||
|
||||
sandbox: (url) ->
|
||||
E.url <%= importHTML('Features/Sandbox') %>
|
||||
|
||||
rmOrigin: (e) ->
|
||||
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
|
||||
# Work around mixed content restrictions (data: URIs have inherited origin).
|
||||
$.open @href
|
||||
e.preventDefault()
|
||||
|
||||
createSauceLink: (link, post) ->
|
||||
return null unless link = link.trim()
|
||||
|
||||
parts = {}
|
||||
for part, i in link.split /;(?=(?:text|boards|types):)/
|
||||
for part, i in link.split /;(?=(?:text|boards|types|sandbox):?)/
|
||||
if i is 0
|
||||
parts['url'] = part
|
||||
else
|
||||
m = part.match /^(\w*):(.*)$/
|
||||
m = part.match /^(\w*):?(.*)$/
|
||||
parts[m[1]] = m[2]
|
||||
parts['text'] or= parts['url'].match(/(\w+)\.\w+\//)?[1] or '?'
|
||||
ext = post.file.URL.match(/[^.]*$/)[0]
|
||||
ext = post.file.url.match(/[^.]*$/)[0]
|
||||
|
||||
skip = false
|
||||
for key of parts
|
||||
parts[key] = parts[key].replace /%(T?URL|IMG|MD5|board|name|%|semi)/g, (parameter) ->
|
||||
type = {
|
||||
'%TURL': post.file.thumbURL
|
||||
'%URL': post.file.URL
|
||||
'%IMG': if ext in ['gif', 'jpg', 'png'] then post.file.URL else post.file.thumbURL
|
||||
'%URL': post.file.url
|
||||
'%IMG': if ext in ['gif', 'jpg', 'png'] then post.file.url else post.file.thumbURL
|
||||
'%MD5': post.file.MD5
|
||||
'%board': post.board.ID
|
||||
'%name': post.file.name
|
||||
@ -55,10 +64,14 @@ Sauce =
|
||||
return null unless !parts['boards'] or post.board.ID in parts['boards'].split ','
|
||||
return null unless !parts['types'] or ext in parts['types'].split ','
|
||||
|
||||
url = parts['url']
|
||||
url = Sauce.sandbox url if parts['sandbox']?
|
||||
|
||||
a = Sauce.link.cloneNode true
|
||||
a.href = parts['url']
|
||||
a.href = url
|
||||
a.textContent = parts['text']
|
||||
a.removeAttribute 'target' if /^javascript:/i.test parts['url']
|
||||
$.on a, 'click', Sauce.rmOrigin if parts['sandbox']?
|
||||
a
|
||||
|
||||
node: ->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -185,7 +185,7 @@ Embedding =
|
||||
el = $.el 'iframe'
|
||||
el.setAttribute 'sandbox', 'allow-scripts'
|
||||
content = <%= html('<html><head><title>${a.dataset.uid}</title></head><body><script src="https://gist.github.com/${a.dataset.uid}.js"></script></body></html>') %>
|
||||
el.src = "data:text/html;charset=utf-8,<!doctype html>#{encodeURIComponent content.innerHTML}"
|
||||
el.src = E.url content
|
||||
el
|
||||
title:
|
||||
api: (uid) -> "https://api.github.com/gists/#{uid}"
|
||||
|
||||
@ -14,6 +14,6 @@ DownloadLink =
|
||||
order: 100
|
||||
open: ({file}) ->
|
||||
return false unless file
|
||||
a.href = file.URL
|
||||
a.href = file.url
|
||||
a.download = file.name
|
||||
true
|
||||
|
||||
@ -5,20 +5,26 @@ ReportLink =
|
||||
a = $.el 'a',
|
||||
className: 'report-link'
|
||||
href: 'javascript:;'
|
||||
textContent: 'Report this post'
|
||||
$.on a, 'click', ReportLink.report
|
||||
|
||||
Menu.menu.addEntry
|
||||
el: a
|
||||
order: 10
|
||||
open: (post) ->
|
||||
ReportLink.url = unless post.isDead
|
||||
"//sys.4chan.org/#{post.board}/imgboard.php?mode=report&no=#{post}"
|
||||
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
|
||||
else if Conf['Archive Report']
|
||||
Redirect.to 'report', {boardID: post.board.ID, postID: post.ID}
|
||||
a.textContent = 'Report to archive'
|
||||
ReportLink.url = Redirect.to 'report', {boardID: post.board.ID, postID: post.ID}
|
||||
ReportLink.height = 350
|
||||
else
|
||||
ReportLink.url = ''
|
||||
!!ReportLink.url
|
||||
|
||||
report: ->
|
||||
{url} = ReportLink
|
||||
{url, height} = ReportLink
|
||||
id = Date.now()
|
||||
set = "toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=685,height=200"
|
||||
set = "toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=700,height=#{height}"
|
||||
window.open url, id, set
|
||||
|
||||
@ -28,10 +28,10 @@ FileInfo =
|
||||
$.extend outputNode, <%= html('@{output}') %>
|
||||
|
||||
formatters:
|
||||
t: -> <%= html('${this.file.URL.match(/[^\/]*$/)[0]}') %>
|
||||
T: -> <%= html('<a href="${this.file.URL}" target="_blank">&{FileInfo.formatters.t.call(this)}</a>') %>
|
||||
l: -> <%= html('<a href="${this.file.URL}" target="_blank">&{FileInfo.formatters.n.call(this)}</a>') %>
|
||||
L: -> <%= html('<a href="${this.file.URL}" target="_blank">&{FileInfo.formatters.N.call(this)}</a>') %>
|
||||
t: -> <%= html('${this.file.url.match(/[^\/]*$/)[0]}') %>
|
||||
T: -> <%= html('<a href="${this.file.url}" target="_blank">&{FileInfo.formatters.t.call(this)}</a>') %>
|
||||
l: -> <%= html('<a href="${this.file.url}" target="_blank">&{FileInfo.formatters.n.call(this)}</a>') %>
|
||||
L: -> <%= html('<a href="${this.file.url}" target="_blank">&{FileInfo.formatters.N.call(this)}</a>') %>
|
||||
n: ->
|
||||
fullname = @file.name
|
||||
shortname = Build.shortFilename @file.name, @isReply
|
||||
@ -46,4 +46,5 @@ FileInfo =
|
||||
K: -> <%= html('${Math.round(this.file.sizeInBytes/1024)} KB') %>
|
||||
M: -> <%= html('${Math.round(this.file.sizeInBytes/1048576*100)/100} MB') %>
|
||||
r: -> <%= html('${this.file.dimensions || "PDF"}') %>
|
||||
g: -> <%= html('?{this.file.tag}{, ${this.file.tag}}{}') %>
|
||||
'%': -> <%= html('%') %>
|
||||
|
||||
@ -60,9 +60,10 @@ Fourchan =
|
||||
|
||||
code: ->
|
||||
return if @isClone
|
||||
for pre, i in $$('.prettyprint', @nodes.comment) when not $.hasClass(pre, 'prettyprinted')
|
||||
$.event 'prettyprint', {ID: @fullID, i: i, html: pre.innerHTML}, window
|
||||
return
|
||||
$.ready =>
|
||||
for pre, i in $$('.prettyprint', @nodes.comment) when not $.hasClass(pre, 'prettyprinted')
|
||||
$.event 'prettyprint', {ID: @fullID, i: i, html: pre.innerHTML}, window
|
||||
return
|
||||
|
||||
math: ->
|
||||
return if (@isClone and doc.contains @origin.nodes.root) or !$ '.math', @nodes.comment
|
||||
|
||||
@ -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,26 @@
|
||||
Report =
|
||||
css: `<%= importCSS('noscript') %>`
|
||||
|
||||
init: ->
|
||||
return unless /\bmode=report\b/.test(location.search) and match = location.search.match /\bno=(\d+)/
|
||||
Captcha.language.fixPage()
|
||||
@postID = +match[1]
|
||||
$.ready @ready
|
||||
|
||||
ready: ->
|
||||
new MutationObserver(Report.resize).observe d.body,
|
||||
childList: true
|
||||
attributes: true
|
||||
subtree: true
|
||||
$.addStyle Report.css
|
||||
Report.archive() if Conf['Archive Report']
|
||||
if $.hasClass doc, 'js-enabled'
|
||||
new MutationObserver(-> Report.fit '.gc-bubbleDefault').observe d.body,
|
||||
childList: true
|
||||
attributes: true
|
||||
subtree: true
|
||||
else
|
||||
Report.fit 'body'
|
||||
|
||||
resize: ->
|
||||
return unless bubble = $ '.gc-bubbleDefault'
|
||||
dy = bubble.getBoundingClientRect().bottom - doc.clientHeight
|
||||
fit: (selector) ->
|
||||
return unless el = $ selector, doc
|
||||
dy = el.getBoundingClientRect().bottom - doc.clientHeight + 8
|
||||
window.resizeBy 0, dy if dy > 0
|
||||
|
||||
archive: ->
|
||||
@ -21,14 +28,20 @@ Report =
|
||||
return unless url = Redirect.to 'report', {boardID: g.BOARD.ID, postID: Report.postID}
|
||||
|
||||
if (message = $ 'h3') and /Report submitted!/.test(message.textContent)
|
||||
$.globalEval 'self.close = function(){};'
|
||||
window.resizeTo 685, 320
|
||||
location.replace url
|
||||
if location.hash is '#redirect'
|
||||
$.globalEval 'self.close = function(){};'
|
||||
window.resizeBy 0, 350 - doc.clientHeight
|
||||
location.replace url
|
||||
return
|
||||
|
||||
link = $.el 'a',
|
||||
href: url
|
||||
textContent: 'Report to fgts'
|
||||
textContent: 'Report to archive'
|
||||
$.on link, 'click', (e) ->
|
||||
unless e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
|
||||
window.resizeTo 685, 320
|
||||
window.resizeBy 0, 350 - doc.clientHeight
|
||||
$.add d.body, [$.tn(' ['), link, $.tn(']')]
|
||||
|
||||
if types = $.id('reportTypes')
|
||||
$.on types, 'change', (e) ->
|
||||
$('form').action = if e.target.value in ['illegal', 'spam'] then '#redirect' else ''
|
||||
|
||||
@ -5,11 +5,11 @@ ThreadStats =
|
||||
statsHTML = <%= html(
|
||||
'<span id="post-count">?</span> / <span id="file-count">?</span>' +
|
||||
'?{Conf["IP Count in Stats"]}{ / <span id="ip-count">?</span>}' +
|
||||
'?{Conf["Page Count in Stats"]}{ / <span id="page-count">?</span>}'
|
||||
'?{Conf["Page Count in Stats"] && g.BOARD.ID !== "f"}{ / <span id="page-count">?</span>}'
|
||||
) %>
|
||||
statsTitle = 'Posts / Files'
|
||||
statsTitle += ' / IPs' if Conf['IP Count in Stats']
|
||||
statsTitle += ' / Page' if Conf['Page Count in Stats']
|
||||
statsTitle += ' / Page' if Conf['Page Count in Stats'] and g.BOARD.ID isnt 'f'
|
||||
|
||||
if Conf['Updater and Stats in Header']
|
||||
@dialog = sc = $.el 'span',
|
||||
@ -31,6 +31,8 @@ ThreadStats =
|
||||
@ipCountEl = $ '#ip-count', sc
|
||||
@pageCountEl = $ '#page-count', sc
|
||||
|
||||
$.on @pageCountEl, 'click', ThreadStats.fetchPage if @pageCountEl
|
||||
|
||||
Thread.callbacks.push
|
||||
name: 'Thread Stats'
|
||||
cb: @node
|
||||
@ -41,7 +43,7 @@ ThreadStats =
|
||||
@posts.forEach (post) ->
|
||||
postCount++
|
||||
fileCount++ if post.file
|
||||
ThreadStats.lastPost = post.info.date if Conf["Page Count in Stats"]
|
||||
ThreadStats.lastPost = post.info.date if ThreadStats.pageCountEl
|
||||
ThreadStats.thread = @
|
||||
ThreadStats.fetchPage()
|
||||
ThreadStats.update postCount, fileCount, @ipCount
|
||||
@ -51,23 +53,23 @@ ThreadStats =
|
||||
return if e.detail[404]
|
||||
{postCount, fileCount, ipCount, newPosts} = e.detail
|
||||
ThreadStats.update postCount, fileCount, ipCount
|
||||
return unless Conf["Page Count in Stats"]
|
||||
return unless ThreadStats.pageCountEl
|
||||
if newPosts.length
|
||||
ThreadStats.lastPost = g.posts[newPosts[newPosts.length - 1]].info.date
|
||||
if ThreadStats.lastPost > ThreadStats.lastPageUpdate and ThreadStats.pageCountEl?.textContent isnt '1'
|
||||
if ThreadStats.pageCountEl?.textContent isnt '1'
|
||||
ThreadStats.fetchPage()
|
||||
|
||||
update: (postCount, fileCount, ipCount) ->
|
||||
{thread, postCountEl, fileCountEl, ipCountEl} = ThreadStats
|
||||
postCountEl.textContent = postCount
|
||||
fileCountEl.textContent = fileCount
|
||||
if ipCount? and Conf["IP Count in Stats"]
|
||||
if ipCount? and ipCountEl
|
||||
ipCountEl.textContent = ipCount
|
||||
(if thread.postLimit and !thread.isSticky then $.addClass else $.rmClass) postCountEl, 'warning'
|
||||
(if thread.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning'
|
||||
|
||||
fetchPage: ->
|
||||
return if !Conf["Page Count in Stats"]
|
||||
return unless ThreadStats.pageCountEl
|
||||
clearTimeout ThreadStats.timeout
|
||||
if ThreadStats.thread.isDead
|
||||
ThreadStats.pageCountEl.textContent = 'Dead'
|
||||
@ -75,14 +77,22 @@ ThreadStats =
|
||||
return
|
||||
ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
|
||||
$.ajax "//a.4cdn.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
|
||||
whenModified: true
|
||||
whenModified: 'ThreadStats'
|
||||
|
||||
onThreadsLoad: ->
|
||||
return unless Conf["Page Count in Stats"] and @status is 200
|
||||
for page in @response
|
||||
for thread in page.threads when thread.no is ThreadStats.thread.ID
|
||||
ThreadStats.pageCountEl.textContent = page.page
|
||||
(if page.page is @response.length then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
|
||||
# Thread data may be stale (modification date given < time of last post). If so, try again on next thread update.
|
||||
ThreadStats.lastPageUpdate = new Date thread.last_modified * $.SECOND
|
||||
return
|
||||
if @status is 200
|
||||
for page in @response
|
||||
for thread in page.threads when thread.no is ThreadStats.thread.ID
|
||||
ThreadStats.pageCountEl.textContent = page.page
|
||||
(if page.page is @response.length then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
|
||||
ThreadStats.lastPageUpdate = new Date thread.last_modified * $.SECOND
|
||||
ThreadStats.retry()
|
||||
return
|
||||
else if @status is 304
|
||||
ThreadStats.retry()
|
||||
|
||||
retry: ->
|
||||
# If thread data is stale (modification date given < time of last post), try again.
|
||||
if ThreadStats.lastPost > ThreadStats.lastPageUpdate and ThreadStats.pageCountEl?.textContent isnt '1'
|
||||
clearTimeout ThreadStats.timeout
|
||||
ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 5 * $.SECOND
|
||||
|
||||
@ -241,7 +241,7 @@ ThreadUpdater =
|
||||
onloadend: ThreadUpdater.cb.load
|
||||
timeout: $.MINUTE
|
||||
,
|
||||
whenModified: true
|
||||
whenModified: 'ThreadUpdater'
|
||||
|
||||
updateThreadStatus: (type, status) ->
|
||||
return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status
|
||||
|
||||
@ -20,7 +20,7 @@ ThreadWatcher =
|
||||
|
||||
$.on d, 'QRPostSuccessful', @cb.post
|
||||
$.on sc, 'click', @toggleWatcher
|
||||
$.on @refreshButton, 'click', @fetchAllStatus
|
||||
$.on @refreshButton, 'click', @buttonFetchAll
|
||||
$.on @closeButton, 'click', @toggleWatcher
|
||||
|
||||
$.on d, '4chanXInitFinished', @ready
|
||||
@ -84,6 +84,9 @@ ThreadWatcher =
|
||||
return unless e.button is 0 and e.altKey
|
||||
ThreadWatcher.toggle @thread
|
||||
e.preventDefault()
|
||||
$.on @nodes.thumb.parentNode, 'mousedown', (e) ->
|
||||
# Prevent highlighting thumbnail in Firefox.
|
||||
e.preventDefault() if e.button is 0 and e.altKey
|
||||
|
||||
ready: ->
|
||||
$.off d, '4chanXInitFinished', ThreadWatcher.ready
|
||||
@ -136,11 +139,13 @@ ThreadWatcher =
|
||||
boardID = g.BOARD.ID
|
||||
db.forceSync()
|
||||
for threadID, data of db.data.boards[boardID] when not data?.isDead and threadID not of g.BOARD.threads
|
||||
if Conf['Auto Prune'] or not (data and typeof data is 'object')
|
||||
ThreadWatcher.db.delete {boardID, threadID}
|
||||
if Conf['Auto Prune'] or not (data and typeof data is 'object') # corrupt data
|
||||
db.delete {boardID, threadID}
|
||||
else
|
||||
if Conf['Show Unread Count']
|
||||
ThreadWatcher.fetchStatus {boardID, threadID, data}
|
||||
data.isDead = true
|
||||
ThreadWatcher.db.set {boardID, threadID, val: data}
|
||||
db.set {boardID, threadID, val: data}
|
||||
ThreadWatcher.refresh()
|
||||
onThreadRefresh: (e) ->
|
||||
thread = g.threads[e.detail.threadID]
|
||||
@ -148,9 +153,19 @@ ThreadWatcher =
|
||||
# Update dead status.
|
||||
ThreadWatcher.add thread
|
||||
|
||||
fetchCount:
|
||||
fetched: 0
|
||||
fetching: 0
|
||||
requests: []
|
||||
fetched: 0
|
||||
|
||||
clearRequests: ->
|
||||
ThreadWatcher.requests = []
|
||||
ThreadWatcher.fetched = 0
|
||||
ThreadWatcher.status.textContent = ''
|
||||
$.rmClass ThreadWatcher.refreshButton, 'fa-spin'
|
||||
|
||||
abort: ->
|
||||
for req in ThreadWatcher.requests when req.readyState isnt 4 # DONE
|
||||
req.abort()
|
||||
ThreadWatcher.clearRequests()
|
||||
|
||||
fetchAuto: ->
|
||||
clearTimeout ThreadWatcher.timeout
|
||||
@ -164,6 +179,12 @@ ThreadWatcher =
|
||||
db.save()
|
||||
ThreadWatcher.timeout = setTimeout ThreadWatcher.fetchAuto, interval
|
||||
|
||||
buttonFetchAll: ->
|
||||
if ThreadWatcher.requests.length
|
||||
ThreadWatcher.abort()
|
||||
else
|
||||
ThreadWatcher.fetchAllStatus()
|
||||
|
||||
fetchAllStatus: ->
|
||||
ThreadWatcher.db.forceSync()
|
||||
ThreadWatcher.unreaddb.forceSync()
|
||||
@ -176,26 +197,23 @@ ThreadWatcher =
|
||||
fetchStatus: (thread, force) ->
|
||||
{boardID, threadID, data} = thread
|
||||
return if data.isDead and not force
|
||||
{fetchCount} = ThreadWatcher
|
||||
if fetchCount.fetching is 0
|
||||
if ThreadWatcher.requests.length is 0
|
||||
ThreadWatcher.status.textContent = '...'
|
||||
$.addClass ThreadWatcher.refreshButton, 'fa-spin'
|
||||
fetchCount.fetching++
|
||||
$.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json",
|
||||
req = $.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json",
|
||||
onloadend: ->
|
||||
ThreadWatcher.parseStatus.call @, thread
|
||||
timeout: $.MINUTE
|
||||
,
|
||||
whenModified: if force then false else 'ThreadWatcher'
|
||||
ThreadWatcher.requests.push req
|
||||
|
||||
parseStatus: ({boardID, threadID, data}) ->
|
||||
{fetchCount} = ThreadWatcher
|
||||
fetchCount.fetched++
|
||||
if fetchCount.fetched is fetchCount.fetching
|
||||
fetchCount.fetched = 0
|
||||
fetchCount.fetching = 0
|
||||
status = ''
|
||||
$.rmClass ThreadWatcher.refreshButton, 'fa-spin'
|
||||
ThreadWatcher.fetched++
|
||||
if ThreadWatcher.fetched is ThreadWatcher.requests.length
|
||||
ThreadWatcher.clearRequests()
|
||||
else
|
||||
status = "#{Math.round fetchCount.fetched / fetchCount.fetching * 100}%"
|
||||
ThreadWatcher.status.textContent = status
|
||||
ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
|
||||
|
||||
if @status is 200 and @response
|
||||
isDead = !!@response.posts[0].archived
|
||||
@ -214,17 +232,23 @@ ThreadWatcher =
|
||||
for postObj in @response.posts
|
||||
continue unless postObj.no > lastReadPost
|
||||
continue if QR.db?.get {boardID, threadID, postID: postObj.no}
|
||||
|
||||
unread++
|
||||
|
||||
continue unless QR.db and postObj.com
|
||||
regexp = /<a [^>]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g
|
||||
|
||||
quotesYou = false
|
||||
regexp = /<a [^>]*\bhref="(?:\/([^\/]+)\/thread\/)?(\d+)?(?:#p(\d+))?"/g
|
||||
while match = regexp.exec postObj.com
|
||||
if QR.db.get {
|
||||
boardID: match[1] or boardID
|
||||
threadID: match[2] or threadID
|
||||
postID: match[3] or match[2] or threadID
|
||||
}
|
||||
quotingYou++
|
||||
continue
|
||||
quotesYou = true
|
||||
break
|
||||
if quotesYou and not Filter.isHidden(Build.parseJSON postObj, boardID)
|
||||
quotingYou++
|
||||
|
||||
if isDead isnt data.isDead or unread isnt data.unread or quotingYou isnt data.quotingYou
|
||||
data.isDead = isDead
|
||||
@ -238,6 +262,8 @@ ThreadWatcher =
|
||||
ThreadWatcher.db.delete {boardID, threadID}
|
||||
else
|
||||
data.isDead = true
|
||||
delete data.unread
|
||||
delete data.quotingYou
|
||||
ThreadWatcher.db.set {boardID, threadID, val: data}
|
||||
|
||||
ThreadWatcher.refresh()
|
||||
|
||||
@ -128,7 +128,7 @@ Unread =
|
||||
openNotification: (post) ->
|
||||
return unless Header.areNotificationsEnabled
|
||||
notif = new Notification "#{post.info.nameBlock} replied to you",
|
||||
body: post.info[if Conf['Remove Spoilers'] or Conf['Reveal Spoilers'] then 'comment' else 'commentSpoilered']
|
||||
body: post.info.commentDisplay
|
||||
icon: Favicon.logo
|
||||
notif.onclick = ->
|
||||
Header.scrollToIfNeeded post.nodes.root, true
|
||||
@ -154,6 +154,10 @@ Unread =
|
||||
Unread.update()
|
||||
|
||||
read: $.debounce 100, (e) ->
|
||||
# Update the lastReadPost when hidden posts are added to the thread.
|
||||
if !Unread.posts.size and Unread.readCount isnt Unread.thread.posts.keys.length
|
||||
Unread.saveLastReadPost()
|
||||
|
||||
return if d.hidden or !Unread.posts.size
|
||||
height = doc.clientHeight
|
||||
|
||||
|
||||
@ -1,11 +1,38 @@
|
||||
Captcha.fixes =
|
||||
selectors:
|
||||
image: '.rc-imageselect-target > .rc-imageselect-tile > img'
|
||||
imageKeys: '789456123uiojklm'.split('').concat(['Comma', 'Period'])
|
||||
|
||||
css: '''
|
||||
.rc-imageselect-target > div:focus {
|
||||
outline: 2px solid #4a90e2;
|
||||
}
|
||||
.rc-button-default:focus {
|
||||
box-shadow: inset 0 0 0 2px #0063d6;
|
||||
}
|
||||
'''
|
||||
|
||||
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) ->
|
||||
@ -17,39 +44,71 @@ Captcha.fixes =
|
||||
$.queueTask focus
|
||||
|
||||
initPopup: ->
|
||||
$.addStyle "#{@selectors.image}:focus {outline: 2px solid #4a90e2;}"
|
||||
$.addStyle @css
|
||||
@fixImages()
|
||||
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 = $$ @selectors.image).length
|
||||
focus = @images[0].tabIndex isnt 0
|
||||
@images = $$ '.rc-imageselect-target > div'
|
||||
for img in @images
|
||||
img.tabIndex = 0
|
||||
@focusImage() if focus
|
||||
@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}[e.keyCode] # Up, Down, Left, Right
|
||||
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()
|
||||
|
||||
17
src/Posting/Captcha.language.coffee
Normal file
17
src/Posting/Captcha.language.coffee
Normal file
@ -0,0 +1,17 @@
|
||||
Captcha.language =
|
||||
init: ->
|
||||
return unless Conf['captchaLanguage'].trim() and d.cookie.indexOf('pass_enabled=1') < 0 and !Conf['Hide Original Post Form']
|
||||
$.onExists doc, '#captchaFormPart', true, (node) ->
|
||||
$.onExists node, 'iframe', true, Captcha.language.fixIframe
|
||||
|
||||
fixPage: ->
|
||||
return unless Conf['captchaLanguage'].trim() and d.cookie.indexOf('pass_enabled=1') < 0
|
||||
$.onExists doc, 'iframe', true, Captcha.language.fixIframe
|
||||
|
||||
fixIframe: (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
|
||||
@ -1,226 +0,0 @@
|
||||
Captcha.noscript =
|
||||
lifetime: 2 * $.MINUTE
|
||||
iframeURL: '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
|
||||
|
||||
init: ->
|
||||
return if d.cookie.indexOf('pass_enabled=1') >= 0
|
||||
return unless @isEnabled = !!$.id 'g-recaptcha'
|
||||
|
||||
container = $.el 'div',
|
||||
className: 'captcha-img'
|
||||
title: 'Reload reCAPTCHA'
|
||||
|
||||
input = $.el 'input',
|
||||
className: 'captcha-input field'
|
||||
title: 'Verification'
|
||||
autocomplete: 'off'
|
||||
spellcheck: false
|
||||
@nodes = {container, input}
|
||||
|
||||
$.on input, 'keydown', @keydown.bind @
|
||||
$.on @nodes.container, 'click', =>
|
||||
@reload()
|
||||
@nodes.input.focus()
|
||||
|
||||
@conn = new Connection null, "#{location.protocol}//www.google.com",
|
||||
challenge: @load.bind @
|
||||
token: @save.bind @
|
||||
error: @error.bind @
|
||||
|
||||
$.addClass QR.nodes.el, 'has-captcha'
|
||||
$.after QR.nodes.com.parentNode, [container, input]
|
||||
|
||||
@captchas = []
|
||||
$.get 'captchas', [], ({captchas}) ->
|
||||
QR.captcha.sync captchas
|
||||
QR.captcha.clear()
|
||||
$.sync 'captchas', @sync
|
||||
|
||||
@beforeSetup()
|
||||
@setup()
|
||||
|
||||
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'
|
||||
cb = ->
|
||||
canvas = $.el 'canvas'
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
canvas.getContext('2d').drawImage(img, 0, 0)
|
||||
conn.send {challenge: canvas.toDataURL()}
|
||||
if img.complete
|
||||
cb()
|
||||
else
|
||||
$.on img, 'load', cb
|
||||
|
||||
timers: {}
|
||||
|
||||
cb:
|
||||
focus: -> QR.captcha.setup false, true
|
||||
|
||||
beforeSetup: ->
|
||||
{container, input} = @nodes
|
||||
container.hidden = true
|
||||
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 (@needed() or force)
|
||||
if !@nodes.iframe
|
||||
@nodes.iframe = $.el 'iframe',
|
||||
id: 'qr-captcha-iframe'
|
||||
src: @iframeURL
|
||||
$.add d.body, @nodes.iframe
|
||||
@conn.target = @nodes.iframe.contentWindow
|
||||
else if !@occupied or force
|
||||
@nodes.iframe.src = @iframeURL
|
||||
@occupied = true
|
||||
@nodes.input.focus() if focus
|
||||
|
||||
afterSetup: ->
|
||||
{container, input} = @nodes
|
||||
container.hidden = false
|
||||
input.placeholder = 'Verification'
|
||||
@count()
|
||||
$.off input, 'focus click', @cb.focus
|
||||
|
||||
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
|
||||
QR.nodes.el.style.top = ''
|
||||
QR.nodes.el.style.bottom = '0px'
|
||||
|
||||
destroy: ->
|
||||
return unless @isEnabled
|
||||
$.rm @nodes.img if @nodes.img
|
||||
delete @nodes.img
|
||||
$.rm @nodes.iframe if @nodes.iframe
|
||||
delete @nodes.iframe
|
||||
delete @occupied
|
||||
@beforeSetup()
|
||||
|
||||
sync: (captchas=[]) ->
|
||||
QR.captcha.captchas = captchas
|
||||
QR.captcha.count()
|
||||
|
||||
getOne: ->
|
||||
@clear()
|
||||
if captcha = @captchas.shift()
|
||||
@count()
|
||||
$.set 'captchas', @captchas
|
||||
captcha.response
|
||||
else if /\S/.test @nodes.input.value
|
||||
(cb) =>
|
||||
@submitCB = cb
|
||||
@sendResponse()
|
||||
else
|
||||
null
|
||||
|
||||
sendResponse: ->
|
||||
response = @nodes.input.value
|
||||
if /\S/.test response
|
||||
@conn.send {response}
|
||||
|
||||
save: (token) ->
|
||||
delete @occupied
|
||||
@nodes.input.value = ''
|
||||
if @submitCB
|
||||
@submitCB token
|
||||
delete @submitCB
|
||||
if @needed() then @reload() else @destroy()
|
||||
else
|
||||
$.forceSync 'captchas'
|
||||
@captchas.push
|
||||
response: token
|
||||
timeout: @timeout
|
||||
@count()
|
||||
$.set 'captchas', @captchas
|
||||
@reload()
|
||||
|
||||
error: (message) ->
|
||||
@occupied = true
|
||||
@nodes.input.value = ''
|
||||
if @submitCB
|
||||
@submitCB()
|
||||
delete @submitCB
|
||||
QR.error "Captcha Error: #{message}"
|
||||
|
||||
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: (src) ->
|
||||
{container, input, img} = @nodes
|
||||
@occupied = true
|
||||
@timeout = Date.now() + @lifetime
|
||||
unless img
|
||||
img = @nodes.img = new Image()
|
||||
$.one img, 'load', @afterSetup.bind @
|
||||
$.on img, 'load', -> @hidden = false
|
||||
$.add container, img
|
||||
img.src = src
|
||||
input.value = ''
|
||||
@clear()
|
||||
clearTimeout @timers.expire
|
||||
@timers.expire = setTimeout @expire.bind(@), @lifetime
|
||||
|
||||
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.
|
||||
clearTimeout @timers.clear
|
||||
if @captchas.length
|
||||
@timers.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now()
|
||||
|
||||
expire: ->
|
||||
return unless @nodes.iframe
|
||||
if not d.hidden and (@needed() or d.activeElement is @nodes.input)
|
||||
@reload()
|
||||
else
|
||||
@destroy()
|
||||
|
||||
reload: ->
|
||||
@nodes.iframe.src = @iframeURL
|
||||
@occupied = true
|
||||
@nodes.img?.hidden = true
|
||||
|
||||
keydown: (e) ->
|
||||
if e.keyCode is 8 and not @nodes.input.value
|
||||
if @nodes.iframe then @reload() else @setup()
|
||||
else if e.keyCode is 13 and e.shiftKey
|
||||
@sendResponse()
|
||||
else
|
||||
return
|
||||
e.preventDefault()
|
||||
@ -5,6 +5,11 @@ Captcha.v2 =
|
||||
return if d.cookie.indexOf('pass_enabled=1') >= 0
|
||||
return unless @isEnabled = !!$.id 'g-recaptcha'
|
||||
|
||||
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}) ->
|
||||
QR.captcha.sync captchas
|
||||
@ -25,10 +30,21 @@ Captcha.v2 =
|
||||
# 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
|
||||
@ -51,7 +67,7 @@ Captcha.v2 =
|
||||
|
||||
setup: (focus, force) ->
|
||||
return unless @isEnabled and (@needed() or force)
|
||||
@shouldFocus = true if focus
|
||||
@shouldFocus = true if focus and not QR.inBubble()
|
||||
if @timeouts.destroy
|
||||
clearTimeout @timeouts.destroy
|
||||
delete @timeouts.destroy
|
||||
@ -60,6 +76,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 +86,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.contentWindow
|
||||
|
||||
setupJS: ->
|
||||
$.globalEval '''
|
||||
(function() {
|
||||
function render() {
|
||||
@ -101,7 +131,7 @@ Captcha.v2 =
|
||||
return
|
||||
|
||||
setupIFrame: (iframe) ->
|
||||
@setupTime = Date.now()
|
||||
Captcha.language.fixIframe iframe
|
||||
$.addClass QR.nodes.el, 'captcha-open'
|
||||
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
|
||||
QR.nodes.el.style.top = null
|
||||
@ -138,22 +168,23 @@ Captcha.v2 =
|
||||
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 /https?:\/\/www\.google\.com\/recaptcha\//.test(d.activeElement.src)
|
||||
if @needed()
|
||||
if QR.cooldown.auto or Conf['Post on Captcha Completion']
|
||||
@shouldFocus = true
|
||||
else
|
||||
QR.nodes.status.focus()
|
||||
if focus
|
||||
if QR.cooldown.auto or Conf['Post on Captcha Completion']
|
||||
@shouldFocus = true
|
||||
else
|
||||
QR.nodes.status.focus()
|
||||
@reload()
|
||||
else
|
||||
focus = d.activeElement?.nodeName is 'IFRAME' and d.activeElement.src?[...38] is 'https://www.google.com/recaptcha/api2/'
|
||||
if pasted
|
||||
@destroy()
|
||||
else
|
||||
@ -172,7 +203,7 @@ Captcha.v2 =
|
||||
@captchas = @captchas[i..]
|
||||
@count()
|
||||
$.set 'captchas', @captchas
|
||||
@setup true
|
||||
@setup(d.activeElement is QR.nodes.status)
|
||||
|
||||
count: ->
|
||||
@nodes.counter.textContent = "Captchas: #{@captchas.length}"
|
||||
@ -181,9 +212,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);
|
||||
})();
|
||||
'''
|
||||
|
||||
@ -9,13 +9,7 @@ QR =
|
||||
|
||||
return if g.VIEW is 'archive'
|
||||
|
||||
$.globalEval 'document.documentElement.dataset.jsEnabled = true;'
|
||||
version = if Conf['Force Noscript Captcha'] or !doc.dataset.jsEnabled
|
||||
'noscript'
|
||||
else if Conf['Use Recaptcha v1']
|
||||
'v1'
|
||||
else
|
||||
'v2'
|
||||
version = if Conf['Use Recaptcha v1'] then 'v1' else 'v2'
|
||||
@captcha = Captcha[version]
|
||||
|
||||
$.on d, '4chanXInitFinished', @initReady
|
||||
@ -42,7 +36,7 @@ QR =
|
||||
|
||||
if Conf['Hide Original Post Form']
|
||||
$.addClass doc, 'hide-original-post-form'
|
||||
if !doc.dataset.jsEnabled
|
||||
unless $.hasClass doc, 'js-enabled'
|
||||
# Prevent unnecessary loading of fallback iframe.
|
||||
$.onExists doc, '#postForm noscript', true, $.rm
|
||||
|
||||
@ -136,19 +130,25 @@ QR =
|
||||
|
||||
focus: ->
|
||||
$.queueTask ->
|
||||
unless $$('.goog-bubble-content > iframe').some((el) -> el.getBoundingClientRect().top >= 0)
|
||||
focus = d.activeElement and QR.nodes.el.contains(d.activeElement)
|
||||
$[if focus then 'addClass' else 'rmClass'] QR.nodes.el, 'focus'
|
||||
if chrome?
|
||||
# XXX Stop anomalous scrolling on space/tab in/into captcha iframe.
|
||||
if d.activeElement and QR.nodes.el.contains(d.activeElement) and d.activeElement.nodeName is 'IFRAME'
|
||||
QR.scrollY = window.scrollY
|
||||
$.on d, 'scroll', QR.scrollLock
|
||||
else
|
||||
$.off d, 'scroll', QR.scrollLock
|
||||
unless QR.inBubble()
|
||||
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.captcha.isEnabled and !QR.captcha.noscript
|
||||
if QR.inCaptcha()
|
||||
QR.scrollY = window.scrollY
|
||||
$.on d, 'scroll', QR.scrollLock
|
||||
else
|
||||
$.off d, 'scroll', QR.scrollLock
|
||||
|
||||
inBubble: ->
|
||||
d.activeElement in $$('.goog-bubble-content > iframe')
|
||||
|
||||
inCaptcha: ->
|
||||
(d.activeElement?.nodeName is 'IFRAME' and QR.nodes.el.contains(d.activeElement)) or (QR.hasFocus and QR.inBubble())
|
||||
|
||||
scrollLock: ->
|
||||
if d.activeElement and QR.nodes.el.contains(d.activeElement) and d.activeElement.nodeName is 'IFRAME'
|
||||
if QR.inCaptcha()
|
||||
window.scroll window.scrollX, QR.scrollY
|
||||
else
|
||||
$.off d, 'scroll', QR.scrollLock
|
||||
@ -290,10 +290,10 @@ QR =
|
||||
|
||||
characterCount: ->
|
||||
counter = QR.nodes.charCount
|
||||
count = QR.nodes.com.textLength
|
||||
count = QR.nodes.com.value.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '_').length
|
||||
counter.textContent = count
|
||||
counter.hidden = count < 1000
|
||||
(if count > 1500 then $.addClass else $.rmClass) counter, 'warning'
|
||||
(if count > 2000 then $.addClass else $.rmClass) counter, 'warning'
|
||||
|
||||
getFile: ->
|
||||
$.event 'QRFile', QR.selected?.file
|
||||
@ -537,14 +537,13 @@ 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 Chromium treats width and height as min-width and min-height
|
||||
if !chrome? 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()
|
||||
|
||||
@ -15,7 +15,7 @@ QuotePreview =
|
||||
return
|
||||
|
||||
mouseover: (e) ->
|
||||
return if $.hasClass @, 'inlined'
|
||||
return if $.hasClass(@, 'inlined') or !d.contains(@)
|
||||
|
||||
{boardID, threadID, postID} = Get.postDataFromLink @
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user