Merge branch 'v3'

Conflicts:
	CHANGELOG.md
	LICENSE
	README.md
	builds/appchan-x.user.js
	builds/crx/manifest.json
	builds/crx/script.js
	package.json
	src/General/Header.coffee
	src/General/Main.coffee
	src/General/css/font-awesome.css
	src/General/html/Features/Index-navlinks.html
	src/Miscellaneous/ExpandComment.coffee
	src/Miscellaneous/Keybinds.coffee
	src/Monitoring/ThreadStats.coffee
	src/Posting/QR.captcha.coffee
	src/Posting/QR.coffee
	src/Theming/Banner.coffee
This commit is contained in:
Zixaphir 2014-05-03 06:20:04 -07:00
commit c04c83f915
35 changed files with 1355 additions and 672 deletions

View File

@ -1,3 +1,29 @@
**ccd0**
- Update due to more Recaptcha changes.
- 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.
- WebM files are checked for audio before posting (Firefox only).
- Max resolution updated, now 10000x10000.
- Check dimensions and duration of .webm files before posting.
- 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.
- Thumbnails for .webm files in Quick Reply.
- Revert captcha fixes of 1.4.2 as Google appears to have reverted the changes on its end. This restores captcha caching.
- Quick fix for moot breaking captcha.
- Restore `Comment Expansion`.
- Another update to handle HTML changes.
- Use new URLs.
- Bugfixes.
**fgts**
- Update archive list.
**MayhemYDG**
- Update 4chan namespaces support.
- Better handling of webm playback errors.
- Bugfixes
**woxxy**
- Remove /v/ from stable Foolz archive.
### v2.9.20 ### v2.9.20
*2014-04-20* *2014-04-20*

View File

@ -94,10 +94,6 @@ module.exports = (grunt) ->
push: false push: false
shell: shell:
options:
stdout: true
stderr: true
failOnError: true
checkout: checkout:
command: 'git checkout <%= pkg.meta.mainBranch %>' command: 'git checkout <%= pkg.meta.mainBranch %>'
commit: commit:

View File

@ -1,5 +1,5 @@
/* /*
* appchan x - Version 2.9.20 - 2014-05-02 * appchan x - Version 2.9.20 - 2014-05-03
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE

View File

@ -1,4 +1,11 @@
<<<<<<< HEAD
# Get Appchan X [HERE](http://zixaphir.github.io/appchan-x/). # Get Appchan X [HERE](http://zixaphir.github.io/appchan-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
>>>>>>> v3
1. Make sure both your **browser** and **Appchan X** are up to date. 1. Make sure both your **browser** and **Appchan X** are up to date.
2. Disable your other extensions & scripts to identify conflicts. 2. Disable your other extensions & scripts to identify conflicts.

View File

@ -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 it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

View File

@ -105,7 +105,7 @@ a[href="javascript:;"] {
:root.bottom-header body { :root.bottom-header body {
margin-bottom: 2em; margin-bottom: 2em;
} }
body > .desktop:not(#boardNavDesktop):not(#boardNavDesktopFoot), body > .desktop:not(hr):not(.navLinks):not(#boardNavDesktop):not(#boardNavDesktopFoot),
:root.fourchan-x #navtopright, :root.fourchan-x #navtopright,
:root.fourchan-x #navbotright, :root.fourchan-x #navbotright,
:root.fourchan-x:not(.show-original-top-board-list) #boardNavDesktop, :root.fourchan-x:not(.show-original-top-board-list) #boardNavDesktop,
@ -725,8 +725,7 @@ a.hide-announcement {
/* QR */ /* QR */
:root.hide-original-post-form #postForm, :root.hide-original-post-form #postForm,
:root.hide-original-post-form .postingMode, :root.hide-original-post-form #togglePostFormLink,
:root.hide-original-post-form #togglePostForm,
#qr.autohide:not(.has-focus):not(:hover) > form { #qr.autohide:not(.has-focus):not(:hover) > form {
display: none; display: none;
} }

