Merge branch 'master' into v1

Conflicts:
	src/Posting/QR.coffee
This commit is contained in:
ccd0 2015-06-21 08:18:29 -07:00
commit 2762363093
68 changed files with 6678 additions and 2956 deletions

View File

@ -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/.

View File

@ -8,6 +8,12 @@ module.exports = (grunt) ->
json = (data) ->
"`#{JSON.stringify(data).replace(/`/g, '\\`')}`"
importCSS = (filenames...) ->
grunt.template.process(
filenames.map((name) -> grunt.file.read "src/General/css/#{name}.css").join(''),
{data: grunt.config 'pkg'}
).trim().replace(/\n+/g, '\n').split(/^/m).map(JSON.stringify).join(' +\n').replace(/`/g, '\\`')
importHTML = (filename) ->
html grunt.template.process(grunt.file.read("src/General/html/#{filename}.html").replace(/^ +/gm, '').replace(/\r?\n/g, ''), data: grunt.config('pkg'))
@ -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

View File

@ -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/
*/

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

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

View File

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

View File

@ -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&#39;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&#39;re using. You should back up your settings regularly to prevent them from being lost due to bugs.</p>

3513
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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",

View File

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

View File

@ -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) ->
{'&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>'}[c]
text.replace(/<[^>]*>/g, '').replace /&(amp|#039|quot|lt|gt|#44);/g, (c) ->
{'&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>', '&#44;': ','}[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

View File

@ -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

View File

@ -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': [

View File

@ -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

View File

@ -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}"

View File

@ -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) ->

View File

@ -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]

View File

@ -103,6 +103,7 @@ Settings =
description = arr[1]
div = $.el 'div',
<%= html('<label><input type="checkbox" name="${key}">${key}</label><span class="description">: ${description}</span>') %>
div.hidden = true if 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: ->

View 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;
}

View File

@ -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
View File

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

View File

@ -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>
}
}

View File

@ -1,5 +1,5 @@
?{!isOP}{<div class="sideArrows" id="sa${postID}">&gt;&gt;</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}">&gt;&gt;</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>

View File

@ -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"}{ &nbsp; <span>[<a href="/${boardID}/thread/${threadID}" class="replylink">Reply</a>]</span>}
?{!o.isReply && g.VIEW === "index"}{ &nbsp; <span>[<a href="/${boardID}/thread/${threadID}" class="replylink">Reply</a>]</span>}
</span>
</div>

View 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>

View File

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

View File

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

View File

@ -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>

View File

@ -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

View File

@ -1,5 +1,5 @@
class Connection
constructor: (@target, @origin, @cb) ->
constructor: (@target, @origin, @cb={}) ->
$.on window, 'message', @onMessage
send: (data) =>

View File

@ -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

View File

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

View File

@ -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'

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: ->

View File

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

View File

@ -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}"

View File

@ -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

View File

@ -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

View File

@ -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('%') %>

View File

@ -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

View File

@ -204,6 +204,8 @@ Keybinds =
'Enter'
when 27
'Esc'
when 32
'Space'
when 37
'Left'
when 38
@ -212,9 +214,15 @@ Keybinds =
'Right'
when 40
'Down'
when 188
'Comma'
when 190
'Period'
else
if 48 <= kc <= 57 or 65 <= kc <= 90 # 0-9, A-Z
String.fromCharCode(kc).toLowerCase()
else if 96 <= kc <= 105 # numpad 0-9
String.fromCharCode(kc - 48).toLowerCase()
else
null
if key

View File

@ -1,19 +1,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 ''

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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()

View 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

View File

@ -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()

View File

@ -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);
})();
'''

View File

@ -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()

View File

@ -15,7 +15,7 @@ QuotePreview =
return
mouseover: (e) ->
return if $.hasClass @, 'inlined'
return if $.hasClass(@, 'inlined') or !d.contains(@)
{boardID, threadID, postID} = Get.postDataFromLink @