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:
Zixaphir 2013-04-08 07:50:39 -07:00
commit 0a5bd72be0
15 changed files with 2252 additions and 1476 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -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;

View File

@ -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
<% } %>

View File

@ -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

View File

@ -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"
},

View File

@ -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
View 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?()

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -14,7 +14,7 @@
"run_at": "document_start"
}],
"homepage_url": "<%= meta.page %>",
"minimum_chrome_version": "25",
"minimum_chrome_version": "26",
"permissions": [
"storage"
]

View File

@ -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: ->