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). 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
**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")] **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. - Based on v1.10.9.4.
- Make images in image captcha selectable with arrow keys. - 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
**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")] **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/. - (Hasumi) Update archive.moe: Add /gif/.

View File

@ -8,6 +8,12 @@ module.exports = (grunt) ->
json = (data) -> json = (data) ->
"`#{JSON.stringify(data).replace(/`/g, '\\`')}`" "`#{JSON.stringify(data).replace(/`/g, '\\`')}`"
importCSS = (filenames...) ->
grunt.template.process(
filenames.map((name) -> grunt.file.read "src/General/css/#{name}.css").join(''),
{data: grunt.config 'pkg'}
).trim().replace(/\n+/g, '\n').split(/^/m).map(JSON.stringify).join(' +\n').replace(/`/g, '\\`')
importHTML = (filename) -> importHTML = (filename) ->
html grunt.template.process(grunt.file.read("src/General/html/#{filename}.html").replace(/^ +/gm, '').replace(/\r?\n/g, ''), data: grunt.config('pkg')) html grunt.template.process(grunt.file.read("src/General/html/#{filename}.html").replace(/^ +/gm, '').replace(/\r?\n/g, ''), data: grunt.config('pkg'))
@ -16,13 +22,14 @@ module.exports = (grunt) ->
parts = [] parts = []
text = template text = template
while text while text
if part = text.match /^[^{}]+(?!{)/ if part = text.match /^(?:[^{}\\]|\\.)+(?!{)/
text = text[part[0].length..] text = text[part[0].length..]
context = (context + part[0]) unescaped = part[0].replace /\\(.)/g, '$1'
context = (context + unescaped)
.replace(/(=['"])[^'"<>]*/g, '$1') .replace(/(=['"])[^'"<>]*/g, '$1')
.replace(/(<\w+)( [\w-]+((?=[ >])|=''|=""))*/g, '$1') .replace(/(<\w+)( [\w-]+((?=[ >])|=''|=""))*/g, '$1')
.replace(/^([^'"<>]+|<\/?\w+>)*/, '') .replace(/^([^'"<>]+|<\/?\w+>)*/, '')
parts.push json part[0] parts.push json unescaped
else if part = text.match /^([^}]){([^}`]*)}/ else if part = text.match /^([^}]){([^}`]*)}/
text = text[part[0].length..] text = text[part[0].length..]
unless context is '' or (part[1] is '$' and /\=['"]$/.test context) or part[1] is '?' 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: options: process: Object.create(null, data:
get: -> get: ->
pkg = grunt.config 'pkg' pkg = grunt.config 'pkg'
pkg.importCSS = importCSS
pkg.importHTML = importHTML pkg.importHTML = importHTML
pkg.html = html pkg.html = html
pkg.assert = assert pkg.assert = assert
@ -184,6 +192,7 @@ module.exports = (grunt) ->
""".split('\n').join('&&') """.split('\n').join('&&')
stable: stable:
command: """ command: """
git push . HEAD:bstable
git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.meta.version %>." git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.meta.version %>."
git checkout gh-pages git checkout gh-pages
git pull git pull

View File

@ -72,9 +72,6 @@
* audio/beep.wav from http://freesound.org/people/pierrecartoons1979/sounds/90112/ * audio/beep.wav from http://freesound.org/people/pierrecartoons1979/sounds/90112/
* cc-by-nc-3.0 * cc-by-nc-3.0
* *
* 4chan/4chan-JS (https://github.com/4chan/4chan-JS) * Font Awesome by Dave Gandy (http://fontawesome.io)
* Copyright (c) 2012-2013, 4chan LLC * license: http://fontawesome.io/license/
* All rights reserved.
*
* license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
*/ */

View File

@ -28,7 +28,8 @@ Only the latest stable version of 4chan X is available.
## Other browsers ## 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. 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 ## Beta version
New features and non-urgent bugfixes are released on the beta channel for further testing before they are moved the stable version. Please [report](https://github.com/ccd0/4chan-x/issues) any issues you find, and be sure to mention which version you're using. You should back up your settings regularly to prevent them from being lost due to bugs. New features and non-urgent bugfixes are released on the beta channel for further testing before they are moved the stable version. Please [report](https://github.com/ccd0/4chan-x/issues) any issues you find, and be sure to mention which version you're using. You should back up your settings regularly to prevent them from being lost due to bugs.

Binary file not shown.

View File

@ -1,6 +1,6 @@
// ==UserScript== // ==UserScript==
// @name 4chan X beta // @name 4chan X beta
// @version 1.10.10.0 // @version 1.11.0.6
// @minGMVer 1.14 // @minGMVer 1.14
// @minFFVer 26 // @minFFVer 26
// @namespace 4chan-X // @namespace 4chan-X
@ -10,9 +10,11 @@
// @match *://sys.4chan.org/* // @match *://sys.4chan.org/*
// @match *://a.4cdn.org/* // @match *://a.4cdn.org/*
// @match *://i.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/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @match https://www.google.com/recaptcha/api2/frame?*&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_getValue
// @grant GM_setValue // @grant GM_setValue
// @grant GM_deleteValue // @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== // ==UserScript==
// @name 4chan X // @name 4chan X
// @version 1.10.10.0 // @version 1.11.0.6
// @minGMVer 1.14 // @minGMVer 1.14
// @minFFVer 26 // @minFFVer 26
// @namespace 4chan-X // @namespace 4chan-X
@ -10,9 +10,11 @@
// @match *://sys.4chan.org/* // @match *://sys.4chan.org/*
// @match *://a.4cdn.org/* // @match *://a.4cdn.org/*
// @match *://i.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/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*
// @match https://www.google.com/recaptcha/api2/frame?*&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_getValue
// @grant GM_setValue // @grant GM_setValue
// @grant GM_deleteValue // @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'?> <?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'> <gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'> <app appid='lacclbnghgdicfifcamcmcnilckjamag'>
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X-beta.crx' version='1.10.10.0' /> <updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X-beta.crx' version='1.11.0.6' />
</app> </app>
</gupdate> </gupdate>

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'> <gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='lacclbnghgdicfifcamcmcnilckjamag'> <app appid='lacclbnghgdicfifcamcmcnilckjamag'>
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X.crx' version='1.10.10.0' /> <updatecheck codebase='https://ccd0.github.io/4chan-x/builds/4chan-X.crx' version='1.11.0.6' />
</app> </app>
</gupdate> </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> <h2 id="other-browsers">Other browsers</h2>
<p>This fork of 4chan X is not guaranteed to work correctly in other browsers, but you are welcome to try your luck. Pull requests to fix the bugs you will likely find are always welcome. You may fare better with <a href="https://github.com/loadletter/4chan-x">loadletter&#39;s fork</a>, which has fewer features but less dependence on browser-specific APIs.</p> <p>This fork of 4chan X is not guaranteed to work correctly in other browsers, but you are welcome to try your luck. Pull requests to fix the bugs you will likely find are always welcome. You may fare better with <a href="https://github.com/loadletter/4chan-x">loadletter&#39;s fork</a>, which has fewer features but less dependence on browser-specific APIs.</p>
<ul> <ul>
<li><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> </ul>
<h2 id="beta-version">Beta version</h2> <h2 id="beta-version">Beta version</h2>
<p>New features and non-urgent bugfixes are released on the beta channel for further testing before they are moved the stable version. Please <a href="https://github.com/ccd0/4chan-x/issues">report</a> any issues you find, and be sure to mention which version you&#39;re using. You should back up your settings regularly to prevent them from being lost due to bugs.</p> <p>New features and non-urgent bugfixes are released on the beta channel for further testing before they are moved the stable version. Please <a href="https://github.com/ccd0/4chan-x/issues">report</a> any issues you find, and be sure to mention which version you&#39;re using. You should back up your settings regularly to prevent them from being lost due to bugs.</p>

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.", "description": "Cross-browser userscript for maximum lurking on 4chan.",
"meta": { "meta": {
"name": "4chan X", "name": "4chan X",
"version": "1.10.10.0", "version": "1.11.0.6",
"date": "2015-04-18T07:45:01.778Z", "date": "2015-06-21T06:05:51.745Z",
"repo": "https://github.com/ccd0/4chan-x/", "repo": "https://github.com/ccd0/4chan-x/",
"page": "https://github.com/ccd0/4chan-x", "page": "https://github.com/ccd0/4chan-x",
"downloads": "https://ccd0.github.io/4chan-x/builds/", "downloads": "https://ccd0.github.io/4chan-x/builds/",
@ -21,9 +21,11 @@
"*://sys.4chan.org/*", "*://sys.4chan.org/*",
"*://a.4cdn.org/*", "*://a.4cdn.org/*",
"*://i.4cdn.org/*", "*://i.4cdn.org/*",
"*://www.4chan.org/banned",
"*://www.4chan.org/feedback",
"https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*", "https://www.google.com/recaptcha/api2/anchor?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*",
"https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*", "https://www.google.com/recaptcha/api2/frame?*&k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*",
"*://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc" "*://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc*"
], ],
"suffix": { "suffix": {
"stable": "", "stable": "",
@ -38,7 +40,7 @@
"dev": " dev" "dev": " dev"
}, },
"min": { "min": {
"chrome": "32", "chrome": "34",
"firefox": "26", "firefox": "26",
"greasemonkey": "1.14" "greasemonkey": "1.14"
} }
@ -52,14 +54,14 @@
"grunt-contrib-coffee": "^0.13.0", "grunt-contrib-coffee": "^0.13.0",
"grunt-contrib-concat": "^0.5.1", "grunt-contrib-concat": "^0.5.1",
"grunt-contrib-copy": "^0.8.0", "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-contrib-watch": "^0.6.1",
"grunt-markdown": "^0.7.0", "grunt-markdown": "^0.7.0",
"grunt-shell": "^1.1.2", "grunt-shell": "^1.1.2",
"grunt-webstore-upload": "^0.8.2", "grunt-webstore-upload": "^0.8.2",
"jszip": "^2.5.0", "jszip": "^2.5.0",
"load-grunt-tasks": "^3.1.0", "load-grunt-tasks": "^3.2.0",
"npm-shrinkwrap": "^5.3.0" "npm-shrinkwrap": "^5.4.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -5,7 +5,7 @@
"http": false, "http": false,
"https": true, "https": true,
"software": "foolfuuka", "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"] "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, "uid": 3,
@ -32,8 +32,8 @@
"http": true, "http": true,
"https": true, "https": true,
"software": "foolfuuka", "software": "foolfuuka",
"boards": ["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", "w", "wg"] "files": ["c", "d", "e", "i", "lgbt", "t", "u"]
}, { }, {
"uid": 8, "uid": 8,
"name": "Rebecca Black Tech", "name": "Rebecca Black Tech",

View File

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

View File

@ -2,10 +2,12 @@ Build =
staticPath: '//s.4cdn.org/image/' staticPath: '//s.4cdn.org/image/'
gifIcon: if window.devicePixelRatio >= 2 then '@2x.gif' else '.gif' gifIcon: if window.devicePixelRatio >= 2 then '@2x.gif' else '.gif'
spoilerRange: {} spoilerRange: {}
unescape: (text) -> unescape: (text) ->
return text unless text? return text unless text?
text.replace(/<[^>]*>/g, '').replace /&(amp|#039|quot|lt|gt);/g, (c) -> text.replace(/<[^>]*>/g, '').replace /&(amp|#039|quot|lt|gt|#44);/g, (c) ->
{'&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>'}[c] {'&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>', '&#44;': ','}[c]
shortFilename: (filename) -> shortFilename: (filename) ->
threshold = 30 threshold = 30
ext = filename.match(/\.?[^\.]*$/)[0] ext = filename.match(/\.?[^\.]*$/)[0]
@ -13,93 +15,100 @@ Build =
"#{filename[...threshold - 5]}(...)#{ext}" "#{filename[...threshold - 5]}(...)#{ext}"
else else
filename filename
spoilerThumb: (boardID) -> spoilerThumb: (boardID) ->
if spoilerRange = Build.spoilerRange[boardID] if spoilerRange = Build.spoilerRange[boardID]
# Randomize the spoiler image. # Randomize the spoiler image.
"#{Build.staticPath}spoiler-#{boardID}#{Math.floor 1 + spoilerRange * Math.random()}.png" "#{Build.staticPath}spoiler-#{boardID}#{Math.floor 1 + spoilerRange * Math.random()}.png"
else else
"#{Build.staticPath}spoiler.png" "#{Build.staticPath}spoiler.png"
sameThread: (boardID, threadID) -> sameThread: (boardID, threadID) ->
g.VIEW is 'thread' and g.BOARD.ID is boardID and g.THREADID is +threadID g.VIEW is 'thread' and g.BOARD.ID is boardID and g.THREADID is +threadID
postURL: (boardID, threadID, postID) -> postURL: (boardID, threadID, postID) ->
if Build.sameThread boardID, threadID if Build.sameThread boardID, threadID
"#p#{postID}" "#p#{postID}"
else else
"/#{boardID}/thread/#{threadID}#p#{postID}" "/#{boardID}/thread/#{threadID}#p#{postID}"
postFromObject: (data, boardID, suppressThumb) ->
parseJSON: (data, boardID) ->
o = o =
# id # id
postID: data.no postID: data.no
threadID: data.resto or data.no threadID: data.resto or data.no
boardID: boardID boardID: boardID
# info isReply: !!data.resto
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 ''}
# thread status # thread status
isSticky: !!data.sticky isSticky: !!data.sticky
isClosed: !!data.closed isClosed: !!data.closed
isArchived: !!data.archived isArchived: !!data.archived
# file # file status
if data.filedeleted fileDeleted: !!data.filedeleted
o.file = o.info =
isDeleted: true subject: Build.unescape data.sub
else if data.ext 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 = o.file =
name: (Build.unescape data.filename) + data.ext name: (Build.unescape data.filename) + data.ext
timestamp: "#{data.tim}#{data.ext}"
url: if boardID is 'f' 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 else
"//i.4cdn.org/#{boardID}/#{data.tim}#{data.ext}" "#{location.protocol}//i.4cdn.org/#{boardID}/#{data.tim}#{data.ext}"
height: data.h height: data.h
width: data.w width: data.w
MD5: data.md5 MD5: data.md5
size: data.fsize size: $.bytesToString data.fsize
turl: "//i.4cdn.org/#{boardID}/#{data.tim}s.jpg" thumbURL: "#{location.protocol}//i.4cdn.org/#{boardID}/#{data.tim}s.jpg"
theight: data.tn_h theight: data.tn_h
twidth: data.tn_w twidth: data.tn_w
isSpoiler: !!data.spoiler isSpoiler: !!data.spoiler
isDeleted: false
tag: data.tag 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 Build.post o, suppressThumb
post: (o, suppressThumb) -> post: (o, suppressThumb) ->
### {postID, threadID, boardID, file} = o
This function contains code from 4chan-JS (https://github.com/4chan/4chan-JS). {subject, email, name, tripcode, capcode, uniqueID, flagCode, flag, dateUTC, dateText, commentHTML} = o.info
@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
{staticPath, gifIcon} = Build {staticPath, gifIcon} = Build
### Post Info ### ### Post Info ###
if capcode if capcode
capcodeLC = capcode.split('_')[0] capcodeUC = capcode.split(' ')[0]
capcodeUC = capcodeLC[0].toUpperCase() + capcodeLC[1..] capcodeLC = capcodeUC.toLowerCase()
capcodeText = capcodeUC if capcode is 'Admin Emeritus'
capcodeLong = {'Admin': 'Administrator', 'Mod': 'Moderator'}[capcodeUC] or capcodeUC
capcodePlural = "#{capcodeLong}s"
capcodeDescription = "a 4chan #{capcodeLong}"
if capcode is 'admin_emeritus'
capcodeText = 'Admin Emeritus'
capcodePlural = 'the Administrator Emeritus' capcodePlural = 'the Administrator Emeritus'
capcodeDescription = "4chan's founding Administrator" 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 postLink = Build.postURL boardID, threadID, postID
quoteLink = if Build.sameThread boardID, threadID quoteLink = if Build.sameThread boardID, threadID
@ -111,17 +120,17 @@ Build =
### File Info ### ### 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 shortFilename = Build.shortFilename file.name
fileSize = $.bytesToString file.size fileThumb = if file.isSpoiler then Build.spoilerThumb(boardID) else file.thumbURL.replace(protocol, '')
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
fileBlock = <%= importHTML('Build/File') %> fileBlock = <%= importHTML('Build/File') %>
### Whole Post ### ### Whole Post ###
postClass = if isOP then 'op' else 'reply' postClass = if o.isReply then 'reply' else 'op'
wholePost = <%= importHTML('Build/Post') %> wholePost = <%= importHTML('Build/Post') %>
@ -130,13 +139,15 @@ Build =
id: "pc#{postID}" id: "pc#{postID}"
$.extend container, wholePost $.extend container, wholePost
# Fix pathnames # Fix quotelinks
for quote in $$ '.quotelink', container for quote in $$ '.quotelink', container
href = quote.getAttribute 'href' href = quote.getAttribute 'href'
if (href[0] is '#') and !(Build.sameThread boardID, threadID) if (href[0] is '#') and !(Build.sameThread boardID, threadID)
quote.href = "/#{boardID}/thread/#{threadID}" + href quote.href = "/#{boardID}/thread/#{threadID}" + href
else if (match = href.match /^\/([^\/]+)\/thread\/(\d+)/) and (Build.sameThread match[1], match[2]) else if (match = href.match /^\/([^\/]+)\/thread\/(\d+)/) and (Build.sameThread match[1], match[2])
quote.href = href.match(/(#[^#]*)?$/)[0] or '#' 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 container

View File

@ -38,22 +38,37 @@ BuildTest =
for postData in posts for postData in posts
if postData.no is post.ID if postData.no is post.ID
t1 = new Date().getTime() 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() t2 = new Date().getTime()
BuildTest.time += t2 - t1 BuildTest.time += t2 - t1
post2 = new Post root, post.thread, post.board post2 = new Post root, post.thread, post.board
fail = false
x = post.normalizedOriginal x = post.normalizedOriginal
y = post2.normalizedOriginal y = post2.normalizedOriginal
if x.isEqualNode y unless x.isEqualNode y
c.log "#{post.fullID} correct" fail = true
else
c.log "#{post.fullID} differs" c.log "#{post.fullID} differs"
BuildTest.postsFailed++
[x2, y2] = BuildTest.firstDiff x, y [x2, y2] = BuildTest.firstDiff x, y
c.log x2 c.log x2
c.log y2 c.log y2
c.log x.outerHTML c.log x.outerHTML
c.log y.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.postsRemaining--
BuildTest.report() if BuildTest.postsRemaining is 0 BuildTest.report() if BuildTest.postsRemaining is 0
post2.isFetchedQuote = true post2.isFetchedQuote = true
@ -61,8 +76,9 @@ BuildTest =
testAll: -> testAll: ->
g.posts.forEach (post) -> g.posts.forEach (post) ->
unless post.isClone or post.isFetchedQuote or $ '.abbr', post.nodes.comment unless post.isClone or post.isFetchedQuote
BuildTest.testOne post unless (abbr = $ '.abbr', post.nodes.comment) and /Comment too long\./.test(abbr.textContent)
BuildTest.testOne post
return return
postsRemaining: 0 postsRemaining: 0

View File

@ -421,13 +421,11 @@ Config =
'Open new threads or replies to a thread from the index in a new tab.' 'Open new threads or replies to a thread from the index in a new tab.'
1 1
] ]
<% if (type === 'userscript') { %>
'Remember QR Size': [ 'Remember QR Size': [
false false
'Remember the size of the Quick reply.' 'Remember the size of the Quick reply.'
1 1
] ]
<% } %>
'Remember Spoiler': [ 'Remember Spoiler': [
false false
'Remember the spoiler state, instead of resetting after posting.' 'Remember the spoiler state, instead of resetting after posting.'
@ -485,7 +483,7 @@ Config =
] ]
'Captcha Fixes': [ 'Captcha Fixes': [
true true
'Make captcha more keyboard-navigable.' 'Make captcha easier to use, especially with the keyboard.'
] ]
'Quote Links': 'Quote Links':
@ -592,6 +590,9 @@ Config =
'Fit Height': [ 'Fit Height': [
true true
] ]
'Stretch to Fit': [
false
]
'Scroll to Post': [ 'Scroll to Post': [
true true
] ]
@ -674,12 +675,14 @@ Config =
sauces: """ sauces: """
https://www.google.com/searchbyimage?image_url=%IMG https://www.google.com/searchbyimage?image_url=%IMG
http://iqdb.org/?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 #//tineye.com/search?url=%IMG
#https://www.yandex.com/images/search?rpt=imageview&img_url=%IMG #https://www.yandex.com/images/search?rpt=imageview&img_url=%IMG
#//saucenao.com/search.php?url=%IMG #//saucenao.com/search.php?url=%IMG
#http://3d.iqdb.org/?url=%IMG #http://3d.iqdb.org/?url=%IMG
# tools:
#http://regex.info/exif.cgi?imgurl=%URL #http://regex.info/exif.cgi?imgurl=%URL
#//imgops.com/%URL;types:gif,jpg,png
# uploaders: # uploaders:
#//imgur.com/upload?url=%URL;types:gif,jpg,png,pdf;text:Upload to imgur #//imgur.com/upload?url=%URL;types:gif,jpg,png,pdf;text:Upload to imgur
# "View Same" in archives: # "View Same" in archives:
@ -743,11 +746,13 @@ Config =
#options:"sage";boards:jp;always #options:"sage";boards:jp;always
""" """
captchaLanguage: ''
time: '%m/%d/%y(%a)%H:%M:%S' time: '%m/%d/%y(%a)%H:%M:%S'
backlink: '>>%id' backlink: '>>%id'
fileInfo: '%l (%p%s, %r)' fileInfo: '%l (%p%s, %r%g)'
favicon: 'ferongr' favicon: 'ferongr'
@ -830,7 +835,7 @@ Config =
'Pause/play videos in the gallery.' 'Pause/play videos in the gallery.'
] ]
'Slideshow': [ 'Slideshow': [
's' 'Ctrl+Right'
'Toggle the gallery slideshow mode.' 'Toggle the gallery slideshow mode.'
] ]
'fappeTyme': [ 'fappeTyme': [

View File

@ -3,7 +3,7 @@ Get =
{OP} = thread {OP} = thread
excerpt = "/#{thread.board}/ - " + ( excerpt = "/#{thread.board}/ - " + (
OP.info.subject?.trim() or OP.info.subject?.trim() or
OP.info.comment.replace(/\n+/g, ' // ') or OP.info.commentDisplay.replace(/\n+/g, ' // ') or
OP.info.nameBlock) OP.info.nameBlock)
return "#{excerpt[...70]}..." if excerpt.length > 73 return "#{excerpt[...70]}..." if excerpt.length > 73
excerpt excerpt

View File

@ -19,3 +19,6 @@ E.cat = (templates) ->
html = '' html = ''
html += x.innerHTML for x in templates html += x.innerHTML for x in templates
html 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", Index.req = $.ajax "//a.4cdn.org/#{g.BOARD}/catalog.json",
onloadend: (e) -> Index.load e, state onloadend: (e) -> Index.load e, state
, ,
whenModified: true whenModified: 'Index'
$.addClass Index.button, 'fa-spin' $.addClass Index.button, 'fa-spin'
load: (e, state) -> load: (e, state) ->

View File

@ -2,11 +2,18 @@ Main =
init: -> init: ->
if location.hostname is 'www.google.com' if location.hostname is 'www.google.com'
if location.pathname is '/recaptcha/api/fallback' if location.pathname is '/recaptcha/api/fallback'
$.ready -> Captcha.noscript.initFrame() $.ready -> Captcha.v2.initFrame()
else $.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) ->
$.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) -> if enabled
if enabled $.ready -> Captcha.fixes.init()
$.ready -> Captcha.fixes.init() 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 return
g.threads = new SimpleDict() g.threads = new SimpleDict()
@ -57,6 +64,9 @@ Main =
$.onExists doc, 'body', false, Main.initStyle $.onExists doc, 'body', false, Main.initStyle
initFeatures: -> initFeatures: ->
if location.hostname in ['boards.4chan.org', 'sys.4chan.org']
$.globalEval 'document.documentElement.classList.add("js-enabled");'
switch location.hostname switch location.hostname
when 'a.4cdn.org' when 'a.4cdn.org'
return return
@ -298,17 +308,13 @@ Main =
$.ready -> $.ready ->
cb() if Main.isThisPageLegit() cb() if Main.isThisPageLegit()
css: `<%= css: `<%= importCSS('font-awesome', 'noscript', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon') %>`
grunt.template.process(
['font-awesome', 'style', 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'].map(function(name) { cssWWW: `<%= importCSS('noscript', 'www') %>`
return grunt.file.read('src/General/css/'+name+'.css');
}).join(''),
{data: {type: type}}
).trim().replace(/\n+/g, '\n').split(/^/m).map(JSON.stringify).join(' +\n').replace(/`/g, '\\`')
%>`
features: [ features: [
['Polyfill', Polyfill] ['Polyfill', Polyfill]
['Captcha Language', Captcha.language]
['Redirect', Redirect] ['Redirect', Redirect]
['Header', Header] ['Header', Header]
['Catalog Links', CatalogLinks] ['Catalog Links', CatalogLinks]

View File

@ -103,6 +103,7 @@ Settings =
description = arr[1] description = arr[1]
div = $.el 'div', div = $.el 'div',
<%= html('<label><input type="checkbox" name="${key}">${key}</label><span class="description">: ${description}</span>') %> <%= html('<label><input type="checkbox" name="${key}">${key}</label><span class="description">: ${description}</span>') %>
div.hidden = true if chrome? and key is 'Remember QR Size' # XXX not supported
input = $ 'input', div input = $ 'input', div
$.on input, 'change', -> $.on input, 'change', ->
@parentNode.parentNode.dataset.checked = @checked @parentNode.parentNode.dataset.checked = @checked
@ -314,7 +315,7 @@ Settings =
items = {} items = {}
inputs = {} inputs = {}
for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss', 'customCooldown'] for name in ['captchaLanguage', 'boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss', 'customCooldown']
input = $ "[name='#{name}']", section input = $ "[name='#{name}']", section
items[name] = Conf[name] items[name] = Conf[name]
inputs[name] = input inputs[name] = input
@ -323,9 +324,9 @@ Settings =
else if name is 'favicon' else if name is 'favicon'
$.on input, 'change', $.cb.value $.on input, 'change', $.cb.value
$.on input, 'change', Settings[name] $.on input, 'change', Settings[name]
else else
$.on input, 'input', $.cb.value $.on input, 'input', $.cb.value
$.on input, 'input', Settings[name] $.on input, 'input', Settings[name] if name of Settings
# Quick Reply Personas # Quick Reply Personas
ta = $ '.personafield', section ta = $ '.personafield', section
@ -338,8 +339,8 @@ Settings =
for key, val of items for key, val of items
input = inputs[key] input = inputs[key]
input.value = val input.value = val
continue if key in ['usercss', 'customCooldown'] if key of Settings and key isnt 'usercss'
Settings[key].call input Settings[key].call input
return return
interval = $ 'input[name="Interval"]', section interval = $ 'input[name="Interval"]', section
@ -454,13 +455,15 @@ Settings =
data = data =
isReply: true isReply: true
file: file:
URL: '//i.4cdn.org/g/1334437723720.jpg' url: '//i.4cdn.org/g/1334437723720.jpg'
name: 'd9bb2efc98dd0df141a94399ff5880b7.jpg' name: 'd9bb2efc98dd0df141a94399ff5880b7.jpg'
size: '276 KB' size: '276 KB'
sizeInBytes: 276 * 1024 sizeInBytes: 276 * 1024
dimensions: '1280x720' dimensions: '1280x720'
isImage: true isImage: true
isVideo: false
isSpoiler: true isSpoiler: true
tag: 'Loop'
FileInfo.format @value, data, @nextElementSibling FileInfo.format @value, data, @nextElementSibling
favicon: -> 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 */ /* party hats */
pointer-events: none; pointer-events: none;
} }
marquee, :root:not(.js-enabled) #postForm {
.postMessage marquee + br, display: table;
.postMessage marquee + br + br {
display: none;
} }
/* Anti-autoplay */ /* Anti-autoplay */
@ -166,6 +164,9 @@ audio.controls-added {
#thread-watcher { #thread-watcher {
z-index: 10; z-index: 10;
} }
:root.fixed:not(.gallery-open) #header-bar:not(.autohide) {
z-index: 5;
}
/* Header */ /* Header */
.fixed.top-header body { .fixed.top-header body {
@ -532,6 +533,9 @@ div[data-checked="false"] > .suboption-list {
.section-filter textarea { .section-filter textarea {
height: 500px; height: 500px;
} }
.section-filter a, .section-advanced a {
text-decoration: underline;
}
.section-sauce textarea { .section-sauce textarea {
height: 350px; height: 350px;
} }
@ -867,12 +871,15 @@ span.hide-announcement {
border: none; border: none;
box-shadow: none; box-shadow: none;
} }
:root.float #thread-stats > .move > span { :root.float #thread-stats > .move > :not(#page-count) {
pointer-events: none; pointer-events: none;
} }
:root.float #thread-stats { :root.float #thread-stats {
padding: 0px 3px; padding: 0px 3px;
} }
#page-count {
cursor: pointer;
}
/* Quote */ /* Quote */
.catalog-thread > .comment > span.quote, #arc-list span.quote { .catalog-thread > .comment > span.quote, #arc-list span.quote {
@ -1134,9 +1141,7 @@ input[name="Default Volume"] {
display: none !important; display: none !important;
} }
#qr select, #qr select,
#url-button, #qr-filename-container > a,
#custom-cooldown-button,
#dump-button,
.remove, .remove,
.captcha-img { .captcha-img {
cursor: pointer; cursor: pointer;
@ -1148,6 +1153,10 @@ input[name="Default Volume"] {
min-width: 300px; min-width: 300px;
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
} }
#qr > form {
max-height: calc(100vh - 75px);
overflow-y: auto;
}
#qrtab { #qrtab {
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
} }
@ -1158,13 +1167,6 @@ input[name="Default Volume"] {
float: right; float: right;
padding: 0 3px; padding: 0 3px;
} }
#qr .warning {
min-height: 1.6em;
vertical-align: middle;
padding: 0 1px;
border-width: 1px;
border-style: solid;
}
.qr-link-container { .qr-link-container {
text-align: center; text-align: center;
} }
@ -1227,7 +1229,7 @@ input.field.tripped:not(:hover):not(:focus) {
top: 2px; top: 2px;
} }
/* Noscript Recaptcha */ /* Recaptcha v1 */
.captcha-img { .captcha-img {
margin: 0px; margin: 0px;
text-align: center; text-align: center;
@ -1240,9 +1242,6 @@ input.field.tripped:not(:hover):not(:focus) {
width: 100%; width: 100%;
margin: 1px 0 0; margin: 1px 0 0;
} }
#qr-captcha-iframe {
display: none;
}
/* Recaptcha v2 */ /* Recaptcha v2 */
#qr .captcha-root { #qr .captcha-root {
@ -1268,6 +1267,21 @@ input.field.tripped:not(:hover):not(:focus) {
display: block; display: block;
width: 100%; 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 Input, Submit Button */
#file-n-submit { #file-n-submit {
@ -1282,6 +1296,7 @@ input.field.tripped:not(:hover):not(:focus) {
background: linear-gradient(to bottom, #F8F8F8, #DCDCDC) no-repeat; background: linear-gradient(to bottom, #F8F8F8, #DCDCDC) no-repeat;
border: 1px solid #BBB; border: 1px solid #BBB;
border-radius: 2px; border-radius: 2px;
height: 100%;
} }
#qr-file-button { #qr-file-button {
width: 15%; width: 15%;
@ -1462,6 +1477,8 @@ a:only-of-type > .remove {
} }
.textarea { .textarea {
position: relative; position: relative;
display: -webkit-flex;
display: flex;
} }
:root.webkit .textarea { :root.webkit .textarea {
margin-bottom: -2px; margin-bottom: -2px;
@ -1475,6 +1492,9 @@ a:only-of-type > .remove {
right: 1px; right: 1px;
pointer-events: none; pointer-events: none;
} }
#char-count.warning {
color: red;
}
/* Menu */ /* Menu */
.menu-button:not(.fa-bars) { .menu-button:not(.fa-bars) {
@ -1498,7 +1518,7 @@ a:only-of-type > .remove {
height: 15px; height: 15px;
text-align: center; text-align: center;
} }
.menu-button + .container:not(:empty) { .menu-button + .container :first-child {
margin-left: -5px; margin-left: -5px;
} }
#menu { #menu {
@ -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 img,
.gal-fit-height .gal-image video { .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); max-height: calc(100vh - 25px);
} }
.gal-image iframe { .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}{
?{file.isDeleted}{ <div class="file" id="f${postID}">
<span class="fileThumb"> ?{boardID === "f"}{
<img src="${staticPath}filedeleted-res${gifIcon}" alt="File deleted." class="fileDeletedRes retina">
</span>
}{?{boardID === "f"}{
<div class="fileInfo"><span class="fileText" id="fT${postID}"> <div class="fileInfo"><span class="fileText" id="fT${postID}">
File: File:
<a data-width="${file.width}" data-height="${file.height}" href="${file.url}" target="_blank">${file.name}</a> <a data-width="${file.width}" data-height="${file.height}" href="${fileURL}" target="_blank">${file.name}</a>
-(${fileSize}, ${fileDims}, ${file.tag}) -(${file.size}, ${file.dimensions}?{file.tag}{, ${file.tag}})
</span></div> </span></div>
}{ }{
<div class="fileText" id="fT${postID}"?{file.isSpoiler}{ title="${file.name}"}> <div class="fileText" id="fT${postID}"?{file.isSpoiler}{ title="${file.name}"}>
File: 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}} ?{file.isSpoiler}{Spoiler Image}{${shortFilename}}
</a> </a>
(${fileSize}, ${fileDims}) (${file.size}, ${file.dimensions || "PDF"})
</div> </div>
<a class="fileThumb?{file.isSpoiler}{ imgspoiler}{}" href="${file.url}" target="_blank"> <a class="fileThumb?{file.isSpoiler}{ imgspoiler}{}" href="${fileURL}" target="_blank">
<img <img
?{suppressThumb}{ data-src="${fileThumb}"}{ src="${fileThumb}"} ?{suppressThumb}{ data-src="${fileThumb}"}{ src="${fileThumb}"}
alt="${fileSize}" alt="${file.size}"
data-md5="${file.MD5}" data-md5="${file.MD5}"
style="height: ${file.isSpoiler ? 100 : file.theight}px; width: ${file.isSpoiler ? 100 : file.twidth}px;" style="height: ${file.isSpoiler ? 100 : file.theight}px; width: ${file.isSpoiler ? 100 : file.twidth}px;"
> >
</a> </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>} ?{o.isReply}{<div class="sideArrows" id="sa${postID}">&gt;&gt;</div>}
<div id="p${postID}" class="post ${postClass}?{capcode === "admin_highlight"}{ highlightPost}"> <div id="p${postID}" class="post ${postClass}?{o.capcodeHighlight}{ highlightPost}">
?{isOP}{&{fileBlock}&{postInfo}}{&{postInfo}&{fileBlock}} ?{o.isReply}{&{postInfo}&{fileBlock}}{&{fileBlock}&{postInfo}}
<blockquote class="postMessage" id="m${postID}">&{comment}</blockquote> <blockquote class="postMessage" id="m${postID}">&{commentHTML}</blockquote>
</div> </div>

View File

@ -1,24 +1,24 @@
<div class="postInfo desktop" id="pi${postID}"> <div class="postInfo desktop" id="pi${postID}">
<input type="checkbox" name="${postID}" value="delete"> <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}}"> <span class="nameBlock?{capcode}{ capcode${capcodeUC}}">
?{email}{<a href="mailto:${encodeURIComponent(email).replace(/%40/g, "@")}" class="useremail">} ?{email}{<a href="mailto:${encodeURIComponent(email).replace(/%40/g, "@")}" class="useremail">}
<span class="name?{capcode}{ capcode}">${name}</span> <span class="name?{capcode}{ capcode}">${name}</span>
?{tripcode}{ <span class="postertrip">${tripcode}</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>} ?{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">} ?{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>} ?{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>
<span class="dateTime" data-utc="${dateUTC}">${date}</span> <span class="dateTime" data-utc="${dateUTC}">${dateText}</span>
<span class="postNum?{!(boardID === "f" && isOP)}{ desktop}"> <span class="postNum?{!(boardID === "f" && !o.isReply)}{ desktop}">
<a href="${postLink}" title="Link to this post">No.</a> <a href="${postLink}" title="Link to this post">No.</a>
<a href="${quoteLink}" title="Reply to this post">${postID}</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.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.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">} ?{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> </span>
</div> </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> </table>
</fieldset> </fieldset>
<fieldset>
<legend>Captcha Language</legend>
<div>Choose from <a href="https://developers.google.com/recaptcha/docs/language" target="_blank">list of language codes</a>. Leave blank to autoselect.</div>
<div><input name="captchaLanguage" class="field" spellcheck="false"></div>
</fieldset>
<fieldset> <fieldset>
<legend>Custom Board Navigation</legend> <legend>Custom Board Navigation</legend>
<div><textarea name="boardnav" class="field" spellcheck="false"></textarea></div> <div><textarea name="boardnav" class="field" spellcheck="false"></textarea></div>
@ -41,7 +47,7 @@
<fieldset> <fieldset>
<legend>Time Formatting <span class="warning" data-feature="Time Formatting">is disabled.</span></legend> <legend>Time Formatting <span class="warning" data-feature="Time Formatting">is disabled.</span></legend>
<div><input name="time" class="field" spellcheck="false">: <span class="time-preview"></span></div> <div><input name="time" class="field" spellcheck="false">: <span class="time-preview"></span></div>
<div>Supported <a href="//en.wikipedia.org/wiki/Date_%28Unix%29#Formatting">format specifiers</a>:</div> <div>Supported <a href="http://man7.org/linux/man-pages/man1/date.1.html" target="_blank">format specifiers</a>:</div>
<div>Day: <code>%a</code>, <code>%A</code>, <code>%d</code>, <code>%e</code></div> <div>Day: <code>%a</code>, <code>%A</code>, <code>%d</code>, <code>%e</code></div>
<div>Month: <code>%m</code>, <code>%b</code>, <code>%B</code></div> <div>Month: <code>%m</code>, <code>%b</code>, <code>%B</code></div>
<div>Year: <code>%y</code>, <code>%Y</code></div> <div>Year: <code>%y</code>, <code>%Y</code></div>
@ -64,6 +70,7 @@
<div>Spoiler indicator: <code>%p</code></div> <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>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>Resolution: <code>%r</code> (Displays &#039;PDF&#039; for PDF files)</div>
<div>Tag: <code>%g</code>
<div>Literal <code>%</code>: <code>%%</code></div> <div>Literal <code>%</code>: <code>%%</code></div>
</fieldset> </fieldset>

View File

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

View File

@ -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 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 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 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: <ul>These parameters will be replaced by their corresponding values:
<li><code>%TURL</code>: Thumbnail URL.</li> <li><code>%TURL</code>: Thumbnail URL.</li>
<li><code>%URL</code>: Full image URL.</li> <li><code>%URL</code>: Full image URL.</li>

View File

@ -64,8 +64,8 @@ $.ajax = do ->
options.onerror?() options.onerror?()
return return
if whenModified if whenModified
r.setRequestHeader 'If-Modified-Since', lastModified[url] if url of lastModified r.setRequestHeader 'If-Modified-Since', lastModified[whenModified][url] if lastModified[whenModified]?[url]?
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified' $.on r, 'load', -> (lastModified[whenModified] or= {})[url] = r.getResponseHeader 'Last-Modified'
if /\.json$/.test url if /\.json$/.test url
r.responseType = 'json' r.responseType = 'json'
$.extend r, options $.extend r, options

View File

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

View File

@ -126,41 +126,41 @@ class Fetcher
@threadID = +data.thread_num @threadID = +data.thread_num
o = o =
# id
postID: @postID postID: @postID
threadID: @threadID threadID: @threadID
boardID: @boardID boardID: @boardID
# info isReply: @postID isnt @threadID
name: data.name o.info =
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 ''
subject: data.title 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 flagCode: data.poster_country
flagName: data.poster_country_name flag: data.poster_country_name
date: data.fourchan_date
dateUTC: data.timestamp dateUTC: data.timestamp
comment: comment dateText: data.fourchan_date
# file commentHTML: comment
delete o.info.uniqueID if o.info.capcode
if data.media?.media_filename if data.media?.media_filename
o.file = o.file =
name: data.media.media_filename name: data.media.media_filename
timestamp: data.media.media_orig
url: data.media.media_link or data.media.remote_media_link or 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 height: data.media.media_h
width: data.media.media_w width: data.media.media_w
MD5: data.media.media_hash MD5: data.media.media_hash
size: data.media.media_size size: $.bytesToString data.media.media_size
turl: data.media.thumb_link or "//i.4cdn.org/#{@boardID}/#{data.media.preview_orig}" thumbURL: data.media.thumb_link or "#{location.protocol}//i.4cdn.org/#{@boardID}/#{data.media.preview_orig}"
theight: data.media.preview_h theight: data.media.preview_h
twidth: data.media.preview_w twidth: data.media.preview_w
isSpoiler: data.media.spoiler is '1' 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 board = g.boards[@boardID] or
new Board @boardID new Board @boardID
@ -168,7 +168,7 @@ class Fetcher
new Thread @threadID, board new Thread @threadID, board
post = new Post Build.post(o), thread, board post = new Post Build.post(o), thread, board
post.kill() post.kill()
post.file.thumbURL = o.file.turl if post.file post.file.thumbURL = o.file.thumbURL if post.file
post.isFetchedQuote = true post.isFetchedQuote = true
Main.callbackNodes Post, [post] Main.callbackNodes Post, [post]
@insert post @insert post

View File

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

View File

@ -56,7 +56,7 @@ class Post
@nodes.nameBlock.textContent.trim() @nodes.nameBlock.textContent.trim()
if subject = $ '.subject', info if subject = $ '.subject', info
@nodes.subject = subject @nodes.subject = subject
@info.subject = subject.textContent @info.subject = subject.textContent or undefined
if name = $ '.name', info if name = $ '.name', info
@nodes.name = name @nodes.name = name
@info.name = name.textContent @info.name = name.textContent
@ -106,22 +106,28 @@ class Post
# 'Comment too long'... # 'Comment too long'...
# EXIF data. (/p/) # EXIF data. (/p/)
# Rolls. (/tg/) # 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. # Preceding and following new lines.
# Trailing spaces. # Trailing spaces.
bq = @nodes.comment.cloneNode true commentDisplay = @info.comment
for node in $$ '.abbr, .exif, b, marquee', bq unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers']
$.rm node spoilers = $$ 's', bq
@info.comment = @nodesToText bq if spoilers.length
for node in spoilers
# Get the comment's text with spoilers hidden. $.replace node, $.tn '[spoiler]'
spoilers = $$ 's', bq commentDisplay = @nodesToText bq
@info.commentSpoilered = if spoilers.length @info.commentDisplay = commentDisplay.trim().replace /\s+$/gm, ''
for node in spoilers
$.replace node, $.tn '[spoiler]'
@nodesToText bq
else
@info.comment
nodesToText: (bq) -> nodesToText: (bq) ->
text = "" text = ""
@ -129,7 +135,7 @@ class Post
i = 0 i = 0
while node = nodes.snapshotItem i++ while node = nodes.snapshotItem i++
text += node.data or '\n' text += node.data or '\n'
text.trim().replace /\s+$/gm, '' text
parseQuotes: -> parseQuotes: ->
@quotes = [] @quotes = []
@ -170,12 +176,13 @@ class Post
@file = @file =
text: fileText text: fileText
link: link link: link
URL: link.href url: link.href
name: fileText.title or link.title or link.textContent name: fileText.title or link.title or link.textContent
size: info[1] size: info[1]
isImage: /(jpg|png|gif)$/i.test link.href isImage: /(jpg|png|gif)$/i.test link.href
isVideo: /webm$/i.test link.href isVideo: /webm$/i.test link.href
dimensions: info[0].match(/\d+x\d+/)?[0] dimensions: info[0].match(/\d+x\d+/)?[0]
tag: info[0].match(/,[^,]*, ([a-z]+)\)/i)?[1]
size = +@file.size.match(/[\d.]+/)[0] size = +@file.size.match(/[\d.]+/)[0]
unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0] unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0]
size *= 1024 while unit-- > 0 size *= 1024 while unit-- > 0
@ -183,7 +190,7 @@ class Post
if (thumb = $ '.fileThumb > [data-md5]', fileEl) if (thumb = $ '.fileThumb > [data-md5]', fileEl)
$.extend @file, $.extend @file,
thumb: thumb 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 MD5: thumb.dataset.md5
isSpoiler: $.hasClass thumb.parentNode, 'imgspoiler' isSpoiler: $.hasClass thumb.parentNode, 'imgspoiler'

View File

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

View File

@ -34,7 +34,7 @@ ImageCommon =
return true return true
error: (file, post, delay, cb) -> error: (file, post, delay, cb) ->
src = post.file.URL.split '/' src = post.file.url.split '/'
URL = Redirect.to 'file', URL = Redirect.to 'file',
boardID: post.board.ID boardID: post.board.ID
filename: src[src.length - 1] filename: src[src.length - 1]
@ -51,10 +51,10 @@ ImageCommon =
cb URL cb URL
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
$.ajax post.file.URL, $.ajax post.file.url,
onloadend: -> onloadend: ->
if @status is 200 if @status is 200
URL = post.file.URL URL = post.file.url
else else
post.kill true if @status is 404 post.kill true if @status is 404
redirect() redirect()
@ -74,7 +74,7 @@ ImageCommon =
post.kill true post.kill true
redirect() redirect()
else else
URL = post.file.URL URL = post.file.url
<% } %> <% } %>
# Add controls, but not until the mouse is moved over the video. # 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 post.nodes.root, 'expanded-image'
$.rmClass file.thumb, 'expanding' $.rmClass file.thumb, 'expanding'
$.rm file.videoControls if file.videoControls $.rm file.videoControls if file.videoControls
file.thumb.parentNode.href = file.URL file.thumb.parentNode.href = file.url
file.thumb.parentNode.target = '_blank' file.thumb.parentNode.target = '_blank'
for x in ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView'] for x in ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']
delete file[x] delete file[x]
@ -175,7 +175,7 @@ ImageExpand =
el = file.fullImage = $.el (if isVideo then 'video' else 'img') el = file.fullImage = $.el (if isVideo then 'video' else 'img')
el.dataset.fullID = post.fullID el.dataset.fullID = post.fullID
$.on el, 'error', ImageExpand.error $.on el, 'error', ImageExpand.error
el.src = src or file.URL el.src = src or file.url
el.className = 'full-image' el.className = 'full-image'
$.after thumb, el $.after thumb, el

View File

@ -32,7 +32,7 @@ ImageHover =
el = $.el (if isVideo then 'video' else 'img') el = $.el (if isVideo then 'video' else 'img')
el.dataset.fullID = post.fullID el.dataset.fullID = post.fullID
$.on el, 'error', error $.on el, 'error', error
el.src = file.URL el.src = file.url
if Conf['Restart when Opened'] if Conf['Restart when Opened']
ImageCommon.rewind el ImageCommon.rewind el

View File

@ -44,7 +44,7 @@ ImageLoader =
video.setAttribute 'muted', 'muted' video.setAttribute 'muted', 'muted'
video.dataset.md5 = thumb.dataset.md5 video.dataset.md5 = thumb.dataset.md5
video.style[attr] = thumb.style[attr] for attr in ['height', 'width', 'maxHeight', 'maxWidth'] video.style[attr] = thumb.style[attr] for attr in ['height', 'width', 'maxHeight', 'maxWidth']
video.src = file.URL video.src = file.url
$.replace thumb, video $.replace thumb, video
file.thumb = video file.thumb = video
file.videoThumb = true file.videoThumb = true
@ -52,9 +52,9 @@ ImageLoader =
prefetch: (post) -> prefetch: (post) ->
{file} = post {file} = post
return unless file 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 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) replace = Conf["Replace #{type}"] and !/spoiler/.test(thumb.src or thumb.dataset.src)
return unless replace or Conf['prefetch'] return unless replace or Conf['prefetch']
return unless [post, post.clones...].some (clone) -> doc.contains clone.nodes.root 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' el = $.el if isImage then 'img' else 'video'
if replace and isImage if replace and isImage
$.on el, 'load', -> $.on el, 'load', ->
clone.file.thumb.src = URL for clone in post.clones clone.file.thumb.src = url for clone in post.clones
thumb.src = URL thumb.src = url
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289 # XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289
thumb.removeAttribute 'data-src' thumb.removeAttribute 'data-src'
el.src = URL el.src = url
toggle: -> toggle: ->
if Conf['prefetch'] = @checked if Conf['prefetch'] = @checked

View File

@ -7,7 +7,7 @@ Metadata =
cb: @node cb: @node
node: -> node: ->
return unless @file and /webm$/i.test @file.URL return unless @file and /webm$/i.test @file.url
if @isClone if @isClone
el = $ '.webm-title', @file.text el = $ '.webm-title', @file.text
else else
@ -21,7 +21,7 @@ Metadata =
load: -> load: ->
$.rmClass @parentNode, 'error' $.rmClass @parentNode, 'error'
$.addClass @parentNode, 'loading' $.addClass @parentNode, 'loading'
CrossOrigin.binary Get.postFromNode(@).file.URL, (data) => CrossOrigin.binary Get.postFromNode(@).file.url, (data) =>
$.rmClass @parentNode, 'loading' $.rmClass @parentNode, 'loading'
if data? if data?
title = Metadata.parse data title = Metadata.parse data

View File

@ -16,26 +16,35 @@ Sauce =
name: 'Sauce' name: 'Sauce'
cb: @node 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) -> createSauceLink: (link, post) ->
return null unless link = link.trim() return null unless link = link.trim()
parts = {} parts = {}
for part, i in link.split /;(?=(?:text|boards|types):)/ for part, i in link.split /;(?=(?:text|boards|types|sandbox):?)/
if i is 0 if i is 0
parts['url'] = part parts['url'] = part
else else
m = part.match /^(\w*):(.*)$/ m = part.match /^(\w*):?(.*)$/
parts[m[1]] = m[2] parts[m[1]] = m[2]
parts['text'] or= parts['url'].match(/(\w+)\.\w+\//)?[1] or '?' parts['text'] or= parts['url'].match(/(\w+)\.\w+\//)?[1] or '?'
ext = post.file.URL.match(/[^.]*$/)[0] ext = post.file.url.match(/[^.]*$/)[0]
skip = false skip = false
for key of parts for key of parts
parts[key] = parts[key].replace /%(T?URL|IMG|MD5|board|name|%|semi)/g, (parameter) -> parts[key] = parts[key].replace /%(T?URL|IMG|MD5|board|name|%|semi)/g, (parameter) ->
type = { type = {
'%TURL': post.file.thumbURL '%TURL': post.file.thumbURL
'%URL': post.file.URL '%URL': post.file.url
'%IMG': if ext in ['gif', 'jpg', 'png'] then post.file.URL else post.file.thumbURL '%IMG': if ext in ['gif', 'jpg', 'png'] then post.file.url else post.file.thumbURL
'%MD5': post.file.MD5 '%MD5': post.file.MD5
'%board': post.board.ID '%board': post.board.ID
'%name': post.file.name '%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['boards'] or post.board.ID in parts['boards'].split ','
return null unless !parts['types'] or ext in parts['types'].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 = Sauce.link.cloneNode true
a.href = parts['url'] a.href = url
a.textContent = parts['text'] a.textContent = parts['text']
a.removeAttribute 'target' if /^javascript:/i.test parts['url'] a.removeAttribute 'target' if /^javascript:/i.test parts['url']
$.on a, 'click', Sauce.rmOrigin if parts['sandbox']?
a a
node: -> node: ->

View File

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

View File

@ -185,7 +185,7 @@ Embedding =
el = $.el 'iframe' el = $.el 'iframe'
el.setAttribute 'sandbox', 'allow-scripts' 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>') %> 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 el
title: title:
api: (uid) -> "https://api.github.com/gists/#{uid}" api: (uid) -> "https://api.github.com/gists/#{uid}"

View File

@ -14,6 +14,6 @@ DownloadLink =
order: 100 order: 100
open: ({file}) -> open: ({file}) ->
return false unless file return false unless file
a.href = file.URL a.href = file.url
a.download = file.name a.download = file.name
true true

View File

@ -5,20 +5,26 @@ ReportLink =
a = $.el 'a', a = $.el 'a',
className: 'report-link' className: 'report-link'
href: 'javascript:;' href: 'javascript:;'
textContent: 'Report this post'
$.on a, 'click', ReportLink.report $.on a, 'click', ReportLink.report
Menu.menu.addEntry Menu.menu.addEntry
el: a el: a
order: 10 order: 10
open: (post) -> open: (post) ->
ReportLink.url = unless post.isDead unless post.isDead or (post.thread.isDead and not post.thread.isArchived)
"//sys.4chan.org/#{post.board}/imgboard.php?mode=report&no=#{post}" 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'] 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 !!ReportLink.url
report: -> report: ->
{url} = ReportLink {url, height} = ReportLink
id = Date.now() 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 window.open url, id, set

View File

@ -28,10 +28,10 @@ FileInfo =
$.extend outputNode, <%= html('@{output}') %> $.extend outputNode, <%= html('@{output}') %>
formatters: formatters:
t: -> <%= html('${this.file.URL.match(/[^\/]*$/)[0]}') %> t: -> <%= html('${this.file.url.match(/[^\/]*$/)[0]}') %>
T: -> <%= html('<a href="${this.file.URL}" target="_blank">&{FileInfo.formatters.t.call(this)}</a>') %> 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>') %>
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: -> n: ->
fullname = @file.name fullname = @file.name
shortname = Build.shortFilename @file.name, @isReply shortname = Build.shortFilename @file.name, @isReply
@ -46,4 +46,5 @@ FileInfo =
K: -> <%= html('${Math.round(this.file.sizeInBytes/1024)} KB') %> K: -> <%= html('${Math.round(this.file.sizeInBytes/1024)} KB') %>
M: -> <%= html('${Math.round(this.file.sizeInBytes/1048576*100)/100} MB') %> M: -> <%= html('${Math.round(this.file.sizeInBytes/1048576*100)/100} MB') %>
r: -> <%= html('${this.file.dimensions || "PDF"}') %> r: -> <%= html('${this.file.dimensions || "PDF"}') %>
g: -> <%= html('?{this.file.tag}{, ${this.file.tag}}{}') %>
'%': -> <%= html('%') %> '%': -> <%= html('%') %>

View File

@ -60,9 +60,10 @@ Fourchan =
code: -> code: ->
return if @isClone return if @isClone
for pre, i in $$('.prettyprint', @nodes.comment) when not $.hasClass(pre, 'prettyprinted') $.ready =>
$.event 'prettyprint', {ID: @fullID, i: i, html: pre.innerHTML}, window for pre, i in $$('.prettyprint', @nodes.comment) when not $.hasClass(pre, 'prettyprinted')
return $.event 'prettyprint', {ID: @fullID, i: i, html: pre.innerHTML}, window
return
math: -> math: ->
return if (@isClone and doc.contains @origin.nodes.root) or !$ '.math', @nodes.comment return if (@isClone and doc.contains @origin.nodes.root) or !$ '.math', @nodes.comment

View File

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

View File

@ -1,19 +1,26 @@
Report = Report =
css: `<%= importCSS('noscript') %>`
init: -> init: ->
return unless /\bmode=report\b/.test(location.search) and match = location.search.match /\bno=(\d+)/ return unless /\bmode=report\b/.test(location.search) and match = location.search.match /\bno=(\d+)/
Captcha.language.fixPage()
@postID = +match[1] @postID = +match[1]
$.ready @ready $.ready @ready
ready: -> ready: ->
new MutationObserver(Report.resize).observe d.body, $.addStyle Report.css
childList: true
attributes: true
subtree: true
Report.archive() if Conf['Archive Report'] 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: -> fit: (selector) ->
return unless bubble = $ '.gc-bubbleDefault' return unless el = $ selector, doc
dy = bubble.getBoundingClientRect().bottom - doc.clientHeight dy = el.getBoundingClientRect().bottom - doc.clientHeight + 8
window.resizeBy 0, dy if dy > 0 window.resizeBy 0, dy if dy > 0
archive: -> archive: ->
@ -21,14 +28,20 @@ Report =
return unless url = Redirect.to 'report', {boardID: g.BOARD.ID, postID: Report.postID} return unless url = Redirect.to 'report', {boardID: g.BOARD.ID, postID: Report.postID}
if (message = $ 'h3') and /Report submitted!/.test(message.textContent) if (message = $ 'h3') and /Report submitted!/.test(message.textContent)
$.globalEval 'self.close = function(){};' if location.hash is '#redirect'
window.resizeTo 685, 320 $.globalEval 'self.close = function(){};'
location.replace url window.resizeBy 0, 350 - doc.clientHeight
location.replace url
return return
link = $.el 'a', link = $.el 'a',
href: url href: url
textContent: 'Report to fgts' textContent: 'Report to archive'
$.on link, 'click', (e) -> $.on link, 'click', (e) ->
unless e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 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(']')] $.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( statsHTML = <%= html(
'<span id="post-count">?</span> / <span id="file-count">?</span>' + '<span id="post-count">?</span> / <span id="file-count">?</span>' +
'?{Conf["IP Count in Stats"]}{ / <span id="ip-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 = 'Posts / Files'
statsTitle += ' / IPs' if Conf['IP Count in Stats'] 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'] if Conf['Updater and Stats in Header']
@dialog = sc = $.el 'span', @dialog = sc = $.el 'span',
@ -31,6 +31,8 @@ ThreadStats =
@ipCountEl = $ '#ip-count', sc @ipCountEl = $ '#ip-count', sc
@pageCountEl = $ '#page-count', sc @pageCountEl = $ '#page-count', sc
$.on @pageCountEl, 'click', ThreadStats.fetchPage if @pageCountEl
Thread.callbacks.push Thread.callbacks.push
name: 'Thread Stats' name: 'Thread Stats'
cb: @node cb: @node
@ -41,7 +43,7 @@ ThreadStats =
@posts.forEach (post) -> @posts.forEach (post) ->
postCount++ postCount++
fileCount++ if post.file 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.thread = @
ThreadStats.fetchPage() ThreadStats.fetchPage()
ThreadStats.update postCount, fileCount, @ipCount ThreadStats.update postCount, fileCount, @ipCount
@ -51,23 +53,23 @@ ThreadStats =
return if e.detail[404] return if e.detail[404]
{postCount, fileCount, ipCount, newPosts} = e.detail {postCount, fileCount, ipCount, newPosts} = e.detail
ThreadStats.update postCount, fileCount, ipCount ThreadStats.update postCount, fileCount, ipCount
return unless Conf["Page Count in Stats"] return unless ThreadStats.pageCountEl
if newPosts.length if newPosts.length
ThreadStats.lastPost = g.posts[newPosts[newPosts.length - 1]].info.date 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() ThreadStats.fetchPage()
update: (postCount, fileCount, ipCount) -> update: (postCount, fileCount, ipCount) ->
{thread, postCountEl, fileCountEl, ipCountEl} = ThreadStats {thread, postCountEl, fileCountEl, ipCountEl} = ThreadStats
postCountEl.textContent = postCount postCountEl.textContent = postCount
fileCountEl.textContent = fileCount fileCountEl.textContent = fileCount
if ipCount? and Conf["IP Count in Stats"] if ipCount? and ipCountEl
ipCountEl.textContent = ipCount ipCountEl.textContent = ipCount
(if thread.postLimit and !thread.isSticky then $.addClass else $.rmClass) postCountEl, 'warning' (if thread.postLimit and !thread.isSticky then $.addClass else $.rmClass) postCountEl, 'warning'
(if thread.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning' (if thread.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning'
fetchPage: -> fetchPage: ->
return if !Conf["Page Count in Stats"] return unless ThreadStats.pageCountEl
clearTimeout ThreadStats.timeout clearTimeout ThreadStats.timeout
if ThreadStats.thread.isDead if ThreadStats.thread.isDead
ThreadStats.pageCountEl.textContent = 'Dead' ThreadStats.pageCountEl.textContent = 'Dead'
@ -75,14 +77,22 @@ ThreadStats =
return return
ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 2 * $.MINUTE ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
$.ajax "//a.4cdn.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad, $.ajax "//a.4cdn.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
whenModified: true whenModified: 'ThreadStats'
onThreadsLoad: -> onThreadsLoad: ->
return unless Conf["Page Count in Stats"] and @status is 200 if @status is 200
for page in @response for page in @response
for thread in page.threads when thread.no is ThreadStats.thread.ID for thread in page.threads when thread.no is ThreadStats.thread.ID
ThreadStats.pageCountEl.textContent = page.page ThreadStats.pageCountEl.textContent = page.page
(if page.page is @response.length then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning' (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
ThreadStats.lastPageUpdate = new Date thread.last_modified * $.SECOND ThreadStats.retry()
return 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 onloadend: ThreadUpdater.cb.load
timeout: $.MINUTE timeout: $.MINUTE
, ,
whenModified: true whenModified: 'ThreadUpdater'
updateThreadStatus: (type, status) -> updateThreadStatus: (type, status) ->
return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status

View File

@ -20,7 +20,7 @@ ThreadWatcher =
$.on d, 'QRPostSuccessful', @cb.post $.on d, 'QRPostSuccessful', @cb.post
$.on sc, 'click', @toggleWatcher $.on sc, 'click', @toggleWatcher
$.on @refreshButton, 'click', @fetchAllStatus $.on @refreshButton, 'click', @buttonFetchAll
$.on @closeButton, 'click', @toggleWatcher $.on @closeButton, 'click', @toggleWatcher
$.on d, '4chanXInitFinished', @ready $.on d, '4chanXInitFinished', @ready
@ -84,6 +84,9 @@ ThreadWatcher =
return unless e.button is 0 and e.altKey return unless e.button is 0 and e.altKey
ThreadWatcher.toggle @thread ThreadWatcher.toggle @thread
e.preventDefault() e.preventDefault()
$.on @nodes.thumb.parentNode, 'mousedown', (e) ->
# Prevent highlighting thumbnail in Firefox.
e.preventDefault() if e.button is 0 and e.altKey
ready: -> ready: ->
$.off d, '4chanXInitFinished', ThreadWatcher.ready $.off d, '4chanXInitFinished', ThreadWatcher.ready
@ -136,11 +139,13 @@ ThreadWatcher =
boardID = g.BOARD.ID boardID = g.BOARD.ID
db.forceSync() db.forceSync()
for threadID, data of db.data.boards[boardID] when not data?.isDead and threadID not of g.BOARD.threads 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') if Conf['Auto Prune'] or not (data and typeof data is 'object') # corrupt data
ThreadWatcher.db.delete {boardID, threadID} db.delete {boardID, threadID}
else else
if Conf['Show Unread Count']
ThreadWatcher.fetchStatus {boardID, threadID, data}
data.isDead = true data.isDead = true
ThreadWatcher.db.set {boardID, threadID, val: data} db.set {boardID, threadID, val: data}
ThreadWatcher.refresh() ThreadWatcher.refresh()
onThreadRefresh: (e) -> onThreadRefresh: (e) ->
thread = g.threads[e.detail.threadID] thread = g.threads[e.detail.threadID]
@ -148,9 +153,19 @@ ThreadWatcher =
# Update dead status. # Update dead status.
ThreadWatcher.add thread ThreadWatcher.add thread
fetchCount: requests: []
fetched: 0 fetched: 0
fetching: 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: -> fetchAuto: ->
clearTimeout ThreadWatcher.timeout clearTimeout ThreadWatcher.timeout
@ -164,6 +179,12 @@ ThreadWatcher =
db.save() db.save()
ThreadWatcher.timeout = setTimeout ThreadWatcher.fetchAuto, interval ThreadWatcher.timeout = setTimeout ThreadWatcher.fetchAuto, interval
buttonFetchAll: ->
if ThreadWatcher.requests.length
ThreadWatcher.abort()
else
ThreadWatcher.fetchAllStatus()
fetchAllStatus: -> fetchAllStatus: ->
ThreadWatcher.db.forceSync() ThreadWatcher.db.forceSync()
ThreadWatcher.unreaddb.forceSync() ThreadWatcher.unreaddb.forceSync()
@ -176,26 +197,23 @@ ThreadWatcher =
fetchStatus: (thread, force) -> fetchStatus: (thread, force) ->
{boardID, threadID, data} = thread {boardID, threadID, data} = thread
return if data.isDead and not force return if data.isDead and not force
{fetchCount} = ThreadWatcher if ThreadWatcher.requests.length is 0
if fetchCount.fetching is 0
ThreadWatcher.status.textContent = '...' ThreadWatcher.status.textContent = '...'
$.addClass ThreadWatcher.refreshButton, 'fa-spin' $.addClass ThreadWatcher.refreshButton, 'fa-spin'
fetchCount.fetching++ req = $.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json",
$.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json",
onloadend: -> onloadend: ->
ThreadWatcher.parseStatus.call @, thread ThreadWatcher.parseStatus.call @, thread
timeout: $.MINUTE
,
whenModified: if force then false else 'ThreadWatcher'
ThreadWatcher.requests.push req
parseStatus: ({boardID, threadID, data}) -> parseStatus: ({boardID, threadID, data}) ->
{fetchCount} = ThreadWatcher ThreadWatcher.fetched++
fetchCount.fetched++ if ThreadWatcher.fetched is ThreadWatcher.requests.length
if fetchCount.fetched is fetchCount.fetching ThreadWatcher.clearRequests()
fetchCount.fetched = 0
fetchCount.fetching = 0
status = ''
$.rmClass ThreadWatcher.refreshButton, 'fa-spin'
else else
status = "#{Math.round fetchCount.fetched / fetchCount.fetching * 100}%" ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
ThreadWatcher.status.textContent = status
if @status is 200 and @response if @status is 200 and @response
isDead = !!@response.posts[0].archived isDead = !!@response.posts[0].archived
@ -214,17 +232,23 @@ ThreadWatcher =
for postObj in @response.posts for postObj in @response.posts
continue unless postObj.no > lastReadPost continue unless postObj.no > lastReadPost
continue if QR.db?.get {boardID, threadID, postID: postObj.no} continue if QR.db?.get {boardID, threadID, postID: postObj.no}
unread++ unread++
continue unless QR.db and postObj.com 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 while match = regexp.exec postObj.com
if QR.db.get { if QR.db.get {
boardID: match[1] or boardID boardID: match[1] or boardID
threadID: match[2] or threadID threadID: match[2] or threadID
postID: match[3] or match[2] or threadID postID: match[3] or match[2] or threadID
} }
quotingYou++ quotesYou = true
continue 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 if isDead isnt data.isDead or unread isnt data.unread or quotingYou isnt data.quotingYou
data.isDead = isDead data.isDead = isDead
@ -238,6 +262,8 @@ ThreadWatcher =
ThreadWatcher.db.delete {boardID, threadID} ThreadWatcher.db.delete {boardID, threadID}
else else
data.isDead = true data.isDead = true
delete data.unread
delete data.quotingYou
ThreadWatcher.db.set {boardID, threadID, val: data} ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh() ThreadWatcher.refresh()

View File

@ -128,7 +128,7 @@ Unread =
openNotification: (post) -> openNotification: (post) ->
return unless Header.areNotificationsEnabled return unless Header.areNotificationsEnabled
notif = new Notification "#{post.info.nameBlock} replied to you", 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 icon: Favicon.logo
notif.onclick = -> notif.onclick = ->
Header.scrollToIfNeeded post.nodes.root, true Header.scrollToIfNeeded post.nodes.root, true
@ -154,6 +154,10 @@ Unread =
Unread.update() Unread.update()
read: $.debounce 100, (e) -> 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 return if d.hidden or !Unread.posts.size
height = doc.clientHeight height = doc.clientHeight

View File

@ -1,11 +1,38 @@
Captcha.fixes = Captcha.fixes =
selectors: imageKeys: '789456123uiojklm'.split('').concat(['Comma', 'Period'])
image: '.rc-imageselect-target > .rc-imageselect-tile > img'
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: -> init: ->
switch location.pathname.split('/')[3] switch location.pathname.split('/')[3]
when 'anchor' then @initMain() when 'anchor' then @initMain()
when 'frame' then @initPopup() when 'frame' then @initPopup()
when 'fallback' then @initNoscript()
initMain: -> initMain: ->
$.onExists d.body, '#recaptcha-anchor', true, (checkbox) -> $.onExists d.body, '#recaptcha-anchor', true, (checkbox) ->
@ -17,39 +44,71 @@ Captcha.fixes =
$.queueTask focus $.queueTask focus
initPopup: -> initPopup: ->
$.addStyle "#{@selectors.image}:focus {outline: 2px solid #4a90e2;}" $.addStyle @css
@fixImages() @fixImages()
new MutationObserver(=> @fixImages()).observe d.body, {childList: true, subtree: true} new MutationObserver(=> @fixImages()).observe d.body, {childList: true, subtree: true}
$.on d, 'keydown', @keybinds.bind(@) $.on d, 'keydown', @keybinds.bind(@)
initNoscript: ->
@noscript = true
@images = $$ '.fbc-payload-imageselect > input'
return unless @images.length
$.addStyle @cssNoscript
@addLabels()
$.on d, 'keydown', @keybinds.bind(@)
$.on $('.fbc-imageselect-challenge > form'), 'submit', @checkForm.bind(@)
fixImages: -> fixImages: ->
return unless (@images = $$ @selectors.image).length @images = $$ '.rc-imageselect-target > div'
focus = @images[0].tabIndex isnt 0
for img in @images for img in @images
img.tabIndex = 0 img.tabIndex = 0
@focusImage() if focus @addTooltips @images if @images.length
focusImage: -> addLabels: ->
# XXX Image is not focusable at first in Firefox; to be refactored when I figure out why. imageSelect = $ '.fbc-payload-imageselect'
img = @images[0] labels = for checkbox, i in @images
$.asap -> checkbox.id = "checkbox-#{i}"
return true unless doc.contains img label = $.el 'label',
img.focus() htmlFor: checkbox.id
d.activeElement is img label.dataset.row = i // 3
, -> label.dataset.col = i % 3
label
$.add imageSelect, labels
@addTooltips labels
addTooltips: (nodes) ->
for node, i in nodes
node.title = "#{@imageKeys[i]} or #{@imageKeys[i+9][0].toUpperCase()}#{@imageKeys[i+9][1..]}"
return
checkForm: (e) ->
n = 0
n++ for checkbox in @images when checkbox.checked
e.preventDefault() if n is 0
keybinds: (e) -> keybinds: (e) ->
return unless @images and doc.contains(@images[0]) and d.activeElement return unless @images and doc.contains(@images[0])
reload = $.id 'recaptcha-reload-button'
verify = $.id 'recaptcha-verify-button' reload = $ '#recaptcha-reload-button, .fbc-button-reload'
verify = $ '#recaptcha-verify-button, .fbc-button-verify > input'
x = @images.indexOf d.activeElement x = @images.indexOf d.activeElement
if x < 0 if x < 0
return unless $('.rc-controls').contains d.activeElement
x = if d.activeElement is verify then 11 else 9 x = if d.activeElement is verify then 11 else 9
return unless dx = {38: 9, 40: 3, 37: 11, 39: 1}[e.keyCode] # Up, Down, Left, Right key = Keybinds.keyCode e
x = (x + dx) % 12
if x is 10 if !@noscript and key is 'Space' and x < 9
x = if dx is 11 then 9 else 11 @images[x].click()
(@images[x] or {9: reload, 11: verify}[x]).focus() 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.preventDefault()
e.stopPropagation() 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 if d.cookie.indexOf('pass_enabled=1') >= 0
return unless @isEnabled = !!$.id 'g-recaptcha' 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 = [] @captchas = []
$.get 'captchas', [], ({captchas}) -> $.get 'captchas', [], ({captchas}) ->
QR.captcha.sync captchas QR.captcha.sync captchas
@ -25,10 +30,21 @@ Captcha.v2 =
# XXX Greasemonkey 1.x workaround to gain access to GM_* functions. # XXX Greasemonkey 1.x workaround to gain access to GM_* functions.
$.queueTask => @save false $.queueTask => @save false
initFrame: ->
if token = $('.fbc-verification-token > textarea')?.value
conn = new Connection window.parent, "#{location.protocol}//boards.4chan.org"
conn.send {token}
shouldFocus: false shouldFocus: false
timeouts: {} timeouts: {}
postsCount: 0 postsCount: 0
noscriptURL: ->
url = '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
if lang = Conf['captchaLanguage'].trim()
url += "&hl=#{encodeURIComponent lang}"
url
needed: -> needed: ->
captchaCount = @captchas.length captchaCount = @captchas.length
captchaCount++ if QR.req captchaCount++ if QR.req
@ -51,7 +67,7 @@ Captcha.v2 =
setup: (focus, force) -> setup: (focus, force) ->
return unless @isEnabled and (@needed() or force) return unless @isEnabled and (@needed() or force)
@shouldFocus = true if focus @shouldFocus = true if focus and not QR.inBubble()
if @timeouts.destroy if @timeouts.destroy
clearTimeout @timeouts.destroy clearTimeout @timeouts.destroy
delete @timeouts.destroy delete @timeouts.destroy
@ -60,6 +76,7 @@ Captcha.v2 =
if @nodes.container if @nodes.container
if @shouldFocus and iframe = $ 'iframe', @nodes.container if @shouldFocus and iframe = $ 'iframe', @nodes.container
iframe.focus() iframe.focus()
QR.focus() # Event handler not fired in Firefox
delete @shouldFocus delete @shouldFocus
return return
@ -69,6 +86,19 @@ Captcha.v2 =
childList: true childList: true
subtree: true subtree: true
if @noscript
@setupNoscript()
else
@setupJS()
setupNoscript: ->
iframe = $.el 'iframe',
id: 'qr-captcha-iframe'
src: @noscriptURL()
$.add @nodes.container, iframe
@conn.target = iframe.contentWindow
setupJS: ->
$.globalEval ''' $.globalEval '''
(function() { (function() {
function render() { function render() {
@ -101,7 +131,7 @@ Captcha.v2 =
return return
setupIFrame: (iframe) -> setupIFrame: (iframe) ->
@setupTime = Date.now() Captcha.language.fixIframe iframe
$.addClass QR.nodes.el, 'captcha-open' $.addClass QR.nodes.el, 'captcha-open'
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
QR.nodes.el.style.top = null QR.nodes.el.style.top = null
@ -138,22 +168,23 @@ Captcha.v2 =
else else
null null
save: (pasted) -> save: (pasted, token) ->
$.forceSync 'captchas' $.forceSync 'captchas'
@captchas.push @captchas.push
response: $('textarea', @nodes.container).value response: token or $('textarea', @nodes.container).value
timeout: (if pasted then @setupTime else Date.now()) + @lifetime timeout: Date.now() + @lifetime
$.set 'captchas', @captchas $.set 'captchas', @captchas
@count() @count()
focus = d.activeElement?.nodeName is 'IFRAME' and /https?:\/\/www\.google\.com\/recaptcha\//.test(d.activeElement.src)
if @needed() if @needed()
if QR.cooldown.auto or Conf['Post on Captcha Completion'] if focus
@shouldFocus = true if QR.cooldown.auto or Conf['Post on Captcha Completion']
else @shouldFocus = true
QR.nodes.status.focus() else
QR.nodes.status.focus()
@reload() @reload()
else else
focus = d.activeElement?.nodeName is 'IFRAME' and d.activeElement.src?[...38] is 'https://www.google.com/recaptcha/api2/'
if pasted if pasted
@destroy() @destroy()
else else
@ -172,7 +203,7 @@ Captcha.v2 =
@captchas = @captchas[i..] @captchas = @captchas[i..]
@count() @count()
$.set 'captchas', @captchas $.set 'captchas', @captchas
@setup true @setup(d.activeElement is QR.nodes.status)
count: -> count: ->
@nodes.counter.textContent = "Captchas: #{@captchas.length}" @nodes.counter.textContent = "Captchas: #{@captchas.length}"
@ -181,9 +212,12 @@ Captcha.v2 =
@timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now() @timeouts.clear = setTimeout @clear.bind(@), @captchas[0].timeout - Date.now()
reload: -> reload: ->
$.globalEval ''' if @noscript
(function() { $('iframe', @nodes.container).src = @noscriptURL()
var container = document.querySelector("#qr .captcha-container"); else
window.grecaptcha.reset(container.dataset.widgetID); $.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' return if g.VIEW is 'archive'
$.globalEval 'document.documentElement.dataset.jsEnabled = true;' version = if Conf['Use Recaptcha v1'] then 'v1' else 'v2'
version = if Conf['Force Noscript Captcha'] or !doc.dataset.jsEnabled
'noscript'
else if Conf['Use Recaptcha v1']
'v1'
else
'v2'
@captcha = Captcha[version] @captcha = Captcha[version]
$.on d, '4chanXInitFinished', @initReady $.on d, '4chanXInitFinished', @initReady
@ -42,7 +36,7 @@ QR =
if Conf['Hide Original Post Form'] if Conf['Hide Original Post Form']
$.addClass doc, '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. # Prevent unnecessary loading of fallback iframe.
$.onExists doc, '#postForm noscript', true, $.rm $.onExists doc, '#postForm noscript', true, $.rm
@ -136,19 +130,25 @@ QR =
focus: -> focus: ->
$.queueTask -> $.queueTask ->
unless $$('.goog-bubble-content > iframe').some((el) -> el.getBoundingClientRect().top >= 0) unless QR.inBubble()
focus = d.activeElement and QR.nodes.el.contains(d.activeElement) QR.hasFocus = d.activeElement and QR.nodes.el.contains(d.activeElement)
$[if focus then 'addClass' else 'rmClass'] QR.nodes.el, 'focus' QR.nodes.el.classList.toggle 'focus', QR.hasFocus
if chrome? # XXX Stop unwanted scrolling due to captcha.
# XXX Stop anomalous scrolling on space/tab in/into captcha iframe. if QR.captcha.isEnabled and !QR.captcha.noscript
if d.activeElement and QR.nodes.el.contains(d.activeElement) and d.activeElement.nodeName is 'IFRAME' if QR.inCaptcha()
QR.scrollY = window.scrollY QR.scrollY = window.scrollY
$.on d, 'scroll', QR.scrollLock $.on d, 'scroll', QR.scrollLock
else else
$.off d, 'scroll', QR.scrollLock $.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: -> 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 window.scroll window.scrollX, QR.scrollY
else else
$.off d, 'scroll', QR.scrollLock $.off d, 'scroll', QR.scrollLock
@ -290,10 +290,10 @@ QR =
characterCount: -> characterCount: ->
counter = QR.nodes.charCount 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.textContent = count
counter.hidden = count < 1000 counter.hidden = count < 1000
(if count > 1500 then $.addClass else $.rmClass) counter, 'warning' (if count > 2000 then $.addClass else $.rmClass) counter, 'warning'
getFile: -> getFile: ->
$.event 'QRFile', QR.selected?.file $.event 'QRFile', QR.selected?.file
@ -537,14 +537,13 @@ QR =
event = if node.nodeName is 'SELECT' then 'change' else 'input' event = if node.nodeName is 'SELECT' then 'change' else 'input'
$.on nodes[name], event, save $.on nodes[name], event, save
<% if (type === 'userscript') { %> # XXX Chromium treats width and height as min-width and min-height
if Conf['Remember QR Size'] if !chrome? and Conf['Remember QR Size']
$.get 'QR Size', '', (item) -> $.get 'QR Size', '', (item) ->
nodes.com.style.cssText = item['QR Size'] nodes.com.style.cssText = item['QR Size']
$.on nodes.com, 'mouseup', (e) -> $.on nodes.com, 'mouseup', (e) ->
return if e.button isnt 0 return if e.button isnt 0
$.set 'QR Size', @style.cssText $.set 'QR Size', @style.cssText
<% } %>
QR.generatePostableThreadsList() QR.generatePostableThreadsList()
QR.persona.init() QR.persona.init()

View File

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