View File

@ -37,7 +37,7 @@
"grunt-contrib-concat": "~0.4.0", "grunt-contrib-concat": "~0.4.0",
"grunt-contrib-copy": "~0.5.0", "grunt-contrib-copy": "~0.5.0",
"grunt-contrib-watch": "~0.6.1", "grunt-contrib-watch": "~0.6.1",
"grunt-shell": "~0.6.4", "grunt-shell": "~0.7.0",
"load-grunt-tasks": "~0.4.0" "load-grunt-tasks": "~0.4.0"
}, },
"repository": { "repository": {

View File

@ -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",
@ -123,6 +123,6 @@
"https": true, "https": true,
"withCredentials": true, "withCredentials": true,
"software": "foolfuuka", "software": "foolfuuka",
"boards": ["a", "biz", "co", "d", "diy", "gd", "jp", "m", "s4s", "sci", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"], "boards": ["a", "biz", "co", "d", "diy", "gd", "jp", "m", "s4s", "sci", "sp", "tg", "tv", "u", "vg", "vp", "vr", "wsg"],
"files": ["a", "biz", "d", "diy", "gd", "jp", "m", "s4s", "sci", "tg", "u", "vg", "vp", "vr", "wsg"] "files": ["a", "biz", "d", "diy", "gd", "jp", "m", "s4s", "sci", "tg", "u", "vg", "vp", "vr", "wsg"]
}] }]

View File

@ -15,6 +15,11 @@ Build =
thumbRotate: do -> thumbRotate: do ->
n = 0 n = 0
-> n = (n + 1) % 3 -> n = (n + 1) % 3
path: (boardID, threadID, postID, fragment) ->
path = "/#{boardID}/thread/#{threadID}"
path += "/#{g.SLUG}" if g.SLUG? and threadID is g.THREADID
path += "##{fragment or 'p'}#{postID}" if postID
path
postFromObject: (data, boardID) -> postFromObject: (data, boardID) ->
o = o =
# id # id
@ -178,11 +183,12 @@ Build =
'' ''
if isOP and g.VIEW is 'index' if isOP and g.VIEW is 'index'
pageNum = Index.liveThreadData.keys.indexOf("#{postID}") // Index.threadsNumPerPage pageNum = Index.liveThreadData.keys.indexOf("#{postID}") // Index.threadsNumPerPage + 1
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 = " &nbsp; <span>[<a href='/#{boardID}/thread/#{threadID}' class=replylink>Reply</a>]</span>" replyLink = " &nbsp; <span>[<a href='#{Build.path boardID, threadID}' class=replylink>Reply</a>]</span>"
else else
pageIcon = replyLink = '' pageIcon = ''
replyLink = ''
container = $.el 'div', container = $.el 'div',
id: "pc#{postID}" id: "pc#{postID}"
@ -208,13 +214,13 @@ Build =
' </span> ' + ' </span> ' +
"<span class=dateTime data-utc=#{dateUTC}>#{date}</span> " + "<span class=dateTime data-utc=#{dateUTC}>#{date}</span> " +
"<span class='postNum'>" + "<span class='postNum'>" +
"<a href=#{"/#{boardID}/thread/#{threadID}#p#{postID}"} title='Highlight this post'>No.</a>" + "<a href=#{Build.path boardID, threadID, postID} title='Link to this post'>No.</a>" +
"<a href='#{ "<a href='#{
if g.VIEW is 'thread' and g.THREADID is threadID if g.VIEW is 'thread' and g.THREADID is threadID
"javascript:quote(#{postID})" "javascript:quote(#{postID})"
else else
"/#{boardID}/thread/#{threadID}#q#{postID}" Build.path boardID, threadID, postID, 'q'
}' title='Quote this post'>#{postID}</a>" + }' title='Reply to this post'>#{postID}</a>" +
pageIcon + sticky + closed + replyLink + pageIcon + sticky + closed + replyLink +
'</span>' + '</span>' +
'</div>' + '</div>' +
@ -225,11 +231,11 @@ Build =
'</div>' '</div>'
# Fix quote pathnames in index or cross-{board,thread} posts
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 unless href[0] is '#'
href = "#{threadID}#{href}" if href[0] is '#' quote.href = Build.path boardID, threadID, href[2..]
quote.href = "/#{boardID}/thread/#{href}" # Fix pathnames
container container
@ -241,7 +247,7 @@ Build =
$.el 'a', $.el 'a',
className: 'summary' className: 'summary'
textContent: text.join ' ' textContent: text.join ' '
href: "/#{boardID}/thread/#{threadID}" href: Build.path boardID, threadID
thread: (board, data, full) -> thread: (board, data, full) ->
Build.spoilerRange[board] = data.custom_spoiler Build.spoilerRange[board] = data.custom_spoiler
@ -275,7 +281,7 @@ Build =
postCount = data.replies + 1 postCount = data.replies + 1
fileCount = data.images + !!data.ext fileCount = data.images + !!data.ext
pageCount = Index.liveThreadData.keys.indexOf("#{thread.ID}") // Index.threadsNumPerPage pageCount = Index.liveThreadData.keys.indexOf("#{thread.ID}") // Index.threadsNumPerPage + 1
subject = if thread.OP.info.subject subject = if thread.OP.info.subject
"<div class='subject'>#{thread.OP.info.subject}</div>" "<div class='subject'>#{thread.OP.info.subject}</div>"

