Add QR hastily.

This commit is contained in:
Nicolas Stepien 2013-02-12 01:20:33 +01:00
parent 9588a7e86f
commit 34a11f2e74
4 changed files with 1909 additions and 2 deletions

File diff suppressed because one or more lines are too long

View File

@ -269,6 +269,222 @@ a[href="javascript:;"] {
display: none !important;
}
/* QR */
#qr > .move {
min-width: 300px;
overflow: hidden;
box-sizing: border-box;
-moz-box-sizing: border-box;
padding: 0 2px;
}
#qr > .move > span {
float: right;
}
#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {
cursor: pointer;
}
#qr select,
#qr > form {
margin: 0;
}
#dump {
background: -webkit-linear-gradient(#EEE, #CCC);
background: -moz-linear-gradient(#EEE, #CCC);
background: -o-linear-gradient(#EEE, #CCC);
background: linear-gradient(#EEE, #CCC);
width: 10%;
}
.gecko #dump {
padding: 1px 0 2px;
}
#dump:hover, #dump:focus {
background: -webkit-linear-gradient(#FFF, #DDD);
background: -moz-linear-gradient(#FFF, #DDD);
background: -o-linear-gradient(#FFF, #DDD);
background: linear-gradient(#FFF, #DDD);
}
#dump:active, .dump #dump:not(:hover):not(:focus) {
background: -webkit-linear-gradient(#CCC, #DDD);
background: -moz-linear-gradient(#CCC, #DDD);
background: -o-linear-gradient(#CCC, #DDD);
background: linear-gradient(#CCC, #DDD);
}
#qr:not(.dump) #replies, .dump > form > label {
display: none;
}
#replies {
display: block;
height: 100px;
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}
#replies > div {
counter-reset: thumbnails;
top: 0; right: 0; bottom: 0; left: 0;
margin: 0; padding: 0;
overflow: hidden;
position: absolute;
white-space: pre;
}
#replies > div:hover {
bottom: -10px;
overflow-x: auto;
z-index: 1;
}
.thumbnail {
background-color: rgba(0,0,0,.2) !important;
background-position: 50% 20% !important;
background-size: cover !important;
border: 1px solid #666;
box-sizing: border-box;
-moz-box-sizing: border-box;
cursor: move;
display: inline-block;
height: 90px; width: 90px;
margin: 5px; padding: 2px;
opacity: .5;
outline: none;
overflow: hidden;
position: relative;
text-shadow: 0 1px 1px #000;
-webkit-transition: opacity .25s ease-in-out;
-moz-transition: opacity .25s ease-in-out;
-o-transition: opacity .25s ease-in-out;
transition: opacity .25s ease-in-out;
vertical-align: top;
}
.thumbnail:hover, .thumbnail:focus {
opacity: .9;
}
.thumbnail#selected {
opacity: 1;
}
.thumbnail::before {
counter-increment: thumbnails;
content: counter(thumbnails);
color: #FFF;
font-weight: 700;
padding: 3px;
position: absolute;
top: 0;
right: 0;
text-shadow: 0 0 3px #000, 0 0 8px #000;
}
.thumbnail.drag {
box-shadow: 0 0 10px rgba(0,0,0,.5);
}
.thumbnail.over {
border-color: #FFF;
}
.thumbnail > span {
color: #FFF;
}
.remove {
background: none;
color: #E00;
font-weight: 700;
padding: 3px;
}
.remove:hover::after {
content: " Remove";
}
.thumbnail > label {
background: rgba(0,0,0,.5);
color: #FFF;
right: 0; bottom: 0; left: 0;
position: absolute;
text-align: center;
}
.thumbnail > label > input {
margin: 0;
}
#addReply {
color: #333;
font-size: 3.5em;
line-height: 100px;
}
#addReply:hover, #addReply:focus {
color: #000;
}
.field {
border: 1px solid #CCC;
box-sizing: border-box;
-moz-box-sizing: border-box;
color: #333;
font: 13px sans-serif;
margin: 0;
padding: 2px 4px 3px;
-webkit-transition: color .25s, border .25s;
-moz-transition: color .25s, border .25s;
-o-transition: color .25s, border .25s;
transition: color .25s, border .25s;
}
.field:-moz-placeholder,
.field:hover:-moz-placeholder {
color: #AAA;
}
.field:hover, .field:focus {
border-color: #999;
color: #000;
outline: none;
}
#qr > form > div:first-child > .field:not(#dump) {
width: 30%;
}
#qr textarea.field {
display: -webkit-box;
min-height: 160px;
min-width: 100%;
}
#qr.captcha textarea.field {
min-height: 120px;
}
.textarea {
position: relative;
}
#charCount {
color: #000;
background: hsla(0, 0%, 100%, .5);
font-size: 8pt;
margin: 1px;
position: absolute;
bottom: 0;
right: 0;
pointer-events: none;
}
#charCount.warning {
color: red;
}
.captchainput > .field {
min-width: 100%;
}
.captchaimg {
background: #FFF;
outline: 1px solid #CCC;
outline-offset: -1px;
text-align: center;
}
.captchaimg > img {
display: block;
height: 57px;
width: 300px;
}
#qr [type=file] {
margin: 1px 0;
width: 70%;
}
#qr [type=submit] {
margin: 1px 0;
padding: 1px; /* not Gecko */
width: 30%;
}
.gecko #qr [type=submit] {
padding: 0 1px; /* Gecko does not respect box-sizing: border-box */
}
/* Menu */
.menu-button {
display: inline-block;

View File

@ -760,6 +760,783 @@ Recursive =
break
return
QR =
init: ->
return unless Conf['Quick Reply']
if Conf['Hide Original Post Form']
Main.css += """
#postForm, .postingMode {
display: none;
}
"""
Post::callbacks.push
name: 'Quick Reply'
cb: @node
$.ready @readyInit
readyInit: ->
if Conf['Persistent QR']
QR.dialog()
QR.hide() if Conf['Auto Hide QR']
$.on d, 'dragover', QR.dragOver
$.on d, 'drop', QR.dropFile
$.on d, 'dragstart dragend', QR.drag
node: ->
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
open: ->
if QR.el
QR.el.hidden = false
QR.unhide()
else
QR.dialog()
close: ->
QR.el.hidden = true
QR.abort()
d.activeElement.blur()
$.rmClass QR.el, 'dump'
for i in QR.replies
QR.replies[0].rm()
QR.cooldown.auto = false
QR.status()
QR.resetFileInput()
if not Conf['Remember Spoiler'] and (spoiler = $.id 'spoiler').checked
spoiler.click()
QR.cleanError()
hide: ->
d.activeElement.blur()
$.addClass QR.el, 'autohide'
$.id('autohide').checked = true
unhide: ->
$.rmClass QR.el, 'autohide'
$.id('autohide').checked = false
toggleHide: ->
@checked and QR.hide() or QR.unhide()
error: (err) ->
el = $ '.warning', QR.el
if typeof err is 'string'
el.textContent = err
else
el.innerHTML = null
$.add el, err
QR.open()
if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
# Focus the captcha input on captcha error.
$('[autocomplete]', QR.el).focus()
alert el.textContent if d.hidden
cleanError: ->
$('.warning', QR.el).textContent = null
status: (data={}) ->
return unless QR.el
if g.dead
value = 404
disabled = true
QR.cooldown.auto = false
value = data.progress or QR.cooldown.seconds or value
{input} = QR.status
input.value =
if QR.cooldown.auto and Conf['Cooldown']
if value then "Auto #{value}" else 'Auto'
else
value or 'Submit'
input.disabled = disabled or false
cooldown:
init: ->
return unless Conf['Cooldown']
QR.cooldown.types =
thread: switch g.BOARD
when 'q' then 86400
when 'b', 'soc', 'r9k' then 600
else 300
sage: if g.BOARD is 'q' then 600 else 60
file: if g.BOARD is 'q' then 300 else 30
post: if g.BOARD is 'q' then 60 else 30
QR.cooldown.cooldowns = $.get "#{g.BOARD}.cooldown", {}
QR.cooldown.start()
$.sync "#{g.BOARD}.cooldown", QR.cooldown.sync
start: ->
return if QR.cooldown.isCounting
QR.cooldown.isCounting = true
QR.cooldown.count()
sync: (cooldowns) ->
# Add each cooldowns, don't overwrite everything in case we
# still need to purge one in the current tab to auto-post.
for id of cooldowns
QR.cooldown.cooldowns[id] = cooldowns[id]
QR.cooldown.start()
set: (data) ->
return unless Conf['Cooldown']
start = Date.now()
if data.delay
cooldown = delay: data.delay
else
isSage = /sage/i.test data.post.email
hasFile = !!data.post.file
isReply = data.isReply
type =
unless isReply
'thread'
else if isSage
'sage'
else if hasFile
'file'
else
'post'
cooldown =
isReply: isReply
isSage: isSage
hasFile: hasFile
timeout: start + QR.cooldown.types[type] * $.SECOND
QR.cooldown.cooldowns[start] = cooldown
$.set "#{g.BOARD}.cooldown", QR.cooldown.cooldowns
QR.cooldown.start()
unset: (id) ->
delete QR.cooldown.cooldowns[id]
$.set "#{g.BOARD}.cooldown", QR.cooldown.cooldowns
count: ->
if Object.keys(QR.cooldown.cooldowns).length
setTimeout QR.cooldown.count, 1000
else
$.delete "#{g.BOARD}.cooldown"
delete QR.cooldown.isCounting
delete QR.cooldown.seconds
QR.status()
return
if (isReply = if g.REPLY then true else QR.threadSelector.value isnt 'new')
post = QR.replies[0]
isSage = /sage/i.test post.email
hasFile = !!post.file
now = Date.now()
seconds = null
{types, cooldowns} = QR.cooldown
for start, cooldown of cooldowns
if 'delay' of cooldown
if cooldown.delay
seconds = Math.max seconds, cooldown.delay--
else
seconds = Math.max seconds, 0
QR.cooldown.unset start
continue
if isReply is cooldown.isReply
# Only cooldowns relevant to this post can set the seconds value.
# Unset outdated cooldowns that can no longer impact us.
type =
unless isReply
'thread'
else if isSage and cooldown.isSage
'sage'
else if hasFile and cooldown.hasFile
'file'
else
'post'
elapsed = Math.floor (now - start) / 1000
if elapsed >= 0 # clock changed since then?
seconds = Math.max seconds, types[type] - elapsed
unless start <= now <= cooldown.timeout
QR.cooldown.unset start
# Update the status when we change posting type.
# Don't get stuck at some random number.
# Don't interfere with progress status updates.
update = seconds isnt null or !!QR.cooldown.seconds
QR.cooldown.seconds = seconds
QR.status() if update
QR.submit() if seconds is 0 and QR.cooldown.auto
quote: (e) ->
e?.preventDefault()
QR.open()
ta = $ 'textarea', QR.el
unless g.REPLY or ta.value
QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', @).id[1..]
# Make sure we get the correct number, even with XXX censors
id = @previousSibling.hash[2..]
text = ">>#{id}\n"
sel = d.getSelection()
if (s = sel.toString().trim()) and id is $.x('ancestor-or-self::blockquote', sel.anchorNode)?.id.match(/\d+$/)[0]
# XXX Opera doesn't retain `\n`s?
s = s.replace /\n/g, '\n>'
text += ">#{s}\n"
caretPos = ta.selectionStart
# Replace selection for text.
ta.value = ta.value[...caretPos] + text + ta.value[ta.selectionEnd..]
# Move the caret to the end of the new quote.
range = caretPos + text.length
ta.setSelectionRange range, range
ta.focus()
# Fire the 'input' event
$.event ta, new Event 'input'
characterCount: ->
counter = QR.charaCounter
count = @textLength
counter.textContent = count
counter.hidden = count < 1000
(if count > 1500 then $.addClass else $.rmClass) counter, 'warning'
drag: (e) ->
# Let it drag anything from the page.
toggle = if e.type is 'dragstart' then $.off else $.on
toggle d, 'dragover', QR.dragOver
toggle d, 'drop', QR.dropFile
dragOver: (e) ->
e.preventDefault()
e.dataTransfer.dropEffect = 'copy' # cursor feedback
dropFile: (e) ->
# Let it only handle files from the desktop.
return unless e.dataTransfer.files.length
e.preventDefault()
QR.open()
QR.fileInput.call e.dataTransfer
$.addClass QR.el, 'dump'
fileInput: ->
QR.cleanError()
# Set or change current reply's file.
if @files.length is 1
file = @files[0]
if file.size > @max
QR.error 'File too large.'
QR.resetFileInput()
else if -1 is QR.mimeTypes.indexOf file.type
QR.error 'Unsupported file type.'
QR.resetFileInput()
else
QR.selected.setFile file
return
# Create new replies with these files.
for file in @files
if file.size > @max
QR.error "File #{file.name} is too large."
break
else if -1 is QR.mimeTypes.indexOf file.type
QR.error "#{file.name}: Unsupported file type."
break
unless QR.replies[QR.replies.length - 1].file
# set last reply's file
QR.replies[QR.replies.length - 1].setFile file
else
new QR.reply().setFile file
$.addClass QR.el, 'dump'
QR.resetFileInput() # reset input
resetFileInput: ->
$('[type=file]', QR.el).value = null
replies: []
reply: class
constructor: ->
# set values, or null, to avoid 'undefined' values in inputs
prev = QR.replies[QR.replies.length-1]
persona = $.get 'QR.persona', {}
@name = if prev then prev.name else persona.name or null
@email = if prev and !/^sage$/.test prev.email then prev.email else persona.email or null
@sub = if prev and Conf['Remember Subject'] then prev.sub else if Conf['Remember Subject'] then persona.sub else null
@spoiler = if prev and Conf['Remember Spoiler'] then prev.spoiler else false
@com = null
@el = $.el 'a',
className: 'thumbnail'
draggable: true
href: 'javascript:;'
innerHTML: '<a class=remove>×</a><label hidden><input type=checkbox> Spoiler</label><span></span>'
$('input', @el).checked = @spoiler
$.on @el, 'click', => @select()
$.on $('.remove', @el), 'click', (e) =>
e.stopPropagation()
@rm()
$.on $('label', @el), 'click', (e) => e.stopPropagation()
$.on $('input', @el), 'change', (e) =>
@spoiler = e.target.checked
$.id('spoiler').checked = @spoiler if @el.id is 'selected'
$.before $('#addReply', QR.el), @el
$.on @el, 'dragstart', @dragStart
$.on @el, 'dragenter', @dragEnter
$.on @el, 'dragleave', @dragLeave
$.on @el, 'dragover', @dragOver
$.on @el, 'dragend', @dragEnd
$.on @el, 'drop', @drop
QR.replies.push @
setFile: (@file) ->
@el.title = "#{file.name} (#{$.bytesToString file.size})"
$('label', @el).hidden = false if QR.spoiler
unless /^image/.test file.type
@el.style.backgroundImage = null
return
# XXX Opera does not support window.URL
return unless url = window.URL or window.webkitURL
url.revokeObjectURL @url
# Create a redimensioned thumbnail.
fileUrl = url.createObjectURL file
img = $.el 'img'
$.on img, 'load', =>
# Generate thumbnails only if they're really big.
# Resized pictures through canvases look like ass,
# so we generate thumbnails `s` times bigger then expected
# to avoid crappy resized quality.
s = 90*3
if img.height < s or img.width < s
@url = fileUrl
@el.style.backgroundImage = "url(#{@url})"
return
if img.height <= img.width
img.width = s / img.height * img.width
img.height = s
else
img.height = s / img.width * img.height
img.width = s
c = $.el 'canvas'
c.height = img.height
c.width = img.width
c.getContext('2d').drawImage img, 0, 0, img.width, img.height
# Support for toBlob fucking when?
data = atob c.toDataURL().split(',')[1]
# DataUrl to Binary code from Aeosynth's 4chan X repo
l = data.length
ui8a = new Uint8Array l
for i in [0...l]
ui8a[i] = data.charCodeAt i
@url = url.createObjectURL new Blob [ui8a], type: 'image/png'
@el.style.backgroundImage = "url(#{@url})"
url.revokeObjectURL? fileUrl
img.src = fileUrl
rmFile: ->
QR.resetFileInput()
delete @file
@el.title = null
@el.style.backgroundImage = null
$('label', @el).hidden = true if QR.spoiler
(window.URL or window.webkitURL).revokeObjectURL? @url
select: ->
QR.selected?.el.id = null
QR.selected = @
@el.id = 'selected'
# Scroll the list to center the focused reply.
rectEl = @el.getBoundingClientRect()
rectList = @el.parentNode.getBoundingClientRect()
@el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2
# Load this reply's values.
for data in ['name', 'email', 'sub', 'com']
$("[name=#{data}]", QR.el).value = @[data]
QR.characterCount.call $ 'textarea', QR.el
$('#spoiler', QR.el).checked = @spoiler
dragStart: ->
$.addClass @, 'drag'
dragEnter: ->
$.addClass @, 'over'
dragLeave: ->
$.rmClass @, 'over'
dragOver: (e) ->
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
drop: ->
el = $ '.drag', @parentNode
index = (el) -> Array::slice.call(el.parentNode.children).indexOf el
oldIndex = index el
newIndex = index @
if oldIndex < newIndex
$.after @, el
else
$.before @, el
reply = QR.replies.splice(oldIndex, 1)[0]
QR.replies.splice newIndex, 0, reply
dragEnd: ->
$.rmClass @, 'drag'
if el = $ '.over', @parentNode
$.rmClass el, 'over'
rm: ->
QR.resetFileInput()
$.rm @el
index = QR.replies.indexOf @
if QR.replies.length is 1
new QR.reply().select()
else if @el.id is 'selected'
(QR.replies[index-1] or QR.replies[index+1]).select()
QR.replies.splice index, 1
(window.URL or window.webkitURL).revokeObjectURL? @url
captcha:
init: ->
return if -1 isnt d.cookie.indexOf 'pass_enabled='
return unless @isEnabled = !!$.id 'captchaFormPart'
if $.id 'recaptcha_challenge_field_holder'
@ready()
else
@onready = => @ready()
$.on $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready
ready: ->
if @challenge = $.id 'recaptcha_challenge_field_holder'
$.off $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready
delete @onready
else
return
$.addClass QR.el, 'captcha'
$.after $('.textarea', QR.el), $.el 'div',
className: 'captchaimg'
title: 'Reload'
innerHTML: '<img>'
$.after $('.captchaimg', QR.el), $.el 'div',
className: 'captchainput'
innerHTML: '<input title=Verification class=field autocomplete=off size=1>'
@img = $ '.captchaimg > img', QR.el
@input = $ '.captchainput > input', QR.el
$.on @img.parentNode, 'click', @reload
$.on @input, 'keydown', @keydown
$.on @challenge, 'DOMNodeInserted', => @load()
$.sync 'captchas', (arr) => @count arr.length
@count $.get('captchas', []).length
# start with an uncached captcha
@reload()
save: ->
return unless response = @input.value
captchas = $.get 'captchas', []
# Remove old captchas.
while (captcha = captchas[0]) and captcha.time < Date.now()
captchas.shift()
captchas.push
challenge: @challenge.firstChild.value
response: response
time: @timeout
$.set 'captchas', captchas
@count captchas.length
@reload()
load: ->
# Timeout is available at RecaptchaState.timeout in seconds.
# We use 5-1 minutes to give upload some time.
@timeout = Date.now() + 4*$.MINUTE
challenge = @challenge.firstChild.value
@img.alt = challenge
@img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}"
@input.value = null
count: (count) ->
@input.placeholder = switch count
when 0
'Verification (Shift + Enter to cache)'
when 1
'Verification (1 cached captcha)'
else
"Verification (#{count} cached captchas)"
@input.alt = count # For XTRM RICE.
reload: (focus) ->
# the "t" argument prevents the input from being focused
$.globalEval 'javascript:Recaptcha.reload("t")'
# Focus if we meant to.
QR.captcha.input.focus() if focus
keydown: (e) ->
c = QR.captcha
if e.keyCode is 8 and not c.input.value
c.reload()
else if e.keyCode is 13 and e.shiftKey
c.save()
else
return
e.preventDefault()
dialog: ->
QR.el = UI.dialog 'qr', 'top:0;right:0;', '
<div class=move>
Quick Reply <input type=checkbox id=autohide title=Auto-hide>
<span> <a class=close title=Close>×</a></span>
</div>
<form>
<div><input id=dump type=button title="Dump list" value=+ class=field><input name=name title=Name placeholder=Name class=field size=1><input name=email title=E-mail placeholder=E-mail class=field size=1><input name=sub title=Subject placeholder=Subject class=field size=1></div>
<div id=replies><div><a id=addReply href=javascript:; title="Add a reply">+</a></div></div>
<div class=textarea><textarea name=com title=Comment placeholder=Comment class=field></textarea><span id=charCount></span></div>
<div><input type=file title="Shift+Click to remove the selected file." multiple size=16><input type=submit></div>
<label id=spoilerLabel><input type=checkbox id=spoiler> Spoiler Image</label>
<div class=warning></div>
</form>'
if Conf['Remember QR size'] and $.engine is 'gecko'
$.on ta = $('textarea', QR.el), 'mouseup', ->
$.set 'QR.size', @style.cssText
ta.style.cssText = $.get 'QR.size', ''
# Allow only this board's supported files.
mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) ->
switch type
when 'jpg'
'image/jpeg'
when 'pdf'
'application/pdf'
when 'swf'
'application/x-shockwave-flash'
else
"image/#{type}"
QR.mimeTypes = mimeTypes.split ', '
# Add empty mimeType to avoid errors with URLs selected in Window's file dialog.
QR.mimeTypes.push ''
fileInput = $ 'input[type=file]', QR.el
fileInput.max = $('input[name=MAX_FILE_SIZE]').value
fileInput.accept = mimeTypes if $.engine isnt 'presto' # Opera's accept attribute is fucked up
QR.spoiler = !!$ 'input[name=spoiler]'
spoiler = $ '#spoilerLabel', QR.el
spoiler.hidden = !QR.spoiler
QR.charaCounter = $ '#charCount', QR.el
ta = $ 'textarea', QR.el
unless g.REPLY
# Make a list with visible threads and an option to create a new one.
threads = '<option value=new>New thread</option>'
for thread in $$ '.thread'
id = thread.id[1..]
threads += "<option value=#{id}>Thread #{id}</option>"
QR.threadSelector =
if g.BOARD is 'f'
$('select[name=filetag]').cloneNode true
else
$.el 'select'
innerHTML: threads
title: 'Create a new thread / Reply to a thread'
$.prepend $('.move > span', QR.el), QR.threadSelector
$.on QR.threadSelector, 'mousedown', (e) -> e.stopPropagation()
$.on $('#autohide', QR.el), 'change', QR.toggleHide
$.on $('.close', QR.el), 'click', QR.close
$.on $('#dump', QR.el), 'click', -> QR.el.classList.toggle 'dump'
$.on $('#addReply', QR.el), 'click', -> new QR.reply().select()
$.on $('form', QR.el), 'submit', QR.submit
$.on ta, 'input', -> QR.selected.el.lastChild.textContent = @value
$.on ta, 'input', QR.characterCount
$.on fileInput, 'change', QR.fileInput
$.on fileInput, 'click', (e) -> if e.shiftKey then QR.selected.rmFile() or e.preventDefault()
$.on spoiler.firstChild, 'change', -> $('input', QR.selected.el).click()
$.on $('.warning', QR.el), 'click', QR.cleanError
new QR.reply().select()
# save selected reply's data
for name in ['name', 'email', 'sub', 'com']
# The input event replaces keyup, change and paste events.
$.on $("[name=#{name}]", QR.el), 'input', ->
QR.selected[@name] = @value
# Disable auto-posting if you're typing in the first reply
# during the last 5 seconds of the cooldown.
if QR.cooldown.auto and QR.selected is QR.replies[0] and 0 < QR.cooldown.seconds <= 5
QR.cooldown.auto = false
QR.status.input = $ 'input[type=submit]', QR.el
QR.status()
QR.cooldown.init()
QR.captcha.init()
$.add d.body, QR.el
# Create a custom event when the QR dialog is first initialized.
# Use it to extend the QR's functionalities, or for XTRM RICE.
$.event QR.el, new CustomEvent 'QRDialogCreation',
bubbles: true
submit: (e) ->
e?.preventDefault()
if QR.cooldown.seconds
QR.cooldown.auto = !QR.cooldown.auto
QR.status()
return
QR.abort()
reply = QR.replies[0]
if g.BOARD is 'f' and not g.REPLY
filetag = QR.threadSelector.value
threadID = 'new'
else
threadID = g.THREAD_ID or QR.threadSelector.value
# prevent errors
if threadID is 'new'
threadID = null
if g.BOARD in ['vg', 'q'] and !reply.sub
err = 'New threads require a subject.'
else unless reply.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm'
err = 'No file selected.'
else if g.BOARD is 'f' and filetag is '9999'
err = 'Invalid tag specified.'
else unless reply.com or reply.file
err = 'No file selected.'
if QR.captcha.isEnabled and !err
# get oldest valid captcha
captchas = $.get 'captchas', []
# remove old captchas
while (captcha = captchas[0]) and captcha.time < Date.now()
captchas.shift()
if captcha = captchas.shift()
challenge = captcha.challenge
response = captcha.response
else
challenge = QR.captcha.img.alt
if response = QR.captcha.input.value then QR.captcha.reload()
$.set 'captchas', captchas
QR.captcha.count captchas.length
unless response
err = 'No valid captcha.'
else
response = response.trim()
# one-word-captcha:
# If there's only one word, duplicate it.
response = "#{response} #{response}" unless /\s/.test response
if err
# stop auto-posting
QR.cooldown.auto = false
QR.status()
QR.error err
return
QR.cleanError()
# Enable auto-posting if we have stuff to post, disable it otherwise.
QR.cooldown.auto = QR.replies.length > 1
if Conf['Auto Hide QR'] and not QR.cooldown.auto
QR.hide()
if not QR.cooldown.auto and $.x 'ancestor::div[@id="qr"]', d.activeElement
# Unfocus the focused element if it is one within the QR and we're not auto-posting.
d.activeElement.blur()
# Starting to upload might take some time.
# Provide some feedback that we're starting to submit.
QR.status progress: '...'
post =
resto: threadID
name: reply.name
email: reply.email
sub: reply.sub
com: reply.com
upfile: reply.file
filetag: filetag
spoiler: reply.spoiler
textonly: textOnly
mode: 'regist'
pwd: if m = d.cookie.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value
recaptcha_challenge_field: challenge
recaptcha_response_field: response
callbacks =
onload: ->
QR.response @response
onerror: ->
# Connection error, or
# CORS disabled error on www.4chan.org/banned
QR.cooldown.auto = false
QR.status()
QR.error $.el 'a',
href: '//www.4chan.org/banned',
target: '_blank',
textContent: 'Connection error, or you are banned.'
opts =
form: $.formData post
upCallbacks:
onload: ->
# Upload done, waiting for response.
QR.status progress: '...'
onprogress: (e) ->
# Uploading...
QR.status progress: "#{Math.round e.loaded / e.total * 100}%"
QR.ajax = $.ajax $.id('postForm').parentNode.action, callbacks, opts
response: (html) ->
doc = d.implementation.createHTMLDocument ''
doc.documentElement.innerHTML = html
if ban = $ '.banType', doc # banned/warning
board = $('.board', doc).innerHTML
err = $.el 'span', innerHTML:
if ban.textContent.toLowerCase() is 'banned'
"You are banned on #{board}! ;_;<br>" +
"Click <a href=//www.4chan.org/banned target=_blank>here</a> to see the reason."
else
"You were issued a warning on #{board} as #{$('.nameBlock', doc).innerHTML}.<br>" +
"Reason: #{$('.reason', doc).innerHTML}"
else if err = doc.getElementById 'errmsg' # error!
$('a', err)?.target = '_blank' # duplicate image link
else if doc.title isnt 'Post successful!'
err = 'Connection error with sys.4chan.org.'
if err
if /captcha|verification/i.test(err.textContent) or err is 'Connection error with sys.4chan.org.'
# Remove the obnoxious 4chan Pass ad.
if /mistyped/i.test err.textContent
err = 'Error: You seem to have mistyped the CAPTCHA.'
# Enable auto-post if we have some cached captchas.
QR.cooldown.auto =
if QR.captcha.isEnabled
!!$.get('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!
# On connection error, the post most likely didn't go through.
QR.cooldown.set delay: 2
else # stop auto-posting
QR.cooldown.auto = false
QR.status()
QR.error err
return
reply = QR.replies[0]
persona = $.get 'QR.persona', {}
persona =
name: reply.name
email: if /^sage$/.test reply.email then persona.email else reply.email
sub: if Conf['Remember Subject'] then reply.sub else null
$.set 'QR.persona', persona
[_, threadID, postID] = doc.body.lastChild.textContent.match /thread:(\d+),no:(\d+)/
# Post/upload confirmed as successful.
$.event QR.el, new CustomEvent 'QRPostSuccessful',
bubbles: true
detail:
threadID: threadID
postID: postID
QR.cooldown.set
post: reply
isReply: threadID isnt '0'
if threadID is '0' # new thread
# auto-noko
location.pathname = "/#{g.BOARD}/res/#{postID}"
else
# Enable auto-posting if we have stuff to post, disable it otherwise.
QR.cooldown.auto = QR.replies.length > 1
if Conf['Open Reply in New Tab'] and !g.REPLY and !QR.cooldown.auto
$.open "//boards.4chan.org/#{g.BOARD}/res/#{threadID}#p#{postID}"
if Conf['Persistent QR'] or QR.cooldown.auto
reply.rm()
else
QR.close()
QR.status()
QR.resetFileInput()
abort: ->
QR.ajax?.abort()
delete QR.ajax
QR.status()
Menu =
init: ->
return if g.VIEW is 'catalog' or !Conf['Menu']

View File

@ -295,6 +295,7 @@ Main =
initFeature 'Thread Hiding', ThreadHiding
initFeature 'Reply Hiding', ReplyHiding
initFeature 'Recursive', Recursive
initFeature 'Quick Reply', QR
initFeature 'Menu', Menu
initFeature 'Report Link', ReportLink
initFeature 'Thread Hiding (Menu)', ThreadHiding.menu