Remove the 'Open Reply in New Tab' config and make it the default behavior.

Various QR fixes.
Tiny styling adjustments.
You can now create threads when outside of the index.
Allow selection-to-quote to work on any text inside the quoted post, not just the comment. Close #789.
This commit is contained in:
Nicolas Stepien 2013-02-12 23:05:09 +01:00
parent 0f0d410209
commit a9b427f0d2
12 changed files with 385 additions and 330 deletions

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,11 @@ alpha
Access the list of boards directly from the Header. Access the list of boards directly from the Header.
From the Header's menu, access to: From the Header's menu, access to:
Settings Settings
Quick Reply Quick Reply shortcut
Can be auto-hidden. Can be auto-hidden.
QR changes:
Creating threads outside of the index is now possible.
Selection-to-quote also applies to selected text inside the post, not just inside the comment.
Added touch and multi-touch support for dragging windows. Added touch and multi-touch support for dragging windows.
The Thread Updater will pause when offline, and resume when online. The Thread Updater will pause when offline, and resume when online.
Added Thread & Post Hiding in the Menu, with individual settings. Added Thread & Post Hiding in the Menu, with individual settings.

View File

@ -3,6 +3,9 @@
background-color: #D6DAF0; background-color: #D6DAF0;
border-color: #B7C5D9; border-color: #B7C5D9;
} }
:root.burichan .field:focus {
border-color: #98E;
}
/* Header */ /* Header */
:root.burichan #header-bar { :root.burichan #header-bar {
@ -19,6 +22,11 @@
background-color: rgba(255, 255, 255, .14); background-color: rgba(255, 255, 255, .14);
} }
/* QR */
:root.burichan .qrpreview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */ /* Menu */
:root.burichan .entry:not(:last-child) { :root.burichan .entry:not(:last-child) {
border-bottom: 1px solid #B7C5D9; border-bottom: 1px solid #B7C5D9;

View File

@ -3,6 +3,9 @@
background-color: #F0E0D6; background-color: #F0E0D6;
border-color: #D9BFB7; border-color: #D9BFB7;
} }
:root.futaba .field:focus {
border-color: #EA8;
}
/* Header */ /* Header */
:root.futaba #header-bar { :root.futaba #header-bar {
@ -19,6 +22,11 @@
background-color: rgba(255, 255, 255, .14); background-color: rgba(255, 255, 255, .14);
} }
/* QR */
:root.futaba .qrpreview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */ /* Menu */
:root.futaba .entry:not(:last-child) { :root.futaba .entry:not(:last-child) {
border-bottom: 1px solid #D9BFB7; border-bottom: 1px solid #D9BFB7;

View File

@ -3,6 +3,9 @@
background-color: #DDD; background-color: #DDD;
border-color: #CCC; border-color: #CCC;
} }
:root.photon .field:focus {
border-color: #EA8;
}
/* Header */ /* Header */
:root.photon #header-bar { :root.photon #header-bar {
@ -19,6 +22,11 @@
background-color: rgba(255, 255, 255, .14); background-color: rgba(255, 255, 255, .14);
} }
/* QR */
:root.photon .qrpreview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */ /* Menu */
:root.photon .entry:not(:last-child) { :root.photon .entry:not(:last-child) {
border-bottom: 1px solid #CCC; border-bottom: 1px solid #CCC;

View File

@ -5,6 +5,28 @@
display: block; display: block;
padding: 0; padding: 0;
} }
.field {
border: 1px solid #CCC;
-moz-box-sizing: border-box;
box-sizing: border-box;
color: #333;
font: 13px sans-serif;
margin: 0;
padding: 2px 4px 3px;
outline: none;
-webkit-transition: color .25s, border-color .25s;
transition: color .25s, border-color .25s;
}
.field:-moz-placeholder,
.field:hover:-moz-placeholder {
color: #AAA !important;
}
.field:hover {
border-color: #999;
}
.field:hover, .field:focus {
color: #000;
}
.move { .move {
cursor: move; cursor: move;
} }
@ -79,21 +101,15 @@ a[href="javascript:;"] {
border-width: 0 0 1px; border-width: 0 0 1px;
padding: 4px; padding: 4px;
position: relative; position: relative;
transition: all .1s ease-in-out;
-o-transition: all .1s ease-in-out;
-moz-transition: all .1s ease-in-out;
-webkit-transition: all .1s ease-in-out; -webkit-transition: all .1s ease-in-out;
transition: all .1s ease-in-out;
} }
#header-bar.autohide:not(:hover) { #header-bar.autohide:not(:hover) {
margin-bottom: -1em; margin-bottom: -1em;
transform: translateY(-100%);
-o-transform: translateY(-100%);
-moz-transform: translateY(-100%);
-webkit-transform: translateY(-100%); -webkit-transform: translateY(-100%);
transition: all .75s .25s ease-in-out; transform: translateY(-100%);
-o-transition: all .75s .25s ease-in-out;
-moz-transition: all .75s .25s ease-in-out;
-webkit-transition: all .75s .25s ease-in-out; -webkit-transition: all .75s .25s ease-in-out;
transition: all .75s .25s ease-in-out;
} }
#toggle-header-bar { #toggle-header-bar {
cursor: n-resize; cursor: n-resize;
@ -129,10 +145,8 @@ a[href="javascript:;"] {
width: 500px; width: 500px;
max-width: 100%; max-width: 100%;
position: relative; position: relative;
transition: all .25s ease-in-out;
-o-transition: all .25s ease-in-out;
-moz-transition: all .25s ease-in-out;
-webkit-transition: all .25s ease-in-out; -webkit-transition: all .25s ease-in-out;
transition: all .25s ease-in-out;
} }
.notification.error { .notification.error {
background-color: hsla(0, 100%, 40%, .9); background-color: hsla(0, 100%, 40%, .9);
@ -154,6 +168,7 @@ a[href="javascript:;"] {
position: absolute; position: absolute;
} }
.message { .message {
-moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
padding: 4px 20px; padding: 4px 20px;
max-height: 200px; max-height: 200px;
@ -234,8 +249,8 @@ a[href="javascript:;"] {
display: none; display: none;
} }
#ihover { #ihover {
box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box;
max-height: 100%; max-height: 100%;
max-width: 75%; max-width: 75%;
padding-bottom: 16px; padding-bottom: 16px;
@ -270,11 +285,15 @@ a[href="javascript:;"] {
} }
/* QR */ /* QR */
.hide-original-post-form #postForm,
.hide-original-post-form .postingMode {
display: none;
}
#qr > .move { #qr > .move {
min-width: 300px; min-width: 300px;
overflow: hidden; overflow: hidden;
box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box;
padding: 0 2px; padding: 0 2px;
} }
#qr > .move > span { #qr > .move > span {
@ -283,15 +302,16 @@ a[href="javascript:;"] {
#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning { #autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {
cursor: pointer; cursor: pointer;
} }
#qr select, #qr select {
#qr > form {
margin: 0; margin: 0;
} }
#dump { #dump {
background: -webkit-linear-gradient(#EEE, #CCC); background: -webkit-linear-gradient(#EEE, #CCC);
background: -moz-linear-gradient(#EEE, #CCC);
background: -o-linear-gradient(#EEE, #CCC);
background: linear-gradient(#EEE, #CCC); background: linear-gradient(#EEE, #CCC);
border: 1px solid #CCC;
margin: 0;
padding: 2px 4px 3px;
outline: none;
width: 10%; width: 10%;
} }
.gecko #dump { .gecko #dump {
@ -299,14 +319,10 @@ a[href="javascript:;"] {
} }
#dump:hover, #dump:focus { #dump:hover, #dump:focus {
background: -webkit-linear-gradient(#FFF, #DDD); background: -webkit-linear-gradient(#FFF, #DDD);
background: -moz-linear-gradient(#FFF, #DDD);
background: -o-linear-gradient(#FFF, #DDD);
background: linear-gradient(#FFF, #DDD); background: linear-gradient(#FFF, #DDD);
} }
#dump:active, .dump #dump:not(:hover):not(:focus) { #dump:active, .dump #dump:not(:hover):not(:focus) {
background: -webkit-linear-gradient(#CCC, #DDD); background: -webkit-linear-gradient(#CCC, #DDD);
background: -moz-linear-gradient(#CCC, #DDD);
background: -o-linear-gradient(#CCC, #DDD);
background: linear-gradient(#CCC, #DDD); background: linear-gradient(#CCC, #DDD);
} }
#qr:not(.dump) #replies, .dump > form > label { #qr:not(.dump) #replies, .dump > form > label {
@ -322,7 +338,7 @@ a[href="javascript:;"] {
user-select: none; user-select: none;
} }
#replies > div { #replies > div {
counter-reset: thumbnails; counter-reset: qrpreviews;
top: 0; right: 0; bottom: 0; left: 0; top: 0; right: 0; bottom: 0; left: 0;
margin: 0; padding: 0; margin: 0; padding: 0;
overflow: hidden; overflow: hidden;
@ -334,103 +350,72 @@ a[href="javascript:;"] {
overflow-x: auto; overflow-x: auto;
z-index: 1; z-index: 1;
} }
.thumbnail { .qrpreview {
background-color: rgba(0,0,0,.2) !important; background-position: 50% 20%;
background-position: 50% 20% !important; background-size: cover;
background-size: cover !important; border: 1px solid #808080;
border: 1px solid #666; color: #FFF !important;
box-sizing: border-box; font-size: 12px;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box;
cursor: move; cursor: move;
display: inline-block; display: inline-block;
height: 90px; width: 90px; height: 90px; width: 90px;
margin: 5px; padding: 2px; margin: 5px; padding: 2px;
opacity: .5; opacity: .6;
outline: none; outline: none;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
text-shadow: 0 1px 1px #000; text-shadow: 0 1px 1px #000;
-webkit-transition: opacity .25s ease-in-out; -webkit-transition: opacity .25s ease-in-out;
-moz-transition: opacity .25s ease-in-out;
-o-transition: opacity .25s ease-in-out;
transition: opacity .25s ease-in-out; transition: opacity .25s ease-in-out;
vertical-align: top; vertical-align: top;
} }
.thumbnail:hover, .thumbnail:focus { .qrpreview:hover, .qrpreview:focus {
opacity: .9; opacity: .9;
color: #FFF !important;
} }
.thumbnail#selected { .qrpreview#selected {
opacity: 1; opacity: 1;
} }
.thumbnail::before { .qrpreview::before {
counter-increment: thumbnails; counter-increment: qrpreviews;
content: counter(thumbnails); content: counter(qrpreviews);
color: #FFF;
font-weight: 700; font-weight: 700;
padding: 3px; text-shadow: 0 0 3px #000, 0 0 5px #000;
position: absolute; position: absolute;
top: 0; top: 3px; right: 3px;
right: 0;
text-shadow: 0 0 3px #000, 0 0 8px #000;
} }
.thumbnail.drag { .qrpreview.drag {
box-shadow: 0 0 10px rgba(0,0,0,.5); border-color: red;
border-style: dashed;
} }
.thumbnail.over { .qrpreview.over {
border-color: #FFF; border-color: #FFF;
} border-style: dashed;
.thumbnail > span {
color: #FFF;
} }
.remove { .remove {
background: none; color: #E00 !important;
color: #E00;
font-weight: 700; font-weight: 700;
padding: 3px; padding: 3px;
} }
.remove:hover::after { .remove:hover::after {
content: " Remove"; content: ' Remove';
} }
.thumbnail > label { .qrpreview > label {
background: rgba(0,0,0,.5); background: rgba(0, 0, 0, .5);
color: #FFF;
right: 0; bottom: 0; left: 0; right: 0; bottom: 0; left: 0;
position: absolute; position: absolute;
text-align: center; text-align: center;
} }
.thumbnail > label > input { .qrpreview > label > input {
margin: 0; margin: 1px 0;
vertical-align: bottom;
} }
#addReply { #addReply {
color: #333;
font-size: 3.5em; font-size: 3.5em;
line-height: 100px; line-height: 100px;
} }
#addReply:hover, #addReply:focus {
color: #000;
}
.field {
border: 1px solid #CCC;
box-sizing: border-box;
-moz-box-sizing: border-box;
color: #333;
font: 13px sans-serif;
margin: 0;
padding: 2px 4px 3px;
-webkit-transition: color .25s, border .25s;
-moz-transition: color .25s, border .25s;
-o-transition: color .25s, border .25s;
transition: color .25s, border .25s;
}
.field:-moz-placeholder,
.field:hover:-moz-placeholder {
color: #AAA;
}
.field:hover, .field:focus {
border-color: #999;
color: #000;
outline: none;
}
#qr > form > div:first-child > .field:not(#dump) { #qr > form > div:first-child > .field:not(#dump) {
width: 30%; width: 30%;
} }