View File

@ -317,7 +317,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':
@ -913,7 +913,7 @@ Config =
backlink: '>>%id' backlink: '>>%id'
fileInfo: '%L (%p%s, %r)' fileInfo: '%l (%p%s, %r)'
favicon: 'ferongr' favicon: 'ferongr'
@ -1028,19 +1028,19 @@ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
] ]
# 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': [
@ -1065,11 +1065,11 @@ box-shadow: inset 2px 2px 2px rgba(0,0,0,0.2);
] ]
# 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': [

View File

@ -147,6 +147,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'
@ -172,9 +173,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 ->
@ -322,8 +321,8 @@ Index =
else else
'Show' 'Show'
Index.sort() Index.sort()
if Conf['Index Mode'] is 'paged' and Index.getCurrentPage() > 0 if Conf['Index Mode'] is 'paged' and Index.getCurrentPage() > 1
Index.pageNav 0 Index.pageNav 1
else else
Index.buildIndex() Index.buildIndex()
@ -381,7 +380,7 @@ Index =
return return
e.preventDefault() e.preventDefault()
return if Index.cb.indexNav a, true return if Index.cb.indexNav a, true
Index.userPageNav +a.pathname.split('/')[2] Index.userPageNav +a.pathname.split('/')[2] or 1
headerNav: (e) -> headerNav: (e) ->
a = e.target a = e.target
@ -421,10 +420,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
@ -432,7 +431,7 @@ Index =
pageNav: (pageNum) -> pageNav: (pageNum) ->
return if Index.currentPage is pageNum and not Index.root.parentElement return if Index.currentPage is pageNum and not Index.root.parentElement
Navigate.pushState if pageNum is 0 then './' else pageNum Navigate.pushState if pageNum is 1 then './' else pageNum
Index.pageLoad pageNum Index.pageLoad pageNum
pageLoad: (pageNum) -> pageLoad: (pageNum) ->
@ -451,20 +450,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
@ -477,11 +478,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
@ -489,7 +490,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
@ -514,7 +515,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()
@ -565,7 +566,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
@ -622,7 +623,7 @@ Index =
Index.liveThreadData.forEach (threadData) -> Index.liveThreadData.forEach (threadData) ->
threadRoot = Build.thread g.BOARD, threadData threadRoot = Build.thread g.BOARD, threadData
if thread = g.BOARD.threads[threadData.no] if thread = g.BOARD.threads[threadData.no]
thread.setPage i // Index.threadsNumPerPage thread.setPage i // Index.threadsNumPerPage + 1
thread.setCount 'post', threadData.replies + 1, threadData.bumplimit thread.setCount 'post', threadData.replies + 1, threadData.bumplimit
thread.setCount 'file', threadData.images + !!threadData.ext, threadData.imagelimit thread.setCount 'file', threadData.images + !!threadData.ext, threadData.imagelimit
thread.setStatus 'Sticky', !!threadData.sticky thread.setStatus 'Sticky', !!threadData.sticky
@ -760,7 +761,7 @@ Index =
nodes = [] nodes = []
switch Conf['Index Mode'] switch Conf['Index Mode']
when 'paged', 'infinite' when 'paged', 'infinite'
pageNum = Index.getCurrentPage() pageNum = Index.getCurrentPage() - 1
threadsPerPage = Index.getThreadsNumPerPage() threadsPerPage = Index.getThreadsNumPerPage()
threads = [] threads = []
@ -799,7 +800,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()

