Merge branch 'v3'

Conflicts:
	.gitignore
	CHANGELOG.md
	Gruntfile.coffee
	LICENSE
	builds/crx/manifest.json
	builds/crx/script.js
	latest.js
	package.json
	src/General/Config.coffee
	src/General/Globals.coffee
	src/General/Header.coffee
	src/General/Main.coffee
	src/General/UI.coffee
	src/General/css/style.css
	src/General/meta/manifest.json
	src/Posting/QuickReply.coffee
This commit is contained in:
Zixaphir 2013-07-21 09:01:27 -07:00
commit 16b35dffce
42 changed files with 2221 additions and 1241 deletions

9
.gitignore vendored
View File

@ -2,9 +2,14 @@ node_modules/
*~
*.db
tmp-crx/
tmp-userjs/
tmp-userscript/
<<<<<<< HEAD
builds/4chan-X.zip
Gruntfile.js
builds/4chan-*
Gruntfile.js
Gruntfile.js
=======
builds/4chan-X-Chrome.zip
builds/4chan-X-Opera.nex
Gruntfile.js
>>>>>>> v3

View File

@ -1,7 +1,10 @@
**MayhemYDG**:
- Remove /s4s/ from warosu archive
- Fix CAPTCHA duplication on the report page
- Small bug fixes
- Fix impossibility to create new threads when in dead threads.
- Drop Opera <15 support.
- Fix flag filtering on /sp/ and /int/.
- Minor fixes.
**seaweedchan**:
- Add `.active` class to `.menu-button` when clicked (and remove on menu close)
@ -9,6 +12,10 @@
- Revert Mayhem's updater changes which caused silly issues
- Rename `Indicate Spoilers` to `Reveal Spoilers`
- If `Reveal Spoilers` is enabled but `Remove Spoilers` is not, act as if the spoiler is hovered
- Add a new option to hide "4chan X has been updated to ____" notifications for those having issues with them.
- Update archives
- Add `.active` class to `.menu-button` when clicked (and remove on menu close)
- Move /v/ and /vg/ back to Foolz archive
**Tracerneo**:
- Add ID styling for IDs with black text

View File

@ -183,7 +183,6 @@ module.exports = (grunt) ->
grunt.registerTask 'release', [
'default'
'compress:crx'
'copy:opera'
'shell:commit'
'shell:push'
]

View File

