'
#main
for key, obj of config.main
ul = $.el 'ul',
textContent: key
for key, arr of obj
checked = if conf[key] then 'checked' else ''
description = arr[1]
li = $.el 'li',
innerHTML: ": #{description}"
$.bind $('input', li), 'click', $.cb.checked
$.add ul, li
$.add $('#main', dialog), ul
hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {}
hiddenNum = Object.keys(g.hiddenReplies).length + Object.keys(hiddenThreads).length
li = $.el 'li',
innerHTML: " : Forget all hidden posts. Useful if you accidentally hide a post and have `Show Stubs` disabled."
$.bind $('button', li), 'click', options.clearHidden
$.add $('ul:nth-child(2)', dialog), li
#filter & sauce
for ta in $$ 'textarea', dialog
ta.textContent = conf[ta.name]
$.bind ta, 'change', $.cb.value
#rice
(back = $ '[name=backlink]', dialog).value = conf['backlink']
(time = $ '[name=time]', dialog).value = conf['time']
$.bind back, 'keyup', options.backlink
$.bind time, 'keyup', options.time
#keybinds
for input in $$ '#keybinds input', dialog
input.type = 'text'
input.value = conf[input.name]
$.bind input, 'keydown', options.keybind
###
Two parent divs are necessary to center on all browsers.
Only one when Firefox and Opera will support flexboxes correctly.
https://bugzilla.mozilla.org/show_bug.cgi?id=579776
###
overlay = $.el 'div', id: 'overlay'
$.bind overlay, 'click', -> $.rm overlay
$.bind dialog.firstElementChild, 'click', (e) -> e.stopPropagation()
$.add overlay, dialog
$.add d.body, overlay
options.time.call time
options.backlink.call back
clearHidden: (e) ->
#'hidden' might be misleading; it's the number of IDs we're *looking* for,
# not the number of posts actually hidden on the page.
$.delete "hiddenReplies/#{g.BOARD}/"
$.delete "hiddenThreads/#{g.BOARD}/"
@textContent = "hidden: 0"
g.hiddenReplies = {}
keybind: (e) ->
e.preventDefault()
e.stopPropagation()
return unless (key = keybinds.keyCode e)?
@value = key
$.set @name, key
conf[@name] = key
time: (e) ->
$.set 'time', @value
conf['time'] = @value
Time.foo()
Time.date = new Date()
$('#timePreview').textContent = Time.funk Time
backlink: (e) ->
$.set 'backlink', @value
conf['backlink'] = @value
$('#backlinkPreview').textContent = conf['backlink'].replace /%id/, '123456789'
QR =
#captcha caching for report form
#report queueing
#check if captchas can be reused on eg dup file error
init: ->
#can't reply in some stickies, recaptcha may be blocked, eg by noscript
return unless $('form[name=post]') and $('#recaptcha_response_field')
g.callbacks.push (root) ->
quote = $ '.quotejs + a', root
$.bind quote, 'click', QR.quote
$.add d.body, $.el 'iframe',
name: 'iframe'
hidden: true
# nuke id so qr's field focuses on recaptcha reload, instead of normal form's
$('#recaptcha_response_field').id = ''
holder = $ '#recaptcha_challenge_field_holder'
$.bind holder, 'DOMNodeInserted', QR.captchaNode
QR.captchaNode target: holder.firstChild
QR.accept = $('.rules').textContent.match(/: (.+) /)[1].replace /\w+/g, (type) ->
switch type
when 'JPG'
'image/JPEG'
when 'PDF'
'application/' + type
else
'image/' + type
QR.MAX_FILE_SIZE = $('input[name=MAX_FILE_SIZE]').value
QR.spoiler = if $('.postarea label') then ' ' else ''
if conf['Persistent QR']
QR.dialog()
$('textarea', QR.qr).blur()
if conf['Auto Hide QR']
$('#autohide', QR.qr).checked = true
if conf['Cooldown']
$.bind window, 'storage', (e) -> QR.cooldown() if e.key is "#{NAMESPACE}cooldown/#{g.BOARD}"
attach: (file) ->
files = $ '#files', QR.qr
box = $.el 'li',
innerHTML: "X"
$.bind $('.x', box), 'click', QR.rmThumb
$.add box, file
$.add files, box
QR.stats()
QR.foo()
rmThumb: ->
$.rm @parentNode
QR.stats()
captchaNode: (e) ->
QR.captcha =
challenge: e.target.value
time: Date.now()
QR.captchaImg()
captchaImg: ->
{qr} = QR
return unless qr
c = QR.captcha.challenge
$('#captcha img', qr).src = "http://www.google.com/recaptcha/api/image?c=#{c}"
captchaPush: (el) ->
{captcha} = QR
captcha.response = el.value
captchas = $.get 'captchas', []
captchas.push captcha
$.set 'captchas', captchas
el.value = ''
QR.captchaReload()
QR.stats captchas
captchaShift: ->
captchas = $.get 'captchas', []
cutoff = Date.now() - 5*HOUR + 5*MINUTE
while captcha = captchas.shift()
if captcha.time > cutoff
break
$.set 'captchas', captchas
QR.stats captchas
captcha
stats: (captchas) ->
{qr} = QR
captchas or= $.get 'captchas', []
images = $$ '#files input', qr
$('#qr_stats', qr).textContent = "#{images.length} / #{captchas.length}"
captchaReload: ->
window.location = 'javascript:Recaptcha.reload()'
change: (e) ->
file = @files[0]
if file.size > QR.MAX_FILE_SIZE
alert 'Error: File too large.'
QR.foo @
return
if @parentNode.className is 'wat'
QR.attach @
fr = new FileReader()
img = $ 'img', @parentNode
fr.onload = (e) ->
img.src = e.target.result
fr.readAsDataURL file
close: ->
$.rm QR.qr
QR.qr = null
cooldown: ->
return unless g.REPLY and QR.qr
cooldown = $.get "cooldown/#{g.BOARD}", 0
now = Date.now()
n = Math.ceil (cooldown - now) / 1000
b = $ 'form button', QR.qr
if n > 0
$.extend b,
textContent: n
disabled: true
setTimeout QR.cooldown, 1000
else
$.extend b,
textContent: 'Submit'
disabled: false
QR.submit() if $('#autopost', QR.qr).checked
foo: (old) ->
input = $.el 'input',
type: 'file'
name: 'upfile'
accept: QR.accept
$.bind input, 'change', QR.change
if old
$.replace old, file
else
$.add $('.wat', QR.qr), input
dialog: (text='', tid) ->
tid or= g.THREAD_ID or ''
QR.qr = qr = ui.dialog 'qr', 'top: 0; right: 0;', "
X
"
#XXX use dom methods to set values instead of injecting raw user input into your html -_-;
QR.reset()
QR.cooldown() if conf['Cooldown']
QR.foo()
$.bind $('.close', qr), 'click', QR.close
$.bind $('form', qr), 'submit', QR.submit
$.bind $('#recaptcha_response_field', qr), 'keydown', QR.keydown
QR.captchaImg()
QR.stats()
$.add d.body, qr
ta = $ 'textarea', qr
ta.value = text
l = text.length
ta.setSelectionRange l, l
ta.focus()
keydown: (e) ->
kc = e.keyCode
v = @value
if kc is 8 and not v #backspace, empty
QR.captchaReload()
return
return unless e.keyCode is 13 and v #enter, not empty
QR.captchaPush @
e.preventDefault()
QR.submit() #derpy, but prevents checking for content twice
quote: (e, blank) ->
e?.preventDefault()
tid = $.x('ancestor::div[@class="thread"]/div', @)?.id
id = @textContent
text = ">>#{id}\n"
sel = getSelection()
bq = $.x('ancestor::blockquote', sel.anchorNode)
if id == $.x('preceding-sibling::input', bq)?.name
if s = sel.toString().replace /\n/g, '\n>'
text += ">#{s}\n"
{qr} = QR
if not qr
QR.dialog text, tid
return
$('#autohide', qr).checked = false
ta = $ 'textarea', qr
v = ta.value
ss = ta.selectionStart
ta.value = v[0...ss] + text + v[ss..]
i = ss + text.length
ta.setSelectionRange i, i
ta.focus()
$('[name=resto]', qr).value or= tid
receive: (data) ->
$('iframe[name=iframe]').src = 'about:blank'
{qr} = QR
row = $('#files input[form]', qr)?.parentNode
data = JSON.parse data
{textContent, href} = data
if QR.op
window.location = href
return
if textContent
$.extend $('a.error', qr), data
if textContent is 'Error: Duplicate file entry detected.'
$.rm row if row
QR.stats()
setTimeout QR.submit, 1000
else if textContent is 'You seem to have mistyped the verification.'
setTimeout QR.submit, 1000
return
$.rm row if row
QR.stats()
if conf['Persistent QR'] or $('#files input', qr)?.files.length
QR.reset()
else
QR.close()
if conf['Cooldown']
cooldown = Date.now() + (if QR.sage then 60 else 30)*SECOND
$.set "cooldown/#{g.BOARD}", cooldown
QR.cooldown()
reset: ->
{qr} = QR
c = d.cookie
$('[name=name]', qr).value = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else ''
$('[name=email]', qr).value = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else ''
$('[name=pwd]', qr).value = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value
$('[name=sub]', qr).value = ''
$('[name=spoiler]', qr)?.checked = false unless conf['Remember Spoiler']
$('textarea', qr).value = ''
submit: (e) ->
{qr} = QR
#XXX e is undefined if method is called explicitly, eg, from auto posting
if $('textarea', qr).value or $('#files', qr).childNodes.length
if $('form button', qr).disabled
$('#autopost', qr).checked = true
return
else
if e
alert 'Error: No text entered.'
e.preventDefault()
return
$('.error', qr).textContent = ''
if e and (el = $('#recaptcha_response_field', qr)).value
QR.captchaPush el
if not captcha = QR.captchaShift()
alert 'You forgot to type in the verification.'
e?.preventDefault()
return
{challenge, response} = captcha
$('#challenge', qr).value = challenge
$('#response', qr).value = response
$('#autohide', qr).checked = true if conf['Auto Hide QR']
if input = $ '#files input', qr
input.setAttribute 'form', 'qr_form'
$('#qr_form', qr).submit() if not e
QR.sage = /sage/i.test $('[name=email]', qr).value
id = $('input[name=resto]', qr).value
QR.op = not id
$('[name=email]', qr).value = 'noko' if QR.op
if conf['Thread Watcher'] and conf['Auto Watch Reply']
op = $.id id
if $('img.favicon', op).src is Favicon.empty
watcher.watch op, id
sys: ->
if recaptcha = $ '#recaptcha_response_field' #post reporting
$.bind recaptcha, 'keydown', QR.keydown
return
###
http://code.google.com/p/chromium/issues/detail?id=20773
Let content scripts see other frames (instead of them being undefined)
To access the parent, we have to break out of the sandbox and evaluate
in the global context.
###
$.globalEval ->
$ = (css) -> document.querySelector css
if node = $('table font b')?.firstChild
{textContent, href} = node
else
node = $ 'meta'
href = node.content.match(/url=(.+)/)[1]
data = JSON.stringify { textContent, href }
parent.postMessage data, '*'
#if we're an iframe, parent will blank us
threading =
init: ->
threading.thread $('body > form').firstChild
op: (node) ->
op = $.el 'div',
className: 'op'
$.before node, op
while node.nodeName isnt 'BLOCKQUOTE'
$.add op, node
node = op.nextSibling
$.add op, node #add the blockquote
op.id = $('input', op).name
op
thread: (node) ->
node = threading.op node
return if g.REPLY
div = $.el 'div',
className: 'thread'
$.before node, div
while node.nodeName isnt 'HR'
$.add div, node
node = div.nextSibling
node = node.nextElementSibling #skip text node
#{N,}SFW
unless node.align or node.nodeName is 'CENTER'
threading.thread node
threadHiding =
init: ->
hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {}
for thread in $$ 'div.thread'
op = thread.firstChild
a = $.el 'a',
textContent: '[ - ]'
$.bind a, 'click', threadHiding.cb.hide
$.prepend op, a
if op.id of hiddenThreads
threadHiding.hideHide thread
cb:
hide: (e) ->
thread = @parentNode.parentNode
threadHiding.hide thread
show: (e) ->
thread = @parentNode.parentNode
threadHiding.show thread
toggle: (thread) ->
if thread.classList.contains('stub') or thread.hidden
threadHiding.show thread
else
threadHiding.hide thread
hide: (thread) ->
threadHiding.hideHide thread
id = thread.firstChild.id
hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {}
hiddenThreads[id] = Date.now()
$.set "hiddenThreads/#{g.BOARD}/", hiddenThreads
hideHide: (thread) ->
if conf['Show Stubs']
if span = $ '.omittedposts', thread
num = Number span.textContent.match(/\d+/)[0]
else
num = 0
num += $$('table', thread).length
text = if num is 1 then "1 reply" else "#{num} replies"
name = $('.postername', thread).textContent
trip = $('.postername + .postertrip', thread)?.textContent or ''
a = $.el 'a',
textContent: "[ + ] #{name}#{trip} (#{text})"
$.bind a, 'click', threadHiding.cb.show
div = $.el 'div',
className: 'block'
$.add div, a
$.add thread, div
$.addClass thread, 'stub'
else
thread.hidden = true
thread.nextSibling.hidden = true
show: (thread) ->
$.rm $ 'div.block', thread
$.removeClass thread, 'stub'
thread.hidden = false
thread.nextSibling.hidden = false
id = thread.firstChild.id
hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {}
delete hiddenThreads[id]
$.set "hiddenThreads/#{g.BOARD}/", hiddenThreads
updater =
init: ->
if conf['Scrolling']
if conf['Scroll BG']
updater.focus = true
else
$.bind window, 'focus', (-> updater.focus = true)
$.bind window, 'blur', (-> updater.focus = false)
html = "
-#{conf['Interval']}
"
{checkbox} = config.updater
for name of checkbox
title = checkbox[name][1]
checked = if conf[name] then 'checked' else ''
html += ""
checked = if conf['Auto Update'] then 'checked' else ''
html += "
"
dialog = ui.dialog 'updater', 'bottom: 0; right: 0;', html
updater.count = $ '#count', dialog
updater.timer = $ '#timer', dialog
updater.br = $ 'br[clear]'
for input in $$ 'input', dialog
if input.type is 'checkbox'
$.bind input, 'click', $.cb.checked
$.bind input, 'click', -> conf[@name] = @checked
if input.name is 'Verbose'
$.bind input, 'click', updater.cb.verbose
updater.cb.verbose.call input
else if input.name is 'Auto Update This'
$.bind input, 'click', updater.cb.autoUpdate
updater.cb.autoUpdate.call input
else if input.name is 'Interval'
$.bind input, 'change', -> conf['Interval'] = @value = parseInt(@value) or conf['Interval']
$.bind input, 'change', $.cb.value
else if input.type is 'button'
$.bind input, 'click', updater.update
$.add d.body, dialog
cb:
verbose: ->
if conf['Verbose']
updater.count.textContent = '+0'
updater.timer.hidden = false
else
$.extend updater.count,
className: ''
textContent: 'Thread Updater'
updater.timer.hidden = true
autoUpdate: ->
if @checked
updater.timeoutID = setTimeout updater.timeout, 1000
else
clearTimeout updater.timeoutID
update: ->
if @status is 404
updater.timer.textContent = ''
updater.count.textContent = 404
updater.count.className = 'error'
clearTimeout updater.timeoutID
for input in $$ '#com_submit'
input.disabled = true
input.value = 404
# XXX trailing spaces are trimmed
d.title = d.title.match(/.+-/)[0] + ' 404'
g.dead = true
Favicon.update()
return
updater.timer.textContent = '-' + conf['Interval']
body = $.el 'body',
innerHTML: @responseText
if $('title', body).textContent is '4chan - Banned'
updater.count.textContent = 'banned'
updater.count.className = 'error'
return
replies = $$ '.reply', body
id = Number $('td[id]', updater.br.previousElementSibling)?.id or 0
arr = []
while (reply = replies.pop()) and (reply.id > id)
arr.push reply.parentNode.parentNode.parentNode #table
scroll = conf['Scrolling'] && updater.focus && arr.length && (d.body.scrollHeight - d.body.clientHeight - window.scrollY < 20)
if conf['Verbose']
updater.count.textContent = '+' + arr.length
if arr.length is 0
updater.count.className = ''
else
updater.count.className = 'new'
#XXX add replies in correct order so backlinks resolve
while reply = arr.pop()
$.before updater.br, reply
if scroll
scrollTo 0, d.body.scrollHeight
timeout: ->
updater.timeoutID = setTimeout updater.timeout, 1000
n = 1 + Number updater.timer.textContent
if n is 0
updater.update()
else if n is 10
updater.retry()
else
updater.timer.textContent = n
retry: ->
updater.count.textContent = 'retry'
updater.count.className = ''
updater.update()
update: ->
updater.timer.textContent = 0
updater.request?.abort()
url = location.pathname + '?' + Date.now() # fool the cache
cb = updater.cb.update
updater.request = $.ajax url, cb
watcher =
init: ->
html = '
Thread Watcher
'
watcher.dialog = ui.dialog 'watcher', 'top: 50px; left: 0px;', html
$.add d.body, watcher.dialog
#add watch buttons
inputs = $$ '.op input'
for input in inputs
favicon = $.el 'img',
className: 'favicon'
$.bind favicon, 'click', watcher.cb.toggle
$.before input, favicon
#populate watcher, display watch buttons
watcher.refresh()
$.bind window, 'storage', (e) -> watcher.refresh() if e.key is "#{NAMESPACE}watched"
refresh: ->
watched = $.get 'watched', {}
for div in $$ 'div:not(.move)', watcher.dialog
$.rm div
for board of watched
for id, props of watched[board]
div = $.el 'div'
x = $.el 'a',
textContent: 'X'
$.bind x, 'click', watcher.cb.x
link = $.el 'a', props
$.add div, x, $.tn(' '), link
$.add watcher.dialog, div
watchedBoard = watched[g.BOARD] or {}
for favicon in $$ 'img.favicon'
id = favicon.nextSibling.name
if id of watchedBoard
favicon.src = Favicon.default
else
favicon.src = Favicon.empty
cb:
toggle: (e) ->
watcher.toggle @parentNode
x: (e) ->
[board, _, id] = @nextElementSibling
.getAttribute('href').substring(1).split('/')
watcher.unwatch board, id
toggle: (thread) ->
favicon = $ 'img.favicon', thread
id = favicon.nextSibling.name
if favicon.src == Favicon.empty
watcher.watch thread, id
else # favicon.src == Favicon.default
watcher.unwatch g.BOARD, id
unwatch: (board, id) ->
watched = $.get 'watched', {}
delete watched[board][id]
$.set 'watched', watched
watcher.refresh()
watch: (thread, id) ->
props =
href: "/#{g.BOARD}/res/#{id}"
textContent: getTitle(thread)[...30]
watched = $.get 'watched', {}
watched[g.BOARD] or= {}
watched[g.BOARD][id] = props
$.set 'watched', watched
watcher.refresh()
anonymize =
init: ->
g.callbacks.push (root) ->
name = $ '.commentpostername, .postername', root
name.textContent = 'Anonymous'
if trip = $ '.postertrip', root
if trip.parentNode.nodeName is 'A'
$.rm trip.parentNode
else
$.rm trip
sauce =
init: ->
sauce.prefixes = conf['flavors'].match /^[^#].+$/gm
sauce.names = sauce.prefixes.map (prefix) -> prefix.match(/(\w+)\./)[1]
g.callbacks.push (root) ->
return if root.className is 'inline' or not span = $ '.filesize', root
suffix = $('a', span).href
for prefix, i in sauce.prefixes
link = $.el 'a',
textContent: sauce.names[i]
href: prefix + suffix
target: '_blank'
$.add span, $.tn(' '), link
revealSpoilers =
init: ->
g.callbacks.push (root) ->
return if not (img = $ 'img[alt^=Spoiler]', root) or root.className is 'inline'
img.removeAttribute 'height'
img.removeAttribute 'width'
[_, board, imgID] = img.parentNode.href.match /(\w+)\/src\/(\d+)/
img.src = "http://0.thumbs.4chan.org/#{board}/thumb/#{imgID}s.jpg"
Time =
init: ->
Time.foo()
@parse =
if Date.parse '10/11/11(Tue)18:53'
(node) -> new Date Date.parse(node.textContent) + g.chanOffset*HOUR
else # Firefox the Archaic cannot parse 4chan's time
(node) ->
[_, month, day, year, hour, min] =
node.textContent.match /(\d+)\/(\d+)\/(\d+)\(\w+\)(\d+):(\d+)/
year = "20#{year}"
month -= 1 #months start at 0
hour = g.chanOffset + Number hour
new Date year, month, day, hour, min
g.callbacks.push Time.node
node: (root) ->
return if root.className is 'inline'
node = if posttime = $('.posttime', root) then posttime else $('span[id]', root).previousSibling
Time.date = Time.parse node
time = $.el 'time',
textContent: ' ' + Time.funk(Time) + ' '
$.replace node, time
foo: ->
code = conf['time'].replace /%([A-Za-z])/g, (s, c) ->
if c of Time.formatters
"' + Time.formatters.#{c}() + '"
else
s
Time.funk = Function 'Time', "return '#{code}'"
day: [
'Sunday'
'Monday'
'Tuesday'
'Wednesday'
'Thursday'
'Friday'
'Saturday'
]
month: [
'January'
'February'
'March'
'April'
'May'
'June'
'July'
'August'
'September'
'October'
'November'
'December'
]
zeroPad: (n) -> if n < 10 then '0' + n else n
formatters:
a: -> Time.day[Time.date.getDay()][...3]
A: -> Time.day[Time.date.getDay()]
b: -> Time.month[Time.date.getMonth()][...3]
B: -> Time.month[Time.date.getMonth()]
d: -> Time.zeroPad Time.date.getDate()
e: -> Time.date.getDate()
H: -> Time.zeroPad Time.date.getHours()
I: -> Time.zeroPad Time.date.getHours() % 12 or 12
k: -> Time.date.getHours()
l: -> Time.date.getHours() % 12 or 12
m: -> Time.zeroPad Time.date.getMonth() + 1
M: -> Time.zeroPad Time.date.getMinutes()
p: -> if Time.date.getHours() < 12 then 'AM' else 'PM'
P: -> if Time.date.getHours() < 12 then 'am' else 'pm'
y: -> Time.date.getFullYear() - 2000
getTitle = (thread) ->
el = $ '.filetitle', thread
if not el.textContent
el = $ 'blockquote', thread
if not el.textContent
el = $ '.postername', thread
span = $.el 'span', innerHTML: el.innerHTML.replace / /g, ' '
"/#{g.BOARD}/ - #{span.textContent}"
titlePost =
init: ->
d.title = getTitle()
quoteBacklink =
init: ->
format = conf['backlink'].replace /%id/, "' + id + '"
quoteBacklink.funk = Function 'id', "return'#{format}'"
g.callbacks.push (root) ->
return if root.classList.contains 'inline'
# op or reply
id = $('input', root).name
quotes = {}
for quote in $$ '.quotelink', root
#don't process >>>/b/
continue unless qid = quote.hash[1..]
#duplicate quotes get overwritten
quotes[qid] = quote
for qid of quotes
continue unless el = $.id qid
#don't backlink the op
continue if !conf['OP Backlinks'] and el.className is 'op'
link = $.el 'a',
href: "##{id}"
className: 'backlink'
textContent: quoteBacklink.funk id
if conf['Quote Preview']
$.bind link, 'mouseover', quotePreview.mouseover
$.bind link, 'mousemove', ui.hover
$.bind link, 'mouseout', quotePreview.mouseout
if conf['Quote Inline']
$.bind link, 'click', quoteInline.toggle
unless (container = $ '.container', el) and container.parentNode is el
container = $.el 'span', className: 'container'
root = $('.reportbutton', el) or $('span[id]', el)
$.after root, container
$.add container, $.tn(' '), link
quoteInline =
init: ->
g.callbacks.push (root) ->
for quote in $$ '.quotelink, .backlink', root
continue unless quote.hash
quote.removeAttribute 'onclick'
$.bind quote, 'click', quoteInline.toggle
toggle: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.button isnt 0
e.preventDefault()
id = @hash[1..]
if @classList.contains 'inlined'
quoteInline.rm @, id
else
return if $.x("ancestor::*[@id='#{id}']", @)
quoteInline.add @, id
@classList.toggle 'inlined'
add: (q, id) ->
root = if q.parentNode.nodeName is 'FONT' then q.parentNode else if q.nextSibling then q.nextSibling else q
if el = $.id id
inline = quoteInline.table id, el.innerHTML
if q.className is 'backlink'
$.after q.parentNode, inline
$.x('ancestor::table', el).hidden = true
return
$.after root, inline
else
inline = $.el 'td',
className: 'reply inline'
id: "i#{id}"
innerHTML: "Loading #{id}..."
$.after root, inline
{pathname} = q
threadID = pathname.split('/').pop()
$.cache pathname, (-> quoteInline.parse @, pathname, id, threadID, inline)
rm: (q, id) ->
#select the corresponding table or loading td
table = $.x "following::*[@id='i#{id}']", q
for inlined in $$ 'input', table
if hidden = $.id inlined.name
unless hidden.classList.contains 'op'
$.x('ancestor::table[1]', hidden).hidden = false
$.rm table
parse: (req, pathname, id, threadID, inline) ->
return unless inline.parentNode
if req.status isnt 200
inline.innerHTML = "#{req.status} #{req.statusText}"
return
body = $.el 'body',
innerHTML: req.responseText
if id == threadID #OP
op = threading.op $('body > form', body).firstChild
html = op.innerHTML
else
for reply in $$ 'td.reply', body
if reply.id == id
html = reply.innerHTML
break
newInline = quoteInline.table id, html
for quote in $$ '.quotelink', newInline
if (href = quote.getAttribute('href')) is quote.hash #add pathname to normal quotes
quote.pathname = pathname
else if !g.REPLY and href isnt quote.href #fix x-thread links, not x-board ones
quote.href = "res/#{href}"
link = $ '.quotejs', newInline
link.href = "#{pathname}##{id}"
link.nextSibling.href = "#{pathname}#q#{id}"
$.addClass newInline, 'crossquote'
$.replace inline, newInline
table: (id, html) ->
$.el 'table',
className: 'inline'
id: "i#{id}"
innerHTML: "
#{html}
"
quotePreview =
init: ->
g.callbacks.push (root) ->
for quote in $$ '.quotelink, .backlink', root
continue unless quote.hash
$.bind quote, 'mouseover', quotePreview.mouseover
$.bind quote, 'mousemove', ui.hover
$.bind quote, 'mouseout', quotePreview.mouseout
mouseover: (e) ->
qp = ui.el = $.el 'div',
id: 'qp'
className: 'reply'
$.add d.body, qp
id = @hash[1..]
if el = $.id id
qp.innerHTML = el.innerHTML
$.addClass el, 'qphl' if conf['Quote Highlighting']
if @classList.contains 'backlink'
replyID = $.x('preceding::input', @).name
for quote in $$ '.quotelink', qp
if quote.hash[1..] is replyID
quote.className = 'forwardlink'
else
qp.innerHTML = "Loading #{id}..."
threadID = @pathname.split('/').pop() or $.x('ancestor::div[@class="thread"]/div', @).id
$.cache @pathname, (-> quotePreview.parse @, id, threadID)
ui.hover e
mouseout: ->
$.removeClass el, 'qphl' if el = $.id @hash[1..]
ui.hoverend()
parse: (req, id, threadID) ->
return unless (qp = ui.el) and (qp.innerHTML is "Loading #{id}...")
if req.status isnt 200
qp.innerHTML = "#{req.status} #{req.statusText}"
return
body = $.el 'body',
innerHTML: req.responseText
if id == threadID #OP
op = threading.op $('body > form', body).firstChild
html = op.innerHTML
else
for reply in $$ 'td.reply', body
if reply.id == id
html = reply.innerHTML
break
qp.innerHTML = html
Time.node qp
quoteOP =
init: ->
g.callbacks.push (root) ->
return if root.className is 'inline'
tid = g.THREAD_ID or $.x('ancestor::div[contains(@class,"thread")]/div', root).id
for quote in $$ '.quotelink', root
if quote.hash[1..] is tid
quote.innerHTML += ' (OP)'
reportButton =
init: ->
g.callbacks.push (root) ->
if not a = $ '.reportbutton', root
span = $ 'span[id]', root
a = $.el 'a',
className: 'reportbutton'
innerHTML: '[ ! ]'
$.after span, a
$.after span, $.tn(' ')
$.bind a, 'click', reportButton.report
report: ->
url = "http://sys.4chan.org/#{g.BOARD}/imgboard.php?mode=report&no=#{$.x('preceding-sibling::input', @).name}"
id = "#{NAMESPACE}popup"
set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200"
window.open url, id, set
threadStats =
init: ->
threadStats.posts = 1
threadStats.images = if $ '.op img[md5]' then 1 else 0
html = "
#{threadStats.posts} / #{threadStats.images}
"
dialog = ui.dialog 'stats', 'bottom: 0; left: 0;', html
dialog.className = 'dialog'
threadStats.postcountEl = $ '#postcount', dialog
threadStats.imagecountEl = $ '#imagecount', dialog
$.add d.body, dialog
g.callbacks.push threadStats.node
node: (root) ->
return if root.className
threadStats.postcountEl.textContent = ++threadStats.posts
if $ 'img[md5]', root
threadStats.imagecountEl.textContent = ++threadStats.images
if threadStats.images > 150
threadStats.imagecountEl.className = 'error'
unread =
init: ->
unread.replies = []
d.title = '(0) ' + d.title
$.bind window, 'scroll', unread.scroll
g.callbacks.push unread.node
node: (root) ->
return if root.hidden or root.className
unread.replies.push root
unread.updateTitle()
if unread.replies.length is 1
Favicon.update()
scroll: (e) ->
updater.focus = true
height = d.body.clientHeight
for reply, i in unread.replies
{bottom} = reply.getBoundingClientRect()
if bottom > height #post is not completely read
break
return if i is 0
unread.replies = unread.replies[i..]
unread.updateTitle()
if unread.replies.length is 0
Favicon.update()
updateTitle: ->
d.title = d.title.replace /\d+/, unread.replies.length
Favicon =
init: ->
favicon = $ 'link[rel="shortcut icon"]', d.head
favicon.type = 'image/x-icon'
{href} = favicon
Favicon.default = href
Favicon.unread = if /ws/.test href then Favicon.unreadSFW else Favicon.unreadNSFW
dead: 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAIALAAAAAAQABAAAAIvlI+pq+D9DAgUoFkPDlbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw=='
empty: 'data:image/gif;base64,R0lGODlhEAAQAJEAAAAAAP///9vb2////yH5BAEAAAMALAAAAAAQABAAAAIvnI+pq+D9DBAUoFkPFnbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw=='
unreadDead: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANhJREFUOMutU0EKwjAQzEPFgyBFei209gOKINh6tL3qO3yAB9OHWPTeMZsmJaRpiNjAkE1mMt1stgwA+wdsFgM1oHE4FXmSpWUcRzWBYtozNfKAYdCHCrQuosX9tlk+CBS7NKMMbMF7vXoJtC7Om8HwhXzbCWCSn6qBJHd74FIBVS1jm7czYFSsq7gvpY0s6+ThJwc4743EHnGkIW2YAW+AphkMPj6DJE1LXW3fFUhD2pHBsTznLKCIFCstC3nGNvQZnQa6kX4yMGfdyi7OZaB7wZy93Cx/4xfgv/s+XYFMrAAAAABJRU5ErkJggg%3D%3D'
unreadSFW: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAN9JREFUOMtj+P//PwMlmIEqBkDBfxie2NdVVVFaMikzPXsuCIPYIDFkNWANSAb815t+GI5B/Jj8iQfjapafBWEQG5saDBegK0ja8Ok9EH/AJofXBTBFlUf+/wPi/7jkcYYBCLef/v9/9pX//+cAMYiNLo/uAgZQYMVVLzsLcnYF0GaQ5otv/v+/9BpiEEgMJAdSA1JLlAGXgAZcfoNswGfcBpQDowoW2vi8AFIDUothwOQJvVXIgYUrEEFsqFoGYqLxA7HRiNUAWEIiyQBkGpaUsclhMwCWFpBpvHJUyY0AmdYZKFRtAsoAAAAASUVORK5CYII%3D'
unreadNSFW: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAOBJREFUOMtj+P//PwMlmIEqBkDBfxie2DWxqqykYlJ6dtZcEAaxQWLIasAakAz4n3bGGI5B/JiJ8QfjlsefBWEQG5saDBegKyj5lPQeiD9gk8PrApiinv+V/4D4Py55nGEAwrP+t/9f/X82EM8Bs9Hl0V3AAAqsuGXxZ0HO7vlf8Q+k+eb/i0B8CWwQSAwkB1IDUkuUAbeAmm/9v4ww4DMeA8pKyifBQhufF0BqQGoxDJjcO7kKObBwBSKIDVXLQEw0fiA2GrEaAEtIJBmATMOSMjY5bAbA0gIyjVeOKrkRAMefDK/b7ecEAAAAAElFTkSuQmCC'
update: ->
l = unread.replies.length
favicon = $ 'link[rel="shortcut icon"]', d.head
favicon.href =
if g.dead
if l
Favicon.unreadDead
else
Favicon.dead
else
if l
Favicon.unread
else
Favicon.default
#XXX `favicon.href = href` doesn't work on Firefox
clone = favicon.cloneNode true
$.replace favicon, clone
redirect = ->
switch g.BOARD
when 'g', 'sci'
url = "http://archive.installgentoo.net/cgi-board.pl/#{g.BOARD}/thread/#{g.THREAD_ID}"
when 'lit', 'tv'
url = "http://archive.gentoomen.org/cgi-board.pl/#{g.BOARD}/thread/#{g.THREAD_ID}"
when 'a', 'jp', 'm', 'tg'
url = "http://archive.easymodo.net/#{g.BOARD}/thread/#{g.THREAD_ID}"
when '3', 'adv', 'an', 'ck', 'co', 'fa', 'fit', 'int', 'k', 'mu', 'n', 'o', 'p', 'po', 'soc', 'sp', 'toy', 'trv', 'v', 'vp', 'x'
url = "http://archive.no-ip.org/#{g.BOARD}/thread/#{g.THREAD_ID}"
else
url = "http://boards.4chan.org/#{g.BOARD}"
location.href = url
imgHover =
init: ->
g.callbacks.push (root) ->
return unless thumb = $ 'img[md5]', root
$.bind thumb, 'mouseover', imgHover.mouseover
$.bind thumb, 'mousemove', ui.hover
$.bind thumb, 'mouseout', ui.hoverend
mouseover: (e) ->
ui.el = $.el 'img'
id: 'iHover'
src: @parentNode.href
$.add d.body, ui.el
imgPreloading =
init: ->
g.callbacks.push (root) ->
return unless thumb = $ 'img[md5]', root
src = thumb.parentNode.href
el = $.el 'img', { src }
imgGif =
init: ->
g.callbacks.push (root) ->
return unless thumb = $ 'img[md5]', root
src = thumb.parentNode.href
if /gif$/.test src
thumb.src = src
imgExpand =
init: ->
g.callbacks.push imgExpand.node
imgExpand.dialog()
node: (root) ->
return unless thumb = $ 'img[md5]', root
a = thumb.parentNode
$.bind a, 'click', imgExpand.cb.toggle
if imgExpand.on and root.className isnt 'inline' then imgExpand.expand a.firstChild
cb:
toggle: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.button isnt 0
e.preventDefault()
imgExpand.toggle @
all: (e) ->
imgExpand.on = @checked
if imgExpand.on #expand
for thumb in $$ 'img[md5]:not([hidden])'
imgExpand.expand thumb
else #contract
for thumb in $$ 'img[md5][hidden]'
imgExpand.contract thumb
typeChange: (e) ->
switch @value
when 'full'
klass = ''
when 'fit width'
klass = 'fitwidth'
when 'fit height'
klass = 'fitheight'
when 'fit screen'
klass = 'fitwidth fitheight'
form = $('body > form')
form.className = klass
if form.classList.contains 'fitheight'
$.bind window, 'resize', imgExpand.resize
unless imgExpand.style
imgExpand.style = $.addStyle ''
imgExpand.resize()
else if imgExpand.style
$.unbind window, 'resize', imgExpand.resize
toggle: (a) ->
thumb = a.firstChild
if thumb.hidden
imgExpand.contract thumb
else
imgExpand.expand thumb
contract: (thumb) ->
thumb.hidden = false
$.rm thumb.nextSibling
expand: (thumb) ->
a = thumb.parentNode
img = $.el 'img',
src: a.href
unless a.parentNode.className is 'op'
filesize = $ '.filesize', a.parentNode
[_, max] = filesize.textContent.match /(\d+)x/
img.style.maxWidth = "-moz-calc(#{max}px)"
$.bind img, 'error', imgExpand.error
thumb.hidden = true
$.add a, img
error: (e) ->
thumb = @previousSibling
imgExpand.contract thumb
#navigator.online is not x-browser/os yet
if navigator.appName isnt 'Opera'
req = $.ajax @src, null, 'head'
req.onreadystatechange = (e) -> setTimeout imgExpand.retry, 10000, thumb if @status isnt 404
else unless g.dead
setTimeout imgExpand.retry, 10000, thumb
retry: (thumb) ->
imgExpand.expand thumb unless thumb.hidden
dialog: ->
controls = $.el 'div',
id: 'imgControls'
innerHTML:
"
"
imageType = $.get 'imageType', 'full'
for option in $$ 'option', controls
if option.textContent is imageType
option.selected = true
break
select = $ 'select', controls
imgExpand.cb.typeChange.call select
$.bind select, 'change', $.cb.value
$.bind select, 'change', imgExpand.cb.typeChange
$.bind $('input', controls), 'click', imgExpand.cb.all
delform = $ 'form[name=delform]'
$.prepend delform, controls
resize: ->
imgExpand.style.innerHTML = ".fitheight img + img {max-height:#{innerHeight}px;}"
firstRun =
init: ->
style = $.addStyle "
#navtopr, #navbotr {
position: relative;
}
#navtopr::before {
content: '';
height: 50px;
width: 100px;
background: red;
-webkit-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-o-transform: rotate(-45deg);
-webkit-transform-origin: 100% 200%;
-moz-transform-origin: 100% 200%;
-o-transform-origin: 100% 200%;
position: absolute;
top: 100%;
right: 100%;
z-index: 999;
}
#navtopr::after {
content: '';
border-top: 100px solid red;
border-left: 100px solid transparent;
position: absolute;
top: 100%;
right: 100%;
z-index: 999;
}
#navbotr::before {
content: '';
height: 50px;
width: 100px;
background: red;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-o-transform: rotate(45deg);
-webkit-transform-origin: 100% -100%;
-moz-transform-origin: 100% -100%;
-o-transform-origin: 100% -100%;
position: absolute;
bottom: 100%;
right: 100%;
z-index: 999;
}
#navbotr::after {
content: '';
border-bottom: 100px solid red;
border-left: 100px solid transparent;
position: absolute;
bottom: 100%;
right: 100%;
z-index: 999;
}
"
style.className = 'firstrun'
dialog = $.el 'div',
id: 'overlay'
className: 'firstrun'
innerHTML: "
Click the 4chan X buttons for options; they are at the top and bottom of the page.
Updater options are in the updater dialog in replies at the bottom-right corner of the window.
If you don't see the buttons, try disabling your userstyles.