View File

@ -18,6 +18,10 @@ 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]
g.SLUG = pathname[4] if pathname[4]?
if pathname[2] isnt 'thread'
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
@ -199,14 +203,17 @@ Main =
'This will steal your data.' 'This will steal your data.'
'left=0,top=0,width=500,height=255,toolbar=0,resizable=0' 'left=0,top=0,width=500,height=255,toolbar=0,resizable=0'
$.before styleSelector.previousSibling, [$.tn '['; passLink, $.tn ']\u00A0\u00A0'] $.before styleSelector.previousSibling, [$.tn '['; passLink, $.tn ']\u00A0\u00A0']
# Completely disable the mobile layout
$('link[href*="mobile"', d.head).disabled = true
# Parse HTML or skip it and start building from JSON. # Parse HTML or skip it and start building from JSON.
if !Conf['JSON Navigation'] or g.VIEW is 'thread' if !Conf['JSON Navigation'] or g.VIEW is 'thread'
Main.initThread() Main.initThread()
$.add d.head, $.el 'link', # JSON Navigation may not load on a page that has flags, so force their CSS to always be available.
href: "//s.4cdn.org/css/flags.556.css" $.add d.head, $.el 'link',
rel: "stylesheet" href: "//s.4cdn.org/css/flags.556.css"
rel: "stylesheet"
$.event '4chanXInitFinished' $.event '4chanXInitFinished'

View File

@ -128,8 +128,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, ' '
Index.catalogLink.href = "//boards.4chan.org/#{boardID}/" Index.catalogLink.href = "//boards.4chan.org/#{boardID}/"
@ -226,7 +226,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
@ -257,7 +257,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

View File

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

View File

@ -0,0 +1,36 @@
"""#{if isOP then '' else "<div class=sideArrows id=sa#{postID}>&gt;&gt;</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>"""

View File

@ -25,4 +25,4 @@
<option value="small">Small</option> <option value="small">Small</option>
<option value="large">Large</option> <option value="large">Large</option>
</select> </select>
</span> </span>

View File

@ -1,4 +1,4 @@
<a href="/#{thread.board}/thread/#{thread.ID}" class="thumb"></a> <a href="#{Build.path thread.board.ID, thread.ID}" class="thumb"></a>
<div class="thread-stats" title="Post count / File count / Page count"> <div class="thread-stats" title="Post count / File count / Page count">
<span class="post-count">#{postCount}</span> / <span class="file-count">#{fileCount}</span> / <span class="page-count">#{pageCount}</span> <span class="post-count">#{postCount}</span> / <span class="file-count">#{fileCount}</span> / <span class="page-count">#{pageCount}</span>
<span class="thread-icons"></span> <span class="thread-icons"></span>

View File

