Merge branch 'v3' of git://github.com/MayhemYDG/4chan-x into v3
Conflicts: Gruntfile.coffee src/Filtering/PostHiding.coffee src/General/Build.coffee src/General/lib/$.coffee src/Menu/Menu.coffee src/Posting/QuickReply.coffee
This commit is contained in:
commit
b38f63e313
@ -28,7 +28,7 @@ module.exports = (grunt) ->
|
||||
'src/General/Notice.coffee'
|
||||
'src/Filtering/**/*'
|
||||
'src/Quotelinks/**/*'
|
||||
'src/Linkification/**/*'
|
||||
'src/Posting/QR.coffee'
|
||||
'src/Posting/**/*'
|
||||
'src/Images/**/*'
|
||||
'src/Linkification/**/*'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2715
builds/crx/script.js
2715
builds/crx/script.js
File diff suppressed because it is too large
Load Diff
@ -89,13 +89,4 @@
|
||||
"software": "foolfuuka",
|
||||
"boards": ["a", "co", "gd", "jp", "m", "s4s", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
|
||||
"files": ["a", "gd", "jp", "m", "s4s", "tg", "u", "vg", "vp", "vr", "wsg"]
|
||||
}, {
|
||||
"uid": 14,
|
||||
"name": "Bui's Archive",
|
||||
"domain": "archive.bui.pm",
|
||||
"http": true,
|
||||
"https": true,
|
||||
"software": "foolfuuka",
|
||||
"boards": ["b"],
|
||||
"files": ["b"]
|
||||
}]
|
||||
|
||||
@ -30,10 +30,10 @@
|
||||
"grunt-bump": "~0.0.11",
|
||||
"grunt-concurrent": "~0.4.0",
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-contrib-coffee": "~0.7.0",
|
||||
"grunt-contrib-coffee": "~0.8.0",
|
||||
"grunt-contrib-compress": "~0.5.2",
|
||||
"grunt-contrib-concat": "~0.3.0",
|
||||
"grunt-contrib-copy": "~0.4.1",
|
||||
"grunt-contrib-copy": "~0.5.0",
|
||||
"grunt-contrib-watch": "~0.5.3",
|
||||
"grunt-shell": "~0.6.0",
|
||||
"load-grunt-tasks": "~0.2.0"
|
||||
|
||||
@ -136,10 +136,13 @@ PostHiding =
|
||||
return
|
||||
|
||||
makeButton: (post, type) ->
|
||||
span = $.el 'span',
|
||||
className: "brackets-wrap"
|
||||
textContent: "\u00A0#{if type is 'hide' then '-' else '+'}\u00A0"
|
||||
a = $.el 'a',
|
||||
className: "#{type}-reply-button"
|
||||
innerHTML: "<span class=brackets-wrap> #{if type is 'hide' then '-' else '+'} </span>"
|
||||
href: 'javascript:;'
|
||||
$.add a, span
|
||||
$.on a, 'click', PostHiding.toggle
|
||||
a
|
||||
|
||||
@ -186,10 +189,9 @@ PostHiding =
|
||||
$.add a, $.tn " #{postInfo}"
|
||||
post.nodes.stub = $.el 'div',
|
||||
className: 'stub'
|
||||
$.add post.nodes.stub, if Conf['Menu']
|
||||
[a, $.tn(' '), button = Menu.makeButton post]
|
||||
else
|
||||
a
|
||||
$.add post.nodes.stub, a
|
||||
if Conf['Menu']
|
||||
$.add post.nodes.stub, Menu.makeButton()
|
||||
$.prepend post.nodes.root, post.nodes.stub
|
||||
|
||||
show: (post, showRecursively=Conf['Recursive Hiding']) ->
|
||||
|
||||
@ -164,7 +164,7 @@ ThreadHiding =
|
||||
thread.stub = $.el 'div',
|
||||
className: 'stub'
|
||||
if Conf['Menu']
|
||||
$.add thread.stub, [a, $.tn(' '), Menu.makeButton()]
|
||||
$.add thread.stub, [a, Menu.makeButton()]
|
||||
else
|
||||
$.add thread.stub, a
|
||||
$.prepend root, thread.stub
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
Build =
|
||||
staticPath: '//s.4cdn.org/image/'
|
||||
gifIcon: if window.devicePixelRatio >= 2 then '@2x.gif' else '.gif'
|
||||
spoilerRange: {}
|
||||
shortFilename: (filename, isReply) ->
|
||||
# FILENAME SHORTENING SCIENCE:
|
||||
@ -65,12 +67,12 @@ Build =
|
||||
file
|
||||
} = o
|
||||
isOP = postID is threadID
|
||||
{staticPath, gifIcon} = Build
|
||||
|
||||
staticPath = '//s.4cdn.org/image/'
|
||||
gifIcon = if window.devicePixelRatio >= 2
|
||||
'@2x.gif'
|
||||
tripcode = if tripcode
|
||||
" <span class=postertrip>#{tripcode}</span>"
|
||||
else
|
||||
'.gif'
|
||||
''
|
||||
|
||||
if email
|
||||
emailStart = '<a href="mailto:' + email + '" class="useremail">'
|
||||
@ -79,7 +81,29 @@ Build =
|
||||
emailStart = ''
|
||||
emailEnd = ''
|
||||
|
||||
subject = " <span class=subject>#{subject or ''}</span> "
|
||||
switch capcode
|
||||
when 'admin', 'admin_highlight'
|
||||
capcodeClass = " capcodeAdmin"
|
||||
capcodeStart = " <strong class='capcode hand id_admin'" +
|
||||
"title='Highlight posts by the Administrator'>## Admin</strong>"
|
||||
capcodeIcon = " <img src='#{staticPath}adminicon#{gifIcon}' " +
|
||||
"title='This user is the 4chan Administrator.' class=identityIcon>"
|
||||
when 'mod'
|
||||
capcodeClass = " capcodeMod"
|
||||
capcodeStart = " <strong class='capcode hand id_mod' " +
|
||||
"title='Highlight posts by Moderators'>## Mod</strong>"
|
||||
capcodeIcon = " <img src='#{staticPath}modicon#{gifIcon}' " +
|
||||
"title='This user is a 4chan Moderator.' class=identityIcon>"
|
||||
when 'developer'
|
||||
capcodeClass = " capcodeDeveloper"
|
||||
capcodeStart = " <strong class='capcode hand id_developer' " +
|
||||
"title='Highlight posts by Developers'>## Developer</strong>"
|
||||
capcodeIcon = " <img src='#{staticPath}developericon#{gifIcon}' " +
|
||||
"title='This user is a 4chan Developer.' class=identityIcon>"
|
||||
else
|
||||
capcodeClass = ''
|
||||
capcodeStart = ''
|
||||
capcodeIcon = ''
|
||||
|
||||
userID =
|
||||
if !capcode and uniqueID
|
||||
@ -88,33 +112,6 @@ Build =
|
||||
else
|
||||
''
|
||||
|
||||
switch capcode
|
||||
when 'admin', 'admin_highlight'
|
||||
capcodeClass = " capcodeAdmin"
|
||||
capcodeStart = " <strong class='capcode hand id_admin'" +
|
||||
"title='Highlight posts by the Administrator'>## Admin</strong>"
|
||||
capcode = " <img src='#{staticPath}adminicon#{gifIcon}' " +
|
||||
"alt='This user is the 4chan Administrator.' " +
|
||||
"title='This user is the 4chan Administrator.' class=identityIcon>"
|
||||
when 'mod'
|
||||
capcodeClass = " capcodeMod"
|
||||
capcodeStart = " <strong class='capcode hand id_mod' " +
|
||||
"title='Highlight posts by Moderators'>## Mod</strong>"
|
||||
capcode = " <img src='#{staticPath}modicon#{gifIcon}' " +
|
||||
"alt='This user is a 4chan Moderator.' " +
|
||||
"title='This user is a 4chan Moderator.' class=identityIcon>"
|
||||
when 'developer'
|
||||
capcodeClass = " capcodeDeveloper"
|
||||
capcodeStart = " <strong class='capcode hand id_developer' " +
|
||||
"title='Highlight posts by Developers'>## Developer</strong>"
|
||||
capcode = " <img src='#{staticPath}developericon#{gifIcon}' " +
|
||||
"alt='This user is a 4chan Developer.' " +
|
||||
"title='This user is a 4chan Developer.' class=identityIcon>"
|
||||
else
|
||||
capcodeClass = ''
|
||||
capcodeStart = ''
|
||||
capcode = ''
|
||||
|
||||
flag = unless flagCode
|
||||
''
|
||||
else if boardID is 'pol'
|
||||
@ -132,13 +129,7 @@ Build =
|
||||
"<img src='#{staticPath}filedeleted-res#{gifIcon}' alt='File deleted.' class=fileDeletedRes>" +
|
||||
"</span></div>"
|
||||
else if file
|
||||
ext = file.name[-3..]
|
||||
if !file.twidth and !file.theight and ext is 'gif' # wtf ?
|
||||
file.twidth = file.width
|
||||
file.theight = file.height
|
||||
|
||||
fileSize = $.bytesToString file.size
|
||||
|
||||
fileSize = $.bytesToString file.size
|
||||
fileThumb = file.turl
|
||||
if file.isSpoiler
|
||||
fileSize = "Spoiler Image, #{fileSize}"
|
||||
@ -157,20 +148,17 @@ Build =
|
||||
"<img src='#{fileThumb}' alt='#{fileSize}' data-md5=#{file.MD5} style='height: #{file.theight}px; width: #{file.twidth}px;'>" +
|
||||
"</a>"
|
||||
|
||||
# Ha ha, filenames!
|
||||
# html -> text, translate WebKit's %22s into "s
|
||||
a = $.el 'a', innerHTML: file.name
|
||||
filename = a.textContent.replace /%22/g, '"'
|
||||
|
||||
# shorten filename, get html
|
||||
a.textContent = Build.shortFilename filename
|
||||
shortFilename = a.innerHTML
|
||||
|
||||
# get html
|
||||
a.textContent = filename
|
||||
filename = a.innerHTML.replace /'/g, '''
|
||||
|
||||
fileDims = if ext is 'pdf' then 'PDF' else "#{file.width}x#{file.height}"
|
||||
fileDims = if file.name[-3..] is 'pdf' then 'PDF' else "#{file.width}x#{file.height}"
|
||||
fileInfo = "<div class=fileText id=fT#{postID}#{if file.isSpoiler then " title='#{filename}'" else ''}>File: <a href='#{file.url}' target=_blank>#{file.timestamp}</a>" +
|
||||
"-(#{fileSize}, #{fileDims}#{
|
||||
if file.isSpoiler
|
||||
@ -183,11 +171,6 @@ Build =
|
||||
else
|
||||
fileHTML = ''
|
||||
|
||||
tripcode = if tripcode
|
||||
" <span class=postertrip>#{tripcode}</span>"
|
||||
else
|
||||
''
|
||||
|
||||
sticky = if isSticky
|
||||
" <img src=#{staticPath}sticky#{gifIcon} alt=Sticky title=Sticky class=stickyIcon>"
|
||||
else
|
||||
|
||||
@ -170,8 +170,8 @@ Get =
|
||||
threadID = +data.thread_num
|
||||
o =
|
||||
# id
|
||||
postID: "#{postID}"
|
||||
threadID: "#{threadID}"
|
||||
postID: postID
|
||||
threadID: threadID
|
||||
boardID: boardID
|
||||
# info
|
||||
name: data.name_processed
|
||||
@ -207,8 +207,7 @@ Get =
|
||||
new Board boardID
|
||||
thread = g.threads["#{boardID}.#{threadID}"] or
|
||||
new Thread threadID, board
|
||||
post = new Post Build.post(o, true), thread, board,
|
||||
isArchived: true
|
||||
post = new Post Build.post(o, true), thread, board, {isArchived: true}
|
||||
Main.callbackNodes Post, [post]
|
||||
Get.insert post, root, context
|
||||
parseMarkup: (text) ->
|
||||
|
||||
@ -191,7 +191,7 @@ Main =
|
||||
posts = []
|
||||
for postRoot in $$ '.thread > .postContainer', threadRoot
|
||||
try
|
||||
posts.push new Post postRoot, thread, g.BOARD
|
||||
posts.push post = new Post postRoot, thread, g.BOARD, {isOriginalMarkup: true}
|
||||
catch err
|
||||
# Skip posts that we failed to parse.
|
||||
errors = [] unless errors
|
||||
|
||||
@ -6,33 +6,9 @@
|
||||
''
|
||||
}'>
|
||||
|
||||
<div class='postInfoM mobile' id=pim#{postID}>
|
||||
<span class='nameBlock#{capcodeClass}'>
|
||||
<span class=name>
|
||||
#{name or ''}
|
||||
</span>
|
||||
#{tripcode + capcodeStart + capcode + userID + flag + sticky + closed}
|
||||
<br>#{subject}
|
||||
</span>
|
||||
<span class='dateTime postNum' data-utc=#{dateUTC}>
|
||||
#{date}
|
||||
<a href=#{"/#{boardID}/res/#{threadID}#p#{postID}"}>
|
||||
No.
|
||||
</a>
|
||||
<a href='#{
|
||||
if g.VIEW is 'thread' and g.THREADID is +threadID then
|
||||
"javascript:quote(#{postID})"
|
||||
else
|
||||
"/#{boardID}/res/#{threadID}#q#{postID}"
|
||||
}'>
|
||||
#{postID}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
#{if isOP then fileHTML else ''}
|
||||
|
||||
<div class='postInfo desktop' id=pi#{postID}>
|
||||
<div class='postInfo' id=pi#{postID}>
|
||||
<input type=checkbox name=#{postID} value=delete>
|
||||
#{subject}
|
||||
<span class='nameBlock#{capcodeClass}'>
|
||||
@ -41,7 +17,7 @@
|
||||
#{tripcode + capcodeStart + emailEnd + capcode + userID + flag}
|
||||
</span>#{" "}
|
||||
<span class=dateTime data-utc=#{dateUTC}>#{date}</span>#{" "}
|
||||
<span class='postNum desktop'>
|
||||
<span class='postNum'>
|
||||
<a href=#{"/#{boardID}/res/#{threadID}#p#{postID}"} title='Highlight this post'>No.</a>
|
||||
<a href='#{
|
||||
if g.VIEW is 'thread' and g.THREADID is +threadID then
|
||||
|
||||
@ -316,9 +316,8 @@ $.get = (key, val, cb) ->
|
||||
|
||||
count = 0
|
||||
done = (item) ->
|
||||
{lastError} = chrome.runtime
|
||||
if lastError
|
||||
c.error lastError, lastError.message or 'No message.'
|
||||
if chrome.runtime.lastError
|
||||
c.error chrome.runtime.lastError.message
|
||||
$.extend items, item
|
||||
cb items unless --count
|
||||
|
||||
@ -330,30 +329,41 @@ $.get = (key, val, cb) ->
|
||||
chrome.storage.sync.get syncItems, done
|
||||
|
||||
$.set = do ->
|
||||
items = {}
|
||||
localItems = {}
|
||||
items =
|
||||
sync: {}
|
||||
local: {}
|
||||
timeout = {}
|
||||
|
||||
set = $.debounce $.SECOND, ->
|
||||
setArea = (area) ->
|
||||
return if timeout[area]
|
||||
chrome.storage[area].set items[area], ->
|
||||
if chrome.runtime.lastError
|
||||
c.error chrome.runtime.lastError.message
|
||||
timeout[area] = setTimeout setArea, $.MINUTE, area
|
||||
return
|
||||
items[area] = {}
|
||||
delete timeout[area]
|
||||
|
||||
setAll = $.debounce $.SECOND, ->
|
||||
for key in $.localKeys
|
||||
if key of items
|
||||
(localItems or= {})[key] = items[key]
|
||||
delete items[key]
|
||||
if key of items.sync
|
||||
items.local[key] = items.sync[key]
|
||||
delete items.sync[key]
|
||||
try
|
||||
chrome.storage.local.set localItems
|
||||
chrome.storage.sync.set items
|
||||
items = {}
|
||||
localItems = {}
|
||||
setArea 'local'
|
||||
setArea 'sync'
|
||||
catch err
|
||||
c.error err.stack
|
||||
|
||||
(key, val) ->
|
||||
if typeof key is 'string'
|
||||
items[key] = val
|
||||
items.sync[key] = val
|
||||
else
|
||||
$.extend items, key
|
||||
set()
|
||||
$.extend items.sync, key
|
||||
setAll()
|
||||
|
||||
<% } else { %>
|
||||
|
||||
# http://wiki.greasespot.net/Main_Page
|
||||
$.sync = do ->
|
||||
$.on window, 'storage', ({key, newValue}) ->
|
||||
|
||||
@ -6,6 +6,7 @@ class Post
|
||||
@ID = +root.id[2..]
|
||||
@fullID = "#{@board}.#{@ID}"
|
||||
|
||||
@cleanup root if that.isOriginalMarkup
|
||||
post = $ '.post', root
|
||||
info = $ '.postInfo', post
|
||||
@nodes =
|
||||
@ -149,6 +150,13 @@ class Post
|
||||
if @file.isImage = /(jpg|png|gif)$/i.test @file.name
|
||||
@file.dimensions = fileText.textContent.match(/\d+x\d+/)[0]
|
||||
|
||||
cleanup: (root) ->
|
||||
for node in $$ '.mobile', root
|
||||
$.rm node
|
||||
for node in $$ '.desktop', root
|
||||
$.rmClass node, 'desktop'
|
||||
return
|
||||
|
||||
kill: (file, now) ->
|
||||
now or= new Date()
|
||||
if file
|
||||
|
||||
@ -33,6 +33,8 @@ class Thread
|
||||
className: "#{typeLC}Icon"
|
||||
root = if type is 'Closed' and @isSticky
|
||||
$ '.stickyIcon', @OP.nodes.info
|
||||
else if g.VIEW is 'index'
|
||||
$ '.page-num', @OP.nodes.info
|
||||
else
|
||||
$ '[title="Quote this post"]', @OP.nodes.info
|
||||
$.after root, [$.tn(' '), icon]
|
||||
|
||||
@ -8,20 +8,21 @@ Menu =
|
||||
cb: @node
|
||||
|
||||
node: ->
|
||||
if @isClone
|
||||
$.on $('.menu-button', @nodes.info), 'click', Menu.toggle
|
||||
else
|
||||
$.add @nodes.info, [$.tn('\u00A0'), Menu.makeButton()]
|
||||
return $.on $('.menu-button', @nodes.info), 'click', Menu.toggle if @isClone
|
||||
$.add @nodes.info, Menu.makeButton()
|
||||
|
||||
makeButton: do ->
|
||||
a = $.el 'a',
|
||||
className: 'menu-button brackets-wrap'
|
||||
innerHTML: '<i></i>'
|
||||
href: 'javascript:;'
|
||||
frag = $.nodes [
|
||||
$.tn(' ')
|
||||
$.el 'a',
|
||||
className: 'menu-button'
|
||||
innerHTML: '[<i></i>]'
|
||||
href: 'javascript:;'
|
||||
]
|
||||
->
|
||||
button = a.cloneNode true
|
||||
$.on button, 'click', Menu.toggle
|
||||
button
|
||||
clone = frag.cloneNode true
|
||||
$.on clone.lastElementChild, 'click', Menu.toggle
|
||||
clone
|
||||
|
||||
toggle: (e) ->
|
||||
post = Get.postFromNode @
|
||||
|
||||
124
src/Posting/QR.captcha.coffee
Normal file
124
src/Posting/QR.captcha.coffee
Normal file
@ -0,0 +1,124 @@
|
||||
QR.captcha =
|
||||
init: ->
|
||||
return if d.cookie.indexOf('pass_enabled=1') >= 0
|
||||
return unless @isEnabled = !!$.id 'captchaFormPart'
|
||||
$.asap (-> $.id 'recaptcha_challenge_field_holder'), @ready.bind @
|
||||
|
||||
ready: ->
|
||||
setLifetime = (e) => @lifetime = e.detail
|
||||
$.on window, 'captcha:timeout', setLifetime
|
||||
$.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
|
||||
$.off window, 'captcha:timeout', setLifetime
|
||||
|
||||
imgContainer = $.el 'div',
|
||||
className: 'captcha-img'
|
||||
title: 'Reload reCAPTCHA'
|
||||
innerHTML: '<img>'
|
||||
input = $.el 'input',
|
||||
className: 'captcha-input field'
|
||||
title: 'Verification'
|
||||
autocomplete: 'off'
|
||||
spellcheck: false
|
||||
tabIndex: 55
|
||||
@nodes =
|
||||
challenge: $.id 'recaptcha_challenge_field_holder'
|
||||
img: imgContainer.firstChild
|
||||
input: input
|
||||
|
||||
new MutationObserver(@load.bind @).observe @nodes.challenge,
|
||||
childList: true
|
||||
|
||||
$.on imgContainer, 'click', @reload.bind @
|
||||
$.on input, 'keydown', @keydown.bind @
|
||||
$.on input, 'focus', -> $.addClass QR.nodes.el, 'focus'
|
||||
$.on input, 'blur', -> $.rmClass QR.nodes.el, 'focus'
|
||||
|
||||
$.get 'captchas', [], ({captchas}) =>
|
||||
@sync captchas
|
||||
$.sync 'captchas', @sync
|
||||
# start with an uncached captcha
|
||||
@reload()
|
||||
|
||||
<% if (type === 'userscript') { %>
|
||||
# XXX Firefox lacks focusin/focusout support.
|
||||
$.on input, 'blur', QR.focusout
|
||||
$.on input, 'focus', QR.focusin
|
||||
<% } %>
|
||||
|
||||
$.addClass QR.nodes.el, 'has-captcha'
|
||||
$.after QR.nodes.com.parentNode, [imgContainer, input]
|
||||
|
||||
sync: (captchas) ->
|
||||
QR.captcha.captchas = captchas
|
||||
QR.captcha.count()
|
||||
|
||||
getOne: ->
|
||||
@clear()
|
||||
if captcha = @captchas.shift()
|
||||
{challenge, response} = captcha
|
||||
@count()
|
||||
$.set 'captchas', @captchas
|
||||
else
|
||||
challenge = @nodes.img.alt
|
||||
if response = @nodes.input.value then @reload()
|
||||
if response
|
||||
response = response.trim()
|
||||
# one-word-captcha:
|
||||
# If there's only one word, duplicate it.
|
||||
response = "#{response} #{response}" unless /\s/.test response
|
||||
{challenge, response}
|
||||
|
||||
save: ->
|
||||
return unless response = @nodes.input.value.trim()
|
||||
@captchas.push
|
||||
challenge: @nodes.img.alt
|
||||
response: response
|
||||
timeout: @timeout
|
||||
@count()
|
||||
@reload()
|
||||
$.set 'captchas', @captchas
|
||||
|
||||
clear: ->
|
||||
now = Date.now()
|
||||
for captcha, i in @captchas
|
||||
break if captcha.timeout > now
|
||||
return unless i
|
||||
@captchas = @captchas[i..]
|
||||
@count()
|
||||
$.set 'captchas', @captchas
|
||||
|
||||
load: ->
|
||||
return unless @nodes.challenge.firstChild
|
||||
# -1 minute to give upload some time.
|
||||
@timeout = Date.now() + @lifetime * $.SECOND - $.MINUTE
|
||||
challenge = @nodes.challenge.firstChild.value
|
||||
@nodes.img.alt = challenge
|
||||
@nodes.img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}"
|
||||
@nodes.input.value = null
|
||||
@clear()
|
||||
|
||||
count: ->
|
||||
count = @captchas.length
|
||||
@nodes.input.placeholder = switch count
|
||||
when 0
|
||||
'Verification (Shift + Enter to cache)'
|
||||
when 1
|
||||
'Verification (1 cached captcha)'
|
||||
else
|
||||
"Verification (#{count} cached captchas)"
|
||||
@nodes.input.alt = count # For XTRM RICE.
|
||||
|
||||
reload: (focus) ->
|
||||
# the 't' argument prevents the input from being focused
|
||||
$.globalEval 'Recaptcha.reload("t")'
|
||||
# Focus if we meant to.
|
||||
@nodes.input.focus() if focus
|
||||
|
||||
keydown: (e) ->
|
||||
if e.keyCode is 8 and not @nodes.input.value
|
||||
@reload()
|
||||
else if e.keyCode is 13 and e.shiftKey
|
||||
@save()
|
||||
else
|
||||
return
|
||||
e.preventDefault()
|
||||
573
src/Posting/QuickReply.coffee → src/Posting/QR.coffee
Executable file → Normal file
573
src/Posting/QuickReply.coffee → src/Posting/QR.coffee
Executable file → Normal file
@ -3,6 +3,7 @@ QR =
|
||||
return if !Conf['Quick Reply']
|
||||
|
||||
@db = new DataBoard 'yourPosts'
|
||||
@posts = []
|
||||
|
||||
if Conf['QR Shortcut']
|
||||
sc = $.el 'a',
|
||||
@ -203,185 +204,6 @@ QR =
|
||||
value
|
||||
status.disabled = disabled or false
|
||||
|
||||
persona:
|
||||
pwd: ''
|
||||
always: {}
|
||||
init: ->
|
||||
QR.persona.getPassword()
|
||||
$.get 'QR.personas', Conf['QR.personas'], ({'QR.personas': personas}) ->
|
||||
types =
|
||||
name: []
|
||||
email: []
|
||||
sub: []
|
||||
for item in personas.split '\n'
|
||||
QR.persona.parseItem item.trim(), types
|
||||
for type, arr of types
|
||||
QR.persona.loadPersonas type, arr
|
||||
return
|
||||
|
||||
parseItem: (item, types) ->
|
||||
return if item[0] is '#'
|
||||
return unless match = item.match /(name|email|subject|password):"(.*)"/i
|
||||
[match, type, val] = match
|
||||
|
||||
# Don't mix up item settings with val.
|
||||
item = item.replace match, ''
|
||||
|
||||
boards = item.match(/boards:([^;]+)/i)?[1].toLowerCase() or 'global'
|
||||
return if boards isnt 'global' and g.BOARD.ID not in boards.split ','
|
||||
|
||||
if type is 'password'
|
||||
QR.persona.pwd = val
|
||||
return
|
||||
|
||||
type = 'sub' if type is 'subject'
|
||||
|
||||
if /always/i.test item
|
||||
QR.persona.always[type] = val
|
||||
|
||||
unless val in types[type]
|
||||
types[type].push val
|
||||
|
||||
loadPersonas: (type, arr) ->
|
||||
list = $ "#list-#{type}", QR.nodes.el
|
||||
for val in arr when val
|
||||
$.add list, $.el 'option',
|
||||
textContent: val
|
||||
return
|
||||
|
||||
getPassword: ->
|
||||
unless QR.persona.pwd
|
||||
QR.persona.pwd = if m = d.cookie.match /4chan_pass=([^;]+)/
|
||||
decodeURIComponent m[1]
|
||||
else if input = $.id 'postPassword'
|
||||
input.value
|
||||
else
|
||||
# If we're in a closed thread, #postPassword isn't available.
|
||||
# And since #delPassword.value is only filled on window.onload
|
||||
# we'd rather use #postPassword when we can.
|
||||
$.id('delPassword').value
|
||||
return QR.persona.pwd
|
||||
|
||||
get: (cb) ->
|
||||
$.get 'QR.persona', {}, ({'QR.persona': persona}) ->
|
||||
cb persona
|
||||
|
||||
set: (post) ->
|
||||
$.get 'QR.persona', {}, ({'QR.persona': persona}) ->
|
||||
persona =
|
||||
name: post.name
|
||||
email: if /^sage$/.test post.email then persona.email else post.email
|
||||
sub: if Conf['Remember Subject'] then post.sub else undefined
|
||||
flag: post.flag
|
||||
$.set 'QR.persona', persona
|
||||
|
||||
cooldown:
|
||||
init: ->
|
||||
return unless Conf['Cooldown']
|
||||
setTimers = (e) => QR.cooldown.types = e.detail
|
||||
$.on window, 'cooldown:timers', setTimers
|
||||
$.globalEval 'window.dispatchEvent(new CustomEvent("cooldown:timers", {detail: cooldowns}))'
|
||||
$.off window, 'cooldown:timers', setTimers
|
||||
for type of QR.cooldown.types
|
||||
QR.cooldown.types[type] = +QR.cooldown.types[type]
|
||||
QR.cooldown.upSpd = 0
|
||||
QR.cooldown.upSpdAccuracy = .5
|
||||
key = "cooldown.#{g.BOARD}"
|
||||
$.get key, {}, (item) ->
|
||||
QR.cooldown.cooldowns = item[key]
|
||||
QR.cooldown.start()
|
||||
$.sync key, QR.cooldown.sync
|
||||
start: ->
|
||||
return unless Conf['Cooldown']
|
||||
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 prune 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']
|
||||
{req, post, isReply, threadID, delay} = data
|
||||
start = if req then req.uploadEndTime else Date.now()
|
||||
if delay
|
||||
cooldown = {delay}
|
||||
else
|
||||
if post.file
|
||||
upSpd = post.file.size / ((start - req.uploadStartTime) / $.SECOND)
|
||||
QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2
|
||||
QR.cooldown.upSpd = upSpd
|
||||
cooldown = {isReply, threadID}
|
||||
QR.cooldown.cooldowns[start] = cooldown
|
||||
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
|
||||
QR.cooldown.start()
|
||||
|
||||
unset: (id) ->
|
||||
delete QR.cooldown.cooldowns[id]
|
||||
if Object.keys(QR.cooldown.cooldowns).length
|
||||
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
|
||||
else
|
||||
$.delete "cooldown.#{g.BOARD}"
|
||||
|
||||
count: ->
|
||||
unless Object.keys(QR.cooldown.cooldowns).length
|
||||
$.delete "#{g.BOARD}.cooldown"
|
||||
delete QR.cooldown.isCounting
|
||||
delete QR.cooldown.seconds
|
||||
QR.status()
|
||||
return
|
||||
|
||||
clearTimeout QR.cooldown.timeout
|
||||
QR.cooldown.timeout = setTimeout QR.cooldown.count, $.SECOND
|
||||
|
||||
now = Date.now()
|
||||
post = QR.posts[0]
|
||||
isReply = post.thread isnt 'new'
|
||||
hasFile = !!post.file
|
||||
seconds = null
|
||||
{types, cooldowns, upSpd, upSpdAccuracy} = 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 variable:
|
||||
# reply cooldown with a reply, thread cooldown with a thread
|
||||
elapsed = Math.floor (now - start) / $.SECOND
|
||||
continue if elapsed < 0 # clock changed since then?
|
||||
type = unless isReply
|
||||
'thread'
|
||||
else if hasFile
|
||||
'image'
|
||||
else
|
||||
'reply'
|
||||
maxTimer = Math.max types[type] or 0, types[type + '_intra'] or 0
|
||||
unless start <= now <= start + maxTimer * $.SECOND
|
||||
QR.cooldown.unset start
|
||||
type += '_intra' if isReply and +post.thread is cooldown.threadID
|
||||
seconds = Math.max seconds, types[type] - elapsed
|
||||
|
||||
if seconds and Conf['Cooldown Prediction'] and hasFile and upSpd
|
||||
seconds -= Math.floor post.file.size / upSpd * upSpdAccuracy
|
||||
seconds = if seconds > 0 then seconds else 0
|
||||
# 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 and !QR.req
|
||||
|
||||
quote: (e) ->
|
||||
e?.preventDefault()
|
||||
return unless QR.postingIsEnabled
|
||||
@ -496,399 +318,6 @@ QR =
|
||||
e.preventDefault()
|
||||
QR.nodes.fileInput.click()
|
||||
|
||||
posts: []
|
||||
|
||||
post: class
|
||||
constructor: (select) ->
|
||||
el = $.el 'a',
|
||||
className: 'qr-preview'
|
||||
draggable: true
|
||||
href: 'javascript:;'
|
||||
innerHTML: '<a class="remove fa fa-times-circle" title=Remove></a><label hidden><input type=checkbox> Spoiler</label><span></span>'
|
||||
|
||||
@nodes =
|
||||
el: el
|
||||
rm: el.firstChild
|
||||
label: $ 'label', el
|
||||
spoiler: $ 'input', el
|
||||
span: el.lastChild
|
||||
|
||||
<% if (type === 'userscript') { %>
|
||||
# XXX Firefox lacks focusin/focusout support.
|
||||
for elm in $$ '*', el
|
||||
$.on elm, 'blur', QR.focusout
|
||||
$.on elm, 'focus', QR.focusin
|
||||
<% } %>
|
||||
$.on el, 'click', @select
|
||||
$.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm()
|
||||
$.on @nodes.label, 'click', (e) => e.stopPropagation()
|
||||
$.on @nodes.spoiler, 'change', (e) =>
|
||||
@spoiler = e.target.checked
|
||||
QR.nodes.spoiler.checked = @spoiler if @ is QR.selected
|
||||
$.add QR.nodes.dumpList, el
|
||||
|
||||
for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop']
|
||||
$.on el, event.toLowerCase(), @[event]
|
||||
|
||||
@thread = if g.VIEW is 'thread'
|
||||
g.THREADID
|
||||
else
|
||||
'new'
|
||||
|
||||
prev = QR.posts[QR.posts.length - 1]
|
||||
QR.posts.push @
|
||||
@nodes.spoiler.checked = @spoiler = if prev and Conf['Remember Spoiler']
|
||||
prev.spoiler
|
||||
else
|
||||
false
|
||||
QR.persona.get (persona) =>
|
||||
@name = if 'name' of QR.persona.always
|
||||
QR.persona.always.name
|
||||
else if prev
|
||||
prev.name
|
||||
else
|
||||
persona.name
|
||||
|
||||
@email = if 'email' of QR.persona.always
|
||||
QR.persona.always.email
|
||||
else if prev and !/^sage$/.test prev.email
|
||||
prev.email
|
||||
else
|
||||
persona.email
|
||||
|
||||
@sub = if 'sub' of QR.persona.always
|
||||
QR.persona.always.sub
|
||||
else if Conf['Remember Subject']
|
||||
if prev then prev.sub else persona.sub
|
||||
else
|
||||
''
|
||||
|
||||
if QR.nodes.flag
|
||||
@flag = if prev
|
||||
prev.flag
|
||||
else
|
||||
persona.flag
|
||||
@load() if QR.selected is @ # load persona
|
||||
@select() if select
|
||||
@unlock()
|
||||
|
||||
rm: ->
|
||||
@delete()
|
||||
index = QR.posts.indexOf @
|
||||
if QR.posts.length is 1
|
||||
new QR.post true
|
||||
$.rmClass QR.nodes.el, 'dump'
|
||||
else if @ is QR.selected
|
||||
(QR.posts[index-1] or QR.posts[index+1]).select()
|
||||
QR.posts.splice index, 1
|
||||
QR.status()
|
||||
delete: ->
|
||||
$.rm @nodes.el
|
||||
URL.revokeObjectURL @URL
|
||||
|
||||
lock: (lock=true) ->
|
||||
@isLocked = lock
|
||||
return unless @ is QR.selected
|
||||
for name in ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'] when node = QR.nodes[name]
|
||||
node.disabled = lock
|
||||
@nodes.rm.style.visibility = if lock then 'hidden' else ''
|
||||
(if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
|
||||
@nodes.spoiler.disabled = lock
|
||||
@nodes.el.draggable = !lock
|
||||
|
||||
unlock: ->
|
||||
@lock false
|
||||
|
||||
select: =>
|
||||
if QR.selected
|
||||
QR.selected.nodes.el.id = null
|
||||
QR.selected.forceSave()
|
||||
QR.selected = @
|
||||
@lock @isLocked
|
||||
@nodes.el.id = 'selected'
|
||||
# Scroll the list to center the focused post.
|
||||
rectEl = @nodes.el.getBoundingClientRect()
|
||||
rectList = @nodes.el.parentNode.getBoundingClientRect()
|
||||
@nodes.el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2
|
||||
@load()
|
||||
$.event 'QRPostSelection', @
|
||||
|
||||
load: ->
|
||||
# Load this post's values.
|
||||
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']
|
||||
continue unless node = QR.nodes[name]
|
||||
node.value = @[name] or node.dataset.default or null
|
||||
@showFileData()
|
||||
QR.characterCount()
|
||||
|
||||
save: (input) ->
|
||||
if input.type is 'checkbox'
|
||||
@spoiler = input.checked
|
||||
return
|
||||
{name} = input.dataset
|
||||
@[name] = input.value or input.dataset.default or null
|
||||
switch name
|
||||
when 'thread'
|
||||
QR.status()
|
||||
when 'com'
|
||||
@nodes.span.textContent = @com
|
||||
QR.characterCount()
|
||||
# Disable auto-posting if you're typing in the first post
|
||||
# during the last 5 seconds of the cooldown.
|
||||
if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5
|
||||
QR.cooldown.auto = false
|
||||
when 'filename'
|
||||
return unless @file
|
||||
@file.newName = @filename.replace /[/\\]/g, '-'
|
||||
unless /\.(jpe?g|png|gif|pdf|swf)$/i.test @filename
|
||||
# 4chan will truncate the filename if it has no extension,
|
||||
# but it will always replace the extension by the correct one,
|
||||
# so we suffix it with '.jpg' when needed.
|
||||
@file.newName += '.jpg'
|
||||
@updateFilename()
|
||||
|
||||
forceSave: ->
|
||||
return unless @ is QR.selected
|
||||
# Do this in case people use extensions
|
||||
# that do not trigger the `input` event.
|
||||
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler', 'flag']
|
||||
continue unless node = QR.nodes[name]
|
||||
@save node
|
||||
return
|
||||
|
||||
setFile: (@file) ->
|
||||
@filename = file.name
|
||||
@filesize = $.bytesToString file.size
|
||||
@nodes.label.hidden = false if QR.spoiler
|
||||
URL.revokeObjectURL @URL
|
||||
@showFileData() if @ is QR.selected
|
||||
unless /^image/.test file.type
|
||||
@nodes.el.style.backgroundImage = null
|
||||
return
|
||||
@setThumbnail()
|
||||
|
||||
setThumbnail: ->
|
||||
# Create a redimensioned thumbnail.
|
||||
img = $.el 'img'
|
||||
|
||||
img.onload = =>
|
||||
# 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 * 2 * window.devicePixelRatio
|
||||
s *= 3 if @file.type is 'image/gif' # let them animate
|
||||
{height, width} = img
|
||||
if height < s or width < s
|
||||
@URL = fileURL
|
||||
@nodes.el.style.backgroundImage = "url(#{@URL})"
|
||||
return
|
||||
if height <= width
|
||||
width = s / height * width
|
||||
height = s
|
||||
else
|
||||
height = s / width * height
|
||||
width = s
|
||||
cv = $.el 'canvas'
|
||||
cv.height = img.height = height
|
||||
cv.width = img.width = width
|
||||
cv.getContext('2d').drawImage img, 0, 0, width, height
|
||||
URL.revokeObjectURL fileURL
|
||||
cv.toBlob (blob) =>
|
||||
@URL = URL.createObjectURL blob
|
||||
@nodes.el.style.backgroundImage = "url(#{@URL})"
|
||||
|
||||
fileURL = URL.createObjectURL @file
|
||||
img.src = fileURL
|
||||
|
||||
rmFile: ->
|
||||
return if @isLocked
|
||||
delete @file
|
||||
delete @filename
|
||||
delete @filesize
|
||||
@nodes.el.title = null
|
||||
QR.nodes.fileContainer.title = ''
|
||||
@nodes.el.style.backgroundImage = null
|
||||
@nodes.label.hidden = true if QR.spoiler
|
||||
@showFileData()
|
||||
URL.revokeObjectURL @URL
|
||||
|
||||
updateFilename: ->
|
||||
long = "#{@filename} (#{@filesize})\nCtrl+click to edit filename. Shift+click to clear."
|
||||
@nodes.el.title = long
|
||||
return unless @ is QR.selected
|
||||
QR.nodes.fileContainer.title = long
|
||||
|
||||
showFileData: ->
|
||||
if @file
|
||||
@updateFilename()
|
||||
QR.nodes.filename.value = @filename
|
||||
QR.nodes.spoiler.checked = @spoiler
|
||||
$.addClass QR.nodes.fileSubmit, 'has-file'
|
||||
else
|
||||
$.rmClass QR.nodes.fileSubmit, 'has-file'
|
||||
|
||||
pasteText: (file) ->
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) =>
|
||||
text = e.target.result
|
||||
if @com
|
||||
@com += "\n#{text}"
|
||||
else
|
||||
@com = text
|
||||
if QR.selected is @
|
||||
QR.nodes.com.value = @com
|
||||
@nodes.span.textContent = @com
|
||||
reader.readAsText file
|
||||
|
||||
dragStart: (e) ->
|
||||
e.dataTransfer.setDragImage @, e.layerX, e.layerY
|
||||
$.addClass @, 'drag'
|
||||
dragEnd: -> $.rmClass @, 'drag'
|
||||
dragEnter: -> $.addClass @, 'over'
|
||||
dragLeave: -> $.rmClass @, 'over'
|
||||
|
||||
dragOver: (e) ->
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
|
||||
drop: ->
|
||||
$.rmClass @, 'over'
|
||||
return unless @draggable
|
||||
el = $ '.drag', @parentNode
|
||||
index = (el) -> [el.parentNode.children...].indexOf el
|
||||
oldIndex = index el
|
||||
newIndex = index @
|
||||
(if oldIndex < newIndex then $.after else $.before) @, el
|
||||
post = QR.posts.splice(oldIndex, 1)[0]
|
||||
QR.posts.splice newIndex, 0, post
|
||||
QR.status()
|
||||
|
||||
captcha:
|
||||
init: ->
|
||||
return if d.cookie.indexOf('pass_enabled=1') >= 0
|
||||
return unless @isEnabled = !!$.id 'captchaFormPart'
|
||||
$.asap (-> $.id 'recaptcha_challenge_field_holder'), @ready.bind @
|
||||
|
||||
ready: ->
|
||||
setLifetime = (e) => @lifetime = e.detail
|
||||
$.on window, 'captcha:timeout', setLifetime
|
||||
$.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
|
||||
$.off window, 'captcha:timeout', setLifetime
|
||||
|
||||
imgContainer = $.el 'div',
|
||||
className: 'captcha-img'
|
||||
title: 'Reload reCAPTCHA'
|
||||
innerHTML: '<img>'
|
||||
input = $.el 'input',
|
||||
className: 'captcha-input field'
|
||||
title: 'Verification'
|
||||
autocomplete: 'off'
|
||||
spellcheck: false
|
||||
tabIndex: 55
|
||||
@nodes =
|
||||
challenge: $.id 'recaptcha_challenge_field_holder'
|
||||
img: imgContainer.firstChild
|
||||
input: input
|
||||
|
||||
new MutationObserver(@load.bind @).observe @nodes.challenge,
|
||||
childList: true
|
||||
|
||||
$.on imgContainer, 'click', @reload.bind @
|
||||
$.on input, 'keydown', @keydown.bind @
|
||||
$.on input, 'focus', -> $.addClass QR.nodes.el, 'focus'
|
||||
$.on input, 'blur', -> $.rmClass QR.nodes.el, 'focus'
|
||||
|
||||
$.get 'captchas', [], ({captchas}) =>
|
||||
@sync captchas
|
||||
$.sync 'captchas', @sync
|
||||
# start with an uncached captcha
|
||||
@reload()
|
||||
|
||||
<% if (type === 'userscript') { %>
|
||||
# XXX Firefox lacks focusin/focusout support.
|
||||
$.on input, 'blur', QR.focusout
|
||||
$.on input, 'focus', QR.focusin
|
||||
<% } %>
|
||||
|
||||
$.addClass QR.nodes.el, 'has-captcha'
|
||||
$.after QR.nodes.com.parentNode, [imgContainer, input]
|
||||
|
||||
sync: (captchas) ->
|
||||
QR.captcha.captchas = captchas
|
||||
QR.captcha.count()
|
||||
|
||||
getOne: ->
|
||||
@clear()
|
||||
if captcha = @captchas.shift()
|
||||
{challenge, response} = captcha
|
||||
@count()
|
||||
$.set 'captchas', @captchas
|
||||
else
|
||||
challenge = @nodes.img.alt
|
||||
if response = @nodes.input.value then @reload()
|
||||
if response
|
||||
response = response.trim()
|
||||
# one-word-captcha:
|
||||
# If there's only one word, duplicate it.
|
||||
response = "#{response} #{response}" unless /\s/.test response
|
||||
{challenge, response}
|
||||
|
||||
save: ->
|
||||
return unless response = @nodes.input.value.trim()
|
||||
@captchas.push
|
||||
challenge: @nodes.img.alt
|
||||
response: response
|
||||
timeout: @timeout
|
||||
@count()
|
||||
@reload()
|
||||
$.set 'captchas', @captchas
|
||||
|
||||
clear: ->
|
||||
now = Date.now()
|
||||
for captcha, i in @captchas
|
||||
break if captcha.timeout > now
|
||||
return unless i
|
||||
@captchas = @captchas[i..]
|
||||
@count()
|
||||
$.set 'captchas', @captchas
|
||||
|
||||
load: ->
|
||||
return unless @nodes.challenge.firstChild
|
||||
# -1 minute to give upload some time.
|
||||
@timeout = Date.now() + @lifetime * $.SECOND - $.MINUTE
|
||||
challenge = @nodes.challenge.firstChild.value
|
||||
@nodes.img.alt = challenge
|
||||
@nodes.img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}"
|
||||
@nodes.input.value = null
|
||||
@clear()
|
||||
|
||||
count: ->
|
||||
count = @captchas.length
|
||||
@nodes.input.placeholder = switch count
|
||||
when 0
|
||||
'Verification (Shift + Enter to cache)'
|
||||
when 1
|
||||
'Verification (1 cached captcha)'
|
||||
else
|
||||
"Verification (#{count} cached captchas)"
|
||||
@nodes.input.alt = count
|
||||
|
||||
reload: (focus) ->
|
||||
# the 't' argument prevents the input from being focused
|
||||
$.globalEval 'Recaptcha.reload("t")'
|
||||
# Focus if we meant to.
|
||||
@nodes.input.focus() if focus
|
||||
|
||||
keydown: (e) ->
|
||||
if e.keyCode is 8 and not @nodes.input.value
|
||||
@reload()
|
||||
else if e.keyCode is 13 and e.shiftKey
|
||||
@save()
|
||||
else
|
||||
return
|
||||
e.preventDefault()
|
||||
|
||||
generatePostableThreadsList: ->
|
||||
return unless QR.nodes
|
||||
list = QR.nodes.thread
|
||||
106
src/Posting/QR.cooldown.coffee
Normal file
106
src/Posting/QR.cooldown.coffee
Normal file
@ -0,0 +1,106 @@
|
||||
QR.cooldown =
|
||||
init: ->
|
||||
return unless Conf['Cooldown']
|
||||
setTimers = (e) => QR.cooldown.types = e.detail
|
||||
$.on window, 'cooldown:timers', setTimers
|
||||
$.globalEval 'window.dispatchEvent(new CustomEvent("cooldown:timers", {detail: cooldowns}))'
|
||||
$.off window, 'cooldown:timers', setTimers
|
||||
for type of QR.cooldown.types
|
||||
QR.cooldown.types[type] = +QR.cooldown.types[type]
|
||||
QR.cooldown.upSpd = 0
|
||||
QR.cooldown.upSpdAccuracy = .5
|
||||
key = "cooldown.#{g.BOARD}"
|
||||
$.get key, {}, (item) ->
|
||||
QR.cooldown.cooldowns = item[key]
|
||||
QR.cooldown.start()
|
||||
$.sync key, QR.cooldown.sync
|
||||
start: ->
|
||||
return unless Conf['Cooldown']
|
||||
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 prune 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']
|
||||
{req, post, isReply, threadID, delay} = data
|
||||
start = if req then req.uploadEndTime else Date.now()
|
||||
if delay
|
||||
cooldown = {delay}
|
||||
else
|
||||
if post.file
|
||||
upSpd = post.file.size / ((start - req.uploadStartTime) / $.SECOND)
|
||||
QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2
|
||||
QR.cooldown.upSpd = upSpd
|
||||
cooldown = {isReply, threadID}
|
||||
QR.cooldown.cooldowns[start] = cooldown
|
||||
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
|
||||
QR.cooldown.start()
|
||||
|
||||
unset: (id) ->
|
||||
delete QR.cooldown.cooldowns[id]
|
||||
if Object.keys(QR.cooldown.cooldowns).length
|
||||
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
|
||||
else
|
||||
$.delete "cooldown.#{g.BOARD}"
|
||||
|
||||
count: ->
|
||||
unless Object.keys(QR.cooldown.cooldowns).length
|
||||
$.delete "#{g.BOARD}.cooldown"
|
||||
delete QR.cooldown.isCounting
|
||||
delete QR.cooldown.seconds
|
||||
QR.status()
|
||||
return
|
||||
|
||||
clearTimeout QR.cooldown.timeout
|
||||
QR.cooldown.timeout = setTimeout QR.cooldown.count, $.SECOND
|
||||
|
||||
now = Date.now()
|
||||
post = QR.posts[0]
|
||||
isReply = post.thread isnt 'new'
|
||||
hasFile = !!post.file
|
||||
seconds = null
|
||||
{types, cooldowns, upSpd, upSpdAccuracy} = 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 variable:
|
||||
# reply cooldown with a reply, thread cooldown with a thread
|
||||
elapsed = Math.floor (now - start) / $.SECOND
|
||||
continue if elapsed < 0 # clock changed since then?
|
||||
type = unless isReply
|
||||
'thread'
|
||||
else if hasFile
|
||||
'image'
|
||||
else
|
||||
'reply'
|
||||
maxTimer = Math.max types[type] or 0, types[type + '_intra'] or 0
|
||||
unless start <= now <= start + maxTimer * $.SECOND
|
||||
QR.cooldown.unset start
|
||||
type += '_intra' if isReply and +post.thread is cooldown.threadID
|
||||
seconds = Math.max seconds, types[type] - elapsed
|
||||
|
||||
if seconds and Conf['Cooldown Prediction'] and hasFile and upSpd
|
||||
seconds -= Math.floor post.file.size / upSpd * upSpdAccuracy
|
||||
seconds = if seconds > 0 then seconds else 0
|
||||
# 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 and !QR.req
|
||||
72
src/Posting/QR.persona.coffee
Normal file
72
src/Posting/QR.persona.coffee
Normal file
@ -0,0 +1,72 @@
|
||||
QR.persona =
|
||||
pwd: ''
|
||||
always: {}
|
||||
init: ->
|
||||
QR.persona.getPassword()
|
||||
$.get 'QR.personas', Conf['QR.personas'], ({'QR.personas': personas}) ->
|
||||
types =
|
||||
name: []
|
||||
email: []
|
||||
sub: []
|
||||
for item in personas.split '\n'
|
||||
QR.persona.parseItem item.trim(), types
|
||||
for type, arr of types
|
||||
QR.persona.loadPersonas type, arr
|
||||
return
|
||||
|
||||
parseItem: (item, types) ->
|
||||
return if item[0] is '#'
|
||||
return unless match = item.match /(name|email|subject|password):"(.*)"/i
|
||||
[match, type, val] = match
|
||||
|
||||
# Don't mix up item settings with val.
|
||||
item = item.replace match, ''
|
||||
|
||||
boards = item.match(/boards:([^;]+)/i)?[1].toLowerCase() or 'global'
|
||||
return if boards isnt 'global' and g.BOARD.ID not in boards.split ','
|
||||
|
||||
|
||||
if type is 'password'
|
||||
QR.persona.pwd = val
|
||||
return
|
||||
|
||||
type = 'sub' if type is 'subject'
|
||||
|
||||
if /always/i.test item
|
||||
QR.persona.always[type] = val
|
||||
|
||||
unless val in types[type]
|
||||
types[type].push val
|
||||
|
||||
loadPersonas: (type, arr) ->
|
||||
list = $ "#list-#{type}", QR.nodes.el
|
||||
for val in arr when val
|
||||
$.add list, $.el 'option',
|
||||
textContent: val
|
||||
return
|
||||
|
||||
getPassword: ->
|
||||
unless QR.persona.pwd
|
||||
QR.persona.pwd = if m = d.cookie.match /4chan_pass=([^;]+)/
|
||||
decodeURIComponent m[1]
|
||||
else if input = $.id 'postPassword'
|
||||
input.value
|
||||
else
|
||||
# If we're in a closed thread, #postPassword isn't available.
|
||||
# And since #delPassword.value is only filled on window.onload
|
||||
# we'd rather use #postPassword when we can.
|
||||
$.id('delPassword').value
|
||||
return QR.persona.pwd
|
||||
|
||||
get: (cb) ->
|
||||
$.get 'QR.persona', {}, ({'QR.persona': persona}) ->
|
||||
cb persona
|
||||
|
||||
set: (post) ->
|
||||
$.get 'QR.persona', {}, ({'QR.persona': persona}) ->
|
||||
persona =
|
||||
name: post.name
|
||||
email: if /^sage$/.test post.email then persona.email else post.email
|
||||
sub: if Conf['Remember Subject'] then post.sub else undefined
|
||||
flag: post.flag
|
||||
$.set 'QR.persona', persona
|
||||
265
src/Posting/QR.post.coffee
Normal file
265
src/Posting/QR.post.coffee
Normal file
@ -0,0 +1,265 @@
|
||||
QR.post = class
|
||||
constructor: (select) ->
|
||||
el = $.el 'a',
|
||||
className: 'qr-preview'
|
||||
draggable: true
|
||||
href: 'javascript:;'
|
||||
innerHTML: '<a class="remove fa fa-times-circle" title=Remove></a><label hidden><input type=checkbox> Spoiler</label><span></span>'
|
||||
|
||||
@nodes =
|
||||
el: el
|
||||
rm: el.firstChild
|
||||
label: $ 'label', el
|
||||
spoiler: $ 'input', el
|
||||
span: el.lastChild
|
||||
|
||||
<% if (type === 'userscript') { %>
|
||||
# XXX Firefox lacks focusin/focusout support.
|
||||
for elm in $$ '*', el
|
||||
$.on elm, 'blur', QR.focusout
|
||||
$.on elm, 'focus', QR.focusin
|
||||
<% } %>
|
||||
$.on el, 'click', @select
|
||||
$.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm()
|
||||
$.on @nodes.label, 'click', (e) => e.stopPropagation()
|
||||
$.on @nodes.spoiler, 'change', (e) =>
|
||||
@spoiler = e.target.checked
|
||||
QR.nodes.spoiler.checked = @spoiler if @ is QR.selected
|
||||
$.add QR.nodes.dumpList, el
|
||||
|
||||
for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop']
|
||||
$.on el, event.toLowerCase(), @[event]
|
||||
|
||||
@thread = if g.VIEW is 'thread'
|
||||
g.THREADID
|
||||
else
|
||||
'new'
|
||||
|
||||
prev = QR.posts[QR.posts.length - 1]
|
||||
QR.posts.push @
|
||||
@nodes.spoiler.checked = @spoiler = if prev and Conf['Remember Spoiler']
|
||||
prev.spoiler
|
||||
else
|
||||
false
|
||||
QR.persona.get (persona) =>
|
||||
@name = if 'name' of QR.persona.always
|
||||
QR.persona.always.name
|
||||
else if prev
|
||||
prev.name
|
||||
else
|
||||
persona.name
|
||||
|
||||
@email = if 'email' of QR.persona.always
|
||||
QR.persona.always.email
|
||||
else if prev and !/^sage$/.test prev.email
|
||||
prev.email
|
||||
else
|
||||
persona.email
|
||||
|
||||
@sub = if 'sub' of QR.persona.always
|
||||
QR.persona.always.sub
|
||||
else if Conf['Remember Subject']
|
||||
if prev then prev.sub else persona.sub
|
||||
else
|
||||
''
|
||||
|
||||
if QR.nodes.flag
|
||||
@flag = if prev
|
||||
prev.flag
|
||||
else
|
||||
persona.flag
|
||||
@load() if QR.selected is @ # load persona
|
||||
@select() if select
|
||||
@unlock()
|
||||
|
||||
rm: ->
|
||||
@delete()
|
||||
index = QR.posts.indexOf @
|
||||
if QR.posts.length is 1
|
||||
new QR.post true
|
||||
$.rmClass QR.nodes.el, 'dump'
|
||||
else if @ is QR.selected
|
||||
(QR.posts[index-1] or QR.posts[index+1]).select()
|
||||
QR.posts.splice index, 1
|
||||
QR.status()
|
||||
delete: ->
|
||||
$.rm @nodes.el
|
||||
URL.revokeObjectURL @URL
|
||||
|
||||
lock: (lock=true) ->
|
||||
@isLocked = lock
|
||||
return unless @ is QR.selected
|
||||
for name in ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag'] when node = QR.nodes[name]
|
||||
node.disabled = lock
|
||||
@nodes.rm.style.visibility = if lock then 'hidden' else ''
|
||||
(if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
|
||||
@nodes.spoiler.disabled = lock
|
||||
@nodes.el.draggable = !lock
|
||||
|
||||
unlock: ->
|
||||
@lock false
|
||||
|
||||
select: =>
|
||||
if QR.selected
|
||||
QR.selected.nodes.el.id = null
|
||||
QR.selected.forceSave()
|
||||
QR.selected = @
|
||||
@lock @isLocked
|
||||
@nodes.el.id = 'selected'
|
||||
# Scroll the list to center the focused post.
|
||||
rectEl = @nodes.el.getBoundingClientRect()
|
||||
rectList = @nodes.el.parentNode.getBoundingClientRect()
|
||||
@nodes.el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2
|
||||
@load()
|
||||
$.event 'QRPostSelection', @
|
||||
|
||||
load: ->
|
||||
# Load this post's values.
|
||||
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']
|
||||
continue unless node = QR.nodes[name]
|
||||
node.value = @[name] or node.dataset.default or null
|
||||
@showFileData()
|
||||
QR.characterCount()
|
||||
|
||||
save: (input) ->
|
||||
if input.type is 'checkbox'
|
||||
@spoiler = input.checked
|
||||
return
|
||||
{name} = input.dataset
|
||||
@[name] = input.value or input.dataset.default or null
|
||||
switch name
|
||||
when 'thread'
|
||||
QR.status()
|
||||
when 'com'
|
||||
@nodes.span.textContent = @com
|
||||
QR.characterCount()
|
||||
# Disable auto-posting if you're typing in the first post
|
||||
# during the last 5 seconds of the cooldown.
|
||||
if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5
|
||||
QR.cooldown.auto = false
|
||||
when 'filename'
|
||||
return unless @file
|
||||
@file.newName = @filename.replace /[/\\]/g, '-'
|
||||
unless /\.(jpe?g|png|gif|pdf|swf)$/i.test @filename
|
||||
# 4chan will truncate the filename if it has no extension,
|
||||
# but it will always replace the extension by the correct one,
|
||||
# so we suffix it with '.jpg' when needed.
|
||||
@file.newName += '.jpg'
|
||||
@updateFilename()
|
||||
|
||||
forceSave: ->
|
||||
return unless @ is QR.selected
|
||||
# Do this in case people use extensions
|
||||
# that do not trigger the `input` event.
|
||||
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler', 'flag']
|
||||
continue unless node = QR.nodes[name]
|
||||
@save node
|
||||
return
|
||||
|
||||
setFile: (@file) ->
|
||||
@filename = file.name
|
||||
@filesize = $.bytesToString file.size
|
||||
@nodes.label.hidden = false if QR.spoiler
|
||||
URL.revokeObjectURL @URL
|
||||
@showFileData() if @ is QR.selected
|
||||
unless /^image/.test file.type
|
||||
@nodes.el.style.backgroundImage = null
|
||||
return
|
||||
@setThumbnail()
|
||||
|
||||
setThumbnail: ->
|
||||
# Create a redimensioned thumbnail.
|
||||
img = $.el 'img'
|
||||
|
||||
img.onload = =>
|
||||
# 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 * 2 * window.devicePixelRatio
|
||||
s *= 3 if @file.type is 'image/gif' # let them animate
|
||||
{height, width} = img
|
||||
if height < s or width < s
|
||||
@URL = fileURL
|
||||
@nodes.el.style.backgroundImage = "url(#{@URL})"
|
||||
return
|
||||
if height <= width
|
||||
width = s / height * width
|
||||
height = s
|
||||
else
|
||||
height = s / width * height
|
||||
width = s
|
||||
cv = $.el 'canvas'
|
||||
cv.height = img.height = height
|
||||
cv.width = img.width = width
|
||||
cv.getContext('2d').drawImage img, 0, 0, width, height
|
||||
URL.revokeObjectURL fileURL
|
||||
cv.toBlob (blob) =>
|
||||
@URL = URL.createObjectURL blob
|
||||
@nodes.el.style.backgroundImage = "url(#{@URL})"
|
||||
|
||||
fileURL = URL.createObjectURL @file
|
||||
img.src = fileURL
|
||||
|
||||
rmFile: ->
|
||||
return if @isLocked
|
||||
delete @file
|
||||
delete @filename
|
||||
delete @filesize
|
||||
@nodes.el.title = null
|
||||
QR.nodes.fileContainer.title = ''
|
||||
@nodes.el.style.backgroundImage = null
|
||||
@nodes.label.hidden = true if QR.spoiler
|
||||
@showFileData()
|
||||
URL.revokeObjectURL @URL
|
||||
|
||||
updateFilename: ->
|
||||
long = "#{@filename} (#{@filesize})\nCtrl+click to edit filename. Shift+click to clear."
|
||||
@nodes.el.title = long
|
||||
return unless @ is QR.selected
|
||||
QR.nodes.fileContainer.title = long
|
||||
|
||||
showFileData: ->
|
||||
if @file
|
||||
@updateFilename()
|
||||
QR.nodes.filename.value = @filename
|
||||
QR.nodes.spoiler.checked = @spoiler
|
||||
$.addClass QR.nodes.fileSubmit, 'has-file'
|
||||
else
|
||||
$.rmClass QR.nodes.fileSubmit, 'has-file'
|
||||
|
||||
pasteText: (file) ->
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) =>
|
||||
text = e.target.result
|
||||
if @com
|
||||
@com += "\n#{text}"
|
||||
else
|
||||
@com = text
|
||||
if QR.selected is @
|
||||
QR.nodes.com.value = @com
|
||||
@nodes.span.textContent = @com
|
||||
reader.readAsText file
|
||||
|
||||
dragStart: (e) ->
|
||||
e.dataTransfer.setDragImage @, e.layerX, e.layerY
|
||||
$.addClass @, 'drag'
|
||||
dragEnd: -> $.rmClass @, 'drag'
|
||||
dragEnter: -> $.addClass @, 'over'
|
||||
dragLeave: -> $.rmClass @, 'over'
|
||||
|
||||
dragOver: (e) ->
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
|
||||
drop: ->
|
||||
$.rmClass @, 'over'
|
||||
return unless @draggable
|
||||
el = $ '.drag', @parentNode
|
||||
index = (el) -> [el.parentNode.children...].indexOf el
|
||||
oldIndex = index el
|
||||
newIndex = index @
|
||||
(if oldIndex < newIndex then $.after else $.before) @, el
|
||||
post = QR.posts.splice(oldIndex, 1)[0]
|
||||
QR.posts.splice newIndex, 0, post
|
||||
QR.status()
|
||||
Loading…
x
Reference in New Issue
Block a user