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/General/Notice.coffee'
|
||||||
'src/Filtering/**/*'
|
'src/Filtering/**/*'
|
||||||
'src/Quotelinks/**/*'
|
'src/Quotelinks/**/*'
|
||||||
'src/Linkification/**/*'
|
'src/Posting/QR.coffee'
|
||||||
'src/Posting/**/*'
|
'src/Posting/**/*'
|
||||||
'src/Images/**/*'
|
'src/Images/**/*'
|
||||||
'src/Linkification/**/*'
|
'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",
|
"software": "foolfuuka",
|
||||||
"boards": ["a", "co", "gd", "jp", "m", "s4s", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
|
"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"]
|
"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-bump": "~0.0.11",
|
||||||
"grunt-concurrent": "~0.4.0",
|
"grunt-concurrent": "~0.4.0",
|
||||||
"grunt-contrib-clean": "~0.5.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-compress": "~0.5.2",
|
||||||
"grunt-contrib-concat": "~0.3.0",
|
"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-contrib-watch": "~0.5.3",
|
||||||
"grunt-shell": "~0.6.0",
|
"grunt-shell": "~0.6.0",
|
||||||
"load-grunt-tasks": "~0.2.0"
|
"load-grunt-tasks": "~0.2.0"
|
||||||
|
|||||||
@ -136,10 +136,13 @@ PostHiding =
|
|||||||
return
|
return
|
||||||
|
|
||||||
makeButton: (post, type) ->
|
makeButton: (post, type) ->
|
||||||
|
span = $.el 'span',
|
||||||
|
className: "brackets-wrap"
|
||||||
|
textContent: "\u00A0#{if type is 'hide' then '-' else '+'}\u00A0"
|
||||||
a = $.el 'a',
|
a = $.el 'a',
|
||||||
className: "#{type}-reply-button"
|
className: "#{type}-reply-button"
|
||||||
innerHTML: "<span class=brackets-wrap> #{if type is 'hide' then '-' else '+'} </span>"
|
|
||||||
href: 'javascript:;'
|
href: 'javascript:;'
|
||||||
|
$.add a, span
|
||||||
$.on a, 'click', PostHiding.toggle
|
$.on a, 'click', PostHiding.toggle
|
||||||
a
|
a
|
||||||
|
|
||||||
@ -186,10 +189,9 @@ PostHiding =
|
|||||||
$.add a, $.tn " #{postInfo}"
|
$.add a, $.tn " #{postInfo}"
|
||||||
post.nodes.stub = $.el 'div',
|
post.nodes.stub = $.el 'div',
|
||||||
className: 'stub'
|
className: 'stub'
|
||||||
$.add post.nodes.stub, if Conf['Menu']
|
$.add post.nodes.stub, a
|
||||||
[a, $.tn(' '), button = Menu.makeButton post]
|
if Conf['Menu']
|
||||||
else
|
$.add post.nodes.stub, Menu.makeButton()
|
||||||
a
|
|
||||||
$.prepend post.nodes.root, post.nodes.stub
|
$.prepend post.nodes.root, post.nodes.stub
|
||||||
|
|
||||||
show: (post, showRecursively=Conf['Recursive Hiding']) ->
|
show: (post, showRecursively=Conf['Recursive Hiding']) ->
|
||||||
|
|||||||
@ -164,7 +164,7 @@ ThreadHiding =
|
|||||||
thread.stub = $.el 'div',
|
thread.stub = $.el 'div',
|
||||||
className: 'stub'
|
className: 'stub'
|
||||||
if Conf['Menu']
|
if Conf['Menu']
|
||||||
$.add thread.stub, [a, $.tn(' '), Menu.makeButton()]
|
$.add thread.stub, [a, Menu.makeButton()]
|
||||||
else
|
else
|
||||||
$.add thread.stub, a
|
$.add thread.stub, a
|
||||||
$.prepend root, thread.stub
|
$.prepend root, thread.stub
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
Build =
|
Build =
|
||||||
|
staticPath: '//s.4cdn.org/image/'
|
||||||
|
gifIcon: if window.devicePixelRatio >= 2 then '@2x.gif' else '.gif'
|
||||||
spoilerRange: {}
|
spoilerRange: {}
|
||||||
shortFilename: (filename, isReply) ->
|
shortFilename: (filename, isReply) ->
|
||||||
# FILENAME SHORTENING SCIENCE:
|
# FILENAME SHORTENING SCIENCE:
|
||||||
@ -65,12 +67,12 @@ Build =
|
|||||||
file
|
file
|
||||||
} = o
|
} = o
|
||||||
isOP = postID is threadID
|
isOP = postID is threadID
|
||||||
|
{staticPath, gifIcon} = Build
|
||||||
|
|
||||||
staticPath = '//s.4cdn.org/image/'
|
tripcode = if tripcode
|
||||||
gifIcon = if window.devicePixelRatio >= 2
|
" <span class=postertrip>#{tripcode}</span>"
|
||||||
'@2x.gif'
|
|
||||||
else
|
else
|
||||||
'.gif'
|
''
|
||||||
|
|
||||||
if email
|
if email
|
||||||
emailStart = '<a href="mailto:' + email + '" class="useremail">'
|
emailStart = '<a href="mailto:' + email + '" class="useremail">'
|
||||||
@ -79,7 +81,29 @@ Build =
|
|||||||
emailStart = ''
|
emailStart = ''
|
||||||
emailEnd = ''
|
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 =
|
userID =
|
||||||
if !capcode and uniqueID
|
if !capcode and uniqueID
|
||||||
@ -88,33 +112,6 @@ Build =
|
|||||||
else
|
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
|
flag = unless flagCode
|
||||||
''
|
''
|
||||||
else if boardID is 'pol'
|
else if boardID is 'pol'
|
||||||
@ -132,13 +129,7 @@ Build =
|
|||||||
"<img src='#{staticPath}filedeleted-res#{gifIcon}' alt='File deleted.' class=fileDeletedRes>" +
|
"<img src='#{staticPath}filedeleted-res#{gifIcon}' alt='File deleted.' class=fileDeletedRes>" +
|
||||||
"</span></div>"
|
"</span></div>"
|
||||||
else if file
|
else if file
|
||||||
ext = file.name[-3..]
|
fileSize = $.bytesToString file.size
|
||||||
if !file.twidth and !file.theight and ext is 'gif' # wtf ?
|
|
||||||
file.twidth = file.width
|
|
||||||
file.theight = file.height
|
|
||||||
|
|
||||||
fileSize = $.bytesToString file.size
|
|
||||||
|
|
||||||
fileThumb = file.turl
|
fileThumb = file.turl
|
||||||
if file.isSpoiler
|
if file.isSpoiler
|
||||||
fileSize = "Spoiler Image, #{fileSize}"
|
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;'>" +
|
"<img src='#{fileThumb}' alt='#{fileSize}' data-md5=#{file.MD5} style='height: #{file.theight}px; width: #{file.twidth}px;'>" +
|
||||||
"</a>"
|
"</a>"
|
||||||
|
|
||||||
# Ha ha, filenames!
|
|
||||||
# html -> text, translate WebKit's %22s into "s
|
# html -> text, translate WebKit's %22s into "s
|
||||||
a = $.el 'a', innerHTML: file.name
|
a = $.el 'a', innerHTML: file.name
|
||||||
filename = a.textContent.replace /%22/g, '"'
|
filename = a.textContent.replace /%22/g, '"'
|
||||||
|
|
||||||
# shorten filename, get html
|
# shorten filename, get html
|
||||||
a.textContent = Build.shortFilename filename
|
a.textContent = Build.shortFilename filename
|
||||||
shortFilename = a.innerHTML
|
shortFilename = a.innerHTML
|
||||||
|
|
||||||
# get html
|
# get html
|
||||||
a.textContent = filename
|
a.textContent = filename
|
||||||
filename = a.innerHTML.replace /'/g, '''
|
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>" +
|
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}#{
|
"-(#{fileSize}, #{fileDims}#{
|
||||||
if file.isSpoiler
|
if file.isSpoiler
|
||||||
@ -183,11 +171,6 @@ Build =
|
|||||||
else
|
else
|
||||||
fileHTML = ''
|
fileHTML = ''
|
||||||
|
|
||||||
tripcode = if tripcode
|
|
||||||
" <span class=postertrip>#{tripcode}</span>"
|
|
||||||
else
|
|
||||||
''
|
|
||||||
|
|
||||||
sticky = if isSticky
|
sticky = if isSticky
|
||||||
" <img src=#{staticPath}sticky#{gifIcon} alt=Sticky title=Sticky class=stickyIcon>"
|
" <img src=#{staticPath}sticky#{gifIcon} alt=Sticky title=Sticky class=stickyIcon>"
|
||||||
else
|
else
|
||||||
|
|||||||
@ -170,8 +170,8 @@ Get =
|
|||||||
threadID = +data.thread_num
|
threadID = +data.thread_num
|
||||||
o =
|
o =
|
||||||
# id
|
# id
|
||||||
postID: "#{postID}"
|
postID: postID
|
||||||
threadID: "#{threadID}"
|
threadID: threadID
|
||||||
boardID: boardID
|
boardID: boardID
|
||||||
# info
|
# info
|
||||||
name: data.name_processed
|
name: data.name_processed
|
||||||
@ -207,8 +207,7 @@ Get =
|
|||||||
new Board boardID
|
new Board boardID
|
||||||
thread = g.threads["#{boardID}.#{threadID}"] or
|
thread = g.threads["#{boardID}.#{threadID}"] or
|
||||||
new Thread threadID, board
|
new Thread threadID, board
|
||||||
post = new Post Build.post(o, true), thread, board,
|
post = new Post Build.post(o, true), thread, board, {isArchived: true}
|
||||||
isArchived: true
|
|
||||||
Main.callbackNodes Post, [post]
|
Main.callbackNodes Post, [post]
|
||||||
Get.insert post, root, context
|
Get.insert post, root, context
|
||||||
parseMarkup: (text) ->
|
parseMarkup: (text) ->
|
||||||
|
|||||||
@ -191,7 +191,7 @@ Main =
|
|||||||
posts = []
|
posts = []
|
||||||
for postRoot in $$ '.thread > .postContainer', threadRoot
|
for postRoot in $$ '.thread > .postContainer', threadRoot
|
||||||
try
|
try
|
||||||
posts.push new Post postRoot, thread, g.BOARD
|
posts.push post = new Post postRoot, thread, g.BOARD, {isOriginalMarkup: true}
|
||||||
catch err
|
catch err
|
||||||
# Skip posts that we failed to parse.
|
# Skip posts that we failed to parse.
|
||||||
errors = [] unless errors
|
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 ''}
|
#{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>
|
<input type=checkbox name=#{postID} value=delete>
|
||||||
#{subject}
|
#{subject}
|
||||||
<span class='nameBlock#{capcodeClass}'>
|
<span class='nameBlock#{capcodeClass}'>
|
||||||
@ -41,7 +17,7 @@
|
|||||||
#{tripcode + capcodeStart + emailEnd + capcode + userID + flag}
|
#{tripcode + capcodeStart + emailEnd + capcode + userID + flag}
|
||||||
</span>#{" "}
|
</span>#{" "}
|
||||||
<span class=dateTime data-utc=#{dateUTC}>#{date}</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=#{"/#{boardID}/res/#{threadID}#p#{postID}"} title='Highlight this post'>No.</a>
|
||||||
<a href='#{
|
<a href='#{
|
||||||
if g.VIEW is 'thread' and g.THREADID is +threadID then
|
if g.VIEW is 'thread' and g.THREADID is +threadID then
|
||||||
|
|||||||
@ -316,9 +316,8 @@ $.get = (key, val, cb) ->
|
|||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
done = (item) ->
|
done = (item) ->
|
||||||
{lastError} = chrome.runtime
|
if chrome.runtime.lastError
|
||||||
if lastError
|
c.error chrome.runtime.lastError.message
|
||||||
c.error lastError, lastError.message or 'No message.'
|
|
||||||
$.extend items, item
|
$.extend items, item
|
||||||
cb items unless --count
|
cb items unless --count
|
||||||
|
|
||||||
@ -330,30 +329,41 @@ $.get = (key, val, cb) ->
|
|||||||
chrome.storage.sync.get syncItems, done
|
chrome.storage.sync.get syncItems, done
|
||||||
|
|
||||||
$.set = do ->
|
$.set = do ->
|
||||||
items = {}
|
items =
|
||||||
localItems = {}
|
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
|
for key in $.localKeys
|
||||||
if key of items
|
if key of items.sync
|
||||||
(localItems or= {})[key] = items[key]
|
items.local[key] = items.sync[key]
|
||||||
delete items[key]
|
delete items.sync[key]
|
||||||
try
|
try
|
||||||
chrome.storage.local.set localItems
|
setArea 'local'
|
||||||
chrome.storage.sync.set items
|
setArea 'sync'
|
||||||
items = {}
|
|
||||||
localItems = {}
|
|
||||||
catch err
|
catch err
|
||||||
c.error err.stack
|
c.error err.stack
|
||||||
|
|
||||||
(key, val) ->
|
(key, val) ->
|
||||||
if typeof key is 'string'
|
if typeof key is 'string'
|
||||||
items[key] = val
|
items.sync[key] = val
|
||||||
else
|
else
|
||||||
$.extend items, key
|
$.extend items.sync, key
|
||||||
set()
|
setAll()
|
||||||
|
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
|
||||||
# http://wiki.greasespot.net/Main_Page
|
# http://wiki.greasespot.net/Main_Page
|
||||||
$.sync = do ->
|
$.sync = do ->
|
||||||
$.on window, 'storage', ({key, newValue}) ->
|
$.on window, 'storage', ({key, newValue}) ->
|
||||||
|
|||||||
@ -6,6 +6,7 @@ class Post
|
|||||||
@ID = +root.id[2..]
|
@ID = +root.id[2..]
|
||||||
@fullID = "#{@board}.#{@ID}"
|
@fullID = "#{@board}.#{@ID}"
|
||||||
|
|
||||||
|
@cleanup root if that.isOriginalMarkup
|
||||||
post = $ '.post', root
|
post = $ '.post', root
|
||||||
info = $ '.postInfo', post
|
info = $ '.postInfo', post
|
||||||
@nodes =
|
@nodes =
|
||||||
@ -149,6 +150,13 @@ class Post
|
|||||||
if @file.isImage = /(jpg|png|gif)$/i.test @file.name
|
if @file.isImage = /(jpg|png|gif)$/i.test @file.name
|
||||||
@file.dimensions = fileText.textContent.match(/\d+x\d+/)[0]
|
@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) ->
|
kill: (file, now) ->
|
||||||
now or= new Date()
|
now or= new Date()
|
||||||
if file
|
if file
|
||||||
|
|||||||
@ -33,6 +33,8 @@ class Thread
|
|||||||
className: "#{typeLC}Icon"
|
className: "#{typeLC}Icon"
|
||||||
root = if type is 'Closed' and @isSticky
|
root = if type is 'Closed' and @isSticky
|
||||||
$ '.stickyIcon', @OP.nodes.info
|
$ '.stickyIcon', @OP.nodes.info
|
||||||
|
else if g.VIEW is 'index'
|
||||||
|
$ '.page-num', @OP.nodes.info
|
||||||
else
|
else
|
||||||
$ '[title="Quote this post"]', @OP.nodes.info
|
$ '[title="Quote this post"]', @OP.nodes.info
|
||||||
$.after root, [$.tn(' '), icon]
|
$.after root, [$.tn(' '), icon]
|
||||||
|
|||||||
@ -8,20 +8,21 @@ Menu =
|
|||||||
cb: @node
|
cb: @node
|
||||||
|
|
||||||
node: ->
|
node: ->
|
||||||
if @isClone
|
return $.on $('.menu-button', @nodes.info), 'click', Menu.toggle if @isClone
|
||||||
$.on $('.menu-button', @nodes.info), 'click', Menu.toggle
|
$.add @nodes.info, Menu.makeButton()
|
||||||
else
|
|
||||||
$.add @nodes.info, [$.tn('\u00A0'), Menu.makeButton()]
|
|
||||||
|
|
||||||
makeButton: do ->
|
makeButton: do ->
|
||||||
a = $.el 'a',
|
frag = $.nodes [
|
||||||
className: 'menu-button brackets-wrap'
|
$.tn(' ')
|
||||||
innerHTML: '<i></i>'
|
$.el 'a',
|
||||||
href: 'javascript:;'
|
className: 'menu-button'
|
||||||
|
innerHTML: '[<i></i>]'
|
||||||
|
href: 'javascript:;'
|
||||||
|
]
|
||||||
->
|
->
|
||||||
button = a.cloneNode true
|
clone = frag.cloneNode true
|
||||||
$.on button, 'click', Menu.toggle
|
$.on clone.lastElementChild, 'click', Menu.toggle
|
||||||
button
|
clone
|
||||||
|
|
||||||
toggle: (e) ->
|
toggle: (e) ->
|
||||||
post = Get.postFromNode @
|
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']
|
return if !Conf['Quick Reply']
|
||||||
|
|
||||||
@db = new DataBoard 'yourPosts'
|
@db = new DataBoard 'yourPosts'
|
||||||
|
@posts = []
|
||||||
|
|
||||||
if Conf['QR Shortcut']
|
if Conf['QR Shortcut']
|
||||||
sc = $.el 'a',
|
sc = $.el 'a',
|
||||||
@ -203,185 +204,6 @@ QR =
|
|||||||
value
|
value
|
||||||
status.disabled = disabled or false
|
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) ->
|
quote: (e) ->
|
||||||
e?.preventDefault()
|
e?.preventDefault()
|
||||||
return unless QR.postingIsEnabled
|
return unless QR.postingIsEnabled
|
||||||
@ -496,399 +318,6 @@ QR =
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
QR.nodes.fileInput.click()
|
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: ->
|
generatePostableThreadsList: ->
|
||||||
return unless QR.nodes
|
return unless QR.nodes
|
||||||
list = QR.nodes.thread
|
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