@ -101,8 +101,9 @@ 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+
(\d+) # postID (.*)? # thread slug
\#p(\d+) # postID
$ $
/// ///
@ -135,6 +136,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].data.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,17 +150,13 @@ 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
$.rm node $.rm node
for node in $$ '[id]', post for node in $$ '[id]:not(.exif)', post
node.removeAttribute 'id' node.removeAttribute 'id'
for node in $$ '.desktop', root for node in $$ '.desktop', root
$.rmClass node, 'desktop' $.rmClass node, 'desktop'

View File

@ -49,7 +49,7 @@ class Thread
else if g.VIEW is 'index' else if g.VIEW is 'index'
$ '.page-num', @OP.nodes.info $ '.page-num', @OP.nodes.info
else else
$ '[title="Quote this post"]', @OP.nodes.info $ '[title="Reply to this post"]', @OP.nodes.info
$.after root, [$.tn(' '), icon] $.after root, [$.tn(' '), icon]
return unless @catalogView return unless @catalogView

View File

@ -193,7 +193,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
@ -202,8 +202,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

View File

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

View File

@ -26,8 +26,8 @@ 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
@ -41,7 +41,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
@ -54,10 +54,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

View File

@ -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']
@ -76,22 +78,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']
@ -113,10 +121,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'
@ -144,17 +153,23 @@ 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
@ -195,10 +210,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 QR.postingIsEnabled return unless Conf['Quick Reply'] and QR.postingIsEnabled
do 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
do QR.nodes.com.focus do QR.nodes.com.focus
@ -239,7 +254,7 @@ Keybinds =
open: (thread, tab) -> open: (thread, tab) ->
return if g.VIEW isnt 'index' return if g.VIEW isnt 'index'
url = "/#{thread.board}/thread/#{thread}" url = Build.path thread.board.ID, thread.ID
if tab if tab
$.open url $.open url
else else

View File

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

View File

@ -160,7 +160,7 @@ Unread =
threadID: data.thread.ID threadID: data.thread.ID
postID: ID postID: ID
} }
QuoteYou.lastRead = data.nodes.root QuoteMarkers.lastRead = data.nodes.root
return unless ID return unless ID

View File

@ -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.
@ -34,24 +44,26 @@ QR.captcha =
img.parentNode.parentNode.hidden = true img.parentNode.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.parentNode.hidden = false img.parentNode.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,
@ -64,33 +76,84 @@ 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()

View File