View File

@ -3,6 +3,9 @@
background-color: #282A2E; background-color: #282A2E;
border-color: #111; border-color: #111;
} }
:root.tomorrow .field:focus {
border-color: #000;
}
/* Header */ /* Header */
:root.tomorrow #header-bar { :root.tomorrow #header-bar {
@ -19,6 +22,11 @@
background-color: rgba(0, 0, 0, .14); background-color: rgba(0, 0, 0, .14);
} }
/* QR */
:root.tomorrow .qrpreview {
background-color: rgba(255, 255, 255, .15);
}
/* Menu */ /* Menu */
:root.tomorrow .entry:not(:last-child) { :root.tomorrow .entry:not(:last-child) {
border-bottom: 1px solid #111; border-bottom: 1px solid #111;

View File

@ -3,6 +3,9 @@
background-color: #D6DAF0; background-color: #D6DAF0;
border-color: #B7C5D9; border-color: #B7C5D9;
} }
:root.yotsuba-b .field:focus {
border-color: #98E;
}
/* Header */ /* Header */
:root.yotsuba-b #header-bar { :root.yotsuba-b #header-bar {
@ -19,6 +22,11 @@
background-color: rgba(255, 255, 255, .14); background-color: rgba(255, 255, 255, .14);
} }
/* QR */
:root.yotsuba-b .qrpreview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */ /* Menu */
:root.yotsuba-b .entry:not(:last-child) { :root.yotsuba-b .entry:not(:last-child) {
border-bottom: 1px solid #B7C5D9; border-bottom: 1px solid #B7C5D9;

View File

@ -3,6 +3,9 @@
background-color: #F0E0D6; background-color: #F0E0D6;
border-color: #D9BFB7; border-color: #D9BFB7;
} }
:root.yotsuba .field:focus {
border-color: #EA8;
}
/* Header */ /* Header */
:root.yotsuba #header-bar { :root.yotsuba #header-bar {
@ -19,6 +22,11 @@
background-color: rgba(255, 255, 255, .14); background-color: rgba(255, 255, 255, .14);
} }
/* QR */
:root.yotsuba .qrpreview {
background-color: rgba(0, 0, 0, .15);
}
/* Menu */ /* Menu */
:root.yotsuba .entry:not(:last-child) { :root.yotsuba .entry:not(:last-child) {
border-bottom: 1px solid #D9BFB7; border-bottom: 1px solid #D9BFB7;

View File

@ -45,7 +45,6 @@ Config =
'Quick Reply': [true, 'WMD.'] 'Quick Reply': [true, 'WMD.']
'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'] 'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.']
'Auto Hide QR': [false, 'Automatically hide the quick reply when posting.'] 'Auto Hide QR': [false, 'Automatically hide the quick reply when posting.']
'Open Reply in New Tab': [false, 'Open replies posted from the board pages in a new tab.']
'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.'] 'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.']
'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'] 'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.']
'Hide Original Post Form': [true, 'Replace the normal post form with a shortcut to open the QR.'] 'Hide Original Post Form': [true, 'Replace the normal post form with a shortcut to open the QR.']

