"
qr.el = ui.dialog 'qr', 'top: 0; right: 0;', html
c = d.cookie
$('input[name=name]', qr.el).value =
if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else ''
$('input[name=email]', qr.el).value =
if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else ''
$('input[name=pwd]', qr.el).value =
if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value
$.on $('input[name=name]', qr.el), 'mousedown', (e) -> e.stopPropagation()
$.on $('input[name=upfile]', qr.el), 'change', qr.validateFileSize
$.on $('#close', qr.el), 'click', qr.close
$.on $('form', qr.el), 'submit', qr.submit
$.on $('#attach', qr.el), 'click', qr.attach
$.on $('img', qr.el), 'click', Recaptcha.reload
$.on $('#dummy', qr.el), 'keydown', Recaptcha.listener
$.on $('#dummy', qr.el), 'keydown', qr.captchaKeydown
$.add d.body, qr.el
message: (data) ->
$('iframe[name=iframe]').src = 'about:blank'
fileCount = $('#files', qr.el).childElementCount
tc = data.textContent
unless /successful!|uploaded!$/.test tc # error message, not a successful post
if tc is undefined
data.textContent = "Connection error with sys.4chan.org."
$.extend $('#error', qr.el), data
$('#recaptcha_response_field', qr.el).value = ''
$('#autohide', qr.el).checked = false
if tc is 'You seem to have mistyped the verification.'
setTimeout qr.autoPost, 1000
else if tc is 'Error: Duplicate file entry detected.' and fileCount
$('textarea', qr.el).value += '\n' + tc + ' ' + data.href
qr.attachNext()
setTimeout qr.autoPost, 1000
return
if qr.el
if g.REPLY and (conf['Persistent QR'] or fileCount)
qr.refresh()
if fileCount
qr.attachNext()
else
qr.close()
if conf['Cooldown']
duration = if qr.sage then 60 else 30
$.set g.BOARD+'/cooldown', Date.now() + duration * 1000
cooldown.start()
node: (root) ->
quote = $ 'a.quotejs:not(:first-child)', root
$.on quote, 'click', qr.quote
postInvalid: ->
content = $('textarea', qr.el).value or $('input[type=file]', qr.el).files.length
return 'Error: No text entered.' unless content
###
captchas expire after 30 minutes, see window.RecaptchaState.timeout.
cutoff 5 minutes before then, b/c posting takes time.
###
cutoff = Date.now() - 25*MINUTE
captchas = $.get 'captchas', []
while captcha = captchas.shift()
if captcha.time > cutoff
break
$.set 'captchas', captchas
$('#captchas', qr.el).textContent = captchas.length + ' captchas'
unless captcha
dummy = $ '#dummy', qr.el
return 'You forgot to type in the verification' unless response = dummy.value
captcha =
challenge: qr.challenge
response: response
dummy.value = ''
Recaptcha.reload()
$('#recaptcha_challenge_field', qr.el).value = captcha.challenge
$('#recaptcha_response_field', qr.el).value = captcha.response
false
quote: (e) ->
e.preventDefault() if e
if qr.el
$('#autohide', qr.el).checked = false
else
qr.dialog @
id = @textContent
text = ">>#{id}\n"
selection = window.getSelection()
if s = selection.toString()
selectionID = $.x('ancestor-or-self::blockquote/preceding-sibling::input', selection.anchorNode)?.name
if selectionID is id
s = s.replace /\n/g, '\n>'
text += ">#{s}\n"
ta = $ 'textarea', qr.el
caretPos = ta.selectionStart
#replace selection for text
ta.value = ta.value.slice(0, caretPos) + text + ta.value.slice(ta.selectionEnd, ta.value.length)
ta.focus()
#move the caret to the end of the new quote
ta.selectionEnd = ta.selectionStart = caretPos + text.length + 1*(engine is 'presto')
refresh: ->
$('[name=sub]', qr.el).value = ''
$('[name=email]', qr.el).value = if m = d.cookie.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else ''
$('[name=com]', qr.el).value = ''
$('[name=recaptcha_response_field]', qr.el).value = ''
$('[name=spoiler]', qr.el)?.checked = false unless conf['Remember Spoiler']
# XXX opera doesn't allow resetting file inputs w/ file.value = ''
oldFile = $ '[type=file]', qr.el
newFile = $.el 'input', type: 'file', name: 'upfile', accept: qr.acceptFiles
$.replace oldFile, newFile
submit: (e) ->
#XXX `e` won't exist if we're here from `qr.submit.call form`.
if msg = qr.postInvalid()
e.preventDefault?()
alert msg
if msg is 'You forgot to type in the verification.'
$('#dummy', qr.el).focus()
return
if conf['Auto Watch Reply'] and conf['Thread Watcher']
if g.REPLY and $('img.favicon').src is Favicon.empty
watcher.watch null, g.THREAD_ID
else
id = $('input[name=resto]', qr.el).value
op = $.id id
if $('img.favicon', op).src is Favicon.empty
watcher.watch op, id
if !e then @submit()
$('#error', qr.el).textContent = ''
$('#autohide', qr.el).checked = true if conf['Auto Hide QR']
qr.sage = /sage/i.test $('input[name=email]', @).value
sys: ->
if recaptcha = $ '#recaptcha_response_field' #post reporting
$.on recaptcha, 'keydown', Recaptcha.listener
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 ->
data = {}
if node = document.querySelector('td b')?.firstChild
data.textContent = node.textContent
data.href = node.href if node.href
parent.postMessage data, '*'
c = $('b')?.lastChild
return unless c and c.nodeType is 8 #comment node
[_, thread, id] = c.textContent.match(/thread:(\d+),no:(\d+)/)
{search} = location
cooldown = /cooldown/.test search
noko = /noko/ .test search
sage = /sage/ .test search
watch = /watch/ .test search
url = "http://boards.4chan.org/#{g.BOARD}"
if watch and thread is '0'
url += "/res/#{id}?watch"
else if noko
url += '/res/'
url += if thread is '0' then id else thread
if cooldown
duration = Date.now() + (if sage then 60 else 30) * 1000
url += '?cooldown=' + duration
if noko
url += '#' + id
window.location = url
validateFileSize: (e) ->
return unless @files[0].size > $('input[name=MAX_FILE_SIZE]').value
file = $.el 'input', type: 'file', name: 'upfile', accept: qr.acceptFiles
$.on file, 'change', qr.validateFileSize
$.replace @, file
$('#error', qr.el).textContent = 'Error: File too large.'
alert 'Error: File too large.'
Recaptcha =
init: ->
#hack to tab from comment straight to recaptcha
for el in $$ '#recaptcha_table a'
el.tabIndex = 1
$.on $('#recaptcha_response_field'), 'keydown', Recaptcha.listener
listener: (e) ->
if e.keyCode is 8 and @value is '' # backspace to reload
Recaptcha.reload()
reload: ->
window.location = 'javascript:Recaptcha.reload()'
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 $$ '.thread'
op = thread.firstChild
a = $.el 'a',
textContent: '[ - ]'
href: 'javascript:;'
$.on a, 'click', threadHiding.cb.hide
$.prepend op, a
if op.id of hiddenThreads
threadHiding.hideHide thread
cb:
hide: ->
thread = @parentNode.parentNode
threadHiding.hide thread
show: ->
thread = @parentNode.parentNode
threadHiding.show thread
toggle: (thread) ->
if /\bstub\b/.test(thread.className) 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']
return if /stub/.test thread.className #already hidden by filter
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',
innerHTML: "[ + ] #{name}#{trip} (#{text})"
href: 'javascript:;'
$.on 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
$.on window, 'focus', (-> updater.focus = true)
$.on 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'
$.on input, 'click', $.cb.checked
$.on input, 'click', -> conf[@name] = @checked
if input.name is 'Verbose'
$.on input, 'click', updater.cb.verbose
updater.cb.verbose.call input
else if input.name is 'Auto Update This'
$.on input, 'click', updater.cb.autoUpdate
updater.cb.autoUpdate.call input
else if input.name is 'Interval'
$.on input, 'change', -> conf['Interval'] = @value = parseInt(@value, 10) or conf['Interval']
$.on input, 'change', $.cb.value
else if input.type is 'button'
$.on input, 'click', updater.update
$.add d.body, dialog
updater.retryCoef = 10
updater.lastModified = 0
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
d.title = d.title.match(/^.+-/)[0] + ' 404'
g.dead = true
Favicon.update()
return
updater.retryCoef = 10
updater.timer.textContent = '-' + conf['Interval']
###
Status Code 304: Not modified
By sending the `If-Modified-Since` header we get a proper status code, and no response.
This saves bandwidth for both the user and the servers, avoid unnecessary computation,
and won't load images and scripts when parsing the response.
###
if @status is 304
if conf['Verbose']
updater.count.textContent = '+0'
updater.count.className = null
return
updater.lastModified = @getResponseHeader 'Last-Modified'
body = $.el 'body',
innerHTML: @responseText
#this only works on Chrome because of cross origin policy
if $('title', body).textContent is '4chan - Banned'
updater.count.textContent = 'Banned'
updater.count.className = 'error'
return
id = $('td[id]', updater.br.previousElementSibling)?.id or 0
frag = d.createDocumentFragment()
for reply in $$('.reply', body).reverse()
if reply.id <= id #make sure to not insert older posts
break
$.prepend frag, reply.parentNode.parentNode.parentNode #table
newPosts = frag.childNodes.length
scroll = conf['Scrolling'] && updater.focus && newPosts && (d.body.scrollHeight - d.body.clientHeight - window.scrollY < 20)
if conf['Verbose']
updater.count.textContent = '+' + newPosts
if newPosts is 0
updater.count.className = null
else
updater.count.className = 'new'
$.before updater.br, frag
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 updater.retryCoef
updater.retryCoef += 10 * (updater.retryCoef < 120)
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()
#fool the cache
url = location.pathname + '?' + Date.now()
updater.request = $.ajax url, updater.cb.update, headers: 'If-Modified-Since': updater.lastModified
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'
$.on favicon, 'click', watcher.cb.toggle
$.before input, favicon
#populate watcher, display watch buttons
watcher.refresh()
if conf['Auto Watch']
unless g.REPLY
$('.postarea form').action += '?watch'
else if /watch/.test(location.search) and $('img.favicon').src is Favicon.empty
watcher.watch null, g.THREAD_ID
$.on window, 'storage', (e) -> watcher.refresh() if e.key is "#{NAMESPACE}watched"
refresh: ->
watched = $.get 'watched', {}
frag = d.createDocumentFragment()
for board of watched
for id, props of watched[board]
x = $.el 'a',
textContent: 'X'
href: 'javascript:;'
$.on x, 'click', watcher.cb.x
link = $.el 'a', props
link.title = link.textContent
div = $.el 'div'
$.add div, x, $.tn(' '), link
$.add frag, div
for div in $$ 'div:not(.move)', watcher.dialog
$.rm div
$.add watcher.dialog, frag
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: ->
watcher.toggle @parentNode
x: ->
[board, _, id] = @nextElementSibling
.getAttribute('href')[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) ->
text = getTitle thread
props =
href: "/#{g.BOARD}/res/#{id}"
textContent: text
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: ->
return unless 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()
# GMT -8 is given as +480; would GMT +8 be -480 ?
chanOffset = 5 - new Date().getTimezoneOffset() / 60
# 4chan = EST = GMT -5
chanOffset-- if $.isDST()
@parse =
if Date.parse '10/11/11(Tue)18:53' is 1318351980000
(node) -> new Date Date.parse(node.textContent) + chanOffset*HOUR
else # Firefox and Opera do not parse 4chan's time format correctly
(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 = 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 /\binline\b/.test root.className
quotes = {}
for quote in $$ '.quotelink', root
#don't process >>>/b/
if qid = quote.hash[1..]
#duplicate quotes get overwritten
quotes[qid] = quote
# op or reply
id = $('input', root).name
a = $.el 'a',
href: "##{id}"
className: if root.hidden then 'filtered backlink' else 'backlink'
textContent: quoteBacklink.funk id
for qid of quotes
continue unless el = $.id qid
#don't backlink the op
continue if el.className is 'op' and !conf['OP Backlinks']
link = a.cloneNode true
if conf['Quote Preview']
$.on link, 'mouseover', quotePreview.mouseover
$.on link, 'mousemove', ui.hover
$.on link, 'mouseout', quotePreview.mouseout
if conf['Quote Inline']
$.on 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'
$.on quote, 'click', quoteInline.toggle
toggle: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
e.preventDefault()
id = @hash[1..]
if /\binlined\b/.test @className
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 g.REPLY and conf['Unread Count'] and (i = unread.replies.indexOf el.parentNode.parentNode.parentNode) isnt -1
unread.replies.splice i, 1
unread.updateTitle()
Favicon.update()
if /\bbacklink\b/.test q.className
$.after q.parentNode, inline
$.addClass $.x('ancestor::table', el), 'forwarded' if conf['Forward Hiding']
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
$.rm table
return unless conf['Forward Hiding']
for inlined in $$ '.backlink.inlined', table
$.removeClass $.x('ancestor::table', $.id inlined.hash[1..]), 'forwarded'
if /\bbacklink\b/.test q.className
$.removeClass $.x('ancestor::table', $.id id), 'forwarded'
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 is 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
$.on quote, 'mouseover', quotePreview.mouseover
$.on quote, 'mousemove', ui.hover
$.on quote, 'mouseout', quotePreview.mouseout
mouseover: (e) ->
qp = ui.el = $.el 'div',
id: 'qp'
className: 'reply dialog'
$.add d.body, qp
id = @hash[1..]
if el = $.id id
qp.innerHTML = el.innerHTML
$.addClass el, 'qphl' if conf['Quote Highlighting']
if /\bbacklink\b/.test @className
replyID = $.x('preceding-sibling::input', @parentNode).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 is 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)'
quoteDR =
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 leads to a different thread id and is located on the same board (index 0)
if quote.pathname.indexOf("res/#{tid}") is -1 and !quote.pathname.indexOf "/#{g.BOARD}/res"
quote.innerHTML += ' (Cross-thread)'
reportButton =
init: ->
g.callbacks.push (root) ->
if not a = $ '.reportbutton', root
span = $ 'span[id]', root
a = $.el 'a',
className: 'reportbutton'
innerHTML: '[ ! ]'
href: 'javascript:;'
$.after span, a
$.after span, $.tn(' ')
$.on a, 'click', reportButton.report
report: ->
url = "http://sys.4chan.org/#{g.BOARD}/imgboard.php?mode=report&no=#{$.x('preceding-sibling::input', @).name}"
id = Date.now()
set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200"
window.open url, id, set
threadStats =
init: ->
dialog = ui.dialog 'stats', 'bottom: 0; left: 0;', '
0 / 0
'
dialog.className = 'dialog'
$.add d.body, dialog
threadStats.posts = threadStats.images = 0
threadStats.imgLimit =
switch g.BOARD
when 'a', 'v'
251
else
151
g.callbacks.push threadStats.node
node: (root) ->
return if /\binline\b/.test root.className
$.id('postcount').textContent = ++threadStats.posts
return unless $ 'img[md5]', root
imgcount = $.id 'imagecount'
imgcount.textContent = ++threadStats.images
if threadStats.images > threadStats.imgLimit
imgcount.className = 'error'
unread =
init: ->
d.title = '(0) ' + d.title
$.on window, 'scroll', unread.scroll
g.callbacks.push unread.node
replies: []
node: (root) ->
return if root.hidden or root.className
unread.replies.push root
unread.updateTitle()
if unread.replies.length is 1
Favicon.update()
scroll: ->
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
@SFW = /ws.ico$/.test href
@default = href
@switch()
switch: ->
switch conf['favicon']
when 'ferongr'
@unreadDead = 'data:unreadDead;base64,R0lGODlhEAAQAOMHAOgLAnMFAL8AAOgLAukMA/+AgP+rq////////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw=='
@unreadSFW = 'data:unreadSFW;base64,R0lGODlhEAAQAOMHAADX8QBwfgC2zADX8QDY8nnl8qLp8v///////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw=='
@unreadNSFW = 'data:unreadNSFW;base64,R0lGODlhEAAQAOMHAFT+ACh5AEncAFT+AFX/Acz/su7/5v///////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw=='
when 'xat-'
@unreadDead = 'data:unreadDead;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA2ElEQVQ4y61TQQrCMBDMQ8WDIEV6LbT2A4og2Hq0veo7fIAH04dY9N4xmyYlpGmI2MCQTWYy3Wy2DAD7B2wWAzWgcTgVeZKlZRxHNYFi2jM18oBh0IcKtC6ixf22WT4IFLs0owxswXu9egm0Ls6bwfCFfNsJYJKfqoEkd3vgUgFVLWObtzNgVKyruC+ljSzr5OEnBzjvjcQecaQhbZgBb4CmGQw+PoMkTUtdbd8VSEPakcGxPOcsoIgUKy0LecY29BmdBrqRfjIwZ93KLs5loHvBnL3cLH/jF+C/+z5dgUysAAAAAElFTkSuQmCC'
@unreadSFW = 'data:unreadSFW;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA30lEQVQ4y2P4//8/AyWYgSoGQMF/GJ7Y11VVUVoyKTM9ey4Ig9ggMWQ1YA1IBvzXm34YjkH8mPyJB+Nqlp8FYRAbmxoMF6ArSNrw6T0Qf8Amh9cFMEWVR/7/A+L/uORxhgEIt5/+/3/2lf//5wAxiI0uj+4CBlBgxVUvOwtydgXQZpDmi2/+/7/0GmIQSAwkB1IDUkuUAZeABlx+g2zAZ9wGlAOjChba+LwAUgNSi2HA5Am9VciBhSsQQWyoWgZiovEDsdGI1QBYQiLJAGQalpSxyWEzAJYWkGm8clTJjQCZ1hkoVG0CygAAAABJRU5ErkJggg=='
@unreadNSFW = 'data:unreadNSFW;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA4ElEQVQ4y2P4//8/AyWYgSoGQMF/GJ7YNbGqrKRiUnp21lwQBrFBYshqwBqQDPifdsYYjkH8mInxB+OWx58FYRAbmxoMF6ArKPmU9B6IP2CTw+sCmKKe/5X/gPg/LnmcYQDCs/63/1/9fzYQzwGz0eXRXcAACqy4ZfFnQc7u+V/xD6T55v+LQHwJbBBIDCQHUgNSS5QBt4Cab/2/jDDgMx4DykrKJ8FCG58XQGpAajEMmNw7uQo5sHAFIogNVctATDR+IDYasRoAS0gkGYBMw5IyNjlsBsDSAjKNV44quREAx58Mr9vt5wQAAAAASUVORK5CYII='
when 'Mayhem'
@unreadDead = 'data:unreadDead;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABIUlEQVQ4jZ2ScWuDMBDFgw4pIkU0WsoQkWAYIkXZH4N9/+/V3dmfXSrKYIFHwt17j8vdGWNMIkgFuaDgzgQnwRs4EQs5KdolUQtagRN0givEDBTEOjgtGs0Zq8F7cKqqusVxrMQLaDUWcjBSrXkn8gs51tpJSWLk9b3HUa0aNIL5gPBR1/V4kJvR7lTwl8GmAm1Gf9+c3S+89qBHa8502AsmSrtBaEBPbIbj0ah2madlNAPEccdgJDfAtWifBjqWKShRBT6KoiH8QlEUn/qt0CCjnNdmPUwmFWzj9Oe6LpKuZXcwqq88z78Pch3aZU3dPwwc2sWlfZKCW5tWluV8kGvXClLm6dYN4/aUqfCbnEOzNDGhGZbNargvxCzvMGfRJD8UaDVvgkzo6QAAAABJRU5ErkJggg=='
@unreadSFW = 'data:unreadSFW;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABCElEQVQ4jZ2S4crCMAxF+0OGDJEPKYrIGKOsiJSx/fJRfSAfTJNyKqXfiuDg0C25N2RJjTGmEVrhTzhw7oStsIEtsVzT4o2Jo9ALThiEM8IdHIgNaHo8mjNWg6/ske8bohPo+63QOLzmooHp8fyAICBSQkVz0QKdsFQEV6WSW/D+7+BbgbIDHcb4Kp61XyjyI16zZ8JemGltQtDBSGxB4/GoN+7TpkkjDCsFArm0IYv3U0BbnYtf8BCy+JytsE0X6VyuKhPPK/GAJ14kvZZDZVV3pZIb8MZr6n4o4PDGKn0S5SdDmyq5PnXQsk+Xbhinp03FFzmHJw6xYRiWm9VxnohZ3vOcxdO8ARmXRvbWdtzQAAAAAElFTkSuQmCC'
@unreadNSFW = 'data:unreadNSFW;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABCklEQVQ4jZ2S0WrDMAxF/TBCCKWMYhZKCSGYmFJMSNjD/mhf239qJXNcjBdTWODgRLpXKJKNMaYROuFTOHEehFb4gJZYrunwxsSXMApOmIQzwgOciE1oRjyaM1aDj+yR7xuiHvT9VmgcXnPRwO/9+wWCgEgJFc1FCwzCVhFclUpuw/u3g3cFyg50GPOjePZ+ocjPeM2RCXthpbUFwQAzsQ2Nx6PeuE+bJo0w7BQI5NKGLN5XAW11LX7BQ8jia7bCLl2kc7mqTLzuxAOeeJH0Wk6VVf0oldyEN15T948CDm+sMiZRfjK0pZIbUwcd+3TphnF62lR8kXN44hAbhmG5WQNnT8zynucsnuYJhFpBfkMzqD4AAAAASUVORK5CYII='
when 'Original'
@unreadDead = 'data:unreadDead;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs='
@unreadSFW = 'data:unreadSFW;base64,R0lGODlhEAAQAKECAAAAAC6Xw////////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs='
@unreadNSFW = 'data:unreadNSFW;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs='
when 'None'
@unreadDead = @dead
@unreadSFW = 'http://static.4chan.org/image/favicon-ws.ico'
@unreadNSFW = 'http://static.4chan.org/image/favicon.ico'
@unread = if @SFW then @unreadSFW else @unreadNSFW
empty: 'data:image/gif;base64,R0lGODlhEAAQAJEAAAAAAP///9vb2////yH5BAEAAAMALAAAAAAQABAAAAIvnI+pq+D9DBAUoFkPFnbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw=='
dead: 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAIALAAAAAAQABAAAAIvlI+pq+D9DAgUoFkPDlbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw=='
update: ->
l = unread.replies.length
favicon = $ 'link[rel="shortcut icon"]', d.head
favicon.href =
if g.dead
if l
@unreadDead
else
@dead
else
if l
@unread
else
@default
#`favicon.href = href` doesn't work on Firefox
#`favicon.href = href` isn't enough on Opera
#Opera won't always update the favicon if the href do not change
if engine isnt 'webkit'
clone = favicon.cloneNode true
favicon.href = null
$.replace favicon, clone
redirect =
init: ->
url =
if location.hostname is 'images.4chan.org'
redirect.image g.BOARD, location.pathname.split('/')[3]
else if /^\d+$/.test g.THREAD_ID
redirect.thread()
location.href = url if url
image: (board, filename) -> #board must be given, the image can originate from a cross-quote
switch board
when 'a', 'jp', 'm', 'tg', 'tv', 'u'
"http://archive.foolz.us/#{board}/full_image/#{filename}"
thread: ->
switch g.BOARD
when 'a', 'jp', 'm', 'tg', 'tv', 'u'
"http://archive.foolz.us/#{g.BOARD}/thread/#{g.THREAD_ID}/"
when 'lit'
"http://fuuka.warosu.org/#{g.BOARD}/thread/#{g.THREAD_ID}"
when 'diy', 'g', 'sci'
"http://archive.installgentoo.net/#{g.BOARD}/thread/#{g.THREAD_ID}"
when '3', 'adv', 'an', 'ck', 'co', 'fa', 'fit', 'int', 'k', 'mu', 'n', 'o', 'p', 'po', 'pol', 'r9k', 'soc', 'sp', 'toy', 'trv', 'v', 'vp', 'x'
"http://archive.no-ip.org/#{g.BOARD}/thread/#{g.THREAD_ID}"
else
"http://boards.4chan.org/#{g.BOARD}"
imgHover =
init: ->
g.callbacks.push (root) ->
return unless thumb = $ 'img[md5]', root
$.on thumb, 'mouseover', imgHover.mouseover
$.on thumb, 'mousemove', ui.hover
$.on thumb, 'mouseout', ui.hoverend
mouseover: ->
ui.el = $.el 'img'
id: 'ihover'
src: @parentNode.href
$.add d.body, ui.el
imgGif =
init: ->
g.callbacks.push (root) ->
return if root.hidden or !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
$.on a, 'click', imgExpand.cb.toggle
if imgExpand.on and !root.hidden and root.className isnt 'inline'
imgExpand.expand a.firstChild
cb:
toggle: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
e.preventDefault()
imgExpand.toggle @
all: ->
imgExpand.on = @checked
if imgExpand.on #expand
for thumb in $$ '.op > a > img[md5]:last-child, table:not([hidden]) img[md5]:last-child'
imgExpand.expand thumb
else #contract
for thumb in $$ 'img[md5][hidden]'
imgExpand.contract thumb
typeChange: ->
switch @value
when 'full'
klass = ''
when 'fit width'
klass = 'fitwidth'
when 'fit height'
klass = 'fitheight'
when 'fit screen'
klass = 'fitwidth fitheight'
$('body > form').className = klass
if /\bfitheight\b/.test klass
$.on window, 'resize', imgExpand.resize
unless imgExpand.style
imgExpand.style = $.addStyle ''
imgExpand.resize()
else if imgExpand.style
$.off window, 'resize', imgExpand.resize
toggle: (a) ->
thumb = a.firstChild
if thumb.hidden
rect = a.parentNode.getBoundingClientRect()
d.body.scrollTop += rect.top if rect.top < 0
d.body.scrollLeft += rect.left if rect.left < 0
imgExpand.contract thumb
else
imgExpand.expand thumb
contract: (thumb) ->
thumb.hidden = false
$.rm thumb.nextSibling
expand: (thumb, url) ->
return if thumb.hidden
a = thumb.parentNode
img = $.el 'img',
src: url or a.href
if engine is 'gecko' and a.parentNode.className isnt 'op'
filesize = $.x('preceding-sibling::span[@class="filesize"]', a).textContent
max = filesize.match /(\d+)x/
img.style.maxWidth = "#{max[1]}px"
$.on img, 'error', imgExpand.error if conf['404 Redirect']
thumb.hidden = true
$.add a, img
error: ->
href = @parentNode.href
thumb = @previousSibling
imgExpand.contract thumb
src = href.split '/'
if @src.split('/')[2] is 'images.4chan.org' and url = redirect.image src[3], src[5]
setTimeout imgExpand.expand, 10000, thumb, url
return
url = href + '?' + Date.now()
#navigator.online is not x-browser/os yet
if engine is 'webkit'
req = $.ajax @src, (->
setTimeout imgExpand.expand, 10000, thumb, url if @status isnt 404
), type: 'head', event: 'onreadystatechange'
#Firefox returns a status code of 0 because of the same origin policy
#Oprah doesn't send any request
else unless g.dead
setTimeout imgExpand.expand, 10000, thumb, url
dialog: ->
controls = $.el 'div',
id: 'imgControls'
innerHTML:
""
imageType = $.get 'imageType', 'full'
select = $ 'select', controls
select.value = imageType
imgExpand.cb.typeChange.call select
$.on select, 'change', $.cb.value
$.on select, 'change', imgExpand.cb.typeChange
$.on $('input', controls), 'click', imgExpand.cb.all
form = $ 'body > form'
$.prepend form, controls
resize: ->
imgExpand.style.innerHTML = ".fitheight img[md5] + img {max-height:#{d.body.clientHeight}px;}"
Main =
init: ->
pathname = location.pathname[1..].split('/')
[g.BOARD, temp] = pathname
if temp is 'res'
g.REPLY = true
g.THREAD_ID = pathname[2]
else
g.PAGENUM = parseInt(temp) or 0
if location.hostname is 'sys.4chan.org'
$.ready qr.sys
return
if location.hostname is 'images.4chan.org'
$.ready -> redirect.init() if d.title is '4chan - 404'
return
$.ready options.init
$.on window, 'message', Main.message
now = Date.now()
if conf['Check for Updates'] and $.get('lastUpdate', 0) < now - 6*HOUR
$.ready -> $.add d.head, $.el 'script', src: 'https://raw.github.com/mayhemydg/4chan-x/master/latest.js'
$.set 'lastUpdate', now
g.hiddenReplies = $.get "hiddenReplies/#{g.BOARD}/", {}
if $.get('lastChecked', 0) < now - 1*DAY
$.set 'lastChecked', now
cutoff = now - 7*DAY
hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {}
for id, timestamp of hiddenThreads
if timestamp < cutoff
delete hiddenThreads[id]
for id, timestamp of g.hiddenReplies
if timestamp < cutoff
delete g.hiddenReplies[id]
$.set "hiddenThreads/#{g.BOARD}/", hiddenThreads
$.set "hiddenReplies/#{g.BOARD}/", g.hiddenReplies
#major features
if conf['Filter']
filter.init()
if conf['Reply Hiding']
replyHiding.init()
if conf['Filter'] or conf['Reply Hiding']
strikethroughQuotes.init()
if conf['Anonymize']
anonymize.init()
if conf['Time Formatting']
Time.init()
if conf['Sauce']
sauce.init()
if conf['Image Auto-Gif']
imgGif.init()
if conf['Image Hover']
imgHover.init()
if conf['Reveal Spoilers']
revealSpoilers.init()
if conf['Report Button']
reportButton.init()
if conf['Quote Inline']
quoteInline.init()
if conf['Quote Preview']
quotePreview.init()
if conf['Quote Backlinks']
quoteBacklink.init()
if conf['Indicate OP quote']
quoteOP.init()
if conf['Indicate Cross-thread Quotes']
quoteDR.init()
$.ready Main.ready
ready: ->
if conf['404 Redirect'] and d.title is '4chan - 404'
redirect.init()
return
if not $.id 'navtopr'
return
$.addClass d.body, engine
$.addStyle Main.css
threading.init()
Favicon.init()
#recaptcha may be blocked, eg by noscript
if (form = $ 'form[name=post]') and (canPost = !!$.id 'recaptcha_response_field')
Recaptcha.init()
if g.REPLY and conf['Auto Watch Reply'] and conf['Thread Watcher']
$.on form, 'submit', -> if $('img.favicon').src is Favicon.empty
watcher.watch null, g.THREAD_ID
#major features
if conf['Auto Noko'] and canPost
form.action += '?noko'
if conf['Cooldown'] and canPost
cooldown.init()
if conf['Image Expansion']
imgExpand.init()
if conf['Quick Reply'] and canPost
qr.init()
if conf['Thread Watcher']
watcher.init()
if conf['Keybinds']
keybinds.init()
if g.REPLY
if conf['Thread Updater']
updater.init()
if conf['Thread Stats']
threadStats.init()
if conf['Reply Navigation']
nav.init()
if conf['Post in Title']
titlePost.init()
if conf['Unread Count']
unread.init()
if conf['Quick Reply'] and conf['Persistent QR'] and canPost
qr.dialog()
if conf['Auto Hide QR']
$('#autohide', qr.el).checked = true
else #not reply
if conf['Thread Hiding']
threadHiding.init()
if conf['Thread Expansion']
expandThread.init()
if conf['Comment Expansion']
expandComment.init()
if conf['Index Navigation']
nav.init()
nodes = $$ '.op, a + table'
for callback in g.callbacks
try
for node in nodes
callback node
catch err
alert err
$.on $('form[name=delform]'), 'DOMNodeInserted', Main.node
message: (e) ->
{origin, data} = e
if origin is 'http://sys.4chan.org'
qr.message data
else if data.version and data.version isnt VERSION and confirm 'An updated version of 4chan X is available, would you like to install it now?'
window.location = "https://raw.github.com/mayhemydg/4chan-x/#{data.version}/4chan_x.user.js"
node: (e) ->
{target} = e
return unless target.nodeName is 'TABLE'
for callback in g.callbacks
try
callback target
catch err
#nothing
css: '
/* dialog styling */
.dialog {
border: 1px solid rgba(0,0,0,.25);
}
.move {
cursor: move;
}
label, .favicon, #qr_form > div > img {
cursor: pointer;
}
a[href="javascript:;"] {
text-decoration: none;
}
.thread.stub > :not(.block),
#content > [name=tab]:not(:checked) + div,
#updater:not(:hover) > :not(.move),
#qp > input, #qp .inline, .forwarded {
display: none;
}
.new {
background: lime;
}
.error {
color: red;
}
#error {
cursor: default;
}
#error[href] {
cursor: pointer;
}
td.replyhider {
vertical-align: top;
}
.filesize + br + a {
float: left;
pointer-events: none;
}
img[md5], img[md5] + img {
pointer-events: all;
}
.fitwidth img[md5] + img {
max-width: 100%;
}
.gecko > .fitwidth img[md5] + img,
.presto > .fitwidth img[md5] + img {
width: 100%;
}
#qp, #ihover {
position: fixed;
}
#ihover {
max-height: 100%;
max-width: 75%;
padding-bottom: 18px;
}
#navlinks {
font-size: 16px;
position: fixed;
top: 25px;
right: 5px;
}
#overlay {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
text-align: center;
background: rgba(0,0,0,.5);
z-index: 1;
}
#overlay::after {
content: "";
display: inline-block;
height: 100%;
vertical-align: middle;
}
#options {
display: inline-block;
padding: 5px;
text-align: left;
vertical-align: middle;
width: 500px;
}
#credits {
float: right;
}
#options ul {
list-style: none;
padding: 0;
}
#options label {
text-decoration: underline;
}
#content > div {
height: 450px;
overflow: auto;
}
#content textarea {
margin: 0;
min-height: 100px;
resize: vertical;
width: 100%;
}
#flavors {
height: 100%;
}
#qr {
position: fixed;
max-height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#qr > div.move {
text-align: right;
}
#qr input[name=name] {
float: left;
}
#qr_form {
clear: left;
}
#qr_form, #qr #com_submit, #qr input[name=upfile] {
margin: 0;
}
#qr textarea {
width: 100%;
height: 125px;
}
#qr #close, #qr #autohide {
float: right;
}
#qr:not(:hover) > #autohide:checked ~ .autohide {
height: 0;
overflow: hidden;
}
/* http://stackoverflow.com/questions/2610497/change-an-inputs-html5-placeholder-color-with-css */
#qr input::-webkit-input-placeholder {
color: grey;
}
#qr input:-moz-placeholder {
color: grey;
}
/* qr reCAPTCHA */
#qr img {
border: 1px solid #AAA;
}
#updater {
position: fixed;
text-align: right;
}
#updater input[type=text] {
width: 50px;
}
#updater:not(:hover) {
border: none;
background: transparent;
}
#stats {
border: none;
position: fixed;
}
#watcher {
position: absolute;
}
#watcher > div {
overflow: hidden;
padding-right: 5px;
padding-left: 5px;
text-overflow: ellipsis;
max-width: 200px;
white-space: nowrap;
}
#watcher > div.move {
text-decoration: underline;
padding-top: 5px;
}
#watcher > div:last-child {
padding-bottom: 5px;
}
#qp {
padding-bottom: 5px;
}
.qphl {
outline: 2px solid rgba(216, 94, 49, .7);
}
.inlined {
opacity: .5;
}
.inline td.reply {
background-color: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(128, 128, 128, 0.5);
}
.filetitle, .replytitle, .postername, .commentpostername, .postertrip {
background: none;
}
.filtered {
text-decoration: line-through;
}
#files > input {
display: block;
}
'
Main.init()