@ -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: ->
@db = new DataBoard 'yourPosts' @db = new DataBoard 'yourPosts'
@posts = [] @posts = []
@ -73,7 +75,7 @@ QR =
node: -> node: ->
if QR.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID} if QR.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID}
$.addClass @nodes.root, 'your-post' $.addClass @nodes.root, 'your-post'
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote $.on $('a[title="Reply to this post"]', @nodes.info), 'click', QR.quote
persist: -> persist: ->
return unless QR.postingIsEnabled return unless QR.postingIsEnabled
@ -111,8 +113,13 @@ QR =
QR.cooldown.auto = false QR.cooldown.auto = false
QR.status() QR.status()
focusin: -> $.addClass QR.nodes.el, 'focus' if QR.captcha.isEnabled and not Conf['Auto-load captcha']
focusout: -> $.rmClass QR.nodes.el, 'focus' QR.captcha.destroy()
focusin: ->
$.addClass QR.nodes.el, 'focus'
focusout: ->
$.rmClass QR.nodes.el, 'focus'
hide: -> hide: ->
d.activeElement.blur() d.activeElement.blur()
@ -137,9 +144,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
@ -219,7 +227,7 @@ QR =
$.prepend frag, $.tn '[code]' $.prepend frag, $.tn '[code]'
$.add frag, $.tn '[/code]' $.add frag, $.tn '[/code]'
for node in $$ 'br', frag for node in $$ 'br', frag
$.replace node, $.tn '\n>' unless node is frag.lastElementChild $.replace node, $.tn '\n>' unless node is frag.lastChild
for node in $$ 's', frag for node in $$ 's', frag
$.replace node, [$.tn('[spoiler]'), node.childNodes..., $.tn '[/spoiler]'] $.replace node, [$.tn('[spoiler]'), node.childNodes..., $.tn '[/spoiler]']
for node in $$ '.prettyprint', frag for node in $$ '.prettyprint', frag
@ -285,55 +293,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)
@ -341,14 +337,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) ->
@ -356,40 +349,37 @@ 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 /^image\//.test file.type isSingle = nfiles is 1
img = new Image() if /^text\//.test file.type
img.onload = => if isSingle
{height, width} = img post = QR.selected
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 else if index isnt 0 or (post = QR.posts[QR.posts.length - 1]).com
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 post = new QR.post()
QR.handleFile file, isSingle, max post.pasteText file
img.src = URL.createObjectURL file return
else unless file.type in QR.mimeTypes
QR.handleFile file, isSingle, max QR.error "#{file.name}: Unsupported file type."
return unless isSingle
handleFile: (file, isSingle, max) -> max = QR.nodes.fileInput.max
max = Math.min(max, QR.max_size_video) if /^video\//.test file.type
if file.size > max if file.size > max
QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})." QR.error "#{file.name}: File too large (file: #{$.bytesToString file.size}, max: #{$.bytesToString max})."
return return unless isSingle
if isSingle if isSingle
post = QR.selected post = QR.selected
else if (post = QR.posts[QR.posts.length - 1]).file else if index isnt 0 or (post = QR.posts[QR.posts.length - 1]).file
post = new QR.post() post = new QR.post()
if /^text/.test file.type if /^text/.test file.type
post.pasteText file return post.pasteText file
else else
post.setFile file 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'
@ -457,16 +447,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'
@ -697,9 +693,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()
@ -733,7 +726,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()
@ -765,12 +757,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
@ -811,21 +814,32 @@ 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()
QR.cooldown.set {req, post, isReply, threadID} QR.cooldown.set {req, post, isReply, threadID}
URL = unless isReply # new thread URL = if threadID is postID # new thread
"/#{g.BOARD}/res/#{threadID}" Build.path g.BOARD.ID, threadID
else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index
"/#{g.BOARD}/res/#{threadID}#p#{postID}" Build.path g.BOARD.ID, threadID, postID
if URL if URL
if Conf['Open Post in New Tab'] if Conf['Open Post in New Tab']

View File

@ -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
@ -171,27 +169,39 @@ 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
el = $.el (if isVideo then 'video' else 'img')
$.on el, (if isVideo then 'loadeddata' else 'load'), =>
# Verify element dimensions.
errors = @checkDimensions el, isVideo
if errors.length
QR.error error for error in errors
@URL = fileURL # this.removeFile will revoke this proper.
return @rmFile()
img.onload = =>
# 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 = el.videoHeight
@URL = fileURL width = el.videoWidth
@nodes.el.style.backgroundImage = "url(#{@URL})" else
return {height, width} = el
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
@ -199,16 +209,42 @@ QR.post = class
height = s / width * height height = s / width * height
width = s width = s
cv = $.el 'canvas' cv = $.el 'canvas'
cv.height = img.height = height cv.height = el.height = height
cv.width = img.width = width cv.width = el.width = width
cv.getContext('2d').drawImage img, 0, 0, width, height cv.getContext('2d').drawImage el, 0, 0, width, height
URL.revokeObjectURL fileURL URL.revokeObjectURL fileURL
cv.toBlob (blob) => cv.toBlob (blob) =>
@URL = URL.createObjectURL blob @URL = URL.createObjectURL blob
@nodes.el.style.backgroundImage = "url(#{@URL})" @nodes.el.style.backgroundImage = "url(#{@URL})"
fileURL = URL.createObjectURL @file fileURL = URL.createObjectURL @file
img.src = fileURL el.src = fileURL
checkDimensions: (el, video) ->
err = []
if video
{videoHeight, videoWidth, duration} = el
max_height = if QR.max_height < QR.max_height_video then QR.max_height else QR.max_height_video
max_width = if QR.max_width < QR.max_width_video then QR.max_width else QR.max_width_video
if videoHeight > max_height or videoWidth > max_width
err.push "#{@file.name}: Video too large (video: #{videoHeight}x#{videoWidth}px, max: #{max_height}x#{max_width}px)"
if videoHeight < QR.min_height or videoWidth < QR.min_width
err.push "#{@file.name}: Video too small (video: #{videoHeight}x#{videoWidth}px, min: #{QR.min_height}x#{QR.min_width}px)"
unless isFinite el.duration
err.push "#{file.name}: Video lacks duration metadata (try remuxing)"
if duration > QR.max_duration_video
err.push "#{@file.name}: Video too long (video: #{duration}s, max: #{QR.max_duration_video}s)"
<% if (type === 'userscript') { %>
if el.mozHasAudio
err.push "#{file.name}: Audio not allowed"
<% } %>
else
{height, width} = el
if height > QR.max_height or width > QR.max_width
err.push "#{@file.name}: Image too large (image: #{height}x#{width}px, max: #{QR.max_height}x#{QR.max_width}px)"
if height < QR.min_height or width < QR.min_width
err.push "#{@file.name}: Image too small (image: #{height}x#{width}px, min: #{QR.min_height}x#{QR.min_width}px)"
err
rmFile: -> rmFile: ->
return if @isLocked return if @isLocked