View File

@ -762,38 +762,41 @@ Recursive =
QR = QR =
init: -> init: ->
return unless Conf['Quick Reply'] return if g.VIEW is 'catalog' or !Conf['Quick Reply']
if Conf['Hide Original Post Form'] if Conf['Hide Original Post Form']
Main.css += """ $.addClass doc, 'hide-original-post-form'
#postForm, .postingMode {
display: none;
}
"""
link = $.el 'a', link = $.el 'a',
className: 'qr-shortcut'
textContent: 'Quick Reply' textContent: 'Quick Reply'
href: 'javascript:;' href: 'javascript:;'
$.on link, 'click', -> $.on link, 'click', ->
Header.menu.close() Header.menu.close()
QR.open() QR.open()
if g.BOARD.ID is 'f'
if g.VIEW is 'index'
QR.threadSelector.value = '9999'
else if g.VIEW is 'thread'
QR.threadSelector.value = g.THREAD
else
QR.threadSelector.value = 'new'
$('textarea', QR.el).focus()
$.event 'AddMenuEntry', $.event 'AddMenuEntry',
type: 'header' type: 'header'
el: link el: link
Post::callbacks.push
name: 'Quick Reply'
cb: @node
$.ready @readyInit
readyInit: ->
if Conf['Persistent QR']
QR.open()
QR.hide() if Conf['Auto Hide QR']
$.on d, 'dragover', QR.dragOver $.on d, 'dragover', QR.dragOver
$.on d, 'drop', QR.dropFile $.on d, 'drop', QR.dropFile
$.on d, 'dragstart dragend', QR.drag $.on d, 'dragstart dragend', QR.drag
$.on d, '4chanXInitFinished', ->
return unless Conf['Persistent QR']
QR.open()
QR.hide() if Conf['Auto Hide QR']
Post::callbacks.push
name: 'Quick Reply'
cb: @node
node: -> node: ->
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote $.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
@ -802,13 +805,14 @@ QR =
if QR.el if QR.el
QR.el.hidden = false QR.el.hidden = false
QR.unhide() QR.unhide()
else return
try try
QR.dialog() QR.dialog()
catch err catch err
Main.handleErrors delete QR.el
message: 'Quick Reply dialog creation crashed.' Main.handleErrors
error: err message: 'Quick Reply dialog creation crashed.'
error: err
close: -> close: ->
QR.el.hidden = true QR.el.hidden = true
QR.abort() QR.abort()
@ -821,7 +825,7 @@ QR =
QR.resetFileInput() QR.resetFileInput()
if not Conf['Remember Spoiler'] and (spoiler = $.id 'spoiler').checked if not Conf['Remember Spoiler'] and (spoiler = $.id 'spoiler').checked
spoiler.click() spoiler.click()
QR.cleanError() QR.cleanNotification()
hide: -> hide: ->
d.activeElement.blur() d.activeElement.blur()
$.addClass QR.el, 'autohide' $.addClass QR.el, 'autohide'
@ -830,7 +834,10 @@ QR =
$.rmClass QR.el, 'autohide' $.rmClass QR.el, 'autohide'
$.id('autohide').checked = false $.id('autohide').checked = false
toggleHide: -> toggleHide: ->
@checked and QR.hide() or QR.unhide() if @checked
QR.hide()
else
QR.unhide()
error: (err) -> error: (err) ->
QR.open() QR.open()
@ -844,20 +851,20 @@ QR =
$('[autocomplete]', QR.el).focus() $('[autocomplete]', QR.el).focus()
alert el.textContent if d.hidden alert el.textContent if d.hidden
QR.lastNotification = new Notification 'warning', el QR.lastNotification = new Notification 'warning', el
cleanError: -> cleanNotification: ->
QR.lastNotification?.close() QR.lastNotification?.close()
delete QR.lastNotification delete QR.lastNotification
status: (data={}) -> status: (data={}) ->
return unless QR.el return unless QR.el
if g.dead if g.dead # XXX
value = 404 value = 404
disabled = true disabled = true
QR.cooldown.auto = false QR.cooldown.auto = false
value = data.progress or QR.cooldown.seconds or value value = data.progress or QR.cooldown.seconds or value
{input} = QR.status {input} = QR.status
input.value = input.value =
if QR.cooldown.auto and Conf['Cooldown'] if QR.cooldown.auto
if value then "Auto #{value}" else 'Auto' if value then "Auto #{value}" else 'Auto'
else else
value or 'Submit' value or 'Submit'
@ -865,7 +872,6 @@ QR =
cooldown: cooldown:
init: -> init: ->
return unless Conf['Cooldown']
QR.cooldown.types = QR.cooldown.types =
thread: switch g.BOARD thread: switch g.BOARD
when 'q' then 86400 when 'q' then 86400
@ -883,12 +889,11 @@ QR =
QR.cooldown.count() QR.cooldown.count()
sync: (cooldowns) -> sync: (cooldowns) ->
# Add each cooldowns, don't overwrite everything in case we # Add each cooldowns, don't overwrite everything in case we
# still need to purge one in the current tab to auto-post. # still need to prune one in the current tab to auto-post.
for id of cooldowns for id of cooldowns
QR.cooldown.cooldowns[id] = cooldowns[id] QR.cooldown.cooldowns[id] = cooldowns[id]
QR.cooldown.start() QR.cooldown.start()
set: (data) -> set: (data) ->
return unless Conf['Cooldown']
start = Date.now() start = Date.now()
if data.delay if data.delay
cooldown = delay: data.delay cooldown = delay: data.delay
@ -926,7 +931,12 @@ QR =
QR.status() QR.status()
return return
if (isReply = if g.REPLY then true else QR.threadSelector.value isnt 'new') isReply =
if g.BOARD.ID is 'f' and g.VIEW is 'thread'
true
else
QR.threadSelector.value isnt 'new'
if isReply
post = QR.replies[0] post = QR.replies[0]
isSage = /sage/i.test post.email isSage = /sage/i.test post.email
hasFile = !!post.file hasFile = !!post.file
@ -973,14 +983,15 @@ QR =
e?.preventDefault() e?.preventDefault()
QR.open() QR.open()
ta = $ 'textarea', QR.el ta = $ 'textarea', QR.el
unless g.REPLY or ta.value if QR.threadSelector and !ta.value and g.BOARD.ID isnt 'f'
QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', @).id[1..] QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', @).id[1..]
# Make sure we get the correct number, even with XXX censors # Make sure we get the correct number, even with XXX censors
id = @previousSibling.hash[2..] post = Get.postFromRoot $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', @
text = ">>#{id}\n" text = ">>#{post}\n"
sel = d.getSelection() sel = d.getSelection()
if (s = sel.toString().trim()) and id is $.x('ancestor-or-self::blockquote', sel.anchorNode)?.id.match(/\d+$/)[0] selectionRoot = $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', sel.anchorNode
if (s = sel.toString().trim()) and post.nodes.root is selectionRoot
# XXX Opera doesn't retain `\n`s? # XXX Opera doesn't retain `\n`s?
s = s.replace /\n/g, '\n>' s = s.replace /\n/g, '\n>'
text += ">#{s}\n" text += ">#{s}\n"
@ -1019,7 +1030,7 @@ QR =
QR.fileInput.call e.dataTransfer QR.fileInput.call e.dataTransfer
$.addClass QR.el, 'dump' $.addClass QR.el, 'dump'
fileInput: -> fileInput: ->
QR.cleanError() QR.cleanNotification()
# Set or change current reply's file. # Set or change current reply's file.
if @files.length is 1 if @files.length is 1
file = @files[0] file = @files[0]
@ -1063,7 +1074,7 @@ QR =
@com = null @com = null
@el = $.el 'a', @el = $.el 'a',
className: 'thumbnail' className: 'qrpreview'
draggable: true draggable: true
href: 'javascript:;' href: 'javascript:;'
innerHTML: '<a class=remove>×</a><label hidden><input type=checkbox> Spoiler</label><span></span>' innerHTML: '<a class=remove>×</a><label hidden><input type=checkbox> Spoiler</label><span></span>'
@ -1187,7 +1198,7 @@ QR =
else if @el.id is 'selected' else if @el.id is 'selected'
(QR.replies[index-1] or QR.replies[index+1]).select() (QR.replies[index-1] or QR.replies[index+1]).select()
QR.replies.splice index, 1 QR.replies.splice index, 1
(window.URL or window.webkitURL).revokeObjectURL? @url (window.URL or window.webkitURL)?.revokeObjectURL @url
captcha: captcha:
init: -> init: ->
@ -1235,9 +1246,8 @@ QR =
@count captchas.length @count captchas.length
@reload() @reload()
load: -> load: ->
# Timeout is available at RecaptchaState.timeout in seconds. # -1 minute to give upload some time.
# We use 5-1 minutes to give upload some time. @timeout = Date.now() + $.unsafeWindow.RecaptchaState.timeout * $.SECOND - $.MINUTE
@timeout = Date.now() + 4*$.MINUTE
challenge = @challenge.firstChild.value challenge = @challenge.firstChild.value
@img.alt = challenge @img.alt = challenge
@img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}" @img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}"
@ -1252,8 +1262,8 @@ QR =
"Verification (#{count} cached captchas)" "Verification (#{count} cached captchas)"
@input.alt = count # For XTRM RICE. @input.alt = count # For XTRM RICE.
reload: (focus) -> reload: (focus) ->
# the "t" argument prevents the input from being focused # the 't' argument prevents the input from being focused
$.globalEval 'javascript:Recaptcha.reload("t")' $.unsafeWindow.Recaptcha.reload 'r'
# Focus if we meant to. # Focus if we meant to.
QR.captcha.input.focus() if focus QR.captcha.input.focus() if focus
keydown: (e) -> keydown: (e) ->
@ -1267,23 +1277,16 @@ QR =
e.preventDefault() e.preventDefault()
dialog: -> dialog: ->
QR.el = UI.dialog 'qr', 'top:0;right:0;', ' QR.el = UI.dialog 'qr', 'top:0;right:0;', """
<div class=move> <div class=move>Quick Reply <input type=checkbox id=autohide title=Auto-hide><span> <a class=close title=Close>×</a></span></div>
Quick Reply <input type=checkbox id=autohide title=Auto-hide> <form>
<span> <a class=close title=Close>×</a></span> <div class=persona><input id=dump type=button title='Dump list' value=+><input name=name title=Name placeholder=Name class=field size=1><input name=email title=E-mail placeholder=E-mail class=field size=1><input name=sub title=Subject placeholder=Subject class=field size=1></div>
</div> <div id=replies><div id=repliesList><a id=addReply href=javascript:; title="Add a reply">+</a></div></div>
<form> <div class=textarea><textarea name=com title=Comment placeholder=Comment class=field></textarea><span id=charCount></span></div>
<div><input id=dump type=button title="Dump list" value=+ class=field><input name=name title=Name placeholder=Name class=field size=1><input name=email title=E-mail placeholder=E-mail class=field size=1><input name=sub title=Subject placeholder=Subject class=field size=1></div> <div><input type=file title="Shift+Click to remove the selected file." multiple size=16><input type=submit></div>
<div id=replies><div><a id=addReply href=javascript:; title="Add a reply">+</a></div></div> <label id=spoilerLabel><input type=checkbox id=spoiler> Spoiler Image</label>
<div class=textarea><textarea name=com title=Comment placeholder=Comment class=field></textarea><span id=charCount></span></div> </form>
<div><input type=file title="Shift+Click to remove the selected file." multiple size=16><input type=submit></div> """
<label id=spoilerLabel><input type=checkbox id=spoiler> Spoiler Image</label>
</form>'
if Conf['Remember QR size'] and $.engine is 'gecko'
$.on ta = $('textarea', QR.el), 'mouseup', ->
$.set 'QR.size', @style.cssText
ta.style.cssText = $.get 'QR.size', ''
# Allow only this board's supported files. # Allow only this board's supported files.
mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) -> mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) ->
@ -1310,21 +1313,24 @@ QR =
QR.charaCounter = $ '#charCount', QR.el QR.charaCounter = $ '#charCount', QR.el
ta = $ 'textarea', QR.el ta = $ 'textarea', QR.el
unless g.REPLY span = $('.move > span', QR.el)
# Make a list with visible threads and an option to create a new one.
# Make a list of visible threads.
if g.BOARD.ID is 'f'
if g.VIEW is 'index'
QR.threadSelector = $('select[name=filetag]').cloneNode true
else
QR.threadSelector = $.el 'select',
title: 'Create a new thread / Reply to a thread'
threads = '<option value=new>New thread</option>' threads = '<option value=new>New thread</option>'
for thread in $$ '.thread' for key, thread of g.BOARD.threads
id = thread.id[1..] threads += "<option value=#{thread.ID}>Thread No.#{thread.ID}</option>"
threads += "<option value=#{id}>Thread #{id}</option>" QR.threadSelector.innerHTML = threads
QR.threadSelector = if g.VIEW is 'thread'
if g.BOARD is 'f' QR.threadSelector.value = g.THREAD
$('select[name=filetag]').cloneNode true if QR.threadSelector
else $.prepend span, QR.threadSelector
$.el 'select' $.on span, 'mousedown', (e) -> e.stopPropagation()
innerHTML: threads
title: 'Create a new thread / Reply to a thread'
$.prepend $('.move > span', QR.el), QR.threadSelector
$.on QR.threadSelector, 'mousedown', (e) -> e.stopPropagation()
$.on $('#autohide', QR.el), 'change', QR.toggleHide $.on $('#autohide', QR.el), 'change', QR.toggleHide
$.on $('.close', QR.el), 'click', QR.close $.on $('.close', QR.el), 'click', QR.close
$.on $('#dump', QR.el), 'click', -> QR.el.classList.toggle 'dump' $.on $('#dump', QR.el), 'click', -> QR.el.classList.toggle 'dump'
@ -1366,20 +1372,20 @@ QR =
QR.abort() QR.abort()
reply = QR.replies[0] reply = QR.replies[0]
if g.BOARD is 'f' and not g.REPLY if g.BOARD.ID is 'f' and g.VIEW is 'index'
filetag = QR.threadSelector.value filetag = QR.threadSelector.value
threadID = 'new' threadID = 'new'
else else
threadID = g.THREAD_ID or QR.threadSelector.value threadID = QR.threadSelector.value
# prevent errors # prevent errors
if threadID is 'new' if threadID is 'new'
threadID = null threadID = null
if g.BOARD in ['vg', 'q'] and !reply.sub if g.BOARD.ID in ['vg', 'q'] and !reply.sub
err = 'New threads require a subject.' err = 'New threads require a subject.'
else unless reply.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm' else unless reply.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm'
err = 'No file selected.' err = 'No file selected.'
else if g.BOARD is 'f' and filetag is '9999' else if g.BOARD.ID is 'f' and filetag is '9999'
err = 'Invalid tag specified.' err = 'Invalid tag specified.'
else unless reply.com or reply.file else unless reply.com or reply.file
err = 'No file selected.' err = 'No file selected.'
@ -1412,7 +1418,7 @@ QR =
QR.status() QR.status()
QR.error err QR.error err
return return
QR.cleanError() QR.cleanNotification()
# Enable auto-posting if we have stuff to post, disable it otherwise. # Enable auto-posting if we have stuff to post, disable it otherwise.
QR.cooldown.auto = QR.replies.length > 1 QR.cooldown.auto = QR.replies.length > 1
@ -1530,14 +1536,13 @@ QR =
post: reply post: reply
isReply: threadID isnt '0' isReply: threadID isnt '0'
# Enable auto-posting if we have stuff to post, disable it otherwise.
QR.cooldown.auto = QR.replies.length > 1
if threadID is '0' # new thread if threadID is '0' # new thread
# auto-noko $.open "/#{g.BOARD}/res/#{postID}"
location.pathname = "/#{g.BOARD}/res/#{postID}" else if g.VIEW is 'reply' and !QR.cooldown.auto # posting from the index
else $.open "//boards.4chan.org/#{g.BOARD}/res/#{threadID}#p#{postID}"
# Enable auto-posting if we have stuff to post, disable it otherwise.
QR.cooldown.auto = QR.replies.length > 1
if Conf['Open Reply in New Tab'] and !g.REPLY and !QR.cooldown.auto
$.open "//boards.4chan.org/#{g.BOARD}/res/#{threadID}#p#{postID}"
if Conf['Persistent QR'] or QR.cooldown.auto if Conf['Persistent QR'] or QR.cooldown.auto
reply.rm() reply.rm()
@ -2092,7 +2097,7 @@ Build =
'<br><em>' + '<br><em>' +
"<a href=#{"/#{board}/res/#{threadID}#p#{postID}"}>No.</a>" + "<a href=#{"/#{board}/res/#{threadID}#p#{postID}"}>No.</a>" +
"<a href='#{ "<a href='#{
if g.VIEW is 'thread' and g.THREAD is threadID if g.VIEW is 'thread' and g.THREAD is +threadID
"javascript:quote(#{postID})" "javascript:quote(#{postID})"
else else
"/#{board}/res/#{threadID}#q#{postID}" "/#{board}/res/#{threadID}#q#{postID}"
@ -2114,7 +2119,7 @@ Build =
"<span class='postNum desktop'>" + "<span class='postNum desktop'>" +
"<a href=#{"/#{board}/res/#{threadID}#p#{postID}"} title='Highlight this post'>No.</a>" + "<a href=#{"/#{board}/res/#{threadID}#p#{postID}"} title='Highlight this post'>No.</a>" +
"<a href='#{ "<a href='#{
if g.VIEW is 'thread' and g.THREAD is threadID if g.VIEW is 'thread' and g.THREAD is +threadID
"javascript:quote(#{postID})" "javascript:quote(#{postID})"
else else
"/#{board}/res/#{threadID}#q#{postID}" "/#{board}/res/#{threadID}#q#{postID}"

View File

@ -148,10 +148,13 @@ class Post
@kill() if that.isArchived @kill() if that.isArchived
kill: (img) -> kill: (img) ->
now = Date.now()
if @file and !@file.isDead if @file and !@file.isDead
@file.isDead = true @file.isDead = true
@file.timeOfDeath = now
return if img return if img
@isDead = true @isDead = true
@timeOfDeath = now
$.addClass @nodes.root, 'dead' $.addClass @nodes.root, 'dead'
# XXX style dead posts. # XXX style dead posts.
@ -325,21 +328,22 @@ Main =
initStyle: -> initStyle: ->
# disable the mobile layout # disable the mobile layout
$('link[href*=mobile]', d.head)?.disabled = true $('link[href*=mobile]', d.head)?.disabled = true
$.addClass doc, $.engine
$.addClass doc, 'fourchan-x'
$.addStyle Main.css $.addStyle Main.css
style = null style = 'yotsuba-b'
mainStyleSheet = $ 'link[title=switch]', d.head mainStyleSheet = $ 'link[title=switch]', d.head
styleSheets = $$ 'link[rel="alternate stylesheet"]', d.head styleSheets = $$ 'link[rel="alternate stylesheet"]', d.head
setStyle = -> setStyle = ->
$.rmClass doc, style if style $.rmClass doc, style
for styleSheet in styleSheets for styleSheet in styleSheets
if styleSheet.href is mainStyleSheet.href if styleSheet.href is mainStyleSheet.href
style = styleSheet.title.toLowerCase().replace('new', '').trim().replace /\s+/g, '-' style = styleSheet.title.toLowerCase().replace('new', '').trim().replace /\s+/g, '-'
break break
$.addClass doc, style $.addClass doc, style
$.addClass doc, $.engine
$.addClass doc, 'fourchan-x'
setStyle() setStyle()
return unless mainStyleSheet
if MutationObserver = window.MutationObserver or window.WebKitMutationObserver or window.OMutationObserver if MutationObserver = window.MutationObserver or window.WebKitMutationObserver or window.OMutationObserver
observer = new MutationObserver setStyle observer = new MutationObserver setStyle
observer.observe mainStyleSheet, observer.observe mainStyleSheet,
@ -351,7 +355,7 @@ Main =
initReady: -> initReady: ->
unless $.hasClass doc, 'fourchan-x' unless $.hasClass doc, 'fourchan-x'
# Something might go wrong! # Something might have gone wrong!
Main.initStyle() Main.initStyle()
if d.title is '4chan - 404 Not Found' if d.title is '4chan - 404 Not Found'
@ -362,28 +366,31 @@ Main =
postID: location.hash postID: location.hash
return return
threads = [] if board = $ '.board'
posts = [] threads = []
posts = []
for boardChild in $('.board').children for boardChild in board.children
continue unless $.hasClass boardChild, 'thread' continue unless $.hasClass boardChild, 'thread'
thread = new Thread boardChild.id[1..], g.BOARD thread = new Thread boardChild.id[1..], g.BOARD
threads.push thread threads.push thread
for threadChild in boardChild.children for threadChild in boardChild.children
continue unless $.hasClass threadChild, 'postContainer' continue unless $.hasClass threadChild, 'postContainer'
try try
posts.push new Post threadChild, thread, g.BOARD posts.push new Post threadChild, thread, g.BOARD
catch err catch err
# Skip posts that we failed to parse. # Skip posts that we failed to parse.
unless errors unless errors
errors = [] errors = []
errors.push errors.push
message: "Parsing of Post No.#{threadChild.id.match(/\d+/)} failed. Post will be skipped." message: "Parsing of Post No.#{threadChild.id.match(/\d+/)} failed. Post will be skipped."
error: err error: err
Main.handleErrors errors if errors Main.handleErrors errors if errors
Main.callbackNodes Thread, threads Main.callbackNodes Thread, threads
Main.callbackNodes Post, posts Main.callbackNodes Post, posts
$.event '4chanXInitFinished'
callbackNodes: (klass, nodes) -> callbackNodes: (klass, nodes) ->
# get the nodes' length only once # get the nodes' length only once