Merge branch 'v3' into Av2
Conflicts: appchan-x.meta.js appchan-x.user.js package.json src/features.coffee src/main.coffee
This commit is contained in:
commit
0a5bd72be0
19
CHANGELOG.md
19
CHANGELOG.md
@ -1,4 +1,10 @@
|
||||
# 3.0.0
|
||||
### 3.0.1 - *2013-04-08*
|
||||
|
||||
- Added the possibility to combine board-list toggle and custom text.
|
||||
- Added Reply Navigation back in, disabled by default.
|
||||
- Fixed Thread Hiding initialization error.
|
||||
|
||||
# 3.0.0 - *2013-04-07*
|
||||
|
||||
**Major rewrite of 4chan X.**
|
||||
|
||||
@ -7,10 +13,16 @@ Header:
|
||||
- The board list can be customized.
|
||||
- The Header can be automatically hidden.
|
||||
|
||||
Extension-related changes for Chrome and Opera:
|
||||
- Installing and updating is now pain-free on Chrome.
|
||||
- Settings will persist on different subdomains and protocols (HTTP/HTTPS).
|
||||
- Settings will persist in Incognito on Chrome.
|
||||
- Clearing your cookies won't erase your settings anymore.
|
||||
- Fixed Chrome's install warning saying that 4chan X would run on all web sites.
|
||||
|
||||
Egocentrism:
|
||||
- `(You)` will be added to quotes linking to your posts.
|
||||
- The Unread tab icon will indicate new unread posts quoting you with an exclamation mark.
|
||||
- Delete links in the post menu will only appear for your posts.
|
||||
|
||||
Quick Reply changes:
|
||||
- Opening text files will insert their content in the comment field.
|
||||
@ -21,11 +33,12 @@ Quick Reply changes:
|
||||
- Closing the QR while uploading will abort the upload and won't close the QR anymore.
|
||||
- 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 support for thread creation in the catalog.
|
||||
- Added thumbnailing support for Opera.
|
||||
|
||||
Image Expansion changes:
|
||||
- The toggle and settings are now located in the Header's shortcuts and menu.
|
||||
- There is now a setting to allow expanding spoilers.
|
||||
- Expanding spoilers along with all non-spoiler images is now optional, and disabled by default.
|
||||
- Expanding OP images won't squish replies anymore.
|
||||
|
||||
Thread Updater changes:
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
1. Make sure both your **browser** and **4chan X** are up to date.
|
||||
2. Disable your other extensions & scripts to identify conflicts.
|
||||
3. If your issue persists, open a [new issue](https://github.com/MayhemYDG/4chan-x/issues) with the following information:
|
||||
1. Precise steps to reproduce the problem.
|
||||
1. Precise steps to reproduce the problem, with the expected and actual results.
|
||||
2. Console errors, if any.
|
||||
3. Browser version.
|
||||
4. Your exported settings.
|
||||
@ -21,7 +21,7 @@ Open your console with:
|
||||
- Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`.
|
||||
- Clone 4chan X.
|
||||
- `cd` into it.
|
||||
- Install 4chan X dependencies with `npm install`.
|
||||
- Install/Update 4chan X dependencies with `npm install`.
|
||||
|
||||
### Build
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ module.exports = (grunt) ->
|
||||
'src/features.coffee'
|
||||
'src/qr.coffee'
|
||||
'src/report.coffee'
|
||||
'src/databoard.coffee'
|
||||
'src/main.coffee'
|
||||
]
|
||||
dest: 'tmp/script.coffee'
|
||||
@ -75,10 +76,10 @@ module.exports = (grunt) ->
|
||||
command: ->
|
||||
release = "#{pkg.meta.name} v#{pkg.version}"
|
||||
return [
|
||||
"git checkout #{pkg.meta.mainBranch}"
|
||||
"git commit -am 'Release #{release}.'"
|
||||
"git tag -a #{pkg.version} -m '#{release}.'"
|
||||
"git tag -af stable -m '#{release}.'"
|
||||
'git checkout ' + pkg.meta.mainBranch,
|
||||
'git commit -am "Release ' + release + '."',
|
||||
'git tag -a ' + pkg.version + ' -m "' + release + '."',
|
||||
'git tag -af stable-v3 -m "' + release + '."'
|
||||
].join(' && ');
|
||||
stdout: true
|
||||
|
||||
|
||||
2002
appchan-x.user.js
2002
appchan-x.user.js
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,18 @@ master
|
||||
GIF thumbnail replacement, unlike Auto-GIF, actually works in /gif/ and /wsg/.
|
||||
Various little performance and readability tweaks.
|
||||
|
||||
2.39.3
|
||||
- Mayhem
|
||||
Add /fa/ and /s4s/ archive redirection.
|
||||
|
||||
2.39.2
|
||||
- Mayhem
|
||||
Fix importing settings containing unicode characters.
|
||||
|
||||
2.39.1
|
||||
- Mayhem
|
||||
Add /gd/, /out/, /vp/ and /vr/ archive redirection.
|
||||
|
||||
2.39.0
|
||||
- Queue
|
||||
Fix rare bug in Relative Post Dates.
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
margin: 0;
|
||||
padding: 2px 4px 3px;
|
||||
outline: none;
|
||||
-webkit-transition: color .25s, border-color .25s, -webkit-flex .25s;
|
||||
transition: color .25s, border-color .25s, -webkit-flex .25s;
|
||||
transition: color .25s, border-color .25s, flex .25s;
|
||||
}
|
||||
.field::-moz-placeholder,
|
||||
@ -107,7 +107,6 @@ a[href="javascript:;"] {
|
||||
display: flex;
|
||||
padding: 3px 4px 4px;
|
||||
position: relative;
|
||||
-webkit-transition: all .1s .05s ease-in-out;
|
||||
transition: all .1s .05s ease-in-out;
|
||||
}
|
||||
#board-list {
|
||||
@ -120,7 +119,6 @@ a[href="javascript:;"] {
|
||||
margin-bottom: -1em;
|
||||
-webkit-transform: translateY(-100%);
|
||||
transform: translateY(-100%);
|
||||
-webkit-transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
|
||||
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
|
||||
}
|
||||
#toggle-header-bar {
|
||||
@ -175,7 +173,6 @@ a[href="javascript:;"] {
|
||||
width: 500px;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
-webkit-transition: all .25s ease-in-out;
|
||||
transition: all .25s ease-in-out;
|
||||
}
|
||||
.notification.error {
|
||||
@ -346,6 +343,9 @@ a[href="javascript:;"] {
|
||||
#updater:not(:hover) > div:not(.move) {
|
||||
display: none;
|
||||
}
|
||||
#updater input[type="button"] {
|
||||
width: 100%;
|
||||
}
|
||||
.new {
|
||||
color: limegreen;
|
||||
}
|
||||
@ -495,8 +495,9 @@ a[href="javascript:;"] {
|
||||
}
|
||||
|
||||
/* QR */
|
||||
.hide-original-post-form #postForm,
|
||||
.hide-original-post-form .postingMode,
|
||||
:root.hide-original-post-form #postForm,
|
||||
:root.hide-original-post-form .postingMode,
|
||||
:root.hide-original-post-form #togglePostForm,
|
||||
#qr.autohide:not(:hover) > form {
|
||||
display: none;
|
||||
}
|
||||
@ -545,11 +546,10 @@ a[href="javascript:;"] {
|
||||
flex: 1;
|
||||
}
|
||||
.persona .field:focus {
|
||||
-webkit-flex: 4;
|
||||
flex: 4;
|
||||
-webkit-flex: 3;
|
||||
flex: 3;
|
||||
}
|
||||
#dump-button {
|
||||
background: -webkit-linear-gradient(#EEE, #CCC);
|
||||
background: linear-gradient(#EEE, #CCC);
|
||||
border: 1px solid #CCC;
|
||||
margin: 0;
|
||||
@ -558,11 +558,9 @@ a[href="javascript:;"] {
|
||||
width: 30px;
|
||||
}
|
||||
#dump-button:hover, #dump-button:focus {
|
||||
background: -webkit-linear-gradient(#FFF, #DDD);
|
||||
background: linear-gradient(#FFF, #DDD);
|
||||
}
|
||||
#dump-button:active, .dump #dump-button:not(:hover):not(:focus) {
|
||||
background: -webkit-linear-gradient(#CCC, #DDD);
|
||||
background: linear-gradient(#CCC, #DDD);
|
||||
}
|
||||
.gecko #dump-button {
|
||||
@ -614,7 +612,6 @@ a[href="javascript:;"] {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-shadow: 0 1px 1px #000;
|
||||
-webkit-transition: opacity .25s ease-in-out;
|
||||
transition: opacity .25s ease-in-out;
|
||||
vertical-align: top;
|
||||
white-space: pre;
|
||||
|
||||
137
lib/$.coffee
137
lib/$.coffee
@ -62,11 +62,6 @@ $.extend $,
|
||||
$.off d, 'DOMContentLoaded', cb
|
||||
fc()
|
||||
$.on d, 'DOMContentLoaded', cb
|
||||
sync: (key, cb) ->
|
||||
key = "#{g.NAMESPACE}#{key}"
|
||||
$.on window, 'storage', (e) ->
|
||||
if e.key is key
|
||||
cb JSON.parse e.newValue
|
||||
formData: (form) ->
|
||||
if form instanceof HTMLFormElement
|
||||
return new FormData form
|
||||
@ -81,15 +76,15 @@ $.extend $,
|
||||
fd.append key, val
|
||||
fd
|
||||
ajax: (url, callbacks, opts={}) ->
|
||||
{type, headers, upCallbacks, form} = opts
|
||||
{type, cred, headers, upCallbacks, form, sync} = opts
|
||||
r = new XMLHttpRequest()
|
||||
type or= form and 'post' or 'get'
|
||||
r.open type, url, true
|
||||
r.open type, url, !sync
|
||||
for key, val of headers
|
||||
r.setRequestHeader key, val
|
||||
$.extend r, callbacks
|
||||
$.extend r.upload, upCallbacks
|
||||
r.withCredentials = type is 'post'
|
||||
r.withCredentials = cred
|
||||
r.send form
|
||||
r
|
||||
cache: do ->
|
||||
@ -101,12 +96,13 @@ $.extend $,
|
||||
else
|
||||
req.callbacks.push cb
|
||||
return
|
||||
rm = -> delete reqs[url]
|
||||
req = $.ajax url,
|
||||
onload: ->
|
||||
cb.call @ for cb in @callbacks
|
||||
onload: (e) ->
|
||||
cb.call @, e for cb in @callbacks
|
||||
delete @callbacks
|
||||
onabort: -> delete reqs[url]
|
||||
onerror: -> delete reqs[url]
|
||||
onabort: rm
|
||||
onerror: rm
|
||||
req.callbacks = [cb]
|
||||
reqs[url] = req
|
||||
cb:
|
||||
@ -244,19 +240,43 @@ $.extend $,
|
||||
# Round to an integer otherwise.
|
||||
Math.round size
|
||||
"#{size} #{['B', 'KB', 'MB', 'GB'][unit]}"
|
||||
|
||||
syncing: {}
|
||||
sync: do ->
|
||||
<% if (type === 'crx') { %>
|
||||
delete: (keys) ->
|
||||
chrome.storage.sync.remove keys
|
||||
get: (key, defaultVal) ->
|
||||
if val = localStorage.getItem g.NAMESPACE + key
|
||||
JSON.parse val
|
||||
else
|
||||
defaultVal
|
||||
set: (key, val) ->
|
||||
chrome.storage.onChanged.addListener (changes) ->
|
||||
for key of changes
|
||||
if cb = $.syncing[key]
|
||||
cb changes[key].newValue
|
||||
return
|
||||
(key, cb) -> $.syncing[key] = cb
|
||||
<% } else { %>
|
||||
window.addEventListener 'storage', (e) ->
|
||||
if cb = $.syncing[e.key]
|
||||
cb JSON.parse e.newValue
|
||||
, false
|
||||
(key, cb) -> $.syncing[g.NAMESPACE + key] = cb
|
||||
<% } %>
|
||||
item: (key, val) ->
|
||||
item = {}
|
||||
item[key] = val
|
||||
chrome.storage.sync.set item
|
||||
item
|
||||
<% if (type === 'crx') { %>
|
||||
# https://developer.chrome.com/extensions/storage.html
|
||||
delete: (keys) ->
|
||||
chrome.storage.sync.remove keys
|
||||
get: (key, val, cb) ->
|
||||
if typeof cb is 'function'
|
||||
items = $.item key, val
|
||||
else
|
||||
items = key
|
||||
cb = val
|
||||
chrome.storage.sync.get items, cb
|
||||
set: (key, val) ->
|
||||
items = if typeof key is 'string'
|
||||
$.item key, val
|
||||
else
|
||||
key
|
||||
chrome.storage.sync.set items
|
||||
<% } else if (type === 'userjs') { %>
|
||||
do ->
|
||||
# http://www.opera.com/docs/userjs/specs/#scriptstorage
|
||||
@ -275,19 +295,35 @@ do ->
|
||||
localStorage.removeItem key
|
||||
delete scriptStorage[key]
|
||||
return
|
||||
$.get = (key, defaultVal) ->
|
||||
if val = scriptStorage[g.NAMESPACE + key]
|
||||
JSON.parse val
|
||||
$.get = (key, val, cb) ->
|
||||
if typeof cb is 'function'
|
||||
items = $.item key, val
|
||||
else
|
||||
defaultVal
|
||||
$.set = (key, val) ->
|
||||
key = g.NAMESPACE + key
|
||||
val = JSON.stringify val
|
||||
# for `storage` events
|
||||
localStorage.setItem key, val
|
||||
scriptStorage[key] = val
|
||||
items = key
|
||||
cb = val
|
||||
$.queueTask ->
|
||||
for key of items
|
||||
if val = scriptStorage[g.NAMESPACE + key]
|
||||
items[key] = JSON.parse val
|
||||
cb items
|
||||
$.set = do ->
|
||||
set = (key, val) ->
|
||||
key = g.NAMESPACE + key
|
||||
val = JSON.stringify val
|
||||
if key of $.syncing
|
||||
# for `storage` events
|
||||
localStorage.setItem key, val
|
||||
scriptStorage[key] = val
|
||||
(keys, val) ->
|
||||
if typeof keys is 'string'
|
||||
set keys, val
|
||||
return
|
||||
for key, val of keys
|
||||
set key, val
|
||||
return
|
||||
<% } else { %>
|
||||
delete: (key) ->
|
||||
# http://wiki.greasespot.net/Main_Page
|
||||
delete: (keys) ->
|
||||
unless keys instanceof Array
|
||||
keys = [keys]
|
||||
for key in keys
|
||||
@ -295,15 +331,30 @@ do ->
|
||||
localStorage.removeItem key
|
||||
GM_deleteValue key
|
||||
return
|
||||
get: (key, defaultVal) ->
|
||||
if val = GM_getValue g.NAMESPACE + key
|
||||
JSON.parse val
|
||||
get: (key, val, cb) ->
|
||||
if typeof cb is 'function'
|
||||
items = $.item key, val
|
||||
else
|
||||
defaultVal
|
||||
set: (key, val) ->
|
||||
key = g.NAMESPACE + key
|
||||
val = JSON.stringify val
|
||||
# for `storage` events
|
||||
localStorage.setItem key, val
|
||||
GM_setValue key, val
|
||||
items = key
|
||||
cb = val
|
||||
$.queueTask ->
|
||||
for key of items
|
||||
if val = GM_getValue g.NAMESPACE + key
|
||||
items[key] = JSON.parse val
|
||||
cb items
|
||||
set: do ->
|
||||
set = (key, val) ->
|
||||
key = g.NAMESPACE + key
|
||||
val = JSON.stringify val
|
||||
if key of $.syncing
|
||||
# for `storage` events
|
||||
localStorage.setItem key, val
|
||||
GM_setValue key, val
|
||||
(keys, val) ->
|
||||
if typeof keys is 'string'
|
||||
set keys, val
|
||||
return
|
||||
for key, val of keys
|
||||
set key, val
|
||||
return
|
||||
<% } %>
|
||||
|
||||
147
lib/ui.coffee
147
lib/ui.coffee
@ -1,13 +1,12 @@
|
||||
UI = do ->
|
||||
dialog = (id, position, html) ->
|
||||
el = d.createElement 'div'
|
||||
el.className = 'dialog'
|
||||
el.innerHTML = html
|
||||
el.id = id
|
||||
el = $.el 'div',
|
||||
className: 'dialog'
|
||||
innerHTML: html
|
||||
id: id
|
||||
el.style.cssText = localStorage.getItem("#{g.NAMESPACE}#{id}.position") or position
|
||||
move = el.querySelector '.move'
|
||||
move.addEventListener 'touchstart', dragstart, false
|
||||
move.addEventListener 'mousedown', dragstart, false
|
||||
move = $ '.move', el
|
||||
$.on move, 'touchstart mousedown', dragstart
|
||||
el
|
||||
|
||||
|
||||
@ -103,8 +102,7 @@ UI = do ->
|
||||
$.rm currentMenu
|
||||
currentMenu = null
|
||||
lastToggledButton = null
|
||||
$.off d, 'click', @close
|
||||
$.off d, 'CloseMenu', @close
|
||||
$.off d, 'click CloseMenu', @close
|
||||
|
||||
findNextEntry: (entry, direction) ->
|
||||
entries = [entry.parentNode.children...]
|
||||
@ -156,18 +154,14 @@ UI = do ->
|
||||
eRect = entry.getBoundingClientRect()
|
||||
cHeight = doc.clientHeight
|
||||
cWidth = doc.clientWidth
|
||||
if eRect.top + sRect.height < cHeight
|
||||
top = '0px'
|
||||
bottom = 'auto'
|
||||
[top, bottom] = if eRect.top + sRect.height < cHeight
|
||||
['0px', 'auto']
|
||||
else
|
||||
top = 'auto'
|
||||
bottom = '0px'
|
||||
if eRect.right + sRect.width < cWidth
|
||||
left = '100%'
|
||||
right = 'auto'
|
||||
['auto', '0px']
|
||||
[left, right] = if eRect.right + sRect.width < cWidth
|
||||
['100%', 'auto']
|
||||
else
|
||||
left = 'auto'
|
||||
right = '100%'
|
||||
['auto', '100%']
|
||||
{style} = submenu
|
||||
style.top = top
|
||||
style.bottom = bottom
|
||||
@ -197,14 +191,13 @@ UI = do ->
|
||||
|
||||
|
||||
dragstart = (e) ->
|
||||
if e.type is 'mousedown' and e.button isnt 0 # not LMB
|
||||
return
|
||||
return if e.type is 'mousedown' and e.button isnt 0 # not LMB
|
||||
# prevent text selection
|
||||
e.preventDefault()
|
||||
el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @
|
||||
if isTouching = e.type is 'touchstart'
|
||||
e = e.changedTouches[e.changedTouches.length - 1]
|
||||
# distance from pointer to el edge is constant; calculate it here.
|
||||
el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @
|
||||
rect = el.getBoundingClientRect()
|
||||
screenHeight = doc.clientHeight
|
||||
screenWidth = doc.clientWidth
|
||||
@ -223,14 +216,13 @@ UI = do ->
|
||||
o.identifier = e.identifier
|
||||
o.move = touchmove.bind o
|
||||
o.up = touchend.bind o
|
||||
d.addEventListener 'touchmove', o.move, false
|
||||
d.addEventListener 'touchend', o.up, false
|
||||
d.addEventListener 'touchcancel', o.up, false
|
||||
$.on d, 'touchmove', o.move
|
||||
$.on d, 'touchend touchcancel', o.up
|
||||
else # mousedown
|
||||
o.move = drag.bind o
|
||||
o.up = dragend.bind o
|
||||
d.addEventListener 'mousemove', o.move, false
|
||||
d.addEventListener 'mouseup', o.up, false
|
||||
$.on d, 'mousemove', o.move
|
||||
$.on d, 'mouseup', o.up
|
||||
touchmove = (e) ->
|
||||
for touch in e.changedTouches
|
||||
if touch.identifier is @identifier
|
||||
@ -240,33 +232,29 @@ UI = do ->
|
||||
{clientX, clientY} = e
|
||||
|
||||
left = clientX - @dx
|
||||
left =
|
||||
if left < 10
|
||||
0
|
||||
else if @width - left < 10
|
||||
null
|
||||
else
|
||||
left / @screenWidth * 100 + '%'
|
||||
left = if left < 10
|
||||
0
|
||||
else if @width - left < 10
|
||||
null
|
||||
else
|
||||
left / @screenWidth * 100 + '%'
|
||||
|
||||
top = clientY - @dy
|
||||
top =
|
||||
if top < 10
|
||||
0
|
||||
else if @height - top < 10
|
||||
null
|
||||
else
|
||||
top / @screenHeight * 100 + '%'
|
||||
top = if top < 10
|
||||
0
|
||||
else if @height - top < 10
|
||||
null
|
||||
else
|
||||
top / @screenHeight * 100 + '%'
|
||||
|
||||
right =
|
||||
if left is null
|
||||
0
|
||||
else
|
||||
null
|
||||
bottom =
|
||||
if top is null
|
||||
0
|
||||
else
|
||||
null
|
||||
right = if left is null
|
||||
0
|
||||
else
|
||||
null
|
||||
bottom = if top is null
|
||||
0
|
||||
else
|
||||
null
|
||||
|
||||
{style} = @
|
||||
style.left = left
|
||||
@ -280,12 +268,11 @@ UI = do ->
|
||||
return
|
||||
dragend = ->
|
||||
if @isTouching
|
||||
d.removeEventListener 'touchmove', @move, false
|
||||
d.removeEventListener 'touchend', @up, false
|
||||
d.removeEventListener 'touchcancel', @up, false
|
||||
$.off d, 'touchmove', @move
|
||||
$.off d, 'touchend touchcancel', @up
|
||||
else # mouseup
|
||||
d.removeEventListener 'mousemove', @move, false
|
||||
d.removeEventListener 'mouseup', @up, false
|
||||
$.off d, 'mousemove', @move
|
||||
$.off d, 'mouseup', @up
|
||||
localStorage.setItem "#{g.NAMESPACE}#{@id}.position", @style.cssText
|
||||
|
||||
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb}) ->
|
||||
@ -294,7 +281,7 @@ UI = do ->
|
||||
el: el
|
||||
style: el.style
|
||||
cb: cb
|
||||
endEvents: endEvents.split ' '
|
||||
endEvents: endEvents
|
||||
latestEvent: latestEvent
|
||||
clientHeight: doc.clientHeight
|
||||
clientWidth: doc.clientWidth
|
||||
@ -302,47 +289,39 @@ UI = do ->
|
||||
o.hover = hover.bind o
|
||||
o.hoverend = hoverend.bind o
|
||||
|
||||
asap = ->
|
||||
if asapTest()
|
||||
o.hover o.latestEvent
|
||||
else
|
||||
o.timeout = setTimeout asap, 25
|
||||
asap()
|
||||
$.asap ->
|
||||
!el.parentNode or asapTest()
|
||||
, ->
|
||||
o.hover o.latestEvent if el.parentNode
|
||||
|
||||
for event in o.endEvents
|
||||
root.addEventListener event, o.hoverend, false
|
||||
root.addEventListener 'mousemove', o.hover, false
|
||||
$.on root, endEvents, o.hoverend
|
||||
$.on root, 'mousemove', o.hover
|
||||
hover = (e) ->
|
||||
@latestEvent = e
|
||||
height = @el.offsetHeight
|
||||
{clientX, clientY} = e
|
||||
|
||||
top = clientY - 120
|
||||
top =
|
||||
if @clientHeight <= height or top <= 0
|
||||
0
|
||||
else if top + height >= @clientHeight
|
||||
@clientHeight - height
|
||||
else
|
||||
top
|
||||
|
||||
if clientX <= @clientWidth - 400
|
||||
left = clientX + 45 + 'px'
|
||||
right = null
|
||||
top = if @clientHeight <= height or top <= 0
|
||||
0
|
||||
else if top + height >= @clientHeight
|
||||
@clientHeight - height
|
||||
else
|
||||
left = null
|
||||
right = @clientWidth - clientX + 45 + 'px'
|
||||
top
|
||||
|
||||
[left, right] = if clientX <= @clientWidth - 400
|
||||
[clientX + 45 + 'px', null]
|
||||
else
|
||||
[null, @clientWidth - clientX + 45 + 'px']
|
||||
|
||||
{style} = @
|
||||
style.top = top + 'px'
|
||||
style.left = left
|
||||
style.right = right
|
||||
hoverend = ->
|
||||
@el.parentNode.removeChild @el
|
||||
for event in @endEvents
|
||||
@root.removeEventListener event, @hoverend, false
|
||||
@root.removeEventListener 'mousemove', @hover, false
|
||||
clearTimeout @timeout
|
||||
$.rm @el
|
||||
$.off @root, @endEvents, @hoverend
|
||||
$.off @root, 'mousemove', @hover
|
||||
@cb.call @ if @cb
|
||||
|
||||
|
||||
|
||||
@ -20,10 +20,10 @@
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-bump": "~0.0.0",
|
||||
"grunt-contrib-clean": "~0.4.0",
|
||||
"grunt-contrib-coffee": "~0.6.4",
|
||||
"grunt-contrib-compress": "~0.4.5",
|
||||
"grunt-contrib-coffee": "~0.6.5",
|
||||
"grunt-contrib-compress": "~0.4.7",
|
||||
"grunt-contrib-concat": "~0.1.3",
|
||||
"grunt-contrib-copy": "~0.4.0",
|
||||
"grunt-contrib-copy": "~0.4.1",
|
||||
"grunt-contrib-watch": "~0.3.1",
|
||||
"grunt-exec": "~0.4.0"
|
||||
},
|
||||
|
||||
@ -49,6 +49,10 @@ Config =
|
||||
false
|
||||
'Add buttons to navigate between threads.'
|
||||
]
|
||||
'Reply Navigation': [
|
||||
false
|
||||
'Add buttons to navigate to top / bottom of thread.'
|
||||
]
|
||||
'Check for Updates': [
|
||||
true
|
||||
'Check for updated versions of <%= meta.name %>.'
|
||||
@ -232,7 +236,7 @@ Config =
|
||||
'Add quote backlinks.'
|
||||
]
|
||||
'OP Backlinks': [
|
||||
false
|
||||
true
|
||||
'Add backlinks to the OP.'
|
||||
]
|
||||
'Quote Inlining': [
|
||||
@ -685,8 +689,8 @@ Config =
|
||||
MD5: ''
|
||||
|
||||
sauces: """
|
||||
http://iqdb.org/?url=%TURL
|
||||
https://www.google.com/searchbyimage?image_url=%TURL
|
||||
http://iqdb.org/?url=%TURL
|
||||
#//tineye.com/search?url=%TURL
|
||||
#http://saucenao.com/search.php?url=%TURL
|
||||
#http://3d.iqdb.org/?url=%TURL
|
||||
@ -829,6 +833,7 @@ https://www.google.com/searchbyimage?image_url=%TURL
|
||||
'x'
|
||||
'Hide thread.'
|
||||
]
|
||||
|
||||
updater:
|
||||
checkbox:
|
||||
'Beep': [
|
||||
|
||||
88
src/databoard.coffee
Normal file
88
src/databoard.coffee
Normal file
@ -0,0 +1,88 @@
|
||||
DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts']
|
||||
|
||||
class DataBoard
|
||||
constructor: (@key, sync) ->
|
||||
@data = Conf[key]
|
||||
$.sync key, @onSync.bind @
|
||||
@clean()
|
||||
return unless sync
|
||||
# Chrome also fires the onChanged callback on the current tab,
|
||||
# so we only start syncing when we're ready.
|
||||
$.on d, '4chanXInitFinished', => @sync = sync
|
||||
|
||||
delete: ({boardID, threadID, postID}) ->
|
||||
if postID
|
||||
delete @data.boards[boardID][threadID][postID]
|
||||
@deleteIfEmpty {boardID, threadID}
|
||||
else if threadID
|
||||
delete @data.boards[boardID][threadID]
|
||||
@deleteIfEmpty {boardID}
|
||||
else
|
||||
delete @data.boards[boardID]
|
||||
$.set @key, @data
|
||||
|
||||
deleteIfEmpty: ({boardID, threadID}) ->
|
||||
if threadID
|
||||
unless Object.keys(@data.boards[boardID][threadID]).length
|
||||
delete @data.boards[boardID][threadID]
|
||||
@deleteIfEmpty {boardID}
|
||||
else unless Object.keys(@data.boards[boardID]).length
|
||||
delete @data.boards[boardID]
|
||||
|
||||
set: ({boardID, threadID, postID, val}) ->
|
||||
if postID
|
||||
((@data.boards[boardID] or= {})[threadID] or= {})[postID] = val
|
||||
else if threadID
|
||||
(@data.boards[boardID] or= {})[threadID] = val
|
||||
else
|
||||
@data.boards[boardID] = val
|
||||
$.set @key, @data
|
||||
|
||||
get: ({boardID, threadID, postID, defaultValue}) ->
|
||||
if board = @data.boards[boardID]
|
||||
unless threadID
|
||||
if postID
|
||||
for ID, thread in board
|
||||
if postID of thread
|
||||
val = thread[postID]
|
||||
break
|
||||
else
|
||||
val = board
|
||||
else if thread = board[threadID]
|
||||
val = if postID
|
||||
thread[postID]
|
||||
else
|
||||
thread
|
||||
val or defaultValue
|
||||
|
||||
clean: ->
|
||||
for boardID of @data.boards
|
||||
@deleteIfEmpty {boardID}
|
||||
|
||||
now = Date.now()
|
||||
if (@data.lastChecked or 0) < now - 12 * $.HOUR
|
||||
@data.lastChecked = now
|
||||
for boardID of @data.boards
|
||||
@ajaxClean boardID
|
||||
|
||||
$.set @key, @data
|
||||
|
||||
ajaxClean: (boardID) ->
|
||||
$.cache "//api.4chan.org/#{boardID}/threads.json", (e) =>
|
||||
if e.target.status is 404
|
||||
# Deleted board.
|
||||
@delete boardID
|
||||
else if e.target.status is 200
|
||||
board = @data.boards[boardID]
|
||||
threads = {}
|
||||
for page in JSON.parse e.target.response
|
||||
for thread in page.threads
|
||||
if thread.no of board
|
||||
threads[thread.no] = board[thread.no]
|
||||
@data.boards[boardID] = threads
|
||||
@deleteIfEmpty {boardID}
|
||||
$.set @key, @data
|
||||
|
||||
onSync: (data) ->
|
||||
@data = data or boards: {}
|
||||
@sync?()
|
||||
1065
src/features.coffee
1065
src/features.coffee
File diff suppressed because it is too large
Load Diff
107
src/main.coffee
107
src/main.coffee
@ -16,7 +16,7 @@ class Thread
|
||||
@fullID = "#{@board}.#{@ID}"
|
||||
@posts = {}
|
||||
|
||||
g.threads["#{board}.#{@}"] = board.threads[@] = @
|
||||
g.threads[@fullID] = board.threads[@] = @
|
||||
|
||||
kill: ->
|
||||
@isDead = true
|
||||
@ -66,7 +66,11 @@ class Post
|
||||
@nodes.date = date
|
||||
@info.date = new Date date.dataset.utc * 1000
|
||||
if Conf['Quick Reply']
|
||||
@info.yours = !!QR.yourPosts.threads[@thread.ID]?.contains(@ID)
|
||||
@info.yours = QR.db.get
|
||||
boardID: @board
|
||||
threadID: @thread
|
||||
postID: @ID
|
||||
|
||||
|
||||
@parseComment()
|
||||
@parseQuotes()
|
||||
@ -109,7 +113,7 @@ class Post
|
||||
@thread.isClosed = !!$ '.closedIcon', @nodes.info
|
||||
|
||||
@clones = []
|
||||
g.posts["#{board}.#{@}"] = thread.posts[@] = board.posts[@] = @
|
||||
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
|
||||
@kill() if that.isArchived
|
||||
|
||||
parseComment: ->
|
||||
@ -160,10 +164,12 @@ class Post
|
||||
kill: (file, now) ->
|
||||
now or= new Date()
|
||||
if file
|
||||
return if @file.isDead
|
||||
@file.isDead = true
|
||||
@file.timeOfDeath = now
|
||||
$.addClass @nodes.root, 'deleted-file'
|
||||
else
|
||||
return if @isDead
|
||||
@isDead = true
|
||||
@timeOfDeath = now
|
||||
$.addClass @nodes.root, 'deleted-post'
|
||||
@ -283,7 +289,7 @@ class Clone extends Post
|
||||
|
||||
|
||||
Main =
|
||||
init: ->
|
||||
init: (items) ->
|
||||
# flatten Config into Conf
|
||||
# and get saved or default values
|
||||
flatten = (parent, obj) ->
|
||||
@ -296,8 +302,14 @@ Main =
|
||||
Conf[parent] = obj
|
||||
return
|
||||
flatten null, Config
|
||||
for key, val of Conf
|
||||
Conf[key] = $.get key, val
|
||||
for db in DataBoards
|
||||
Conf[db] = boards: {}
|
||||
$.get Conf, Main.initFeatures
|
||||
|
||||
$.on d, '4chanMainInit', Main.initStyle
|
||||
|
||||
initFeatures: (items) ->
|
||||
Conf = items
|
||||
|
||||
pathname = location.pathname.split '/'
|
||||
g.BOARD = new Board pathname[1]
|
||||
@ -310,7 +322,7 @@ Main =
|
||||
else
|
||||
'index'
|
||||
if g.VIEW is 'thread'
|
||||
g.THREAD = +pathname[3]
|
||||
g.THREADID = +pathname[3]
|
||||
|
||||
# Check if the current board we're on is SFW or not, so we can handle options that need to know that.
|
||||
if ['b', 'd', 'e', 'gif', 'h', 'hc', 'hm', 'hr', 'pol', 'r', 'r9k', 'rs', 's', 'soc', 't', 'u', 'y'].contains g.BOARD
|
||||
@ -351,6 +363,7 @@ Main =
|
||||
return
|
||||
|
||||
# c.time 'All initializations'
|
||||
|
||||
initFeatures
|
||||
'Polyfill': Polyfill
|
||||
'Emoji': Emoji
|
||||
@ -367,14 +380,14 @@ Main =
|
||||
'Resurrect Quotes': Quotify
|
||||
'Filter': Filter
|
||||
'Thread Hiding': ThreadHiding
|
||||
'Reply Hiding': ReplyHiding
|
||||
'Reply Hiding': PostHiding
|
||||
'Recursive': Recursive
|
||||
'Strike-through Quotes': QuoteStrikeThrough
|
||||
'Quick Reply': QR
|
||||
'Menu': Menu
|
||||
'Report Link': ReportLink
|
||||
'Thread Hiding (Menu)': ThreadHiding.menu
|
||||
'Reply Hiding (Menu)': ReplyHiding.menu
|
||||
'Reply Hiding (Menu)': PostHiding.menu
|
||||
'Delete Link': DeleteLink
|
||||
'Filter (Menu)': Filter.menu
|
||||
'Download Link': DownloadLink
|
||||
@ -391,6 +404,7 @@ Main =
|
||||
'File Info Formatting': FileInfo
|
||||
'Sauce': Sauce
|
||||
'Image Expansion': ImageExpand
|
||||
'Image Expansion (Menu)': ImageExpand.menu
|
||||
'Reveal Spoilers': RevealSpoilers
|
||||
'Image Replace': ImageReplace
|
||||
'Image Hover': ImageHover
|
||||
@ -404,6 +418,7 @@ Main =
|
||||
'Thread Watcher': ThreadWatcher
|
||||
'Index Navigation': Nav
|
||||
'Keybinds': Keybinds
|
||||
|
||||
# c.timeEnd 'All initializations'
|
||||
|
||||
$.on d, 'AddCallback', Main.addCallback
|
||||
@ -413,9 +428,9 @@ Main =
|
||||
if d.title is '4chan - 404 Not Found'
|
||||
if Conf['404 Redirect'] and g.VIEW is 'thread'
|
||||
href = Redirect.to
|
||||
board: g.BOARD
|
||||
threadID: g.THREAD
|
||||
postID: location.hash
|
||||
boardID: g.BOARD.ID
|
||||
threadID: g.THREADID
|
||||
postID: +location.hash.match /\d+/ # post number or 0
|
||||
location.href = href or "/#{g.BOARD}/"
|
||||
return
|
||||
|
||||
@ -479,30 +494,34 @@ Main =
|
||||
Klass::callbacks.push obj.callback
|
||||
|
||||
checkUpdate: ->
|
||||
return unless Main.isThisPageLegit()
|
||||
return unless Conf['Check for Updates'] and Main.isThisPageLegit()
|
||||
# Check for updates after:
|
||||
# - 6 hours since the last update on Opera because it lacks auto-updating.
|
||||
# - 7 days since the last update on Chrome/Firefox.
|
||||
# After that, check for updates every day if we still haven't updated.
|
||||
now = Date.now()
|
||||
freq = <% if (type === 'userjs') { %>6 * $.HOUR<% } else { %>7 * $.DAY<% } %>
|
||||
if $.get('lastupdate', 0) > now - freq or $.get('lastchecked', 0) > now - $.DAY
|
||||
return
|
||||
$.ajax '<%= meta.page %><%= meta.buildsPath %>version', onload: ->
|
||||
return unless @status is 200
|
||||
version = @response
|
||||
return unless /^\d\.\d+\.\d+$/.test version
|
||||
if g.VERSION is version
|
||||
# Don't check for updates too frequently if there wasn't one in a 'long' time.
|
||||
$.set 'lastupdate', now
|
||||
items =
|
||||
lastupdate: 0
|
||||
lastchecked: 0
|
||||
$.get items, (items) ->
|
||||
if items.lastupdate > now - freq or items.lastchecked > now - $.DAY
|
||||
return
|
||||
$.set 'lastchecked', now
|
||||
el = $.el 'span',
|
||||
innerHTML: "Update: <%= meta.name %> v#{version} is out, get it <a href=<%= meta.page %> target=_blank>here</a>."
|
||||
new Notification 'info', el, 2 * $.MINUTE
|
||||
$.ajax '<%= meta.page %><%= meta.buildsPath %>version', onload: ->
|
||||
return unless @status is 200
|
||||
version = @response
|
||||
return unless /^\d\.\d+\.\d+$/.test version
|
||||
if g.VERSION is version
|
||||
# Don't check for updates too frequently if there wasn't one in a 'long' time.
|
||||
$.set 'lastupdate', now
|
||||
return
|
||||
$.set 'lastchecked', now
|
||||
el = $.el 'span',
|
||||
innerHTML: "Update: <%= meta.name %> v#{version} is out, get it <a href=<%= meta.page %> target=_blank>here</a>."
|
||||
new Notification 'info', el, 120
|
||||
|
||||
handleErrors: (errors) ->
|
||||
unless 'length' of errors
|
||||
unless errors instanceof Array
|
||||
error = errors
|
||||
else if errors.length is 1
|
||||
error = errors[0]
|
||||
@ -513,12 +532,10 @@ Main =
|
||||
div = $.el 'div',
|
||||
innerHTML: "#{errors.length} errors occurred. [<a href=javascript:;>show</a>]"
|
||||
$.on div.lastElementChild, 'click', ->
|
||||
if @textContent is 'show'
|
||||
@textContent = 'hide'
|
||||
logs.hidden = false
|
||||
[@textContent, logs.hidden] = if @textContent is 'show'
|
||||
['hide', false]
|
||||
else
|
||||
@textContent = 'show'
|
||||
logs.hidden = true
|
||||
['show', true]
|
||||
|
||||
logs = $.el 'div',
|
||||
hidden: true
|
||||
@ -528,15 +545,31 @@ Main =
|
||||
new Notification 'error', [div, logs], 30
|
||||
|
||||
parseError: (data) ->
|
||||
{message, error} = data
|
||||
c.log message, error
|
||||
c.log message, error.stack
|
||||
Main.logError data
|
||||
message = $.el 'div',
|
||||
textContent: message
|
||||
textContent: data.message
|
||||
error = $.el 'div',
|
||||
textContent: error
|
||||
textContent: data.error
|
||||
[message, error]
|
||||
|
||||
errors: []
|
||||
logError: (data) ->
|
||||
unless Main.errors.length
|
||||
$.on window, 'unload', Main.postErrors
|
||||
c.error data.message, data.error.stack
|
||||
Main.errors.push data
|
||||
|
||||
postErrors: ->
|
||||
errors = Main.errors.map (d) -> d.message + ' ' + d.error.stack
|
||||
$.ajax '<%= meta.page %>errors', {},
|
||||
sync: true
|
||||
form: $.formData
|
||||
n: "<%= meta.name %> v#{g.VERSION}"
|
||||
t: '<%= type %>'
|
||||
ua: window.navigator.userAgent
|
||||
url: window.location.href
|
||||
e: errors.join '\n'
|
||||
|
||||
isThisPageLegit: ->
|
||||
# 404 error page or similar.
|
||||
unless 'thisPageIsLegit' of Main
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"run_at": "document_start"
|
||||
}],
|
||||
"homepage_url": "<%= meta.page %>",
|
||||
"minimum_chrome_version": "25",
|
||||
"minimum_chrome_version": "26",
|
||||
"permissions": [
|
||||
"storage"
|
||||
]
|
||||
|
||||
100
src/qr.coffee
100
src/qr.coffee
@ -1,9 +1,8 @@
|
||||
QR =
|
||||
init: ->
|
||||
return if g.VIEW is 'catalog' or !Conf['Quick Reply']
|
||||
return if !Conf['Quick Reply']
|
||||
|
||||
Misc.clearThreads "yourPosts.#{g.BOARD}"
|
||||
@syncYourPosts()
|
||||
@db = new DataBoard 'yourPosts'
|
||||
|
||||
sc = $.el 'a',
|
||||
className: "qr-shortcut #{unless Conf['Persistent QR'] then 'disabled' else ''}"
|
||||
@ -93,13 +92,6 @@ QR =
|
||||
else
|
||||
QR.unhide()
|
||||
|
||||
syncYourPosts: (yourPosts) ->
|
||||
if yourPosts
|
||||
QR.yourPosts = yourPosts
|
||||
return
|
||||
QR.yourPosts = $.get "yourPosts.#{g.BOARD}", threads: {}
|
||||
$.sync "yourPosts.#{g.BOARD}", QR.syncYourPosts
|
||||
|
||||
error: (err) ->
|
||||
QR.open()
|
||||
if typeof err is 'string'
|
||||
@ -150,10 +142,11 @@ QR =
|
||||
sage: if board is 'q' then 600 else 60
|
||||
file: if board is 'q' then 300 else 30
|
||||
post: if board is 'q' then 60 else 30
|
||||
QR.cooldown.cooldowns = $.get "cooldown.#{board}", {}
|
||||
QR.cooldown.upSpd = 0
|
||||
QR.cooldown.upSpdAccuracy = .5
|
||||
QR.cooldown.start()
|
||||
$.get "cooldown.#{board}", {}, (item) ->
|
||||
QR.cooldown.cooldowns = item["cooldown.#{board}"]
|
||||
QR.cooldown.start()
|
||||
$.sync "cooldown.#{board}", QR.cooldown.sync
|
||||
start: ->
|
||||
return if QR.cooldown.isCounting
|
||||
@ -356,22 +349,13 @@ QR =
|
||||
$.addClass QR.nodes.el, 'dump'
|
||||
resetThreadSelector: ->
|
||||
if g.VIEW is 'thread'
|
||||
QR.nodes.thread.value = g.THREAD
|
||||
QR.nodes.thread.value = g.THREADID
|
||||
else
|
||||
QR.nodes.thread.value = 'new'
|
||||
|
||||
posts: []
|
||||
post: class
|
||||
constructor: ->
|
||||
# set values, or null, to avoid 'undefined' values in inputs
|
||||
prev = QR.posts[QR.posts.length - 1]
|
||||
persona = $.get 'QR.persona', {}
|
||||
@name = if prev then prev.name else persona.name or null
|
||||
@email = if prev and !/^sage$/.test prev.email then prev.email else persona.email or null
|
||||
@sub = if prev and Conf['Remember Subject'] then prev.sub else if Conf['Remember Subject'] then persona.sub else null
|
||||
@spoiler = if prev and Conf['Remember Spoiler'] then prev.spoiler else false
|
||||
@com = null
|
||||
|
||||
constructor: (select) ->
|
||||
el = $.el 'a',
|
||||
className: 'qr-preview'
|
||||
draggable: true
|
||||
@ -398,13 +382,32 @@ QR =
|
||||
for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop']
|
||||
$.on el, event.toLowerCase(), @[event]
|
||||
|
||||
@unlock()
|
||||
prev = QR.posts[QR.posts.length - 1]
|
||||
QR.posts.push @
|
||||
@spoiler = if prev and Conf['Remember Spoiler']
|
||||
prev.spoiler
|
||||
else
|
||||
false
|
||||
$.get 'QR.persona', {}, (item) =>
|
||||
persona = item['QR.persona']
|
||||
@name = if prev
|
||||
prev.name
|
||||
else
|
||||
persona.name
|
||||
@email = if prev and !/^sage$/.test prev.email
|
||||
prev.email
|
||||
else
|
||||
persona.email
|
||||
if Conf['Remember Subject']
|
||||
@sub = if prev then prev.sub else persona.sub
|
||||
@load() if QR.selected is @ # load persona
|
||||
@select() if select
|
||||
@unlock()
|
||||
rm: ->
|
||||
$.rm @nodes.el
|
||||
index = QR.posts.indexOf @
|
||||
if QR.posts.length is 1
|
||||
new QR.post().select()
|
||||
new QR.post true
|
||||
else if @ is QR.selected
|
||||
(QR.posts[index-1] or QR.posts[index+1]).select()
|
||||
QR.posts.splice index, 1
|
||||
@ -433,9 +436,11 @@ QR =
|
||||
rectEl = @nodes.el.getBoundingClientRect()
|
||||
rectList = @nodes.el.parentNode.getBoundingClientRect()
|
||||
@nodes.el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2
|
||||
@load()
|
||||
load: ->
|
||||
# Load this post's values.
|
||||
for name in ['name', 'email', 'sub', 'com']
|
||||
QR.nodes[name].value = @[name]
|
||||
QR.nodes[name].value = @[name] or null
|
||||
@showFileData()
|
||||
QR.characterCount()
|
||||
save: (input) ->
|
||||
@ -586,7 +591,6 @@ QR =
|
||||
$.on window, 'captcha:timeout', setLifetime
|
||||
$.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
|
||||
$.off window, 'captcha:timeout', setLifetime
|
||||
c.log @lifetime
|
||||
|
||||
imgContainer = $.el 'div',
|
||||
className: 'captcha-img'
|
||||
@ -611,8 +615,9 @@ QR =
|
||||
|
||||
$.on imgContainer, 'click', @reload.bind @
|
||||
$.on input, 'keydown', @keydown.bind @
|
||||
$.get 'captchas', [], (item) =>
|
||||
@sync item['captchas']
|
||||
$.sync 'captchas', @sync
|
||||
@sync $.get 'captchas', []
|
||||
# start with an uncached captcha
|
||||
@reload()
|
||||
|
||||
@ -794,13 +799,13 @@ QR =
|
||||
$.on nodes.autohide, 'change', QR.toggleHide
|
||||
$.on nodes.close, 'click', QR.close
|
||||
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
|
||||
$.on nodes.addPost, 'click', -> new QR.post().select()
|
||||
$.on nodes.addPost, 'click', -> new QR.post true
|
||||
$.on nodes.form, 'submit', QR.submit
|
||||
$.on nodes.fileRM, 'click', -> QR.selected.rmFile()
|
||||
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
|
||||
$.on nodes.fileInput, 'change', QR.fileInput
|
||||
|
||||
new QR.post().select()
|
||||
new QR.post true
|
||||
# save selected post's data
|
||||
for name in ['name', 'email', 'sub', 'com']
|
||||
$.on nodes[name], 'input', -> QR.selected.save @
|
||||
@ -839,10 +844,12 @@ QR =
|
||||
err = 'New threads require a subject.'
|
||||
else unless post.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm'
|
||||
err = 'No file selected.'
|
||||
else if g.BOARD.threads[threadID].isSticky
|
||||
else if g.BOARD.threads[threadID].isClosed
|
||||
err = 'You can\'t reply to this thread anymore.'
|
||||
else unless post.com or post.file
|
||||
err = 'No file selected.'
|
||||
else if post.file and g.BOARD.threads[threadID].fileLimit
|
||||
err = 'Max limit of image replies has been reached.'
|
||||
|
||||
if QR.captcha.isEnabled and !err
|
||||
{challenge, response} = QR.captcha.getOne()
|
||||
@ -893,6 +900,7 @@ QR =
|
||||
QR.error $.el 'span',
|
||||
innerHTML: 'Connection error. You may have been <a href=//www.4chan.org/banned target=_blank>banned</a>.'
|
||||
opts =
|
||||
cred: true
|
||||
form: $.formData postData
|
||||
upCallbacks:
|
||||
onload: ->
|
||||
@ -965,21 +973,25 @@ QR =
|
||||
QR.cleanNotifications()
|
||||
QR.notifications.push new Notification 'success', h1.textContent, 5
|
||||
|
||||
persona = $.get 'QR.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 null
|
||||
$.set 'QR.persona', persona
|
||||
$.get 'QR.persona', {}, (item) ->
|
||||
persona = item['QR.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 null
|
||||
$.set 'QR.persona', persona
|
||||
|
||||
[_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/
|
||||
postID = +postID
|
||||
threadID = +threadID or postID
|
||||
isReply = threadID isnt postID
|
||||
|
||||
(QR.yourPosts.threads[threadID] or= []).push postID
|
||||
$.set "yourPosts.#{g.BOARD}", QR.yourPosts
|
||||
|
||||
QR.db.set
|
||||
boardID: g.BOARD.ID
|
||||
threadID: threadID
|
||||
postID: postID
|
||||
val: true
|
||||
|
||||
ThreadUpdater.postID = postID
|
||||
|
||||
# Post/upload confirmed as successful.
|
||||
@ -992,7 +1004,10 @@ QR =
|
||||
# Enable auto-posting if we have stuff to post, disable it otherwise.
|
||||
QR.cooldown.auto = QR.posts.length > 1 and isReply
|
||||
|
||||
post.rm()
|
||||
unless Conf['Persistent QR'] or QR.cooldown.auto
|
||||
QR.close()
|
||||
else
|
||||
post.rm()
|
||||
|
||||
QR.cooldown.set {req, post, isReply}
|
||||
|
||||
@ -1006,9 +1021,6 @@ QR =
|
||||
else
|
||||
window.location = "/#{g.BOARD}/res/#{threadID}"
|
||||
|
||||
unless Conf['Persistent QR'] or QR.cooldown.auto
|
||||
QR.close()
|
||||
|
||||
QR.status()
|
||||
|
||||
abort: ->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user