View File

@ -56,7 +56,7 @@ QuoteBacklink =
buildBacklink: (quoted, quoter) -> buildBacklink: (quoted, quoter) ->
frag = QuoteBacklink.frag.cloneNode true frag = QuoteBacklink.frag.cloneNode true
a = frag.lastElementChild a = frag.lastElementChild
a.href = "/#{quoter.board}/thread/#{quoter.thread}#p#{quoter}" a.href = Build.path quoter.board.ID, quoter.thread.ID, quoter.ID
a.textContent = text = QuoteBacklink.funk quoter.ID a.textContent = text = QuoteBacklink.funk quoter.ID
if quoter.isDead if quoter.isDead
$.addClass a, 'deadlink' $.addClass a, 'deadlink'

View File

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

View File

@ -44,7 +44,7 @@ Quotify =
# Don't add 'deadlink' when quotifying in an archived post, # Don't add 'deadlink' when quotifying in an archived post,
# and we don't know if the post died yet. # and we don't know if the post died yet.
a = $.el 'a', a = $.el 'a',
href: "/#{boardID}/thread/#{post.thread}#p#{postID}" href: Build.path boardID, post.thread.ID, postID
className: if post.isDead then 'quotelink deadlink' else 'quotelink' className: if post.isDead then 'quotelink deadlink' else 'quotelink'
textContent: quote textContent: quote
$.extend a.dataset, {boardID, threadID: post.thread.ID, postID} $.extend a.dataset, {boardID, threadID: post.thread.ID, postID}

View File

@ -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"
@ -17,7 +17,7 @@ Banner =
alt: '4chan' alt: '4chan'
title: 'Click to change' title: 'Click to change'
$.on img, 'click', Banner.cb.toggle $.on img, 'click error', Banner.cb.toggle
Banner.cb.toggle.call img Banner.cb.toggle.call img
$.prepend banner, img $.prepend banner, img
@ -45,7 +45,7 @@ Banner =
-> ->
type = Object.keys(types)[Math.floor 3 * Math.random()] type = Object.keys(types)[Math.floor 3 * Math.random()]
num = Math.floor types[type] * Math.random() num = Math.floor types[type] * Math.random()
@src = "//s.4cdn.org/image/title/#{num}.#{type}" @src = "//s.4cdn.org/image/title/#{num}.#{type}"
click: (e) -> click: (e) ->
@ -96,4 +96,4 @@ Banner =
else else
$.set string, cachedTest $.set string, cachedTest
$.set string2, cachedTest $.set string2, cachedTest
child child