@ -1,5 +1,5 @@
/*
* appchan x - Version 2.1.3 - 2013-07-07
* appchan x - Version 2.1.3 - 2013-07-21
*
* Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
// ==UserScript==
// @name 4chan X
// @version 1.2.17
// @version 1.2.19
// @namespace 4chan-X
// @description Cross-browser userscript for maximum lurking on 4chan.
// @license MIT; https://github.com/seaweedchan/4chan-x/blob/master/LICENSE

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
}],
"homepage_url": "http://zixaphir.github.com/appchan-x/",
"minimum_chrome_version": "24",
"minimum_opera_version": "15",
"permissions": [
"storage"
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
<div class="move" title="Post count / File count / Page count">
<span id="post-count">...</span> / <span id="file-count">...</span> / <span id="page-count">...</span>
</div>

38
html/Posting/QR.html Normal file
View File

@ -0,0 +1,38 @@
<div>
<input type="checkbox" id="autohide" title="Auto-hide">
<select data-name="thread" title="Create a new thread / Reply">
<option value="new">New thread</option>
</select>
<span class="move"></span>
<a href="javascript:;" class="close" title="Close">×</a>
</div>
<form>
<div class="persona">
<input type="button" id="dump-button" title="Dump list" value="+">
<input data-name="name" list="list-name" placeholder="Name" class="field" size="1">
<input data-name="email" list="list-email" placeholder="E-mail" class="field" size="1">
<input data-name="sub" list="list-sub" placeholder="Subject" class="field" size="1">
</div>
<div id="dump-list-container">
<div id="dump-list"></div>
<a id="add-post" href="javascript:;" title="Add a post">+</a>
</div>
<div class="textarea">
<textarea data-name="com" placeholder="Comment" class="field"></textarea>
<span id="char-count"></span>
</div>
<div id="file-n-submit">
<input type="submit">
<input type="button" id="qr-file-button" value="Choose files">
<span id="qr-filename-container">
<span id="qr-no-file">No selected file</span>
<span id="qr-filename"></span>
</span>
<a id="qr-filerm" href="javascript:;" title="Remove file">×</a>
<input type="checkbox" id="qr-file-spoiler" title="Spoiler image">
</div>
<input type="file" multiple hidden>
</form>
<datalist id="list-name"></datalist>
<datalist id="list-email"></datalist>
<datalist id="list-sub"></datalist>

View File

@ -20,15 +20,15 @@
},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-bump": "~0.0.2",
"grunt-concurrent": "~0.2.0",
"grunt-contrib-clean": "~0.4.1",
"grunt-bump": "~0.0.11",
"grunt-concurrent": "~0.3.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-coffee": "~0.7.0",
"grunt-contrib-compress": "~0.5.1",
"grunt-contrib-compress": "~0.5.2",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-watch": "~0.4.4",
"grunt-shell": "~0.2.2"
"grunt-contrib-watch": "~0.5.0",
"grunt-shell": "~0.3.1"
},
"repository": {
"type": "git",

View File

@ -21,7 +21,6 @@ Redirect =
Redirect.post[boardID] = archive
unless boardID of Redirect.file or !archive.files.contains boardID
Redirect.file[boardID] = archive
return
archives:
'Foolz':
@ -29,7 +28,7 @@ Redirect =
'http': false
'https': true
'software': 'foolfuuka'
'boards': ['a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'vp', 'vr', 'wsg']
'boards': ['a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'vg', 'vp', 'vr', 'wsg']
'files': ['a', 'gd', 'jp', 'm', 'q', 'tg', 'vg', 'vp', 'vr', 'wsg']
'NSFW Foolz':
@ -63,25 +62,17 @@ Redirect =
'boards': ['c', 'w', 'wg']
'files': ['c', 'w', 'wg']
'Love is Over':
'domain': 'loveisover.me'
'http': true
'https': true
'software': 'foolfuuka'
'boards': ['d', 'h', 'v']
'files': ['d', 'h', 'v']
'Foolz a Shit':
'domain': 'archive.foolzashit.com'
'http': true
'https': true
'software': 'foolfuuka'
'boards': ['adv', 'asp', 'cm', 'e', 'i', 'lgbt', 'n', 'o', 'p', 'pol', 's', 's4s', 't', 'trv', 'y']
'files': ['adv', 'asp', 'cm', 'e', 'i', 'lgbt', 'n', 'o', 'p', 's', 's4s', 't', 'trv', 'y']
'boards': ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv']
'files': ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv']
'Install Gentoo':
'domain': 'archive.installgentoo.net'
'http': true
'http': false
'https': true
'software': 'fuuka'
'boards': ['diy', 'g', 'sci']
@ -109,6 +100,14 @@ Redirect =
'software': 'fuuka'
'boards': ['3', 'cgl', 'ck', 'fa', 'ic', 'jp', 'lit', 'q', 'tg', 'vr']
'files': ['3', 'cgl', 'ck', 'fa', 'ic', 'jp', 'lit', 'q', 'vr']
'worldathleticproject':
'domain': 'fuuka.worldathleticproject.org'
'http': true
'https': true
'software': 'foolfuuka'
'boards': ['e', 'h', 'p', 's', 'u']
'files': ['e', 'h', 'p', 's', 'u']
to: (dest, data) ->
archive = (if dest is 'search' then Redirect.thread else Redirect[dest])[data.boardID]

View File

@ -209,7 +209,7 @@ Filter =
el = $.el 'a',
href: 'javascript:;'
textContent: text
el.setAttribute 'data-type', type
el.dataset.type = type
$.on el, 'click', Filter.menu.makeFilter
return {

View File

@ -113,7 +113,7 @@ ThreadHiding =
className: "#{type}-thread-button"
innerHTML: "<span class=brackets-wrap>&nbsp;#{if type is 'hide' then '-' else '+'}&nbsp;</span>"
href: 'javascript:;'
a.setAttribute 'data-fullid', thread.fullID
a.dataset.fullID = thread.fullID
$.on a, 'click', ThreadHiding.toggle
a
@ -134,7 +134,7 @@ ThreadHiding =
toggle: (thread) ->
unless thread instanceof Thread
thread = g.threads[@dataset.fullid]
thread = g.threads[@dataset.fullID]
if thread.isHidden
ThreadHiding.show thread
else

View File

@ -108,12 +108,12 @@ Build =
capcodeStart = ''
capcode = ''
flag =
if flagCode
" <img src='#{staticPath}country/#{if boardID is 'pol' then 'troll/' else ''}" +
flagCode.toLowerCase() + ".gif' alt=#{flagCode} title='#{flagName}' class=countryFlag>"
else
''
flag = unless flagCode
''
else if boardID is 'pol'
" <img src='#{staticPath}country/troll/#{flagCode.toLowerCase()}.gif' alt=#{flagCode} title='#{flagName}' class=countryFlag>"
else
" <span title='#{flagName}' class='flag flag-#{flagCode.toLowerCase()}'></span>"
if file?.isDeleted
fileHTML = if isOP

View File

@ -19,7 +19,7 @@ Get =
if index then post.clones[index] else post
postFromNode: (root) ->
Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', root
contextFromLink: (quotelink) ->
contextFromNode: (quotelink) ->
Get.postFromRoot $.x 'ancestor::div[parent::div[@class="thread"]][1]', quotelink
postDataFromLink: (link) ->
if link.hostname is 'boards.4chan.org'
@ -28,9 +28,8 @@ Get =
threadID = path[3]
postID = link.hash[2..]
else # resurrected quote
boardID = link.dataset.boardid
threadID = link.dataset.threadid or 0
postID = link.dataset.postid
{boardID, threadID, postID} = link.dataset
threadID or= 0
return {
boardID: boardID
threadID: +threadID
@ -185,7 +184,7 @@ Get =
# quotes
.replace /((&gt;){2}(&gt;\/[a-z\d]+\/)?\d+)/g, '<span class=deadlink>$1</span>'
threadID = data.thread_num
threadID = +data.thread_num
o =
# id
postID: "#{postID}"

View File

@ -1,6 +1,7 @@
editTheme = {} # Currently editted theme.
editMascot = {} # Which mascot we're editting.
userNavigation = {} # ...
editTheme = {}
editMascot = {}
userNavigation = {}
Conf = {}
c = console
d = document
@ -3074,4 +3075,4 @@ textarea,
border: 1px solid #111 !important;
background-color: #933;
}
"""
"""

View File

@ -55,7 +55,7 @@ Header =
return unless Main.isThisPageLegit()
# Wait for #boardNavMobile instead of #boardNavDesktop,
# it might be incomplete otherwise.
$.asap (-> $.id('boardNavMobile') or d.readyState in ['interactive', 'complete']), @setBoardList
$.asap (-> $.id('boardNavMobile') or d.readyState isnt 'loading'), Header.setBoardList
$.prepend d.body, @bar
$.add d.body, Header.hover
@setBarPosition Conf['Bottom Header']
@ -106,7 +106,7 @@ Header =
list = $ '#custom-board-list', Header.bar
$.rmAll list
return unless text
as = $$('#full-board-list a', Header.bar)
as = $$ '#full-board-list a[title]', Header.bar
nodes = text.match(/[\w@]+((-(all|title|replace|full|index|catalog|url:"[^"]+[^"]"|text:"[^"]+")|\,"[^"]+[^"]"))*|[^\w@]+/g).map (t) ->
if /^[^\w@]/.test t
return $.tn t
@ -141,7 +141,7 @@ Header =
a.textContent
if m = t.match /-(index|catalog)/
a.setAttribute 'data-only', m[1]
a.dataset.only = m[1]
a.href = "//boards.4chan.org/#{board}/"
if m[1] is 'catalog'
a.href += 'catalog'

View File

@ -191,20 +191,18 @@ Main =
threads = []
posts = []
for boardChild in board.children
continue unless $.hasClass boardChild, 'thread'
thread = new Thread boardChild.id[1..], g.BOARD
for threadRoot in $$ '.board > .thread', board
thread = new Thread +threadRoot.id[1..], g.BOARD
threads.push thread
for threadChild in boardChild.children
continue unless $.hasClass threadChild, 'postContainer'
for postRoot in $$ '.thread > .postContainer', threadRoot
try
posts.push new Post threadChild, thread, g.BOARD
posts.push new Post postRoot, thread, g.BOARD
catch err
# Skip posts that we failed to parse.
unless errors
errors = []
errors.push
message: "Parsing of Post No.#{threadChild.id.match(/\d+/)} failed. Post will be skipped."
message: "Parsing of Post No.#{postRoot.id.match(/\d+/)} failed. Post will be skipped."
error: err
Main.handleErrors errors if errors
@ -372,7 +370,7 @@ Main =
unless 'thisPageIsLegit' of Main
Main.thisPageIsLegit = location.hostname is 'boards.4chan.org' and
!$('link[href*="favicon-status.ico"]', d.head) and
d.title not in ['4chan - Temporarily Offline', '4chan - Error']
d.title not in ['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out']
Main.thisPageIsLegit
Main.init()

View File

@ -24,7 +24,8 @@ Settings =
changelog = '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md'
el = $.el 'span',
innerHTML: "<%= meta.name %> has been updated to <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>."
new Notification 'info', el, 30
if Conf['Show Updated Notifications']
new Notification 'info', el, 30
else
$.on d, '4chanXInitFinished', Settings.open
$.set
@ -427,7 +428,6 @@ Settings =
usercss: ->
CustomCSS.update()
keybinds: (section) ->
section.innerHTML = """
<%= grunt.file.read('src/General/html/Settings/Keybinds.html').replace(/>\s+</g, '><').trim() %>

View File

@ -303,13 +303,13 @@ UI = do ->
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, close}) ->
o = {
root: root
el: el
style: el.style
cb: cb
root
el
style: el.style
cb
close: close
endEvents: endEvents
latestEvent: latestEvent
endEvents
latestEvent
clientHeight: doc.clientHeight
clientWidth: doc.clientWidth
}
@ -325,6 +325,11 @@ UI = do ->
if $.x 'ancestor::div[contains(@class,"inline")][1]', root
$.on d, 'keydown', o.hoverend
$.on root, 'mousemove', o.hover
<% if (type === 'userscript') { %>
# Workaround for https://github.com/MayhemYDG/4chan-x/issues/377
o.workaround = (e) -> o.hoverend() unless root.contains e.target
$.on doc, 'mousemove', o.workaround
<% } %>
hover = (e) ->
@latestEvent = e
@ -355,6 +360,10 @@ UI = do ->
$.off @root, @endEvents, @hoverend
$.off d, 'keydown', @hoverend
$.off @root, 'mousemove', @hover
<% if (type === 'userscript') { %>
# Workaround for https://github.com/MayhemYDG/4chan-x/issues/377
$.off doc, 'mousemove', @workaround
<% } %>
@cb.call @ if @cb

1057
src/General/css/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<div class=warning #{if Conf['Filter'] then 'hidden' else ''}><code>Filter</code> is disabled.</div>
<p>
Use <a href=https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions>regular expressions</a>, one per line.<br>
Use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions">regular expressions</a>, one per line.<br>
Lines starting with a <code>#</code> will be ignored.<br>
For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br>
MD5 filtering uses exact string matching, not regular expressions.
@ -26,4 +26,4 @@
Highlighted OPs will have their threads put on top of board pages by default.<br>
For example: <code>top:yes;</code> or <code>top:no;</code>.
</li>
</ul>
</ul>

View File

@ -49,7 +49,7 @@ $.id = (id) ->
d.getElementById id
$.ready = (fc) ->
if d.readyState in ['interactive', 'complete']
unless d.readyState is 'loading'
$.queueTask fc
return
cb = ->
@ -221,9 +221,7 @@ $.event = (event, detail, root=d) ->
$.open = (URL) ->
<% if (type === 'userscript') { %>
# XXX fix GM opening file://// for protocol-less URLs.
# https://github.com/greasemonkey/greasemonkey/issues/1719
GM_openInTab ($.el 'a', href: URL).href
$.open = (URL) -> GM_openInTab URL
<% } else { %>
window.open URL, '_blank'
<% } %>
@ -298,29 +296,21 @@ $.minmax = (value, min, max) ->
value
)
$.syncing = {}
$.item = (key, val) ->
item = {}
item[key] = val
item
$.sync = do ->
$.syncing = {}
<% if (type === 'crx') { %>
$.sync = do ->
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
item
<% if (type === 'crx') { %>
$.localKeys = [
# filters
'name',
@ -372,6 +362,7 @@ $.get = (key, val, cb) ->
if syncItems
count++
chrome.storage.sync.get syncItems, done
$.set = do ->
items = {}
localItems = {}
@ -397,8 +388,13 @@ $.set = do ->
set()
<% } else { %>
# http://wiki.greasespot.net/Main_Page
$.sync = do ->
$.on window, 'storage', (e) ->
if cb = $.syncing[e.key]
cb JSON.parse e.newValue
(key, cb) -> $.syncing[g.NAMESPACE + key] = cb
$.delete = (keys) ->
unless keys instanceof Array
keys = [keys]

View File

@ -59,5 +59,4 @@ class Clone extends Post
@isDead = true if origin.isDead
@isClone = true
index = origin.clones.push(@) - 1
root.setAttribute 'data-clone', index
root.dataset.clone = origin.clones.push(@) - 1

View File

@ -16,29 +16,34 @@ class Post
quotelinks: []
backlinks: info.getElementsByClassName 'backlink'
unless @isReply = $.hasClass post, 'reply'
@thread.OP = @
@thread.isSticky = !!$ '.stickyIcon', info
@thread.isClosed = !!$ '.closedIcon', info
@info = {}
if subject = $ '.subject', info
if subject = $ '.subject', info
@nodes.subject = subject
@info.subject = subject.textContent
if name = $ '.name', info
if name = $ '.name', info
@nodes.name = name
@info.name = name.textContent
if email = $ '.useremail', info
if email = $ '.useremail', info
@nodes.email = email
@info.email = decodeURIComponent email.href[7..]
if tripcode = $ '.postertrip', info
if tripcode = $ '.postertrip', info
@nodes.tripcode = tripcode
@info.tripcode = tripcode.textContent
if uniqueID = $ '.posteruid', info
if uniqueID = $ '.posteruid', info
@nodes.uniqueID = uniqueID
@info.uniqueID = uniqueID.firstElementChild.textContent
if capcode = $ '.capcode.hand', info
if capcode = $ '.capcode.hand', info
@nodes.capcode = capcode
@info.capcode = capcode.textContent.replace '## ', ''
if flag = $ '.countryFlag', info
if flag = $ '.flag, .countryFlag', info
@nodes.flag = flag
@info.flag = flag.title
if date = $ '.dateTime', info
if date = $ '.dateTime', info
@nodes.date = date
@info.date = new Date date.dataset.utc * 1000
@info.yours = QR.db.get
@ -48,43 +53,7 @@ class Post
@parseComment()
@parseQuotes()
if (file = $ '.file', post) and thumb = $ 'img[data-md5]', file
# Supports JPG/PNG/GIF/PDF.
# Flash files are not supported.
alt = thumb.alt
anchor = thumb.parentNode
fileInfo = file.firstElementChild
@file =
info: fileInfo
text: fileInfo.firstElementChild
thumb: thumb
URL: anchor.href
size: alt.match(/[\d.]+\s\w+/)[0]
MD5: thumb.dataset.md5
isSpoiler: $.hasClass anchor, 'imgspoiler'
size = +@file.size.match(/[\d.]+/)[0]
unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0]
size *= 1024 while unit-- > 0
@file.sizeInBytes = size
@file.thumbURL =
if that.isArchived
thumb.src
else
"#{location.protocol}//thumbs.4chan.org/#{board}/thumb/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
# replace %22 with quotes, see:
# crbug.com/81193
# webk.it/62107
# https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909
# http://www.whatwg.org/specs/web-apps/current-work/#multipart-form-data
@file.name = $('span[title]', fileInfo).title.replace /%22/g, '"'
if @file.isImage = /(jpg|png|gif)$/i.test @file.name
@file.dimensions = @file.text.textContent.match(/\d+x\d+/)[0]
unless @isReply = $.hasClass post, 'reply'
@thread.OP = @
@thread.isSticky = !!$ '.stickyIcon', @nodes.info
@thread.isClosed = !!$ '.closedIcon', @nodes.info
@parseFile(that)
@clones = []
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
@ -108,18 +77,18 @@ class Post
nodes = d.evaluate './/br|.//text()', bq, null, 7, null
i = 0
while i < nodes.snapshotLength
text.push if data = nodes.snapshotItem(i++).data then data else '\n'
text.push nodes.snapshotItem(i++).data or '\n'
@info.comment = text.join('').trim().replace /\s+$/gm, ''
parseQuotes: ->
quotes = {}
for quotelink in $$ '.quotelink', @nodes.comment
# Don't add board links. (>>>/b/)
hash = quotelink.hash
{hash} = quotelink
continue unless hash
# Don't add catalog links. (>>>/b/catalog or >>>/b/search)
pathname = quotelink.pathname
{pathname} = quotelink
continue if /catalog$/.test pathname
# Don't add rules links. (>>>/a/rules)
@ -128,14 +97,49 @@ class Post
@nodes.quotelinks.push quotelink
# Don't count capcode replies as quotes. (Admin/Mod/Dev Replies: ...)
continue if quotelink.parentNode.parentNode.className is 'capcodeReplies'
# Don't count capcode replies as quotes in OPs. (Admin/Mod/Dev Replies: ...)
continue if !@isReply and $.hasClass quotelink.parentNode.parentNode, 'capcodeReplies'
# Basically, only add quotes that link to posts on an imageboard.
quotes["#{pathname.split('/')[1]}.#{hash[2..]}"] = true
return if @isClone
@quotes = Object.keys quotes
parseFile: (that) ->
return unless (fileEl = $ '.file', @nodes.post) and thumb = $ 'img[data-md5]', fileEl
# Supports JPG/PNG/GIF/PDF.
# Flash files are not supported.
alt = thumb.alt
anchor = thumb.parentNode
fileInfo = fileEl.firstElementChild
@file =
info: fileInfo
text: fileInfo.firstElementChild
thumb: thumb
URL: anchor.href
size: alt.match(/[\d.]+\s\w+/)[0]
MD5: thumb.dataset.md5
isSpoiler: $.hasClass anchor, 'imgspoiler'
size = +@file.size.match(/[\d.]+/)[0]
unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0]
size *= 1024 while unit-- > 0
@file.sizeInBytes = size
@file.thumbURL = if that.isArchived
thumb.src
else
"#{location.protocol}//thumbs.4chan.org/#{@board}/thumb/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
@file.name = $('span[title]', fileInfo).title
<% if (type === 'crx') { %>
# replace %22 with quotes, see:
# crbug.com/81193
# webk.it/62107
# https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909
# http://www.whatwg.org/specs/web-apps/current-work/#multipart-form-data
@file.name = @file.name.replace /%22/g, '"'
<% } %>
if @file.isImage = /(jpg|png|gif)$/i.test @file.name
@file.dimensions = @file.text.textContent.match(/\d+x\d+/)[0]
kill: (file, now) ->
now or= new Date()
if file
@ -190,10 +194,12 @@ class Post
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
$.rmClass quotelink, 'deadlink'
return
addClone: (context) ->
new Clone @, context
rmClone: (index) ->
@clones.splice index, 1
for clone in @clones[index..]
clone.nodes.root.setAttribute 'data-clone', index++
return
clone.nodes.root.dataset.clone = index++
return

View File

@ -2,8 +2,7 @@ class Thread
callbacks: []
toString: -> @ID
constructor: (ID, @board) ->
@ID = +ID
constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}"
@posts = {}
@ -11,4 +10,4 @@ class Thread
kill: ->
@isDead = true
@timeOfDeath = Date.now()
@timeOfDeath = Date.now()

View File

@ -16,6 +16,7 @@
}],
"homepage_url": "<%= meta.page %>",
"minimum_chrome_version": "24",
"minimum_opera_version": "15",
"permissions": [
"storage"
]

View File

@ -13,7 +13,7 @@ ImageHover =
el = $.el 'img',
id: 'ihover'
src: post.file.URL
el.setAttribute 'data-fullid', post.fullID
el.dataset.fullID = post.fullID
$.add Header.hover, el
UI.hover
root: @
@ -24,7 +24,7 @@ ImageHover =
$.on el, 'error', ImageHover.error
error: ->
return unless doc.contains @
post = g.posts[@dataset.fullid]
post = g.posts[@dataset.fullID]
src = @src.split '/'
if src[2] is 'images.4chan.org'
@ -48,4 +48,4 @@ ImageHover =
post.kill()
else if postObj.filedeleted
clearTimeout timeoutID
post.kill true
post.kill true

View File

@ -4,12 +4,10 @@ Sauce =
links = []
for link in Conf['sauces'].split '\n'
continue if link[0] is '#'
try
links.push @createSauceLink link.trim()
links.push @createSauceLink link.trim() if link[0] isnt '#'
catch err
# Don't add random text plz.
continue
return unless links.length
@links = links
@link = $.el 'a', target: '_blank'

View File

@ -44,9 +44,8 @@ DeleteLink =
return if DeleteLink.cooldown.counting is post
$.off @, 'click', DeleteLink.delete
@textContent = "Deleting #{@textContent}..."
fileOnly = $.hasClass @, 'delete-file'
@textContent = "Deleting #{if fileOnly then 'file' else 'post'}..."
form =
mode: 'usrdel'

View File

@ -8,29 +8,21 @@ Menu =
cb: @node
node: ->
button = Menu.makeButton @
if @isClone
$.replace $('.menu-button', @nodes.info), button
return
$.add @nodes.info, [$.tn('\u00A0'), button]
button = $ '.menu-button', @nodes.info
else
button = Menu.makeButton @
$.add @nodes.info, [$.tn('\u00A0'), button]
$.on button, 'click', Menu.toggle
makeButton: do ->
a = null
(post) ->
->
a or= $.el 'a',
className: 'menu-button brackets-wrap'
innerHTML: '<span class=drop-marker></span>'
href: 'javascript:;'
clone = a.cloneNode true
clone.setAttribute 'data-postid', post.fullID
clone.setAttribute 'data-clone', true if post.isClone
$.on clone, 'click', Menu.toggle
clone
a.cloneNode true
toggle: (e) ->
post =
if @dataset.clone
Get.postFromNode @
else
g.posts[@dataset.postid]
Menu.menu.toggle e, @, post
Menu.menu.toggle e, @, Get.postFromNode @

View File

@ -93,10 +93,10 @@ Keybinds =
window.location = "/#{g.BOARD}/catalog"
# Thread Navigation
when Conf['Next thread']
return if g.VIEW is 'thread'
return if g.VIEW isnt 'index'
Nav.scroll +1
when Conf['Previous thread']
return if g.VIEW is 'thread'
return if g.VIEW isnt 'index'
Nav.scroll -1
when Conf['Expand thread']
ExpandThread.toggle thread

View File

@ -58,8 +58,7 @@ Nav =
# unless we're not at the beginning of the current thread
# (and thus wanting to move to beginning)
# or we're above the first thread and don't want to skip it
unless (delta is -1 and Math.ceil(top) < 0) or (delta is +1 and top > 1)
i += delta
if (delta is -1 and top > -5) or (delta is +1 and top < 5)
top = threads[i + delta]?.getBoundingClientRect().top - topMargin
top = threads[i]?.getBoundingClientRect().top - topMargin
window.scrollBy 0, top

View File

@ -158,8 +158,7 @@ ThreadUpdater =
By sending the `If-Modified-Since` header we get a proper status code, and no response.
This saves bandwidth for both the user and the servers and avoid unnecessary computation.
###
# XXX 304 -> 0 in Opera
[text, klass] = if [0, 304].contains req.status
[text, klass] = if req.status is 304
[null, null]
else
["#{req.statusText} (#{req.status})", 'warning']

View File

@ -36,13 +36,11 @@ Unread =
# Let the header's onload callback handle it.
return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts
if Unread.posts.length
# Scroll to before the first unread post.
prevID = 0
while root = $.x 'preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root
post = Get.postFromRoot root
break if prevID is post.ID
prevID = post.ID
break unless post.isHidden
# Scroll to a non-hidden, non-OP post that's before the first unread post.
post = Unread.posts[0]
while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root
break unless (post = Get.postFromRoot root).isHidden
return unless root
onload = -> root.scrollIntoView false if checkPosition root
else
# Scroll to the last read post.
@ -188,9 +186,7 @@ Unread =
else
Favicon.default
<% if (type !== 'crx') { %>
<% if (type === 'userscript') { %>
# `favicon.href = href` doesn't work on Firefox.
# `favicon.href = href` isn't enough on Opera.
# Opera won't always update the favicon if the href didn't change.
$.add d.head, Favicon.el
<% } %>

View File

@ -97,8 +97,8 @@ QR =
$.rmClass QR.captcha.nodes.input, 'error'
if Conf['QR Shortcut']
$.toggleClass $('.qr-shortcut'), 'disabled'
for i in QR.posts
QR.posts[0].rm()
for post in QR.posts.splice 0, QR.posts.length, new QR.post true
post.delete()
QR.cooldown.auto = false
QR.status()
@ -152,7 +152,8 @@ QR =
status: ->
return unless QR.nodes
if g.DEAD
{thread} = QR.posts[0]
if thread isnt 'new' and g.threads["#{g.BOARD}.#{thread}"].isDead
value = 404
disabled = true
QR.cooldown.auto = false
@ -373,14 +374,10 @@ QR =
e?.preventDefault()
return unless QR.postingIsEnabled
sel = d.getSelection()
selectionRoot = $.x 'ancestor::div[contains(@class,"postContainer")][1]', sel.anchorNode
post = Get.postFromNode @
{OP} = Get.contextFromLink(@).thread
text = ">>#{post}\n"
if (s = sel.toString().trim()) and post.nodes.root is selectionRoot
# XXX Opera doesn't retain `\n`s?
sel = d.getSelection()
post = Get.postFromNode @
text = ">>#{post}\n"
if (s = sel.toString().trim()) and post is Get.postFromNode sel.anchorNode
s = s.replace /\n/g, '\n>'
text += ">#{s}\n"
@ -391,7 +388,7 @@ QR =
$.addClass QR.nodes.el, 'dump'
QR.cooldown.auto = true
{com, thread} = QR.nodes
thread.value = OP.ID unless com.value
thread.value = Get.contextFromNode(@).thread unless com.value
thread.nextElementSibling.firstElementChild.textContent = thread.options[thread.selectedIndex].textContent
caretPos = com.selectionStart
@ -449,7 +446,7 @@ QR =
QR.nodes.fileInput.click()
fileInput: (files) ->
if @ instanceof Element #or files instanceof Event # file input
if files instanceof Event # file input
files = [@files...]
QR.nodes.fileInput.value = null # Don't hold the files from being modified on windows
{length} = files
@ -506,7 +503,7 @@ QR =
for elm in $$ '*', el
$.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin
<% } %>
<% } %>
$.on el, 'click', @select.bind @
$.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm()
$.on @nodes.label, 'click', (e) => e.stopPropagation()
@ -555,7 +552,7 @@ QR =
@unlock()
rm: ->
$.rm @nodes.el
@delete()
index = QR.posts.indexOf @
if QR.posts.length is 1
new QR.post true
@ -563,7 +560,9 @@ QR =
else if @ is QR.selected
(QR.posts[index-1] or QR.posts[index+1]).select()
QR.posts.splice index, 1
return unless window.URL
QR.status()
delete: ->
$.rm @nodes.el
URL.revokeObjectURL @URL
lock: (lock=true) ->
@ -608,15 +607,18 @@ QR =
if input.type is 'checkbox'
@spoiler = input.checked
return
{value} = input
@[input.dataset.name] = value
return if input.nodeName isnt 'TEXTAREA'
@nodes.span.textContent = value
QR.characterCount()
# Disable auto-posting if you're typing in the first post
# during the last 5 seconds of the cooldown.
if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5
QR.cooldown.auto = false
{name} = input.dataset
@[name] = input.value
switch name
when 'thread'
QR.status()
when 'com'
@nodes.span.textContent = @com
QR.characterCount()
# Disable auto-posting if you're typing in the first post
# during the last 5 seconds of the cooldown.
if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5
QR.cooldown.auto = false
forceSave: ->
return unless @ is QR.selected
@ -630,26 +632,15 @@ QR =
@filename = "#{file.name} (#{$.bytesToString file.size})"
@nodes.el.title = @filename
@nodes.label.hidden = false if QR.spoiler
URL.revokeObjectURL @URL if window.URL
URL.revokeObjectURL @URL
@showFileData()
unless /^image/.test file.type
@nodes.el.style.backgroundImage = null
return
@setThumbnail()
setThumbnail: (fileURL) ->
# XXX Opera does not support blob URL
setThumbnail: ->
# Create a redimensioned thumbnail.
unless window.URL
unless fileURL
reader = new FileReader()
reader.onload = (e) =>
@setThumbnail e.target.result
reader.readAsDataURL @file
return
else
fileURL = URL.createObjectURL @file
img = $.el 'img'
img.onload = =>
@ -661,7 +652,7 @@ QR =
s *= 3 if @file.type is 'image/gif' # let them animate
{height, width} = img
if height < s or width < s
@URL = fileURL if window.URL
@URL = fileURL
@nodes.el.style.backgroundImage = "url(#{@URL})"
return
if height <= width
@ -674,10 +665,6 @@ QR =
cv.height = img.height = height
cv.width = img.width = width
cv.getContext('2d').drawImage img, 0, 0, width, height
unless window.URL
@nodes.el.style.backgroundImage = "url(#{cv.toDataURL()})"
delete @URL
return
URL.revokeObjectURL fileURL
applyBlob = (blob) =>
@URL = URL.createObjectURL blob
@ -695,6 +682,7 @@ QR =
applyBlob new Blob [ui8a], type: 'image/png'
fileURL = URL.createObjectURL @file
img.src = fileURL
rmFile: ->
@ -704,7 +692,6 @@ QR =
@nodes.el.style.backgroundImage = null
@nodes.label.hidden = true if QR.spoiler
@showFileData()
return unless window.URL
URL.revokeObjectURL @URL
showFileData: ->
@ -729,33 +716,26 @@ QR =
@nodes.span.textContent = @com
reader.readAsText file
dragStart: ->
$.addClass @, 'drag'
dragEnd: ->
$.rmClass @, 'drag'
dragEnter: ->
$.addClass @, 'over'
dragLeave: ->
$.rmClass @, 'over'
dragStart: -> $.addClass @, 'drag'
dragEnd: -> $.rmClass @, 'drag'
dragEnter: -> $.addClass @, 'over'
dragLeave: -> $.rmClass @, 'over'
dragOver: (e) ->
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
drop: ->
el = $ '.drag', @parentNode
$.rmClass el, 'drag' # Opera doesn't fire dragEnd if we drop it on something else
$.rmClass @, 'over'
$.rmClass @, 'over'
return unless @draggable
el = $ '.drag', @parentNode
index = (el) -> [el.parentNode.children...].indexOf el
oldIndex = index el
newIndex = index @
(if oldIndex < newIndex then $.after else $.before) @, el
post = QR.posts.splice(oldIndex, 1)[0]
QR.posts.splice newIndex, 0, post
QR.status()
captcha:
init: ->
@ -784,20 +764,17 @@ QR =
img: imgContainer.firstChild
input: input
if window.MutationObserver
observer = new MutationObserver @load.bind @
observer.observe @nodes.challenge,
childList: true
else
$.on @nodes.challenge, 'DOMNodeInserted', @load.bind @
new MutationObserver(@load.bind @).observe @nodes.challenge,
childList: true
$.on imgContainer, 'click', @reload.bind @
$.on input, 'keydown', @keydown.bind @
$.on input, 'focus', -> $.addClass QR.nodes.el, 'focus'
$.on input, 'blur', -> $.rmClass QR.nodes.el, 'focus'
$.get 'captchas', [], (item) =>
@sync item['captchas']
$.get 'captchas', [], ({captchas}) =>
@sync captchas
$.sync 'captchas', @sync
# start with an uncached captcha
@reload()
@ -806,12 +783,13 @@ QR =
# XXX Firefox lacks focusin/focusout support.
$.on input, 'blur', QR.focusout
$.on input, 'focus', QR.focusin
<% } %>
<% } %>
$.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.dumpList.parentElement, [imgContainer, input]
sync: (@captchas) ->
sync: (captchas) ->
QR.captcha.captchas = captchas
QR.captcha.count()
getOne: ->
@ -931,10 +909,6 @@ QR =
# Add empty mimeType to avoid errors with URLs selected in Window's file dialog.
QR.mimeTypes.push ''
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
<% if (type !== 'userjs') { %>
# Opera's accept attribute is fucked up
nodes.fileInput.accept = "text/*, #{mimeTypes}"
<% } %>
QR.spoiler = !!$ 'input[name=spoiler]'
if QR.spoiler
@ -969,7 +943,7 @@ QR =
for elm in $$ '*', QR.nodes.el
$.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin
<% } %>
<% } %>
$.on dialog, 'focusin', QR.focusin
$.on dialog, 'focusout', QR.focusout
$.on nodes.autohide, 'change', QR.toggleHide
@ -1133,11 +1107,6 @@ QR =
QR.status()
response: ->
<% if (type === 'userjs') { %>
# The upload.onload callback is not called
# or at least not in time with Opera.
QR.req.upload.onload()
<% } %>
{req} = QR
delete QR.req
@ -1229,15 +1198,15 @@ QR =
QR.cooldown.set {req, post, isReply}
if threadID is postID # new thread
URL = "/#{g.BOARD}/res/#{threadID}"
URL = if threadID is postID # new thread
"/#{g.BOARD}/res/#{threadID}"
else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index
URL = "/#{g.BOARD}/res/#{threadID}#p#{postID}"
"/#{g.BOARD}/res/#{threadID}#p#{postID}"
if URL
if Conf['Open Post in New Tab']
$.open "/#{g.BOARD}/res/#{threadID}"
$.open URL
else
window.location = "/#{g.BOARD}/res/#{threadID}"
window.location = URL
QR.status()

View File

@ -35,7 +35,7 @@ QuoteInline =
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
e.preventDefault()
{boardID, threadID, postID} = Get.postDataFromLink @
context = Get.contextFromLink @
context = Get.contextFromNode @
if $.hasClass @, 'inlined'
QuoteInline.rm @, boardID, threadID, postID, context
else
@ -107,4 +107,4 @@ QuoteInline =
{boardID, threadID, postID} = Get.postDataFromLink inlined
QuoteInline.rm inlined, boardID, threadID, postID, context
$.rmClass inlined, 'inlined'
return
return

View File

@ -22,8 +22,9 @@ QuotePreview =
qp = $.el 'div',
id: 'qp'
className: 'dialog'
$.add Header.hover, qp
Get.postClone boardID, threadID, postID, qp, Get.contextFromLink @
Get.postClone boardID, threadID, postID, qp, Get.contextFromNode @
UI.hover
root: @

View File

@ -21,6 +21,9 @@ Quotify =
if deadlink.parentNode.className is 'prettyprint'
# Don't quotify deadlinks inside code tags,
# un-`span` them.
# This won't be necessary once 4chan
# stops quotifying inside code tags:
# https://github.com/4chan/4chan-JS/issues/77
$.replace deadlink, [deadlink.childNodes...]
return
@ -48,9 +51,8 @@ Quotify =
target: '_blank'
textContent: "#{quote}\u00A0(Dead)"
a.setAttribute 'data-boardid', boardID
a.setAttribute 'data-threadid', post.thread.ID
a.setAttribute 'data-postid', postID
$.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
else if redirect = Redirect.to 'thread', {boardID, threadID: 0, postID}
# Replace the .deadlink span if we can redirect.
a = $.el 'a',
@ -60,9 +62,8 @@ Quotify =
textContent: "#{quote}\u00A0(Dead)"
if Redirect.to 'post', {boardID, postID}
# Make it function as a normal quote if we can fetch the post.
$.addClass a, 'quotelink'
a.setAttribute 'data-boardid', boardID
a.setAttribute 'data-postid', postID
$.addClass a, 'quotelink'
$.extend a.dataset, {boardID, postID}
unless @quotes.contains quoteID
@quotes.push quoteID
@ -73,4 +74,4 @@ Quotify =
$.replace deadlink, a
if $.hasClass a, 'quotelink'
@nodes.quotelinks.push a
@nodes.quotelinks.push a