Merge branch 'ccd0' into v3
Conflicts: CHANGELOG.md LICENSE builds/4chan-X.user.js builds/crx.crx builds/crx/script.js src/General/Build.coffee src/General/Index.coffee src/General/Main.coffee src/General/Navigate.coffee src/General/css/style.css src/General/html/Build/post.html src/General/html/Features/Index-navlinks.html src/General/lib/post.class src/Images/Gallery.coffee src/Images/ImageExpand.coffee src/Images/ImageHover.coffee src/Miscellaneous/Banner.coffee src/Miscellaneous/Keybinds.coffee src/Monitoring/ThreadUpdater.coffee src/Posting/QR.captcha.coffee src/Posting/QR.coffee src/Posting/QR.post.coffee src/Quotelinks/QuoteBacklink.coffee src/Quotelinks/Quotify.coffee
This commit is contained in:
commit
523d6bd9c4
124
CHANGELOG.md
124
CHANGELOG.md
@ -3,6 +3,130 @@
|
|||||||
- Better handling of webm playback errors.
|
- Better handling of webm playback errors.
|
||||||
- Bugfixes
|
- Bugfixes
|
||||||
|
|
||||||
|
### v1.7.27
|
||||||
|
*2014-05-02*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Update due to more Recaptcha changes.
|
||||||
|
|
||||||
|
### v1.7.26
|
||||||
|
*2014-05-02*
|
||||||
|
|
||||||
|
**woxxy**
|
||||||
|
Remove /v/ from stable Foolz archive.
|
||||||
|
|
||||||
|
### v1.7.25
|
||||||
|
*2014-05-01*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- For single files, file errors are reported but no longer stop you from attempting to post. Files with errors are still removed when posting multiple files.
|
||||||
|
- Fix small bugs in file checking, uploading from URLs.
|
||||||
|
- WebM files are checked for audio before posting (Firefox only).
|
||||||
|
- Max resolution updated, now 10000x10000.
|
||||||
|
|
||||||
|
### v1.7.24
|
||||||
|
*2014-04-30*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Fix bug where multiple images selected for posting were out of order.
|
||||||
|
- Check dimensions and duration of .webm files before posting.
|
||||||
|
|
||||||
|
### v1.7.23
|
||||||
|
*2014-04-29*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Partly restore Mayhem's captcha changes reverted in last version. Captchas are now destroyed after posting instead of reloaded, unless `Auto-load captcha` is checked. Captcha caching is still enabled.
|
||||||
|
- Update for changes in Recaptcha, in particular `Recaptcha.reload("t")` no longer working.
|
||||||
|
- Various captcha-related bugfixes.
|
||||||
|
- Thumbnails for .webm files in Quick Reply.
|
||||||
|
|
||||||
|
### v1.7.22
|
||||||
|
*2014-04-27*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Revert captcha fixes of 1.4.2 as Google appears to have reverted the changes on its end. This restores captcha caching.
|
||||||
|
|
||||||
|
### v1.7.21
|
||||||
|
*2014-04-27*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Quick fix for moot breaking captcha.
|
||||||
|
|
||||||
|
### v1.7.20
|
||||||
|
*2014-04-27*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Fix features broken/disabled in catalog: settings link, thread watcher, start thread button, and keybinds.
|
||||||
|
|
||||||
|
### v1.7.19
|
||||||
|
*2014-04-25*
|
||||||
|
|
||||||
|
**fgts**
|
||||||
|
- Update archive list.
|
||||||
|
|
||||||
|
### v1.7.18
|
||||||
|
*2014-04-20*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- CSS bugfix.
|
||||||
|
|
||||||
|
### v1.7.17
|
||||||
|
*2014-04-20*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- More bugfixes.
|
||||||
|
- Restore `Comment Expansion`.
|
||||||
|
|
||||||
|
### v1.7.16
|
||||||
|
*2014-04-19*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Another update to handle HTML changes.
|
||||||
|
|
||||||
|
### v1.7.15
|
||||||
|
*2014-04-19*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Fix Unread Line (broken in 1.7.11)
|
||||||
|
|
||||||
|
### v1.7.14
|
||||||
|
*2014-04-19*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Use new URLs.
|
||||||
|
|
||||||
|
### v1.7.13
|
||||||
|
*2014-04-19*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- More fixes for new HTML.
|
||||||
|
|
||||||
|
### v1.7.12
|
||||||
|
*2014-04-19*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Fix conflict of previous version with Exlinks.
|
||||||
|
|
||||||
|
### v1.7.11
|
||||||
|
*2014-04-19*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Start transition to new HTML.
|
||||||
|
|
||||||
|
### v1.7.10
|
||||||
|
*2014-04-17*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Change Shift+arrow default keybinds Ctrl+arrow to avoid conflict with text selection
|
||||||
|
- Fix (You) in backlinks.
|
||||||
|
|
||||||
|
### v1.7.9
|
||||||
|
*2014-04-13*
|
||||||
|
|
||||||
|
**ccd0**
|
||||||
|
- Bugfixes in JSON navigation and embedding.
|
||||||
|
- More work toward compatibility with new URLs.
|
||||||
|
|
||||||
### v1.7.8
|
### v1.7.8
|
||||||
*2014-04-12*
|
*2014-04-12*
|
||||||
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* 4chan X - Version 1.7.8 - 2014-05-03
|
* 4chan X - Version 1.7.27 - 2014-05-03
|
||||||
*
|
*
|
||||||
* Licensed under the MIT license.
|
* Licensed under the MIT license.
|
||||||
* https://github.com/ccd0/4chan-x/blob/master/LICENSE
|
* https://github.com/ccd0/4chan-x/blob/master/LICENSE
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
Personal fork of Spittie's 4chan X.
|
Fork of [Spittie's 4chan X](https://github.com/Spittie/4chan-x) (itself a fork of [Seaweed's](https://github.com/seaweedchan/4chan-x)).
|
||||||
|
|
||||||
|
Note: If you're looking for a maintained fork of OneeChan, try
|
||||||
|
https://github.com/Nebukazar/OneeChan
|
||||||
|
|
||||||
#### [Why 4chan X needs to access data on every site?](https://github.com/ccd0/4chan-x/wiki/Why-4chan-X-needs-to-access-data-from-every-website%3F)
|
#### [Why 4chan X needs to access data on every site?](https://github.com/ccd0/4chan-x/wiki/Why-4chan-X-needs-to-access-data-from-every-website%3F)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name 4chan X
|
// @name 4chan X
|
||||||
// @version 1.7.8
|
// @version 1.7.27
|
||||||
// @minGMVer 1.14
|
// @minGMVer 1.14
|
||||||
// @minFFVer 26
|
// @minFFVer 26
|
||||||
// @namespace 4chan-X
|
// @namespace 4chan-X
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "4chan X",
|
"name": "4chan X",
|
||||||
"version": "1.7.8",
|
"version": "1.7.27",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"description": "Cross-browser userscript for maximum lurking on 4chan.",
|
"description": "Cross-browser userscript for maximum lurking on 4chan.",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -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/crx.crx' version='1.7.8' />
|
<updatecheck codebase='https://ccd0.github.io/4chan-x/builds/crx.crx' version='1.7.27' />
|
||||||
</app>
|
</app>
|
||||||
</gupdate>
|
</gupdate>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "4chan-X",
|
"name": "4chan-X",
|
||||||
"version": "1.7.8",
|
"version": "1.7.27",
|
||||||
"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",
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"http": true,
|
"http": true,
|
||||||
"https": true,
|
"https": true,
|
||||||
"software": "foolfuuka",
|
"software": "foolfuuka",
|
||||||
"boards": ["a", "biz", "co", "diy", "gd", "jp", "m", "sci", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
|
"boards": ["a", "biz", "co", "diy", "gd", "jp", "m", "sci", "sp", "tg", "tv", "vg", "vp", "vr", "wsg"],
|
||||||
"files": ["a", "biz", "gd", "diy", "jp", "m", "sci", "tg", "vg", "vp", "vr", "wsg"]
|
"files": ["a", "biz", "gd", "diy", "jp", "m", "sci", "tg", "vg", "vp", "vr", "wsg"]
|
||||||
}, {
|
}, {
|
||||||
"uid": 1,
|
"uid": 1,
|
||||||
@ -95,8 +95,8 @@
|
|||||||
"http": true,
|
"http": true,
|
||||||
"https": true,
|
"https": true,
|
||||||
"software": "foolfuuka",
|
"software": "foolfuuka",
|
||||||
"boards": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"],
|
"boards": ["asp", "cm", "h", "hc", "hm", "n", "p", "r", "s", "soc", "y"],
|
||||||
"files": ["cm", "h", "hc", "hm", "r", "s", "soc", "y"]
|
"files": ["asp", "cm", "h", "hc", "hm", "n", "p", "r", "s", "soc", "y"]
|
||||||
}, {
|
}, {
|
||||||
"uid": 16,
|
"uid": 16,
|
||||||
"name": "maware",
|
"name": "maware",
|
||||||
|
|||||||
@ -181,8 +181,6 @@ Build =
|
|||||||
pageNum = Index.liveThreadData.keys.indexOf("#{postID}") // Index.threadsNumPerPage
|
pageNum = Index.liveThreadData.keys.indexOf("#{postID}") // Index.threadsNumPerPage
|
||||||
pageIcon = " <span class=page-num title='This thread is on page #{pageNum} in the original index.'>Page #{pageNum}</span>"
|
pageIcon = " <span class=page-num title='This thread is on page #{pageNum} in the original index.'>Page #{pageNum}</span>"
|
||||||
replyLink = " <span>[<a href='/#{boardID}/thread/#{threadID}' class=replylink>Reply</a>]</span>"
|
replyLink = " <span>[<a href='/#{boardID}/thread/#{threadID}' class=replylink>Reply</a>]</span>"
|
||||||
else
|
|
||||||
pageIcon = replyLink = ''
|
|
||||||
|
|
||||||
container = $.el 'div',
|
container = $.el 'div',
|
||||||
id: "pc#{postID}"
|
id: "pc#{postID}"
|
||||||
@ -225,6 +223,7 @@ Build =
|
|||||||
|
|
||||||
'</div>'
|
'</div>'
|
||||||
|
|
||||||
|
# Fix pathnames
|
||||||
for quote in $$ '.quotelink', container
|
for quote in $$ '.quotelink', container
|
||||||
href = quote.getAttribute 'href'
|
href = quote.getAttribute 'href'
|
||||||
continue if href[0] is '/' # Cross-board quote, or board link
|
continue if href[0] is '/' # Cross-board quote, or board link
|
||||||
|
|||||||
@ -37,6 +37,10 @@ Config =
|
|||||||
true
|
true
|
||||||
'Display dates like "3 minutes ago". Tooltip shows the timestamp.'
|
'Display dates like "3 minutes ago". Tooltip shows the timestamp.'
|
||||||
]
|
]
|
||||||
|
'Comment Expansion': [
|
||||||
|
true
|
||||||
|
'Add buttons to expand too long comments.'
|
||||||
|
]
|
||||||
'File Info Formatting': [
|
'File Info Formatting': [
|
||||||
true
|
true
|
||||||
'Reformat the file information.'
|
'Reformat the file information.'
|
||||||
@ -339,7 +343,7 @@ Config =
|
|||||||
]
|
]
|
||||||
'Auto-load captcha': [
|
'Auto-load captcha': [
|
||||||
false
|
false
|
||||||
'Automatically load the captcha when you open a thread'
|
'Automatically load the captcha when you open a thread, and reload it after you post.'
|
||||||
]
|
]
|
||||||
|
|
||||||
'Quote Links':
|
'Quote Links':
|
||||||
@ -671,19 +675,19 @@ vp-replace
|
|||||||
]
|
]
|
||||||
# Board Navigation
|
# Board Navigation
|
||||||
'Front page': [
|
'Front page': [
|
||||||
'0'
|
'1'
|
||||||
'Jump to front page.'
|
'Jump to front page.'
|
||||||
]
|
]
|
||||||
'Open front page': [
|
'Open front page': [
|
||||||
'Shift+0'
|
'Shift+1'
|
||||||
'Open front page in a new tab.'
|
'Open front page in a new tab.'
|
||||||
]
|
]
|
||||||
'Next page': [
|
'Next page': [
|
||||||
'Shift+Right'
|
'Ctrl+Right'
|
||||||
'Jump to the next page.'
|
'Jump to the next page.'
|
||||||
]
|
]
|
||||||
'Previous page': [
|
'Previous page': [
|
||||||
'Shift+Left'
|
'Ctrl+Left'
|
||||||
'Jump to the previous page.'
|
'Jump to the previous page.'
|
||||||
]
|
]
|
||||||
'Search form': [
|
'Search form': [
|
||||||
@ -708,11 +712,11 @@ vp-replace
|
|||||||
]
|
]
|
||||||
# Thread Navigation
|
# Thread Navigation
|
||||||
'Next thread': [
|
'Next thread': [
|
||||||
'Shift+Down'
|
'Ctrl+Down'
|
||||||
'See next thread.'
|
'See next thread.'
|
||||||
]
|
]
|
||||||
'Previous thread': [
|
'Previous thread': [
|
||||||
'Shift+Up'
|
'Ctrl+Up'
|
||||||
'See previous thread.'
|
'See previous thread.'
|
||||||
]
|
]
|
||||||
'Expand thread': [
|
'Expand thread': [
|
||||||
|
|||||||
@ -108,9 +108,10 @@ Header =
|
|||||||
a.className = 'current'
|
a.className = 'current'
|
||||||
|
|
||||||
cs = $.el 'a',
|
cs = $.el 'a',
|
||||||
id: 'settingsWindowLink'
|
|
||||||
href: 'javascript:;'
|
href: 'javascript:;'
|
||||||
textContent: 'Catalog Settings'
|
textContent: 'Catalog Settings'
|
||||||
|
$.on cs, 'click', () ->
|
||||||
|
$.id('settingsWindowLink').click()
|
||||||
|
|
||||||
@addShortcut cs if g.VIEW is 'catalog'
|
@addShortcut cs if g.VIEW is 'catalog'
|
||||||
|
|
||||||
|
|||||||
@ -127,6 +127,7 @@ Index =
|
|||||||
|
|
||||||
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
|
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
|
||||||
$.rm navLink for navLink in $$ '.navLinks'
|
$.rm navLink for navLink in $$ '.navLinks'
|
||||||
|
$.id('search-box')?.parentNode.remove()
|
||||||
$.after $.x('child::form/preceding-sibling::hr[1]'), Index.navLinks
|
$.after $.x('child::form/preceding-sibling::hr[1]'), Index.navLinks
|
||||||
|
|
||||||
return if g.VIEW isnt 'index'
|
return if g.VIEW isnt 'index'
|
||||||
@ -152,9 +153,7 @@ Index =
|
|||||||
scroll: ->
|
scroll: ->
|
||||||
return if Index.req or Conf['Index Mode'] isnt 'infinite' or (window.scrollY <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread'
|
return if Index.req or Conf['Index Mode'] isnt 'infinite' or (window.scrollY <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread'
|
||||||
Index.currentPage = (Index.currentPage or Index.getCurrentPage()) + 1 # Avoid having to pushState to keep track of the current page
|
Index.currentPage = (Index.currentPage or Index.getCurrentPage()) + 1 # Avoid having to pushState to keep track of the current page
|
||||||
|
|
||||||
return Index.endNotice() if Index.currentPage >= Index.pagesNum
|
return Index.endNotice() if Index.currentPage >= Index.pagesNum
|
||||||
|
|
||||||
Index.buildIndex true
|
Index.buildIndex true
|
||||||
|
|
||||||
endNotice: do ->
|
endNotice: do ->
|
||||||
@ -401,10 +400,10 @@ Index =
|
|||||||
getCurrentPage: ->
|
getCurrentPage: ->
|
||||||
if Conf['Index Mode'] is 'infinite' and Index.currentPage
|
if Conf['Index Mode'] is 'infinite' and Index.currentPage
|
||||||
return Index.currentPage
|
return Index.currentPage
|
||||||
+window.location.pathname.split('/')[2]
|
+window.location.pathname.split('/')[2] or 1
|
||||||
|
|
||||||
userPageNav: (pageNum) ->
|
userPageNav: (pageNum) ->
|
||||||
Navigate.pushState if pageNum is 0 then './' else pageNum
|
Navigate.pushState if pageNum is 1 then './' else pageNum
|
||||||
if Conf['Refreshed Navigation'] and Conf['Index Mode'] isnt 'all pages'
|
if Conf['Refreshed Navigation'] and Conf['Index Mode'] isnt 'all pages'
|
||||||
Index.update pageNum
|
Index.update pageNum
|
||||||
else
|
else
|
||||||
@ -431,20 +430,22 @@ Index =
|
|||||||
Math.ceil Index.sortedThreads.length / Index.getThreadsNumPerPage()
|
Math.ceil Index.sortedThreads.length / Index.getThreadsNumPerPage()
|
||||||
|
|
||||||
getMaxPageNum: ->
|
getMaxPageNum: ->
|
||||||
Math.max 0, Index.getPagesNum() - 1
|
min = 1
|
||||||
|
max = +Index.getPagesNum()
|
||||||
|
if min < max then max else
|
||||||
|
min
|
||||||
togglePagelist: ->
|
togglePagelist: ->
|
||||||
Index.pagelist.hidden = Conf['Index Mode'] isnt 'paged'
|
Index.pagelist.hidden = Conf['Index Mode'] isnt 'paged'
|
||||||
|
|
||||||
buildPagelist: ->
|
buildPagelist: ->
|
||||||
pagesRoot = $ '.pages', Index.pagelist
|
pagesRoot = $ '.pages', Index.pagelist
|
||||||
maxPageNum = Index.getMaxPageNum()
|
maxPageNum = Index.getMaxPageNum()
|
||||||
if pagesRoot.childElementCount isnt maxPageNum + 1
|
if pagesRoot.childElementCount isnt maxPageNum
|
||||||
nodes = []
|
nodes = []
|
||||||
for i in [0..maxPageNum] by 1
|
for i in [1..maxPageNum] by 1
|
||||||
a = $.el 'a',
|
a = $.el 'a',
|
||||||
textContent: i
|
textContent: i
|
||||||
href: if i then i else './'
|
href: if i is 1 then './' else i
|
||||||
nodes.push $.tn('['), a, $.tn '] '
|
nodes.push $.tn('['), a, $.tn '] '
|
||||||
$.rmAll pagesRoot
|
$.rmAll pagesRoot
|
||||||
$.add pagesRoot, nodes
|
$.add pagesRoot, nodes
|
||||||
@ -457,11 +458,11 @@ Index =
|
|||||||
# Previous/Next buttons
|
# Previous/Next buttons
|
||||||
prev = pagesRoot.previousSibling.firstChild
|
prev = pagesRoot.previousSibling.firstChild
|
||||||
next = pagesRoot.nextSibling.firstChild
|
next = pagesRoot.nextSibling.firstChild
|
||||||
href = Math.max pageNum - 1, 0
|
href = Math.max pageNum - 1, 1
|
||||||
prev.href = if href is 0 then './' else href
|
prev.href = if href is 1 then './' else href
|
||||||
prev.firstChild.disabled = href is pageNum
|
prev.firstChild.disabled = href is pageNum
|
||||||
href = Math.min pageNum + 1, maxPageNum
|
href = Math.min pageNum + 1, maxPageNum
|
||||||
next.href = if href is 0 then './' else href
|
next.href = if href is 1 then './' else href
|
||||||
next.firstChild.disabled = href is pageNum
|
next.firstChild.disabled = href is pageNum
|
||||||
# <strong> current page
|
# <strong> current page
|
||||||
if strong = $ 'strong', pagesRoot
|
if strong = $ 'strong', pagesRoot
|
||||||
@ -469,7 +470,7 @@ Index =
|
|||||||
$.replace strong, strong.firstChild
|
$.replace strong, strong.firstChild
|
||||||
else
|
else
|
||||||
strong = $.el 'strong'
|
strong = $.el 'strong'
|
||||||
return unless a = pagesRoot.children[pageNum] # If coming in from a Navigate.navigate, this could break.
|
return unless a = pagesRoot.children[pageNum - 1] # If coming in from a Navigate.navigate, this could break.
|
||||||
$.before a, strong
|
$.before a, strong
|
||||||
$.add strong, a
|
$.add strong, a
|
||||||
|
|
||||||
@ -494,7 +495,7 @@ Index =
|
|||||||
return
|
return
|
||||||
unless d.readyState is 'loading' or Index.root.parentElement
|
unless d.readyState is 'loading' or Index.root.parentElement
|
||||||
$.replace $('.board'), Index.root
|
$.replace $('.board'), Index.root
|
||||||
Index.currentPage = 0
|
Index.currentPage = 1
|
||||||
Index.req?.abort()
|
Index.req?.abort()
|
||||||
Index.notice?.close()
|
Index.notice?.close()
|
||||||
|
|
||||||
@ -545,7 +546,7 @@ Index =
|
|||||||
Navigate.title()
|
Navigate.title()
|
||||||
|
|
||||||
try
|
try
|
||||||
pageNum or= 0
|
pageNum or= 1
|
||||||
if req.status is 200
|
if req.status is 200
|
||||||
Index.parse req.response, pageNum
|
Index.parse req.response, pageNum
|
||||||
else if req.status is 304
|
else if req.status is 304
|
||||||
@ -779,7 +780,7 @@ Index =
|
|||||||
unless Index.searchInput.dataset.searching
|
unless Index.searchInput.dataset.searching
|
||||||
Index.searchInput.dataset.searching = 1
|
Index.searchInput.dataset.searching = 1
|
||||||
Index.pageBeforeSearch = Index.getCurrentPage()
|
Index.pageBeforeSearch = Index.getCurrentPage()
|
||||||
Index.setPage pageNum = 0
|
Index.setPage pageNum = 1
|
||||||
else
|
else
|
||||||
unless Conf['Index Mode'] is 'infinite'
|
unless Conf['Index Mode'] is 'infinite'
|
||||||
pageNum = Index.getCurrentPage()
|
pageNum = Index.getCurrentPage()
|
||||||
|
|||||||
@ -18,6 +18,9 @@ Main =
|
|||||||
return Index.catalogSwitch()
|
return Index.catalogSwitch()
|
||||||
if g.VIEW is 'thread'
|
if g.VIEW is 'thread'
|
||||||
g.THREADID = +pathname[3]
|
g.THREADID = +pathname[3]
|
||||||
|
if pathname[2] isnt 'thread' or pathname.length > 4
|
||||||
|
pathname[2] = 'thread'
|
||||||
|
history.replaceState null, '', pathname.slice(0,4).join('/') + location.hash
|
||||||
|
|
||||||
# flatten Config into Conf
|
# flatten Config into Conf
|
||||||
# and get saved or default values
|
# and get saved or default values
|
||||||
|
|||||||
@ -130,8 +130,8 @@ Navigate =
|
|||||||
|
|
||||||
updateBoard: (boardID) ->
|
updateBoard: (boardID) ->
|
||||||
fullBoardList = $ '#full-board-list', Header.boardList
|
fullBoardList = $ '#full-board-list', Header.boardList
|
||||||
$.rmClass $('.current', fullBoardList), 'current'
|
$.rmClass current, 'current' if current = $ '.current', fullBoardList
|
||||||
$.addClass $("a[href*='/#{boardID}/']", fullBoardList), 'current'
|
$.addClass current, 'current' if current = $ "a[href*='/#{boardID}/']", fullBoardList
|
||||||
Header.generateBoardList Conf['boardnav'].replace /(\r\n|\n|\r)/g, ' '
|
Header.generateBoardList Conf['boardnav'].replace /(\r\n|\n|\r)/g, ' '
|
||||||
|
|
||||||
QR.flagsInput()
|
QR.flagsInput()
|
||||||
@ -232,7 +232,7 @@ Navigate =
|
|||||||
if threadID
|
if threadID
|
||||||
view = 'thread'
|
view = 'thread'
|
||||||
else
|
else
|
||||||
pageNum = +view
|
pageNum = +view or 1 # string to number, '' to 1
|
||||||
view = 'index' # path is "/boardID/". See the problem?
|
view = 'index' # path is "/boardID/". See the problem?
|
||||||
|
|
||||||
path = @pathname
|
path = @pathname
|
||||||
@ -263,7 +263,7 @@ Navigate =
|
|||||||
|
|
||||||
# Moving from index to thread or thread to thread
|
# Moving from index to thread or thread to thread
|
||||||
{load} = Navigate
|
{load} = Navigate
|
||||||
Navigate.req = $.ajax "//a.4cdn.org/#{boardID}/res/#{threadID}.json",
|
Navigate.req = $.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json",
|
||||||
onabort: load
|
onabort: load
|
||||||
onloadend: load
|
onloadend: load
|
||||||
|
|
||||||
|
|||||||
10
src/General/css/font-awesome.css
vendored
10
src/General/css/font-awesome.css
vendored
@ -26,14 +26,12 @@
|
|||||||
|
|
||||||
@font-face{font-family:FontAwesome;src:url('data:application/font-woff;base64,<%= grunt.file.read('node_modules/font-awesome/fonts/fontawesome-webfont.woff', {encoding: 'base64'}) %>') format('woff');font-weight:400;font-style:normal}.fa::before{font-family:FontAwesome;font-weight:400;font-style:normal;-webkit-font-smoothing:antialiased;*margin-right:.3em;text-decoration:inherit;display:none;speak:none}
|
@font-face{font-family:FontAwesome;src:url('data:application/font-woff;base64,<%= grunt.file.read('node_modules/font-awesome/fonts/fontawesome-webfont.woff', {encoding: 'base64'}) %>') format('woff');font-weight:400;font-style:normal}.fa::before{font-family:FontAwesome;font-weight:400;font-style:normal;-webkit-font-smoothing:antialiased;*margin-right:.3em;text-decoration:inherit;display:none;speak:none}
|
||||||
|
|
||||||
:root.shortcut-icons .fa::before,
|
.fa::before {display:inline-block;font-size:13px;visibility:visible}
|
||||||
.menu-button .fa::before,
|
:root:not(.shortcut-icons) #shortcuts .fa::before {display:none}
|
||||||
.hide-reply-button .fa::before,
|
|
||||||
.hide-thread-button .fa::before {display:inline-block;font-size:13px;visibility:visible}
|
|
||||||
:root.shortcut-icons #shortcuts .fa::before{font-size:15px!important;margin-top:-3px!important;position:relative;top:1px}
|
:root.shortcut-icons #shortcuts .fa::before{font-size:15px!important;margin-top:-3px!important;position:relative;top:1px}
|
||||||
:root.shortcut-icons .fa, .menu-button .fa{font-size:0;visibility:hidden}
|
:root.shortcut-icons #shortcuts .fa, .menu-button .fa{font-size:0;visibility:hidden}
|
||||||
:root.shortcut-icons .shortcut.brackets-wrap::after,:root.shortcut-icons .shortcut.brackets-wrap::before{display:none}
|
:root.shortcut-icons .shortcut.brackets-wrap::after,:root.shortcut-icons .shortcut.brackets-wrap::before{display:none}
|
||||||
:root.shortcut-icons a .fa,
|
:root.shortcut-icons #shortcuts a .fa,
|
||||||
.menu-button .fa,
|
.menu-button .fa,
|
||||||
.hide-reply-button .fa,
|
.hide-reply-button .fa,
|
||||||
.hide-thread-button .fa {display:inline}
|
.hide-thread-button .fa {display:inline}
|
||||||
|
|||||||
@ -56,9 +56,12 @@ a[href="javascript:;"] {
|
|||||||
.warning {
|
.warning {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
#boardNavDesktop {
|
#boardNavDesktop, #boardNavMobile {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
body.hasDropDownNav{
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
a {
|
a {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
@ -66,14 +69,17 @@ a {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 0px 2px;
|
padding: 0px 2px;
|
||||||
}
|
}
|
||||||
body>hr, .ad-plea-bottom + hr {
|
body > hr,
|
||||||
|
#blotter hr,
|
||||||
|
.desktop > hr,
|
||||||
|
#delform > hr,
|
||||||
|
#content > hr {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.board > hr:last-of-type {
|
:root.index .board > hr:last-of-type,
|
||||||
border-top-color: transparent !important;
|
:root.thread .board > hr {
|
||||||
}
|
border: 0px;
|
||||||
div.navLinks {
|
margin: 0px;
|
||||||
margin-bottom: -10px !important;
|
|
||||||
}
|
}
|
||||||
.ad-plea {
|
.ad-plea {
|
||||||
display: none;
|
display: none;
|
||||||
@ -485,9 +491,7 @@ div.center:not(.ad-cnt) {
|
|||||||
:root.index-loading .navLinks,
|
:root.index-loading .navLinks,
|
||||||
:root.index-loading .board,
|
:root.index-loading .board,
|
||||||
:root.index-loading .pagelist,
|
:root.index-loading .pagelist,
|
||||||
:root.thread .pagelist {
|
:root.thread .pagelist,
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
:root:not(.catalog-mode) #index-size,
|
:root:not(.catalog-mode) #index-size,
|
||||||
.index:not(.catalog-mode) #returnlink {
|
.index:not(.catalog-mode) #returnlink {
|
||||||
display: none;
|
display: none;
|
||||||
@ -902,7 +906,7 @@ span.hide-announcement {
|
|||||||
:root.hide-original-post-form #postForm,
|
:root.hide-original-post-form #postForm,
|
||||||
:root.hide-original-post-form .postingMode,
|
:root.hide-original-post-form .postingMode,
|
||||||
:root.hide-original-post-form #togglePostForm,
|
:root.hide-original-post-form #togglePostForm,
|
||||||
#qr.autohide:not(.focus):not(:hover) > form,
|
#qr.autohide:not(.focus):not(:hover):not(:active) > form,
|
||||||
.thread #qr select[data-name=thread],
|
.thread #qr select[data-name=thread],
|
||||||
#file-n-submit:not(.has-file) #qr-filerm {
|
#file-n-submit:not(.has-file) #qr-filerm {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
36
src/General/html/Build/post.html
Executable file
36
src/General/html/Build/post.html
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
"""#{if isOP then '' else "<div class=sideArrows id=sa#{postID}>>></div>"}
|
||||||
|
<div id=p#{postID} class='post #{if isOP then 'op' else 'reply'}#{
|
||||||
|
if capcodeIcon is 'admin_highlight' then
|
||||||
|
' highlightPost'
|
||||||
|
else
|
||||||
|
''
|
||||||
|
}'>
|
||||||
|
|
||||||
|
#{if isOP then fileHTML else ''}
|
||||||
|
|
||||||
|
<div class='postInfo' id=pi#{postID}>
|
||||||
|
<input type=checkbox name=#{postID} value=delete>
|
||||||
|
#{' '}<span class=subject>#{subject or ''}</span>#{' '}
|
||||||
|
<span class='nameBlock#{capcodeClass}'>
|
||||||
|
#{emailStart}
|
||||||
|
<span class=name>#{name or ''}</span>
|
||||||
|
#{tripcode + capcodeStart + emailEnd + capcodeIcon + userID + flag}
|
||||||
|
</span>#{" "}
|
||||||
|
<span class=dateTime data-utc=#{dateUTC}>#{date}</span>#{' '}
|
||||||
|
<span class='postNum'>
|
||||||
|
<a href=#{"/#{boardID}/thread/#{threadID}#p#{postID}"} title='Highlight this post'>No.</a>
|
||||||
|
<a href='#{
|
||||||
|
if g.VIEW is 'thread' and g.THREADID is +threadID then
|
||||||
|
"javascript:quote(#{postID})"
|
||||||
|
else
|
||||||
|
"/#{boardID}/thread/#{threadID}#q#{postID}"
|
||||||
|
}' title='Quote this post'>#{postID}</a>
|
||||||
|
#{pageIcon + sticky + closed + replyLink}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
#{if isOP then '' else fileHTML}
|
||||||
|
|
||||||
|
<blockquote class=postMessage id=m#{postID}>#{comment or ''}</blockquote>#{' '}
|
||||||
|
|
||||||
|
</div>"""
|
||||||
@ -27,4 +27,4 @@
|
|||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
<span class=brackets-wrap id=returnlink><a href=.././>Return</a></span>
|
<span class=brackets-wrap id=returnlink><a href=.././>Return</a></span>
|
||||||
<span class=brackets-wrap id=bottomlink><a href="#bottom">Bottom</a></span>
|
<span class=brackets-wrap id=bottomlink><a href="#bottom">Bottom</a></span>
|
||||||
|
|||||||
@ -101,7 +101,7 @@ class Post
|
|||||||
return unless match = quotelink.href.match ///
|
return unless match = quotelink.href.match ///
|
||||||
boards\.4chan\.org/
|
boards\.4chan\.org/
|
||||||
([^/]+) # boardID
|
([^/]+) # boardID
|
||||||
/thread/\d+#p
|
/(?:res|thread)/\d+#p
|
||||||
(\d+) # postID
|
(\d+) # postID
|
||||||
$
|
$
|
||||||
///
|
///
|
||||||
@ -135,6 +135,10 @@ class Post
|
|||||||
thumb.src
|
thumb.src
|
||||||
else
|
else
|
||||||
"#{location.protocol}//t.4cdn.org/#{@board}/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
|
"#{location.protocol}//t.4cdn.org/#{@board}/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
|
||||||
|
@file.isImage = /(jpg|png|gif)$/i.test @file.URL
|
||||||
|
@file.isVideo = /webm$/i.test @file.URL
|
||||||
|
if @file.isImage or @file.isVideo
|
||||||
|
@file.dimensions = fileText.childNodes[2].textContent.match(/\d+x\d+/)?[0]
|
||||||
@file.name = if !@file.isSpoiler and nameNode = $ 'a', fileText
|
@file.name = if !@file.isSpoiler and nameNode = $ 'a', fileText
|
||||||
nameNode.title or nameNode.textContent
|
nameNode.title or nameNode.textContent
|
||||||
else
|
else
|
||||||
@ -145,12 +149,8 @@ class Post
|
|||||||
# webk.it/62107
|
# webk.it/62107
|
||||||
# https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909
|
# https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909
|
||||||
# http://www.whatwg.org/specs/web-apps/current-work/#multipart-form-data
|
# http://www.whatwg.org/specs/web-apps/current-work/#multipart-form-data
|
||||||
@file.name = @file.name.replace /%22/g, '"'
|
@file.name = @file.name?.replace /%22/g, '"'
|
||||||
<% } %>
|
<% } %>
|
||||||
@file.isImage = /(jpg|png|gif)$/i.test @file.name
|
|
||||||
@file.isVideo = /webm$/i.test @file.name
|
|
||||||
if @file.isImage or @file.isVideo
|
|
||||||
@file.dimensions = fileText.textContent.match(/\d+x\d+/)[0]
|
|
||||||
|
|
||||||
cleanup: (root, post) ->
|
cleanup: (root, post) ->
|
||||||
for node in $$ '.mobile', root
|
for node in $$ '.mobile', root
|
||||||
|
|||||||
@ -191,7 +191,7 @@ Gallery =
|
|||||||
if src[2] is 'i.4cdn.org'
|
if src[2] is 'i.4cdn.org'
|
||||||
URL = Redirect.to 'file',
|
URL = Redirect.to 'file',
|
||||||
boardID: src[3]
|
boardID: src[3]
|
||||||
filename: src[5]
|
filename: src[src.length - 1]
|
||||||
if URL
|
if URL
|
||||||
thumb.href = URL
|
thumb.href = URL
|
||||||
return unless Gallery.nodes.current is img
|
return unless Gallery.nodes.current is img
|
||||||
@ -200,8 +200,8 @@ Gallery =
|
|||||||
if g.DEAD or post.isDead or post.file.isDead
|
if g.DEAD or post.isDead or post.file.isDead
|
||||||
return
|
return
|
||||||
|
|
||||||
# XXX CORS for images.4chan.org WHEN?
|
# XXX CORS for i.4cdn.org WHEN?
|
||||||
$.ajax "//a.4cdn.org/#{post.board}/res/#{post.thread}.json", onload: ->
|
$.ajax "//a.4cdn.org/#{post.board}/thread/#{post.thread}.json", onload: ->
|
||||||
return if @status isnt 200
|
return if @status isnt 200
|
||||||
i = 0
|
i = 0
|
||||||
{posts} = @response
|
{posts} = @response
|
||||||
|
|||||||
@ -207,8 +207,8 @@ Linkify =
|
|||||||
el = (type = Linkify.types[a.dataset.key]).el a
|
el = (type = Linkify.types[a.dataset.key]).el a
|
||||||
|
|
||||||
# Set style values.
|
# Set style values.
|
||||||
el.style.cssText = if style = type.style
|
el.style.cssText = if type.style?
|
||||||
style
|
type.style
|
||||||
else
|
else
|
||||||
"border: 0; width: 640px; height: 390px"
|
"border: 0; width: 640px; height: 390px"
|
||||||
|
|
||||||
@ -249,9 +249,10 @@ Linkify =
|
|||||||
ordered_types: [
|
ordered_types: [
|
||||||
key: 'audio'
|
key: 'audio'
|
||||||
regExp: /(.*\.(mp3|ogg|wav))$/
|
regExp: /(.*\.(mp3|ogg|wav))$/
|
||||||
|
style: ''
|
||||||
el: (a) ->
|
el: (a) ->
|
||||||
$.el 'audio',
|
$.el 'audio',
|
||||||
controls: 'controls'
|
controls: true
|
||||||
preload: 'auto'
|
preload: 'auto'
|
||||||
src: a.dataset.uid
|
src: a.dataset.uid
|
||||||
,
|
,
|
||||||
@ -391,10 +392,12 @@ Linkify =
|
|||||||
,
|
,
|
||||||
key: 'Vocaroo'
|
key: 'Vocaroo'
|
||||||
regExp: /.*(?:vocaroo.com\/)([^#\&\?]*).*/
|
regExp: /.*(?:vocaroo.com\/)([^#\&\?]*).*/
|
||||||
style: 'border: 0; width: 150px; height: 45px;'
|
style: ''
|
||||||
el: (a) ->
|
el: (a) ->
|
||||||
$.el 'object',
|
$.el 'audio',
|
||||||
innerHTML: "<embed src='http://vocaroo.com/player.swf?playMediaID=#{a.dataset.uid.replace /^i\//, ''}&autoplay=0' wmode='opaque' width='150' height='45' pluginspage='http://get.adobe.com/flashplayer/' type='application/x-shockwave-flash'></embed>"
|
controls: true
|
||||||
|
preload: 'auto'
|
||||||
|
src: "http://vocaroo.com/media_command.php?media=#{a.dataset.uid.replace /^i\//, ''}&command=download_ogg"
|
||||||
,
|
,
|
||||||
key: 'Vimeo'
|
key: 'Vimeo'
|
||||||
regExp: /.*(?:vimeo.com\/)([^#\&\?]*).*/
|
regExp: /.*(?:vimeo.com\/)([^#\&\?]*).*/
|
||||||
@ -434,6 +437,7 @@ Linkify =
|
|||||||
,
|
,
|
||||||
key: 'video'
|
key: 'video'
|
||||||
regExp: /(.*\.(ogv|webm|mp4))$/
|
regExp: /(.*\.(ogv|webm|mp4))$/
|
||||||
|
style: 'border: 0; width: auto; height: auto;'
|
||||||
el: (a) ->
|
el: (a) ->
|
||||||
$.el 'video',
|
$.el 'video',
|
||||||
controls: 'controls'
|
controls: 'controls'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
Banner =
|
Banner =
|
||||||
init: ->
|
init: ->
|
||||||
$.asap (-> d.body), ->
|
$.asap (-> d.body), ->
|
||||||
$.asap (-> $ '.abovePostForm'), Banner.ready
|
$.asap (-> $ 'hr'), Banner.ready
|
||||||
|
|
||||||
ready: ->
|
ready: ->
|
||||||
banner = $ ".boardBanner"
|
banner = $ ".boardBanner"
|
||||||
@ -9,7 +9,7 @@ Banner =
|
|||||||
|
|
||||||
for child, i in children
|
for child, i in children
|
||||||
if i is 0
|
if i is 0
|
||||||
child.id = "Banner"
|
child.id = "Banner"
|
||||||
child.title = "Click to change"
|
child.title = "Click to change"
|
||||||
$.on child, 'click error', Banner.cb.toggle
|
$.on child, 'click error', Banner.cb.toggle
|
||||||
|
|
||||||
@ -84,4 +84,4 @@ Banner =
|
|||||||
$.set string, cachedTest
|
$.set string, cachedTest
|
||||||
$.set string2, cachedTest
|
$.set string2, cachedTest
|
||||||
|
|
||||||
child
|
child
|
||||||
|
|||||||
@ -23,7 +23,7 @@ ExpandComment =
|
|||||||
return
|
return
|
||||||
return unless a = $ '.abbr > a', post.nodes.comment
|
return unless a = $ '.abbr > a', post.nodes.comment
|
||||||
a.textContent = "Post No.#{post} Loading..."
|
a.textContent = "Post No.#{post} Loading..."
|
||||||
$.cache "//api.4chan.org#{a.pathname}.json", -> ExpandComment.parse @, a, post
|
$.cache "//a.4cdn.org#{a.pathname.split('/').splice(0,4).join('/')}.json", -> ExpandComment.parse @, a, post
|
||||||
contract: (post) ->
|
contract: (post) ->
|
||||||
return unless post.nodes.shortComment
|
return unless post.nodes.shortComment
|
||||||
a = $ '.abbr > a', post.nodes.shortComment
|
a = $ '.abbr > a', post.nodes.shortComment
|
||||||
@ -36,7 +36,7 @@ ExpandComment =
|
|||||||
a.textContent = "Error #{req.statusText} (#{status})"
|
a.textContent = "Error #{req.statusText} (#{status})"
|
||||||
return
|
return
|
||||||
|
|
||||||
posts = JSON.parse(req.response).posts
|
posts = req.response.posts
|
||||||
if spoilerRange = posts[0].custom_spoiler
|
if spoilerRange = posts[0].custom_spoiler
|
||||||
Build.spoilerRange[g.BOARD] = spoilerRange
|
Build.spoilerRange[g.BOARD] = spoilerRange
|
||||||
|
|
||||||
@ -49,10 +49,14 @@ ExpandComment =
|
|||||||
{comment} = post.nodes
|
{comment} = post.nodes
|
||||||
clone = comment.cloneNode false
|
clone = comment.cloneNode false
|
||||||
clone.innerHTML = postObj.com
|
clone.innerHTML = postObj.com
|
||||||
|
# Fix pathnames
|
||||||
for quote in $$ '.quotelink', clone
|
for quote in $$ '.quotelink', clone
|
||||||
href = quote.getAttribute 'href'
|
href = quote.getAttribute 'href'
|
||||||
continue if href[0] is '/' # Cross-board quote, or board link
|
continue if href[0] is '/' # Cross-board quote, or board link
|
||||||
quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
|
if href[0] is '#'
|
||||||
|
quote.href = "#{a.pathname.split('/').splice(0,4).join('/')}#{href}"
|
||||||
|
else
|
||||||
|
quote.href = "#{a.pathname.split('/').splice(0,3).join('/')}/#{href}"
|
||||||
post.nodes.shortComment = comment
|
post.nodes.shortComment = comment
|
||||||
$.replace comment, clone
|
$.replace comment, clone
|
||||||
post.nodes.comment = post.nodes.longComment = clone
|
post.nodes.comment = post.nodes.longComment = clone
|
||||||
|
|||||||
@ -21,9 +21,10 @@ Keybinds =
|
|||||||
{target} = e
|
{target} = e
|
||||||
if target.nodeName in ['INPUT', 'TEXTAREA']
|
if target.nodeName in ['INPUT', 'TEXTAREA']
|
||||||
return unless /(Esc|Alt|Ctrl|Meta|Shift\+\w{2,})/.test key
|
return unless /(Esc|Alt|Ctrl|Meta|Shift\+\w{2,})/.test key
|
||||||
threadRoot = Nav.getThread()
|
unless g.VIEW is 'catalog'
|
||||||
if op = $ '.op', threadRoot
|
threadRoot = Nav.getThread()
|
||||||
thread = Get.postFromNode(op).thread
|
if op = $ '.op', threadRoot
|
||||||
|
thread = Get.postFromNode(op).thread
|
||||||
switch key
|
switch key
|
||||||
# QR & Options
|
# QR & Options
|
||||||
when Conf['Toggle board list']
|
when Conf['Toggle board list']
|
||||||
@ -32,9 +33,10 @@ Keybinds =
|
|||||||
when Conf['Toggle header']
|
when Conf['Toggle header']
|
||||||
Header.toggleBarVisibility()
|
Header.toggleBarVisibility()
|
||||||
when Conf['Open empty QR']
|
when Conf['Open empty QR']
|
||||||
Keybinds.qr threadRoot
|
Keybinds.qr()
|
||||||
when Conf['Open QR']
|
when Conf['Open QR']
|
||||||
Keybinds.qr threadRoot, true
|
return if g.VIEW is 'catalog'
|
||||||
|
Keybinds.qr threadRoot
|
||||||
when Conf['Open settings']
|
when Conf['Open settings']
|
||||||
Settings.open()
|
Settings.open()
|
||||||
when Conf['Close']
|
when Conf['Close']
|
||||||
@ -72,22 +74,28 @@ Keybinds =
|
|||||||
when 'index'
|
when 'index'
|
||||||
if Conf['JSON Navigation'] then Index.update()
|
if Conf['JSON Navigation'] then Index.update()
|
||||||
when Conf['Watch']
|
when Conf['Watch']
|
||||||
|
return if g.VIEW is 'catalog'
|
||||||
ThreadWatcher.toggle thread
|
ThreadWatcher.toggle thread
|
||||||
# Images
|
# Images
|
||||||
when Conf['Expand image']
|
when Conf['Expand image']
|
||||||
|
return if g.VIEW is 'catalog'
|
||||||
Keybinds.img threadRoot
|
Keybinds.img threadRoot
|
||||||
when Conf['Expand images']
|
when Conf['Expand images']
|
||||||
|
return if g.VIEW is 'catalog'
|
||||||
Keybinds.img threadRoot, true
|
Keybinds.img threadRoot, true
|
||||||
when Conf['Open Gallery']
|
when Conf['Open Gallery']
|
||||||
|
return if g.VIEW is 'catalog'
|
||||||
Gallery.cb.toggle()
|
Gallery.cb.toggle()
|
||||||
when Conf['fappeTyme']
|
when Conf['fappeTyme']
|
||||||
|
return if g.VIEW is 'catalog'
|
||||||
FappeTyme.cb.toggle.call {name: 'fappe'}
|
FappeTyme.cb.toggle.call {name: 'fappe'}
|
||||||
when Conf['werkTyme']
|
when Conf['werkTyme']
|
||||||
|
return if g.VIEW is 'catalog'
|
||||||
FappeTyme.cb.toggle.call {name: 'werk'}
|
FappeTyme.cb.toggle.call {name: 'werk'}
|
||||||
# Board Navigation
|
# Board Navigation
|
||||||
when Conf['Front page']
|
when Conf['Front page']
|
||||||
if Conf['JSON Navigation'] and g.VIEW is 'index'
|
if Conf['JSON Navigation'] and g.VIEW is 'index'
|
||||||
Index.userPageNav 0
|
Index.userPageNav 1
|
||||||
else
|
else
|
||||||
window.location = "/#{g.BOARD}/"
|
window.location = "/#{g.BOARD}/"
|
||||||
when Conf['Open front page']
|
when Conf['Open front page']
|
||||||
@ -109,10 +117,11 @@ Keybinds =
|
|||||||
if form = $ '.prev form'
|
if form = $ '.prev form'
|
||||||
window.location = form.action
|
window.location = form.action
|
||||||
when Conf['Search form']
|
when Conf['Search form']
|
||||||
if Conf['JSON Navigation']
|
return unless g.VIEW is 'index'
|
||||||
Index.searchInput.focus()
|
searchInput = if Conf['JSON Navigation'] then Index.searchInput else $.id('search-box')
|
||||||
else
|
Header.scrollToIfNeeded searchInput
|
||||||
$.id('search-btn').click()
|
searchInput.click()
|
||||||
|
searchInput.focus()
|
||||||
when Conf['Paged mode']
|
when Conf['Paged mode']
|
||||||
return unless g.VIEW is 'index' and Conf['Index Mode'] isnt 'paged'
|
return unless g.VIEW is 'index' and Conf['Index Mode'] isnt 'paged'
|
||||||
Index.setIndexMode 'paged'
|
Index.setIndexMode 'paged'
|
||||||
@ -140,24 +149,30 @@ Keybinds =
|
|||||||
return if g.VIEW isnt 'index'
|
return if g.VIEW isnt 'index'
|
||||||
Nav.scroll -1
|
Nav.scroll -1
|
||||||
when Conf['Expand thread']
|
when Conf['Expand thread']
|
||||||
|
return if g.VIEW isnt 'index'
|
||||||
ExpandThread.toggle thread
|
ExpandThread.toggle thread
|
||||||
when Conf['Open thread']
|
when Conf['Open thread']
|
||||||
|
return if g.VIEW isnt 'index'
|
||||||
Keybinds.open thread
|
Keybinds.open thread
|
||||||
when Conf['Open thread tab']
|
when Conf['Open thread tab']
|
||||||
|
return if g.VIEW isnt 'index'
|
||||||
Keybinds.open thread, true
|
Keybinds.open thread, true
|
||||||
# Reply Navigation
|
# Reply Navigation
|
||||||
when Conf['Next reply']
|
when Conf['Next reply']
|
||||||
|
return if g.VIEW is 'catalog'
|
||||||
Keybinds.hl +1, threadRoot
|
Keybinds.hl +1, threadRoot
|
||||||
when Conf['Previous reply']
|
when Conf['Previous reply']
|
||||||
|
return if g.VIEW is 'catalog'
|
||||||
Keybinds.hl -1, threadRoot
|
Keybinds.hl -1, threadRoot
|
||||||
when Conf['Deselect reply']
|
when Conf['Deselect reply']
|
||||||
|
return if g.VIEW is 'catalog'
|
||||||
Keybinds.hl 0, threadRoot
|
Keybinds.hl 0, threadRoot
|
||||||
when Conf['Hide']
|
when Conf['Hide']
|
||||||
PostHiding.toggle thread.OP
|
PostHiding.toggle thread.OP
|
||||||
when Conf['Previous Post Quoting You']
|
when Conf['Previous Post Quoting You']
|
||||||
QuoteMarkers.cb.seek 'preceding'
|
QuoteYou.cb.seek 'preceding'
|
||||||
when Conf['Next Post Quoting You']
|
when Conf['Next Post Quoting You']
|
||||||
QuoteMarkers.cb.seek 'following'
|
QuoteYou.cb.seek 'following'
|
||||||
else
|
else
|
||||||
return
|
return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -191,10 +206,10 @@ Keybinds =
|
|||||||
if e.shiftKey then key = 'Shift+' + key
|
if e.shiftKey then key = 'Shift+' + key
|
||||||
key
|
key
|
||||||
|
|
||||||
qr: (thread, quote) ->
|
qr: (thread) ->
|
||||||
return unless Conf['Quick Reply'] and QR.postingIsEnabled
|
return unless Conf['Quick Reply'] and QR.postingIsEnabled
|
||||||
QR.open()
|
QR.open()
|
||||||
if quote
|
if thread?
|
||||||
QR.quote.call $ 'input', $('.post.highlight', thread) or thread
|
QR.quote.call $ 'input', $('.post.highlight', thread) or thread
|
||||||
QR.nodes.com.focus()
|
QR.nodes.com.focus()
|
||||||
if Conf['QR Shortcut']
|
if Conf['QR Shortcut']
|
||||||
|
|||||||
@ -49,6 +49,7 @@ ThreadStats =
|
|||||||
delete @postCountEl
|
delete @postCountEl
|
||||||
delete @fileCountEl
|
delete @fileCountEl
|
||||||
delete @pageCountEl
|
delete @pageCountEl
|
||||||
|
delete @dialog
|
||||||
|
|
||||||
Thread.callbacks.disconnect 'Thread Stats'
|
Thread.callbacks.disconnect 'Thread Stats'
|
||||||
$.off d, 'ThreadUpdate', ThreadStats.onUpdate
|
$.off d, 'ThreadUpdate', ThreadStats.onUpdate
|
||||||
@ -80,5 +81,5 @@ ThreadStats =
|
|||||||
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 - 1 then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
|
(if page.page is @response.length then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
|
||||||
return
|
return
|
||||||
|
|||||||
@ -21,10 +21,20 @@ QR.captcha =
|
|||||||
|
|
||||||
$.on input, 'blur', QR.focusout
|
$.on input, 'blur', QR.focusout
|
||||||
$.on input, 'focus', QR.focusin
|
$.on input, 'focus', QR.focusin
|
||||||
|
$.on input, 'keydown', QR.captcha.keydown.bind QR.captcha
|
||||||
|
$.on @nodes.img.parentNode, 'click', QR.captcha.reload.bind QR.captcha
|
||||||
|
|
||||||
$.addClass QR.nodes.el, 'has-captcha'
|
$.addClass QR.nodes.el, 'has-captcha'
|
||||||
$.after QR.nodes.com.parentNode, [imgContainer, input]
|
$.after QR.nodes.com.parentNode, [imgContainer, input]
|
||||||
|
|
||||||
|
@captchas = []
|
||||||
|
$.get 'captchas', [], ({captchas}) ->
|
||||||
|
QR.captcha.sync captchas
|
||||||
|
QR.captcha.clear()
|
||||||
|
$.sync 'captchas', @sync
|
||||||
|
|
||||||
|
new MutationObserver(@afterSetup).observe $.id('captchaContainer'), childList: true
|
||||||
|
|
||||||
@beforeSetup()
|
@beforeSetup()
|
||||||
@afterSetup() # reCAPTCHA might have loaded before the QR.
|
@afterSetup() # reCAPTCHA might have loaded before the QR.
|
||||||
|
|
||||||
@ -33,24 +43,26 @@ QR.captcha =
|
|||||||
img.parentNode.hidden = true
|
img.parentNode.hidden = true
|
||||||
input.value = ''
|
input.value = ''
|
||||||
input.placeholder = 'Focus to load reCAPTCHA'
|
input.placeholder = 'Focus to load reCAPTCHA'
|
||||||
|
@count()
|
||||||
$.on input, 'focus', @setup
|
$.on input, 'focus', @setup
|
||||||
@setupObserver = new MutationObserver @afterSetup
|
|
||||||
@setupObserver.observe $.id('captchaContainer'), childList: true
|
|
||||||
|
|
||||||
setup: ->
|
setup: ->
|
||||||
$.globalEval 'loadRecaptcha()'
|
$.globalEval 'loadRecaptcha()'
|
||||||
|
|
||||||
afterSetup: ->
|
afterSetup: ->
|
||||||
return unless challenge = $.id 'recaptcha_challenge_field_holder'
|
return unless challenge = $.id 'recaptcha_challenge_field_holder'
|
||||||
QR.captcha.setupObserver.disconnect()
|
return if challenge is QR.captcha.nodes.challenge
|
||||||
delete QR.captcha.setupObserver
|
|
||||||
|
setLifetime = (e) -> QR.captcha.lifetime = e.detail
|
||||||
|
$.on window, 'captcha:timeout', setLifetime
|
||||||
|
$.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
|
||||||
|
$.off window, 'captcha:timeout', setLifetime
|
||||||
|
|
||||||
{img, input} = QR.captcha.nodes
|
{img, input} = QR.captcha.nodes
|
||||||
img.parentNode.hidden = false
|
img.parentNode.hidden = false
|
||||||
input.placeholder = 'Verification'
|
input.placeholder = 'Verification'
|
||||||
$.off input, 'focus', QR.captcha.setup
|
QR.captcha.count()
|
||||||
$.on input, 'keydown', QR.captcha.keydown.bind QR.captcha
|
$.off input, 'focus', QR.captcha.setup
|
||||||
$.on img.parentNode, 'click', QR.captcha.reload.bind QR.captcha
|
|
||||||
|
|
||||||
QR.captcha.nodes.challenge = challenge
|
QR.captcha.nodes.challenge = challenge
|
||||||
new MutationObserver(QR.captcha.load.bind QR.captcha).observe challenge,
|
new MutationObserver(QR.captcha.load.bind QR.captcha).observe challenge,
|
||||||
@ -63,32 +75,83 @@ QR.captcha =
|
|||||||
$.globalEval 'Recaptcha.destroy()'
|
$.globalEval 'Recaptcha.destroy()'
|
||||||
@beforeSetup()
|
@beforeSetup()
|
||||||
|
|
||||||
|
sync: (captchas) ->
|
||||||
|
QR.captcha.captchas = captchas
|
||||||
|
QR.captcha.count()
|
||||||
|
|
||||||
getOne: ->
|
getOne: ->
|
||||||
challenge = @nodes.img.alt
|
@clear()
|
||||||
response = @nodes.input.value.trim()
|
if captcha = @captchas.shift()
|
||||||
if response and !/\s/.test response
|
{challenge, response} = captcha
|
||||||
|
@count()
|
||||||
|
$.set 'captchas', @captchas
|
||||||
|
else
|
||||||
|
challenge = @nodes.img.alt
|
||||||
|
if response = @nodes.input.value
|
||||||
|
if Conf['Auto-load captcha'] then @reload() else @destroy()
|
||||||
|
if response
|
||||||
|
response = response.trim()
|
||||||
# one-word-captcha:
|
# one-word-captcha:
|
||||||
# If there's only one word, duplicate it.
|
# If there's only one word, duplicate it.
|
||||||
response = "#{response} #{response}"
|
response = "#{response} #{response}" unless /\s/.test response
|
||||||
{challenge, response}
|
{challenge, response}
|
||||||
|
|
||||||
|
save: ->
|
||||||
|
return unless response = @nodes.input.value.trim()
|
||||||
|
@nodes.input.value = ''
|
||||||
|
@captchas.push
|
||||||
|
challenge: @nodes.img.alt
|
||||||
|
response: response
|
||||||
|
timeout: @timeout
|
||||||
|
@count()
|
||||||
|
@reload()
|
||||||
|
$.set 'captchas', @captchas
|
||||||
|
|
||||||
|
clear: ->
|
||||||
|
return unless @captchas.length
|
||||||
|
now = Date.now()
|
||||||
|
for captcha, i in @captchas
|
||||||
|
break if captcha.timeout > now
|
||||||
|
return unless i
|
||||||
|
@captchas = @captchas[i..]
|
||||||
|
@count()
|
||||||
|
$.set 'captchas', @captchas
|
||||||
|
|
||||||
load: ->
|
load: ->
|
||||||
return unless @nodes.challenge.firstChild
|
return unless @nodes.challenge.firstChild
|
||||||
|
return unless challenge_image = $.id 'recaptcha_challenge_image'
|
||||||
# -1 minute to give upload some time.
|
# -1 minute to give upload some time.
|
||||||
|
@timeout = Date.now() + @lifetime * $.SECOND - $.MINUTE
|
||||||
challenge = @nodes.challenge.firstChild.value
|
challenge = @nodes.challenge.firstChild.value
|
||||||
@nodes.img.alt = challenge
|
@nodes.img.alt = challenge
|
||||||
@nodes.img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}"
|
@nodes.img.src = challenge_image.src
|
||||||
@nodes.input.value = null
|
@nodes.input.value = null
|
||||||
|
@clear()
|
||||||
|
|
||||||
|
count: ->
|
||||||
|
count = if @captchas then @captchas.length else 0
|
||||||
|
placeholder = @nodes.input.placeholder.replace /\ \(.*\)$/, ''
|
||||||
|
placeholder += switch count
|
||||||
|
when 0
|
||||||
|
if placeholder is 'Verification' then ' (Shift + Enter to cache)' else ''
|
||||||
|
when 1
|
||||||
|
' (1 cached captcha)'
|
||||||
|
else
|
||||||
|
" (#{count} cached captchas)"
|
||||||
|
@nodes.input.placeholder = placeholder
|
||||||
|
@nodes.input.alt = count # For XTRM RICE.
|
||||||
|
|
||||||
reload: (focus) ->
|
reload: (focus) ->
|
||||||
# the 't' argument prevents the input from being focused
|
# Hack to prevent the input from being focused
|
||||||
$.globalEval 'Recaptcha.reload("t")'
|
$.globalEval 'Recaptcha.reload(); Recaptcha.should_focus = false;'
|
||||||
# Focus if we meant to.
|
# Focus if we meant to.
|
||||||
@nodes.input.focus() if focus
|
@nodes.input.focus() if focus
|
||||||
|
|
||||||
keydown: (e) ->
|
keydown: (e) ->
|
||||||
if e.keyCode is 8 and not @nodes.input.value
|
if e.keyCode is 8 and not @nodes.input.value
|
||||||
@reload()
|
@reload()
|
||||||
|
else if e.keyCode is 13 and e.shiftKey
|
||||||
|
@save()
|
||||||
else
|
else
|
||||||
return
|
return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
QR =
|
QR =
|
||||||
|
mimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash', 'video/webm']
|
||||||
|
|
||||||
init: ->
|
init: ->
|
||||||
return if !Conf['Quick Reply']
|
return if !Conf['Quick Reply']
|
||||||
|
|
||||||
@ -49,7 +51,7 @@ QR =
|
|||||||
if Conf['QR Shortcut']
|
if Conf['QR Shortcut']
|
||||||
$.rmClass $('.qr-shortcut'), 'disabled'
|
$.rmClass $('.qr-shortcut'), 'disabled'
|
||||||
|
|
||||||
$.before $.id('postForm'), link
|
$.before $.id('togglePostForm') or $.id('postForm'), link
|
||||||
|
|
||||||
$.on d, 'QRGetSelectedPost', ({detail: cb}) ->
|
$.on d, 'QRGetSelectedPost', ({detail: cb}) ->
|
||||||
cb QR.selected
|
cb QR.selected
|
||||||
@ -117,6 +119,8 @@ QR =
|
|||||||
post.delete()
|
post.delete()
|
||||||
QR.cooldown.auto = false
|
QR.cooldown.auto = false
|
||||||
QR.status()
|
QR.status()
|
||||||
|
if QR.captcha.isEnabled and not Conf['Auto-load captcha']
|
||||||
|
QR.captcha.destroy()
|
||||||
focusin: ->
|
focusin: ->
|
||||||
$.addClass QR.nodes.el, 'focus'
|
$.addClass QR.nodes.el, 'focus'
|
||||||
focusout: ->
|
focusout: ->
|
||||||
@ -142,9 +146,10 @@ QR =
|
|||||||
el = err
|
el = err
|
||||||
el.removeAttribute 'style'
|
el.removeAttribute 'style'
|
||||||
if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
|
if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
|
||||||
# Focus the captcha input on captcha error.
|
if QR.captcha.captchas.length is 0
|
||||||
QR.captcha.nodes.input.focus()
|
# Focus the captcha input on captcha error.
|
||||||
QR.captcha.setup()
|
QR.captcha.nodes.input.focus()
|
||||||
|
QR.captcha.setup()
|
||||||
if Conf['Captcha Warning Notifications'] and !d.hidden
|
if Conf['Captcha Warning Notifications'] and !d.hidden
|
||||||
QR.notify el
|
QR.notify el
|
||||||
else
|
else
|
||||||
@ -289,55 +294,43 @@ QR =
|
|||||||
QR.handleFiles files
|
QR.handleFiles files
|
||||||
$.addClass QR.nodes.el, 'dump'
|
$.addClass QR.nodes.el, 'dump'
|
||||||
|
|
||||||
handleBlob: (urlBlob, header, url) ->
|
handleBlob: (urlBlob, contentType, contentDisposition, url) ->
|
||||||
name = url.substr(url.lastIndexOf('/')+1, url.length)
|
name = url.match(/([^\/]+)\/*$/)?[1]
|
||||||
#QUALITY coding at work
|
mime = contentType?.match(/[^;]*/)[0] or 'application/octet-stream'
|
||||||
start = header.indexOf("Content-Type: ") + 14
|
match =
|
||||||
endsc = header.substr(start, header.length).indexOf(";")
|
contentDisposition?.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)?[1] or
|
||||||
endnl = header.substr(start, header.length).indexOf("\n") - 1
|
contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1]
|
||||||
end = endnl
|
if match
|
||||||
if (endsc != -1 and endsc < endnl)
|
name = match.replace /\\"/g, '"'
|
||||||
end = endsc
|
|
||||||
mime = header.substr(start, end)
|
|
||||||
blob = new Blob([urlBlob], {type: mime})
|
blob = new Blob([urlBlob], {type: mime})
|
||||||
blob.name = url.substr(url.lastIndexOf('/')+1, url.length)
|
blob.name = name
|
||||||
name_start = header.indexOf('name="') + 6
|
|
||||||
if (name_start - 6 != -1)
|
|
||||||
name_end = header.substr(name_start, header.length).indexOf('"')
|
|
||||||
blob.name = header.substr(name_start, name_end)
|
|
||||||
|
|
||||||
return if blob.type is null
|
|
||||||
QR.error "Unsupported file type."
|
|
||||||
QR.handleFiles([blob])
|
QR.handleFiles([blob])
|
||||||
|
|
||||||
handleUrl: ->
|
handleUrl: ->
|
||||||
url = prompt("Insert an url:")
|
url = prompt("Insert an url:")
|
||||||
return if url is null
|
return if url is null
|
||||||
|
|
||||||
<% if (type === 'crx') { %>
|
<% if (type === 'crx') { %>
|
||||||
xhr = new XMLHttpRequest();
|
xhr = new XMLHttpRequest();
|
||||||
xhr.open('GET', url, true)
|
xhr.open('GET', url, true)
|
||||||
xhr.responseType = 'blob'
|
xhr.responseType = 'blob'
|
||||||
xhr.onload = (e) ->
|
xhr.onload = (e) ->
|
||||||
if @readyState is @DONE && xhr.status is 200
|
if @readyState is @DONE && xhr.status is 200
|
||||||
QR.handleBlob(@response, @getResponseHeader('Content-Type'), url)
|
contentType = @getResponseHeader('Content-Type')
|
||||||
return
|
contentDisposition = @getResponseHeader('Content-Disposition')
|
||||||
|
QR.handleBlob @response, contentType, contentDisposition, url
|
||||||
else
|
else
|
||||||
QR.error "Can't load image."
|
QR.error "Can't load image."
|
||||||
return
|
|
||||||
|
|
||||||
xhr.onerror = (e) ->
|
xhr.onerror = (e) ->
|
||||||
QR.error "Can't load image."
|
QR.error "Can't load image."
|
||||||
return
|
|
||||||
|
|
||||||
xhr.send()
|
xhr.send()
|
||||||
return
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (type === 'userscript') { %>
|
<% if (type === 'userscript') { %>
|
||||||
GM_xmlhttpRequest {
|
GM_xmlhttpRequest
|
||||||
method: "GET",
|
method: "GET"
|
||||||
url: url,
|
url: url
|
||||||
overrideMimeType: "text/plain; charset=x-user-defined",
|
overrideMimeType: "text/plain; charset=x-user-defined"
|
||||||
onload: (xhr) ->
|
onload: (xhr) ->
|
||||||
r = xhr.responseText
|
r = xhr.responseText
|
||||||
data = new Uint8Array(r.length)
|
data = new Uint8Array(r.length)
|
||||||
@ -345,14 +338,11 @@ QR =
|
|||||||
while i < r.length
|
while i < r.length
|
||||||
data[i] = r.charCodeAt(i)
|
data[i] = r.charCodeAt(i)
|
||||||
i++
|
i++
|
||||||
|
contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1]
|
||||||
QR.handleBlob(data, xhr.responseHeaders, url)
|
contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1]
|
||||||
return
|
QR.handleBlob data, contentType, contentDisposition, url
|
||||||
|
onerror: (xhr) ->
|
||||||
onerror: (xhr) ->
|
QR.error "Can't load image."
|
||||||
QR.error "Can't load image."
|
|
||||||
}
|
|
||||||
return
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
handleFiles: (files) ->
|
handleFiles: (files) ->
|
||||||
@ -360,40 +350,96 @@ QR =
|
|||||||
files = [@files...]
|
files = [@files...]
|
||||||
@value = null
|
@value = null
|
||||||
return unless files.length
|
return unless files.length
|
||||||
max = QR.nodes.fileInput.max
|
|
||||||
isSingle = files.length is 1
|
|
||||||
QR.cleanNotifications()
|
QR.cleanNotifications()
|
||||||
for file in files
|
for file, i in files
|
||||||
if file.type is 'application/x-shockwave-flash'
|
QR.handleFile file, i, files.length
|
||||||
QR.handleFile(file, isSingle, max)
|
$.addClass QR.nodes.el, 'dump' unless files.length is 1
|
||||||
else
|
|
||||||
QR.checkDimensions file, isSingle, max
|
|
||||||
$.addClass QR.nodes.el, 'dump' unless isSingle
|
|
||||||
|
|
||||||
checkDimensions: (file, isSingle, max) ->
|
handleFile: (file, index, nfiles) ->
|
||||||
|
if /^text\//.test file.type
|
||||||
|
if nfiles is 1
|
||||||
|
post = QR.selected
|
||||||
|
else if index isnt 0 or (post = QR.posts[QR.posts.length - 1]).com
|
||||||
|
post = new QR.post()
|
||||||
|
post.pasteText file
|
||||||
|
return
|
||||||
|
unless file.type in QR.mimeTypes
|
||||||
|
QR.error "#{file.name}: Unsupported file type."
|
||||||
|
return unless nfiles is 1
|
||||||
|
max = QR.nodes.fileInput.max
|
||||||
|
max = Math.min(max, QR.max_size_video) if /^video\//.test file.type
|
||||||
|
if file.size > max
|
||||||
|
QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
|
||||||
|
return unless nfiles is 1
|
||||||
|
isNewPost = false
|
||||||
|
if nfiles is 1
|
||||||
|
post = QR.selected
|
||||||
|
else if index isnt 0 or (post = QR.posts[QR.posts.length - 1]).file
|
||||||
|
isNewPost = true
|
||||||
|
post = new QR.post()
|
||||||
|
if /^text/.test file.type
|
||||||
|
return post.pasteText file
|
||||||
|
else
|
||||||
|
post.setFile file
|
||||||
|
QR.checkDimensions file, (pass) ->
|
||||||
|
if pass or nfiles is 1
|
||||||
|
post.setFile file
|
||||||
|
else if isNewPost
|
||||||
|
post.rm()
|
||||||
|
|
||||||
|
checkDimensions: (file, cb) ->
|
||||||
if /^image\//.test file.type
|
if /^image\//.test file.type
|
||||||
img = new Image()
|
img = new Image()
|
||||||
img.onload = =>
|
img.onload = =>
|
||||||
{height, width} = img
|
{height, width} = img
|
||||||
return QR.error "#{file.name}: Image too large (image: #{img.height}x#{img.width}px, max: #{QR.max_heigth}x#{QR.max_width}px)" if height > QR.max_heigth or width > QR.max_heigth
|
pass = true
|
||||||
return QR.error "#{file.name}: Image too small (image: #{img.height}x#{img.width}px, min: #{QR.min_heigth}x#{QR.min_width}px)" if height < QR.min_heigth or width < QR.min_heigth
|
if height > QR.max_height or width > QR.max_width
|
||||||
QR.handleFile file, isSingle, max
|
QR.error "#{file.name}: Image too large (image: #{height}x#{width}px, max: #{QR.max_height}x#{QR.max_width}px)"
|
||||||
|
pass = false
|
||||||
|
if height < QR.min_height or width < QR.min_width
|
||||||
|
QR.error "#{file.name}: Image too small (image: #{height}x#{width}px, min: #{QR.min_height}x#{QR.min_width}px)"
|
||||||
|
pass = false
|
||||||
|
cb pass
|
||||||
img.src = URL.createObjectURL file
|
img.src = URL.createObjectURL file
|
||||||
|
else if /^video\//.test file.type
|
||||||
|
video = $.el 'video'
|
||||||
|
$.on video, 'loadedmetadata', =>
|
||||||
|
return unless cb?
|
||||||
|
{videoHeight, videoWidth, duration} = video
|
||||||
|
max_height = Math.min(QR.max_height, QR.max_height_video)
|
||||||
|
max_width = Math.min(QR.max_width, QR.max_width_video)
|
||||||
|
pass = true
|
||||||
|
if videoHeight > max_height or videoWidth > max_width
|
||||||
|
QR.error "#{file.name}: Video too large (video: #{videoHeight}x#{videoWidth}px, max: #{max_height}x#{max_width}px)"
|
||||||
|
pass = false
|
||||||
|
if videoHeight < QR.min_height or videoWidth < QR.min_width
|
||||||
|
QR.error "#{file.name}: Video too small (video: #{videoHeight}x#{videoWidth}px, min: #{QR.min_height}x#{QR.min_width}px)"
|
||||||
|
pass = false
|
||||||
|
unless isFinite video.duration
|
||||||
|
QR.error "#{file.name}: Video lacks duration metadata (try remuxing)"
|
||||||
|
pass = false
|
||||||
|
if duration > QR.max_duration_video
|
||||||
|
QR.error "#{file.name}: Video too long (video: #{duration}s, max: #{QR.max_duration_video}s)"
|
||||||
|
pass = false
|
||||||
|
<% if (type === 'userscript') { %>
|
||||||
|
if video.mozHasAudio
|
||||||
|
QR.error "#{file.name}: Audio not allowed"
|
||||||
|
pass = false
|
||||||
|
<% } %>
|
||||||
|
cb pass
|
||||||
|
cb = null
|
||||||
|
$.on video, 'error', =>
|
||||||
|
return unless cb?
|
||||||
|
if file.type in QR.mimeTypes
|
||||||
|
# only report error here if we should have been able to play the video
|
||||||
|
# otherwise "unsupported type" should already have been shown
|
||||||
|
QR.error "#{file.name}: Video appears corrupt"
|
||||||
|
cb false
|
||||||
|
cb = null
|
||||||
|
video.src = URL.createObjectURL file
|
||||||
else
|
else
|
||||||
QR.handleFile file, isSingle, max
|
cb true
|
||||||
|
|
||||||
handleFile: (file, isSingle, max) ->
|
|
||||||
if file.size > max
|
|
||||||
QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
|
|
||||||
return
|
|
||||||
if isSingle
|
|
||||||
post = QR.selected
|
|
||||||
else if (post = QR.posts[QR.posts.length - 1]).file
|
|
||||||
post = new QR.post()
|
|
||||||
if /^text/.test file.type
|
|
||||||
post.pasteText file
|
|
||||||
else
|
|
||||||
post.setFile file
|
|
||||||
openFileInput: (e) ->
|
openFileInput: (e) ->
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if e.shiftKey and e.type is 'click'
|
if e.shiftKey and e.type is 'click'
|
||||||
@ -458,16 +504,22 @@ QR =
|
|||||||
setNode 'fileInput', '[type=file]'
|
setNode 'fileInput', '[type=file]'
|
||||||
|
|
||||||
rules = $('ul.rules').textContent.trim()
|
rules = $('ul.rules').textContent.trim()
|
||||||
QR.min_width = QR.min_heigth = 1
|
QR.min_width = QR.min_height = 1
|
||||||
QR.max_width = QR.max_heigth = 5000
|
QR.max_width = QR.max_height = 10000
|
||||||
try
|
try
|
||||||
[_, QR.min_width, QR.min_heigth] = rules.match(/.+smaller than (\d+)x(\d+).+/)
|
[_, QR.min_width, QR.min_height] = rules.match(/.+smaller than (\d+)x(\d+).+/)
|
||||||
[_, QR.max_width, QR.max_heigth] = rules.match(/.+greater than (\d+)x(\d+).+/)
|
[_, QR.max_width, QR.max_height] = rules.match(/.+greater than (\d+)x(\d+).+/)
|
||||||
|
for prop in ['min_width', 'min_height', 'max_width', 'max_height']
|
||||||
|
QR[prop] = parseInt QR[prop], 10
|
||||||
catch
|
catch
|
||||||
null
|
null
|
||||||
|
|
||||||
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
|
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
|
||||||
|
|
||||||
|
QR.max_size_video = 3145728
|
||||||
|
QR.max_width_video = QR.max_height_video = 2048
|
||||||
|
QR.max_duration_video = 120
|
||||||
|
|
||||||
QR.spoiler = !!$ 'input[name=spoiler]'
|
QR.spoiler = !!$ 'input[name=spoiler]'
|
||||||
if QR.spoiler
|
if QR.spoiler
|
||||||
$.addClass QR.nodes.el, 'has-spoiler'
|
$.addClass QR.nodes.el, 'has-spoiler'
|
||||||
@ -674,9 +726,6 @@ QR =
|
|||||||
onerror: ->
|
onerror: ->
|
||||||
# Connection error, or www.4chan.org/banned
|
# Connection error, or www.4chan.org/banned
|
||||||
delete QR.req
|
delete QR.req
|
||||||
if QR.captcha.isEnabled
|
|
||||||
QR.captcha.destroy()
|
|
||||||
QR.captcha.setup()
|
|
||||||
post.unlock()
|
post.unlock()
|
||||||
QR.cooldown.auto = false
|
QR.cooldown.auto = false
|
||||||
QR.status()
|
QR.status()
|
||||||
@ -710,7 +759,6 @@ QR =
|
|||||||
{req} = QR
|
{req} = QR
|
||||||
delete QR.req
|
delete QR.req
|
||||||
|
|
||||||
QR.captcha.destroy() if QR.captcha.isEnabled
|
|
||||||
post = QR.posts[0]
|
post = QR.posts[0]
|
||||||
post.unlock()
|
post.unlock()
|
||||||
|
|
||||||
@ -742,12 +790,23 @@ QR =
|
|||||||
err = 'You seem to have mistyped the CAPTCHA.'
|
err = 'You seem to have mistyped the CAPTCHA.'
|
||||||
else if /expired/i.test err.textContent
|
else if /expired/i.test err.textContent
|
||||||
err = 'This CAPTCHA is no longer valid because it has expired.'
|
err = 'This CAPTCHA is no longer valid because it has expired.'
|
||||||
QR.cooldown.auto = false
|
# Enable auto-post if we have some cached captchas.
|
||||||
|
QR.cooldown.auto = if QR.captcha.isEnabled
|
||||||
|
!!QR.captcha.captchas.length
|
||||||
|
else if err is 'Connection error with sys.4chan.org.'
|
||||||
|
true
|
||||||
|
else
|
||||||
|
# Something must've gone terribly wrong if you get captcha errors without captchas.
|
||||||
|
# Don't auto-post indefinitely in that case.
|
||||||
|
false
|
||||||
# Too many frequent mistyped captchas will auto-ban you!
|
# Too many frequent mistyped captchas will auto-ban you!
|
||||||
# On connection error, the post most likely didn't go through.
|
# On connection error, the post most likely didn't go through.
|
||||||
QR.cooldown.set delay: 2
|
QR.cooldown.set delay: 2
|
||||||
else if err.textContent and m = err.textContent.match /wait\s+(\d+)\s+second/i
|
else if err.textContent and m = err.textContent.match /wait\s+(\d+)\s+second/i
|
||||||
QR.cooldown.auto = !QR.captcha.isEnabled
|
QR.cooldown.auto = if QR.captcha.isEnabled
|
||||||
|
!!QR.captcha.captchas.length
|
||||||
|
else
|
||||||
|
true
|
||||||
QR.cooldown.set delay: m[1]
|
QR.cooldown.set delay: m[1]
|
||||||
else # stop auto-posting
|
else # stop auto-posting
|
||||||
QR.cooldown.auto = false
|
QR.cooldown.auto = false
|
||||||
@ -788,12 +847,23 @@ QR =
|
|||||||
# Enable auto-posting if we have stuff left to post, disable it otherwise.
|
# Enable auto-posting if we have stuff left to post, disable it otherwise.
|
||||||
postsCount = QR.posts.length - 1
|
postsCount = QR.posts.length - 1
|
||||||
QR.cooldown.auto = postsCount and isReply
|
QR.cooldown.auto = postsCount and isReply
|
||||||
QR.captcha.setup() if QR.captcha.isEnabled and QR.cooldown.auto
|
if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount
|
||||||
|
notif = new Notification 'Quick reply warning',
|
||||||
|
body: "You are running low on cached captchas. Cache count: #{captchasCount}."
|
||||||
|
icon: Favicon.logo
|
||||||
|
notif.onclick = ->
|
||||||
|
QR.open()
|
||||||
|
QR.captcha.nodes.input.focus()
|
||||||
|
window.focus()
|
||||||
|
notif.onshow = ->
|
||||||
|
setTimeout ->
|
||||||
|
notif.close()
|
||||||
|
, 7 * $.SECOND
|
||||||
|
|
||||||
unless Conf['Persistent QR'] or QR.cooldown.auto
|
unless Conf['Persistent QR'] or QR.cooldown.auto
|
||||||
QR.close()
|
QR.close()
|
||||||
else
|
else
|
||||||
if QR.posts.length > 1
|
if QR.posts.length > 1 and QR.captcha.isEnabled and QR.captcha.captchas.length is 0
|
||||||
QR.captcha.setup()
|
QR.captcha.setup()
|
||||||
post.rm()
|
post.rm()
|
||||||
|
|
||||||
|
|||||||
@ -91,8 +91,6 @@ QR.post = class
|
|||||||
return unless @ is QR.selected
|
return unless @ is QR.selected
|
||||||
for name in ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'] when node = QR.nodes[name]
|
for name in ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'] when node = QR.nodes[name]
|
||||||
node.disabled = lock
|
node.disabled = lock
|
||||||
if QR.captcha.isEnabled
|
|
||||||
QR.captcha.nodes.input.disabled = lock
|
|
||||||
@nodes.rm.style.visibility = if lock then 'hidden' else ''
|
@nodes.rm.style.visibility = if lock then 'hidden' else ''
|
||||||
(if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
|
(if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
|
||||||
@nodes.spoiler.disabled = lock
|
@nodes.spoiler.disabled = lock
|
||||||
@ -167,27 +165,32 @@ QR.post = class
|
|||||||
@showFileData()
|
@showFileData()
|
||||||
else
|
else
|
||||||
@updateFilename()
|
@updateFilename()
|
||||||
unless /^image/.test file.type
|
unless /^(image|video)\//.test file.type
|
||||||
@nodes.el.style.backgroundImage = null
|
@nodes.el.style.backgroundImage = null
|
||||||
return
|
return
|
||||||
@setThumbnail()
|
@setThumbnail()
|
||||||
|
|
||||||
setThumbnail: ->
|
setThumbnail: ->
|
||||||
# Create a redimensioned thumbnail.
|
# Create a redimensioned thumbnail.
|
||||||
img = $.el 'img'
|
isVideo = /^video\//.test @file.type
|
||||||
|
img = $.el (if isVideo then 'video' else 'img')
|
||||||
|
|
||||||
img.onload = =>
|
$.on img, (if isVideo then 'loadeddata' else 'load'), =>
|
||||||
# Generate thumbnails only if they're really big.
|
# Generate thumbnails only if they're really big.
|
||||||
# Resized pictures through canvases look like ass,
|
# Resized pictures through canvases look like ass,
|
||||||
# so we generate thumbnails `s` times bigger then expected
|
# so we generate thumbnails `s` times bigger then expected
|
||||||
# to avoid crappy resized quality.
|
# to avoid crappy resized quality.
|
||||||
s = 90 * 2 * window.devicePixelRatio
|
s = 90 * 2 * window.devicePixelRatio
|
||||||
s *= 3 if @file.type is 'image/gif' # let them animate
|
s *= 3 if @file.type is 'image/gif' # let them animate
|
||||||
{height, width} = img
|
if isVideo
|
||||||
if height < s or width < s
|
height = img.videoHeight
|
||||||
@URL = fileURL
|
width = img.videoWidth
|
||||||
@nodes.el.style.backgroundImage = "url(#{@URL})"
|
else
|
||||||
return
|
{height, width} = img
|
||||||
|
if height < s or width < s
|
||||||
|
@URL = fileURL
|
||||||
|
@nodes.el.style.backgroundImage = "url(#{@URL})"
|
||||||
|
return
|
||||||
if height <= width
|
if height <= width
|
||||||
width = s / height * width
|
width = s / height * width
|
||||||
height = s
|
height = s
|
||||||
|
|||||||
@ -97,7 +97,7 @@ QuoteThreading =
|
|||||||
if post = posts[post.ID]
|
if post = posts[post.ID]
|
||||||
posts.after post, posts[@ID]
|
posts.after post, posts[@ID]
|
||||||
|
|
||||||
else
|
else if posts[@ID]
|
||||||
posts.prepend posts[@ID]
|
posts.prepend posts[@ID]
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -128,4 +128,4 @@ QuoteThreading =
|
|||||||
kb: ->
|
kb: ->
|
||||||
control = $.id 'threadingControl'
|
control = $.id 'threadingControl'
|
||||||
control.checked = not control.checked
|
control.checked = not control.checked
|
||||||
QuoteThreading.toggle.call control
|
QuoteThreading.toggle.call control
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user