Merge from Appchan X: Whitespace and other changes not affecting compiled script.

This commit is contained in:
ccd0 2015-01-30 01:22:40 -08:00
parent 789dcc07eb
commit 94928b7c83
42 changed files with 258 additions and 110 deletions

View File

@ -58,6 +58,7 @@ Redirect =
protocol = Redirect.protocol archive
URL = new String "#{protocol}#{archive.domain}/_/api/chan/post/?board=#{boardID}&num=#{postID}"
return '' unless Redirect.securityCheck URL
URL.archive = archive
URL

View File

@ -6,6 +6,7 @@ Anonymize =
Post.callbacks.push
name: 'Anonymize'
cb: @node
node: ->
return if @info.capcode or @isClone
{name, tripcode, email} = @nodes
@ -17,6 +18,7 @@ Anonymize =
if @info.email
$.replace email, name
delete @nodes.email
archive: ->
$.ready ->
name.textContent = 'Anonymous' for name in $$ '.name'

View File

@ -84,11 +84,13 @@ Filter =
(value) -> regexp is value
else
(value) -> regexp.test value
settings =
hide: !hl
stub: stub
class: hl
top: top
(value, isReply) ->
if isReply and op is 'only' or !isReply and op is 'no'
return false
@ -103,10 +105,7 @@ Filter =
# Continue if there's nothing to filter (no tripcode for example).
continue if value is false
for filter in Filter.filters[key]
unless result = filter value, @isReply
continue
for filter in Filter.filters[key] when result = filter value, @isReply
# Hide
if result.hide
if @isReply

View File

@ -12,12 +12,14 @@ PostHiding =
node: ->
return if !@isReply or @isClone or @isFetchedQuote
if data = PostHiding.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID}
if data.thisPost
PostHiding.hide @, data.makeStub, data.hideRecursively
else
Recursive.apply PostHiding.hide, @, data.makeStub, true
Recursive.add PostHiding.hide, @, data.makeStub, true
return unless Conf['Reply Hiding Buttons']
sideArrows = $('.sideArrows', @nodes.root)
$.replace sideArrows.firstChild, PostHiding.makeButton @, 'hide'

View File

@ -1,18 +1,16 @@
Recursive =
recursives: {}
init: ->
return if g.VIEW not in ['index', 'thread']
return unless g.VIEW in ['index', 'thread']
Post.callbacks.push
name: 'Recursive'
cb: @node
node: ->
return if @isClone or @isFetchedQuote
for quote in @quotes
if obj = Recursive.recursives[quote]
for recursive, i in obj.recursives
recursive @, obj.args[i]...
for quote in @quotes when obj = Recursive.recursives[quote]
for recursive, i in obj.recursives
recursive @, obj.args[i]...
return
add: (recursive, post, args...) ->

View File

@ -334,6 +334,7 @@ Build =
root = $.el 'div',
className: 'catalog-thread'
$.extend root, <%= html(
'<a href="/${thread.board}/thread/${thread.ID}">' +
'&{thumb}' +
@ -352,12 +353,15 @@ Build =
for quote in $$ '.quotelink', root.lastElementChild
href = quote.getAttribute 'href'
quote.href = "/#{thread.board}/thread/#{thread.ID}" + href if href[0] is '#'
for exif in $$ '.abbr, .exif', root.lastElementChild
$.rm exif
for pp in $$ '.prettyprint', root.lastElementChild
cc = $.el 'span', className: 'catalog-code'
$.add cc, [pp.childNodes...]
$.replace pp, cc
for br in $$ 'br', root.lastElementChild when br.previousSibling?.nodeName is 'BR'
$.rm br
@ -366,6 +370,7 @@ Build =
src: "#{staticPath}sticky#{gifIcon}"
className: 'stickyIcon'
title: 'Sticky'
if thread.isClosed
$.add $('.catalog-icons', root), $.el 'img',
src: "#{staticPath}closed#{gifIcon}"
@ -374,6 +379,7 @@ Build =
if data.bumplimit
$.addClass $('.post-count', root), 'warning'
if data.imagelimit
$.addClass $('.file-count', root), 'warning'

View File

@ -1,7 +1,7 @@
Config =
main:
'Miscellaneous':
'JSON Navigation' : [
'JSON Navigation': [
true
'Replace the original board index with one supporting searching, sorting, infinite scrolling, and a catalog mode.'
]
@ -112,6 +112,7 @@ Config =
true
'<%= meta.name %> is NOT designed to work with the native extension.'
]
'Linkification':
'Linkify': [
true
@ -583,69 +584,69 @@ Config =
filter:
name: """
# Filter any namefags:
#/^(?!Anonymous$)/
"""
# Filter any namefags:
#/^(?!Anonymous$)/
"""
uniqueID: """
# Filter a specific ID:
#/Txhvk1Tl/
"""
# Filter a specific ID:
#/Txhvk1Tl/
"""
tripcode: """
# Filter any tripfag
#/^!/
"""
# Filter any tripfag
#/^!/
"""
capcode: """
# Set a custom class for mods:
#/Mod$/;highlight:mod;op:yes
# Set a custom class for moot:
#/Admin$/;highlight:moot;op:yes
"""
# Set a custom class for mods:
#/Mod$/;highlight:mod;op:yes
# Set a custom class for moot:
#/Admin$/;highlight:moot;op:yes
"""
subject: """
# Filter Generals on /v/:
#/general/i;boards:v;op:only
"""
# Filter Generals on /v/:
#/general/i;boards:v;op:only
"""
comment: """
# Filter Stallman copypasta on /g/:
#/what you\'re refer+ing to as linux/i;boards:g
"""
# Filter Stallman copypasta on /g/:
#/what you\'re refer+ing to as linux/i;boards:g
"""
flag: ''
filename: ''
dimensions: """
# Highlight potential wallpapers:
#/1920x1080/;op:yes;highlight;top:no;boards:w,wg
"""
# Highlight potential wallpapers:
#/1920x1080/;op:yes;highlight;top:no;boards:w,wg
"""
filesize: ''
MD5: ''
sauces: """
https://www.google.com/searchbyimage?image_url=%TURL
http://iqdb.org/?url=%TURL
#//tineye.com/search?url=%TURL
#//saucenao.com/search.php?url=%TURL
#http://3d.iqdb.org/?url=%TURL
#http://regex.info/exif.cgi?imgurl=%URL
# uploaders:
#//imgur.com/upload?url=%URL;text:Upload to imgur
# "View Same" in archives:
#https://archive.moe/_/search/image/%MD5/;text:View same on archive.moe
#https://archive.moe/%board/search/image/%MD5/;text:View same on archive.moe/%board/;boards:<%=
grunt.file.readJSON('src/Archive/archives.json').filter(function(x) {return x.uid === 0})[0].files.join(',')
%>
#https://rbt.asia/%board/image/%MD5;text:View same on RBT /%board/;boards:<%=
grunt.file.readJSON('src/Archive/archives.json').filter(function(x) {return x.uid === 8})[0].files.join(',')
%>
# Search with full image only for image file types:
#https://www.google.com/searchbyimage?image_url=%URL;types:gif,jpg,png
#https://www.google.com/searchbyimage?image_url=%TURL;types:webm,pdf
"""
https://www.google.com/searchbyimage?image_url=%TURL
http://iqdb.org/?url=%TURL
#//tineye.com/search?url=%TURL
#//saucenao.com/search.php?url=%TURL
#http://3d.iqdb.org/?url=%TURL
#http://regex.info/exif.cgi?imgurl=%URL
# uploaders:
#//imgur.com/upload?url=%URL;text:Upload to imgur
# "View Same" in archives:
#https://archive.moe/_/search/image/%MD5/;text:View same on archive.moe
#https://archive.moe/%board/search/image/%MD5/;text:View same on archive.moe/%board/;boards:<%=
grunt.file.readJSON('src/Archive/archives.json').filter(function(x) {return x.uid === 0})[0].files.join(',')
%>
#https://rbt.asia/%board/image/%MD5;text:View same on RBT /%board/;boards:<%=
grunt.file.readJSON('src/Archive/archives.json').filter(function(x) {return x.uid === 8})[0].files.join(',')
%>
# Search with full image only for image file types:
#https://www.google.com/searchbyimage?image_url=%URL;types:gif,jpg,png
#https://www.google.com/searchbyimage?image_url=%TURL;types:webm,pdf
"""
FappeT:
werk: false
@ -674,29 +675,29 @@ http://iqdb.org/?url=%TURL
'Custom Board Navigation': true
boardnav: """
[ toggle-all ]
a-replace
c-replace
g-replace
k-replace
v-replace
vg-replace
vr-replace
ck-replace
co-replace
fit-replace
jp-replace
mu-replace
sp-replace
tv-replace
vp-replace
[external-text:"FAQ","<%= meta.faq %>"]
[ toggle-all ]
a-replace
c-replace
g-replace
k-replace
v-replace
vg-replace
vr-replace
ck-replace
co-replace
fit-replace
jp-replace
mu-replace
sp-replace
tv-replace
vp-replace
[external-text:"FAQ","<%= meta.faq %>"]
"""
QR:
'QR.personas': """
#options:"sage";boards:jp;always
"""
"""
time: '%m/%d/%y(%a)%H:%M:%S'

View File

@ -51,7 +51,7 @@ Get =
# get all their backlinks.
posts.forEach (qPost) ->
if fullID in qPost.quotes
handleQuotes qPost, 'quotelinks'
handleQuotes qPost, 'quotelinks'
# Second:
# If we have quote backlinks:
@ -66,10 +66,12 @@ Get =
quotelinks.filter (quotelink) ->
{boardID, postID} = Get.postDataFromLink quotelink
boardID is post.board.ID and postID is post.ID
scriptData: ->
for script in $$ 'script:not([src])', d.head
return script.textContent if /\bcooldowns *=/.test script.textContent
''
postClone: (boardID, threadID, postID, root, context) ->
if post = g.posts["#{boardID}.#{postID}"]
Get.insert post, root, context
@ -81,6 +83,7 @@ Get =
Get.fetchedPost @, boardID, threadID, postID, root, context
else
Get.archivedPost boardID, postID, root, context
insert: (post, root, context) ->
# Stop here if the container has been removed while loading.
return unless root.parentNode
@ -95,6 +98,7 @@ Get =
$.rmAll root
$.add root, nodes.root
$.event 'PostsInserted'
fetchedPost: (req, boardID, threadID, postID, root, context) ->
# In case of multiple callbacks for the same request,
# don't parse the same original post more than once.
@ -127,6 +131,7 @@ Get =
$.cache api, ->
Get.fetchedPost @, boardID, threadID, postID, root, context
return
# The post can be deleted by the time we check a quote.
unless Get.archivedPost boardID, postID, root, context
$.addClass root, 'warning'
@ -141,6 +146,7 @@ Get =
post.isFetchedQuote = true
Main.callbackNodes Post, [post]
Get.insert post, root, context
archivedPost: (boardID, postID, root, context) ->
return false unless Conf['Resurrect Quotes']
return false unless url = Redirect.to 'post', {boardID, postID}
@ -161,6 +167,7 @@ Get =
Get.parseArchivedPost response, boardID, postID, root, context
return true
return false
parseArchivedPost: (data, boardID, postID, root, context) ->
# In case of multiple callbacks for the same request,
# don't parse the same original post more than once.

View File

@ -143,7 +143,10 @@ Header =
$.extend boardList, <%= html(
'<span id="custom-board-list"></span>' +
'<span id="full-board-list" hidden>' +
'<span class="hide-board-list-container brackets-wrap"><a href="javascript:;" class="hide-board-list-button">&nbsp;-&nbsp;</a></span> ' +
'<span class="hide-board-list-container brackets-wrap">' +
'<a href="javascript:;" class="hide-board-list-button">&nbsp;-&nbsp;</a>' +
'</span>' +
' ' +
'<span class="boardList"></span>' +
'</span>'
) %>
@ -185,11 +188,13 @@ Header =
nodes = boardnav.match(/[\w@]+(-(all|title|replace|full|index|catalog|archive|expired|text:"[^"]+"(,"[^"]+")?))*|[^\w@]+/g).map (t) ->
if /^[^\w@]/.test t
return $.tn t
text = url = null
t = t.replace /-text:"([^"]+)"(?:,"([^"]+)")?/g, (m0, m1, m2) ->
text = m1
url = m2
''
if /^toggle-all/.test t
a = $.el 'a',
className: 'show-board-list-button'
@ -197,12 +202,14 @@ Header =
href: 'javascript:;'
$.on a, 'click', Header.toggleBoardList
return a
if /^external/.test t
a = $.el 'a',
href: url or 'javascript:;'
textContent: text or '+'
className: 'external'
return a
boardID = if /^current/.test t
g.BOARD.ID
else
@ -411,7 +418,6 @@ Header =
hash = @location.hash[1..]
return unless /^p\d+$/.test(hash) and post = $.id hash
return if (Get.postFromRoot post).isHidden
Header.scrollTo post
scrollTo: (root, down, needed) ->
@ -451,10 +457,12 @@ Header =
headRect = Header.toggle.getBoundingClientRect()
bottom -= clientHeight - headRect.bottom + headRect.height
bottom
isNodeVisible: (node) ->
return false if d.hidden or !doc.contains node
{height} = node.getBoundingClientRect()
Header.getTopOf(node) + height >= 0 and Header.getBottomOf(node) + height >= 0
isHidden: ->
{top} = Header.bar.getBoundingClientRect()
if Conf['Bottom header']

View File

@ -161,6 +161,7 @@ Index =
catalogNode: ->
$.on @nodes.thumb.parentNode, 'click', Index.onClick
onClick: (e) ->
return if e.button isnt 0
thread = g.threads[@parentNode.dataset.fullID]
@ -169,6 +170,7 @@ Index =
else
return
e.preventDefault()
toggleHide: (thread) ->
$.rm thread.catalogView.nodes.root
if Index.showHiddenThreads
@ -178,6 +180,7 @@ Index =
else
ThreadHiding.hide thread
ThreadHiding.saveHiddenState thread
cycleSortType: ->
types = [Index.selectSort.options...].filter (option) -> !option.disabled
for type, i in types
@ -193,15 +196,18 @@ Index =
'Show'
Index.sort()
Index.buildIndex()
mode: ->
mode = @value
unless mode is 'catalog'
Conf['Previous Index Mode'] = mode
$.set 'Previous Index Mode', mode
Index.pageLoad Index.pushState {mode}
sort: ->
Index.sort()
Index.buildIndex()
size: (e) ->
if Conf['Index Mode'] isnt 'catalog'
$.rmClass Index.root, 'catalog-small'
@ -213,10 +219,12 @@ Index =
$.addClass Index.root, 'catalog-large'
$.rmClass Index.root, 'catalog-small'
Index.buildIndex() if e
replies: ->
Index.buildThreads()
Index.sort()
Index.buildIndex()
popstate: (e) ->
if e?.state
{search, mode} = e.state
@ -236,6 +244,7 @@ Index =
scroll: true
if state.command
Index[if Conf['Refreshed Navigation'] then 'update' else 'pageLoad'] state
pageNav: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
switch e.target.nodeName
@ -249,6 +258,7 @@ Index =
return if a.textContent is 'Catalog'
e.preventDefault()
Index.userPageNav +a.pathname.split('/')[2] or 1
frontPage: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
e.preventDefault()
@ -262,12 +272,14 @@ Index =
1
else
+window.location.pathname.split('/')[2] or 1
userPageNav: (page, noRefresh) ->
state = Index.pushState {page, scroll: true}
if Conf['Refreshed Navigation'] and !noRefresh
Index.update state
else
Index.pageLoad state if state.page
pushState: (state) ->
{pathname, hash} = location
pageBeforeSearch = history.state?.oldpage
@ -309,6 +321,7 @@ Index =
oldpage: pageBeforeSearch
, '', pathname + hash
state
pageLoad: ({sort, search, mode, scroll}) ->
if sort or search?
Index.sort()
@ -318,6 +331,7 @@ Index =
Index.buildIndex()
Index.setPage()
Index.scrollToIndex() if scroll
applyMode: ->
for mode in ['paged', 'infinite', 'all pages', 'catalog']
$[if mode is Conf['Index Mode'] then 'addClass' else 'rmClass'] doc, "#{mode.replace /\ /g, '-'}-mode"
@ -331,8 +345,10 @@ Index =
Math.ceil Index.sortedNodes.length / Index.threadsNumPerPage
else
Index.pagesNum
getMaxPageNum: ->
Math.max 1, Index.getPagesNum()
buildPagelist: ->
pagesRoot = $ '.pages', Index.pagelist
maxPageNum = Index.getMaxPageNum()
@ -345,10 +361,12 @@ Index =
nodes.push $.tn('['), a, $.tn '] '
$.rmAll pagesRoot
$.add pagesRoot, nodes
setPage: ->
pageNum = Index.getCurrentPage()
maxPageNum = Index.getMaxPageNum()
pagesRoot = $ '.pages', Index.pagelist
# Previous/Next buttons
prev = pagesRoot.previousSibling.firstChild
next = pagesRoot.nextSibling.firstChild
@ -358,12 +376,14 @@ Index =
href = Math.min pageNum + 1, maxPageNum
next.href = if href is 1 then './' else href
next.firstChild.disabled = href is pageNum
# <strong> current page
if strong = $ 'strong', pagesRoot
return if +strong.textContent is pageNum
$.replace strong, strong.firstChild
else
strong = $.el 'strong'
a = pagesRoot.children[pageNum - 1]
$.before a, strong
$.add strong, a

View File

@ -33,10 +33,13 @@ Main =
else # string or number
Conf[parent] = obj
return
flatten null, Config
for db in DataBoard.keys
Conf[db] = boards: {}
Conf['selectedArchives'] = {}
$.get Conf, (items) ->
$.extend Conf, items
$.asap (-> doc = d.documentElement), Main.initFeatures
@ -71,7 +74,7 @@ Main =
pathname = location.pathname.split '/'
if pathname[2] isnt 'thread' or pathname.length > 4
pathname[2] = 'thread'
history.replaceState null, '', pathname.slice(0,4).join('/') + location.hash
history.replaceState null, '', pathname[0...4].join('/') + location.hash
# c.time 'All initializations'
for [name, feature] in Main.features

View File

@ -73,11 +73,13 @@ Settings =
delete Settings.dialog
sections: []
addSection: (title, open) ->
if typeof title isnt 'string'
{title, open} = title.detail
hyphenatedTitle = title.toLowerCase().replace /\s+/g, '-'
Settings.sections.push {title, hyphenatedTitle, open}
openSection: ->
if selected = $ '.tab-selected', Settings.dialog
$.rmClass selected, 'tab-selected'
@ -142,12 +144,14 @@ Settings =
localStorage.removeItem "4chan-hide-t-#{boardID}"
$.delete ['hiddenThreads', 'hiddenPosts']
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div
export: ->
# Make sure to export the most recent data.
$.get Conf, (Conf) ->
# XXX don't export archives.
delete Conf['archives']
Settings.downloadExport {version: g.VERSION, date: Date.now(), Conf}
downloadExport: (data) ->
a = $.el 'a',
download: "<%= meta.name %> v#{g.VERSION}-#{data.date}.json"
@ -160,12 +164,14 @@ Settings =
a.click()
import: ->
$('input', @parentNode).click()
onImport: ->
return unless file = @files[0]
output = $('.imp-exp-result')
unless confirm 'Your current settings will be entirely overwritten, are you sure?'
output.textContent = 'Import aborted.'
return
reader = new FileReader()
reader.onload = (e) ->
try
@ -176,6 +182,7 @@ Settings =
output.textContent = 'Import failed due to an error.'
c.error err.stack
reader.readAsText file
loadSettings: (data) ->
version = data.version.split '.'
if version[0] is '2'
@ -259,6 +266,7 @@ Settings =
data.Conf['watchedThreads'] = boards: ThreadWatcher.convert data.Conf['WatchedThreads']
delete data.Conf['WatchedThreads']
$.clear -> $.set data.Conf
reset: ->
if confirm 'Your current settings will be entirely wiped, are you sure?'
$.clear -> window.location.reload() if confirm 'Reset successful. Reload now?'
@ -326,9 +334,11 @@ Settings =
interval = $ 'input[name="Interval"]', section
customCSS = $ 'input[name="Custom CSS"]', section
interval.value = Conf['Interval']
customCSS.checked = Conf['Custom CSS']
inputs['usercss'].disabled = !Conf['Custom CSS']
$.on interval, 'change', ThreadUpdater.cb.interval
$.on customCSS, 'change', Settings.togglecss
$.on $('#apply-css', section), 'click', Settings.usercss
@ -421,10 +431,13 @@ Settings =
boardnav: ->
Header.generateBoardList @value
time: ->
@nextElementSibling.textContent = Time.format @value, new Date()
backlink: ->
@nextElementSibling.textContent = @value.replace /%(?:id|%)/g, (x) -> {'%id': '123456789', '%%': '%'}[x]
fileInfo: ->
data =
isReply: true
@ -437,6 +450,7 @@ Settings =
isImage: true
isSpoiler: true
FileInfo.format @value, data, @nextElementSibling
favicon: ->
Favicon.switch()
Unread.update() if g.VIEW is 'thread' and Conf['Unread Favicon']
@ -445,12 +459,14 @@ Settings =
img[1].src = Favicon.unreadSFW
img[2].src = Favicon.unreadNSFW
img[3].src = Favicon.unreadDead
togglecss: ->
if $('textarea[name=usercss]', $.x 'ancestor::fieldset[1]', @).disabled = !@checked
CustomCSS.rmStyle()
else
CustomCSS.addStyle()
$.cb.checked.call @
usercss: ->
CustomCSS.update()
@ -475,6 +491,7 @@ Settings =
for key, val of items
inputs[key].value = val
return
keybind: (e) ->
return if e.keyCode is 9 # tab
e.preventDefault()

View File

@ -328,6 +328,7 @@ 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://bugzilla.mozilla.org/show_bug.cgi?id=674955
o.workaround = (e) -> o.hoverend(e) unless root.contains e.target

View File

@ -443,6 +443,7 @@ do ->
delete $.oldValue[key]
cb undefined, key
$.on window, 'storage', ({key}) -> onChange key
$.forceSync = (key) ->
# Storage events don't work across origins
# e.g. http://boards.4chan.org and https://boards.4chan.org
@ -482,6 +483,7 @@ $.set = do ->
$.oldValue[key] = val
# for `storage` events
localStorage.setItem key, val
(keys, val) ->
if typeof keys is 'string'
set keys, val

View File

@ -62,7 +62,7 @@ class RandomAccessList
shift: ->
@rm @first.ID
order: ->
order = [item = @first]
order.push item while item = item.next

View File

@ -28,6 +28,7 @@ class Thread
icon.title = "This thread is on page #{pageNum} in the original index."
icon.textContent = "[#{pageNum}]"
@catalogView.nodes.pageCount.textContent = pageNum if @catalogView
setCount: (type, count, reachedLimit) ->
return unless @catalogView
el = @catalogView.nodes["#{type}Count"]
@ -47,6 +48,7 @@ class Thread
typeLC = type.toLowerCase()
icon = $ ".#{typeLC}Icon", @OP.nodes.info
return if !!icon is status
unless status
$.rm icon.previousSibling
$.rm icon
@ -57,6 +59,7 @@ class Thread
alt: type
title: type
className: "#{typeLC}Icon retina"
root = if type isnt 'Sticky' and @isSticky
$ '.stickyIcon', @OP.nodes.info
else

View File

@ -78,6 +78,7 @@ Gallery =
$.on d, 'keydown', cb.keybinds
$.off d, 'keydown', Keybinds.keydown if Conf['Keybinds']
for file in $$ '.post .file' when !$ '.fileDeletedRes, .fileDeleted', file
post = Get.postFromNode file
Gallery.generateThumb post
@ -87,6 +88,7 @@ Gallery =
if Header.getTopOf(candidate) + candidate.getBoundingClientRect().height >= 0
image = candidate
$.addClass doc, 'gallery-open'
$.add d.body, dialog
nodes.thumbs.scrollTop = 0
@ -103,6 +105,7 @@ Gallery =
return if post.isClone or post.isHidden
return unless post.file and (post.file.isImage or post.file.isVideo or Conf['PDF in Gallery'])
return if Gallery.fullIDs[post.fullID]
Gallery.fullIDs[post.fullID] = true
thumb = $.el 'a',
@ -110,13 +113,14 @@ Gallery =
href: post.file.URL
target: '_blank'
title: post.file.name
thumb.dataset.id = Gallery.images.length
thumb.dataset.id = Gallery.images.length
thumb.dataset.post = post.fullID
thumbImg = post.file.thumb.cloneNode false
thumbImg.style.cssText = ''
$.add thumb, thumbImg
$.on thumb, 'click', Gallery.cb.open
Gallery.images.push thumb
@ -135,6 +139,7 @@ Gallery =
elType = 'img'
elType = 'video' if /\.webm$/.test(thumb.href)
elType = 'iframe' if /\.pdf$/.test(thumb.href)
$[if elType is 'iframe' then 'addClass' else 'rmClass'] doc, 'gal-pdf'
file = $.el elType,
title: name.download = name.textContent = thumb.title
@ -142,7 +147,7 @@ Gallery =
Gallery.error file, thumb
file.src = name.href = thumb.href
$.extend file.dataset, thumb.dataset
$.extend file.dataset, thumb.dataset
nodes.current.pause?() unless nodes.current.error
$.replace nodes.current, file
if elType is 'video'
@ -241,10 +246,12 @@ Gallery =
Gallery.cb.open.call(
Gallery.images[+Gallery.nodes.current.dataset.id + 1] or Gallery.images[0]
)
click: (e) ->
return if ImageCommon.onControls e
e.preventDefault()
Gallery.cb.advance()
advance: -> if Gallery.nodes.current.paused then Gallery.nodes.current.play() else Gallery.cb.next()
toggle: -> (if Gallery.nodes then Gallery.cb.close else Gallery.build)()
blank: (e) -> Gallery.cb.close() if e.target is @

View File

@ -7,6 +7,7 @@ ImageExpand =
textContent: 'EAI'
title: 'Expand All Images'
href: 'javascript:;'
$.on @EAI, 'click', @cb.toggleAll
Header.addShortcut @EAI, 3
$.on d, 'scroll visibilitychange', @cb.playVideos
@ -18,14 +19,17 @@ ImageExpand =
node: ->
return unless @file and (@file.isImage or @file.isVideo)
$.on @file.thumb.parentNode, 'click', ImageExpand.cb.toggle
if @isClone
if @file.isExpanding
# If we clone a post where the image is still loading,
# make it loading in the clone too.
ImageExpand.contract @
ImageExpand.expand @
else if @file.isExpanded and @file.isVideo
ImageExpand.setupVideo @, !@origin.file.fullImage?.paused or @origin.file.wasPlaying, @file.fullImage.controls
else if ImageExpand.on and !@isHidden and !@isFetchedQuote and
(Conf['Expand spoilers'] or !@file.isSpoiler) and
(Conf['Expand videos'] or !@file.isVideo)
@ -246,7 +250,7 @@ ImageExpand =
el = $.el 'span',
textContent: 'Image Expansion'
className: 'image-expansion-link'
className: 'image-expansion-link'
{createSubEntry} = ImageExpand.menu
subEntries = []

View File

@ -9,13 +9,16 @@ ImageHover =
CatalogThread.callbacks.push
name: 'Catalog Image Hover'
cb: @catalogNode
node: ->
return unless @file and (@file.isImage or @file.isVideo)
$.on @file.thumb, 'mouseover', ImageHover.mouseover @
catalogNode: ->
{file} = @thread.OP
return unless file and (file.isImage or file.isVideo)
$.on @nodes.thumb, 'mouseover', ImageHover.mouseover @thread.OP
mouseover: (post) -> (e) ->
return unless doc.contains @
{file} = post
@ -30,6 +33,7 @@ ImageHover =
el.dataset.fullID = post.fullID
$.on el, 'error', error
el.src = file.URL
if Conf['Restart when Opened']
ImageCommon.rewind el
ImageCommon.rewind @
@ -45,7 +49,7 @@ ImageHover =
maxWidth = Math.max left, doc.clientWidth - right
maxHeight = doc.clientHeight - padding
scale = Math.min 1, maxWidth / width, maxHeight / height
el.style.maxWidth = "#{scale * width}px"
el.style.maxWidth = "#{scale * width}px"
el.style.maxHeight = "#{scale * height}px"
UI.hover
root: @
@ -61,6 +65,7 @@ ImageHover =
el.pause() if isVideo
$.rm el
el.removeAttribute 'style'
error: (post) -> ->
return if ImageCommon.decodeError @, post
ImageCommon.error @, post, 3 * $.SECOND, (URL) =>

View File

@ -73,6 +73,7 @@ ImageLoader =
if !chrome?
$.on thumb, 'loadeddata', -> @removeAttribute 'poster'
return
el = $.el if isImage then 'img' else 'video'
if replace and isImage
$.on el, 'load', ->

View File

@ -40,7 +40,7 @@ Sauce =
type
ext = post.file.URL.match(/\.([^\.]*)$/)?[1] or ''
return null unless !parts['boards'] or post.board.ID in parts['boards'].split ','
return null unless !parts['types'] or ext in parts['types'].split ','
return null unless !parts['types'] or ext in parts['types'].split ','
a = Sauce.link.cloneNode true
a.href = parts['url']
a.textContent = parts['text']
@ -49,8 +49,7 @@ Sauce =
node: ->
return if @isClone or !@file
nodes = []
for link in Sauce.links
if node = Sauce.createSauceLink link, @
# \u00A0 is nbsp
nodes.push $.tn('\u00A0'), node
for link in Sauce.links when node = Sauce.createSauceLink link, @
# \u00A0 is nbsp
nodes.push $.tn('\u00A0'), node
$.add @file.text, nodes

View File

@ -5,12 +5,14 @@ Menu =
@button = $.el 'a',
className: 'menu-button'
href: 'javascript:;'
$.extend @button, <%= html('<i class="fa fa-angle-down"></i>') %>
@menu = new UI.Menu 'post'
Post.callbacks.push
name: 'Menu'
cb: @node
CatalogThread.callbacks.push
name: 'Menu'
cb: @catalogNode

View File

@ -1,6 +1,6 @@
PSAHiding =
init: ->
return if !Conf['Announcement Hiding']
return unless Conf['Announcement Hiding']
$.addClass doc, 'hide-announcement'
$.one d, '4chanXInitFinished', @setup
@ -24,7 +24,9 @@ PSAHiding =
PSAHiding.btn = btn = $.el 'span',
title: 'Mark announcement as read and hide.'
className: 'hide-announcement'
$.extend btn, <%= html('[<a href="javascript:;">Dismiss</a>]') %>
$.on btn, 'click', PSAHiding.toggle
$.get 'hiddenPSA', 0, ({hiddenPSA}) ->
@ -33,6 +35,7 @@ PSAHiding =
$.rmClass doc, 'hide-announcement'
$.sync 'hiddenPSA', PSAHiding.sync
toggle: (e) ->
if $.hasClass @, 'hide-announcement'
UTC = +$.id('globalMessage').dataset.utc
@ -41,6 +44,7 @@ PSAHiding =
$.event 'CloseMenu'
$.delete 'hiddenPSA'
PSAHiding.sync UTC
sync: (UTC) ->
{psa} = PSAHiding
PSAHiding.hidden = PSAHiding.btn.hidden = UTC? and UTC >= +psa.dataset.utc

View File

@ -55,7 +55,7 @@ Banner =
i = Math.floor(Banner.choices.length * Math.random())
banner = Banner.choices.splice i, 1
$('img', @parentNode).src = "//s.4cdn.org/image/title/#{banner}"
click: (e) ->
if e.ctrlKey or e.metaKey
@contentEditable = true

View File

@ -2,12 +2,15 @@ CustomCSS =
init: ->
return unless Conf['Custom CSS']
@addStyle()
addStyle: ->
@style = $.addStyle Conf['usercss'], 'custom-css', -> $.id 'fourchanx-css'
rmStyle: ->
if @style
$.rm @style
delete @style
update: ->
unless @style
@addStyle()

View File

@ -12,10 +12,13 @@ ExpandComment =
node: ->
if a = $ '.abbr > a:not([onclick])', @nodes.comment
$.on a, 'click', ExpandComment.cb
callbacks: []
cb: (e) ->
e.preventDefault()
ExpandComment.expand Get.postFromNode @
expand: (post) ->
if post.nodes.longComment and !post.nodes.longComment.parentNode
$.replace post.nodes.shortComment, post.nodes.longComment
@ -24,12 +27,14 @@ ExpandComment =
return unless a = $ '.abbr > a', post.nodes.comment
a.textContent = "Post No.#{post} Loading..."
$.cache "//a.4cdn.org#{a.pathname.split('/').splice(0,4).join('/')}.json", -> ExpandComment.parse @, a, post
contract: (post) ->
return unless post.nodes.shortComment
a = $ '.abbr > a', post.nodes.shortComment
a.textContent = 'here'
$.replace post.nodes.longComment, post.nodes.shortComment
post.nodes.comment = post.nodes.shortComment
parse: (req, a, post) ->
{status} = req
unless status in [200, 304]

View File

@ -1,6 +1,6 @@
Fourchan =
init: ->
return if g.VIEW not in ['index', 'thread']
return unless g.VIEW in ['index', 'thread']
if g.BOARD.ID is 'g'
$.globalEval '''

View File

@ -12,6 +12,8 @@ IDColor =
span = $ '.hand', @nodes.uniqueID
return unless span and span.nodeName is 'SPAN'
rgb = IDColor.compute uid
# Style the damn node.
{style} = span
style.color = rgb[3]
style.backgroundColor = "rgb(#{rgb[0]},#{rgb[1]},#{rgb[2]})"
@ -21,16 +23,24 @@ IDColor =
compute: (uid) ->
return IDColor.ids[uid] if IDColor.ids[uid]
# Convert chars to integers, bitshift and math to create a larger integer
# Create a nice string of binary
hash = IDColor.hash uid
# Convert binary string to numerical values with bitshift and '&' truncation.
rgb = [
(hash >> 24) & 0xFF
(hash >> 16) & 0xFF
(hash >> 8) & 0xFF
]
# Weight color luminance values, assign a font color that should be readable.
rgb[3] = if (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) > 125
'#000'
else
'#fff'
# Cache.
@ids[uid] = rgb
hash: (uid) ->
@ -38,4 +48,4 @@ IDColor =
i = 0
while i < 8
msg = (msg << 5) - msg + uid.charCodeAt i++
msg
msg

View File

@ -1,6 +1,6 @@
IDHighlight =
init: ->
return if g.VIEW not in ['index', 'thread']
return unless g.VIEW in ['index', 'thread']
Post.callbacks.push
name: 'Highlight by User ID'

View File

@ -56,16 +56,16 @@ Keybinds =
else
return
when Conf['Spoiler tags']
return if target.nodeName isnt 'TEXTAREA'
return unless target.nodeName is 'TEXTAREA'
Keybinds.tags 'spoiler', target
when Conf['Code tags']
return if target.nodeName isnt 'TEXTAREA'
return unless target.nodeName is 'TEXTAREA'
Keybinds.tags 'code', target
when Conf['Eqn tags']
return if target.nodeName isnt 'TEXTAREA'
return unless target.nodeName is 'TEXTAREA'
Keybinds.tags 'eqn', target
when Conf['Math tags']
return if target.nodeName isnt 'TEXTAREA'
return unless target.nodeName is 'TEXTAREA'
Keybinds.tags 'math', target
when Conf['Toggle sage']
return unless QR.nodes and !QR.nodes.el.hidden

View File

@ -62,7 +62,9 @@ Nav =
# Add extra space to the end of the page if necessary so that all threads can be selected by keybinds.
extra = Header.getTopOf(thread) + doc.clientHeight - d.body.getBoundingClientRect().bottom
d.body.style.marginBottom = "#{extra}px" if extra > 0
Header.scrollTo thread
if extra > 0 and !Nav.haveExtra
Nav.haveExtra = true
$.on d, 'scroll', Nav.removeExtra

View File

@ -8,9 +8,11 @@ RemoveSpoilers =
Post.callbacks.push
name: 'Reveal Spoilers'
cb: @node
CatalogThread.callbacks.push
name: 'Reveal Spoilers'
cb: @node
if g.VIEW is 'archive'
$.ready -> RemoveSpoilers.unspoiler $.id 'arc-list'

View File

@ -5,6 +5,7 @@ Time =
Post.callbacks.push
name: 'Time Formatting'
cb: @node
node: ->
return if @isClone
@nodes.date.textContent = Time.format Conf['time'], @info.date
@ -14,6 +15,7 @@ Time =
Time.formatters[c].call(date)
else
s
day: [
'Sunday'
'Monday'
@ -23,6 +25,7 @@ Time =
'Friday'
'Saturday'
]
month: [
'January'
'February'
@ -37,7 +40,9 @@ Time =
'November'
'December'
]
zeroPad: (n) -> if n < 10 then "0#{n}" else n
formatters:
a: -> Time.day[@getDay()][...3]
A: -> Time.day[@getDay()]

View File

@ -18,6 +18,7 @@ ThreadStats =
$.extend sc, statsHTML
$.ready ->
Header.addShortcut sc
else
@dialog = sc = UI.dialog 'thread-stats', 'bottom: 0px; right: 0px;',
<%= html('<div class="move" title="${statsTitle}">&{statsHTML}</div>') %>

View File

@ -8,7 +8,7 @@ ThreadUpdater =
$.extend sc, <%= html('<span id="update-status"></span><span id="update-timer" title="Update now"></span>') %>
$.ready ->
Header.addShortcut sc
else
else
@dialog = sc = UI.dialog 'updater', 'bottom: 0px; left: 0px;',
<%= html('<div class="move"></div><span id="update-status"></span><span id="update-timer" title="Update now"></span>') %>
$.addClass doc, 'float'
@ -176,7 +176,7 @@ ThreadUpdater =
setInterval: ->
i = ThreadUpdater.interval + 1
if Conf['Optional Increase']
# Lower the max refresh rate limit on visible tabs.
cur = ThreadUpdater.outdateCount or 1

View File

@ -11,10 +11,10 @@ ThreadWatcher =
@db = new DataBoard 'watchedThreads', @refresh, true
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', <%= importHTML('Monitoring/ThreadWatcher') %>
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild
@refreshButton = $ '.move > .refresh', @dialog
@unreaddb = Unread.db or new DataBoard 'lastReadPosts'
$.on d, 'QRPostSuccessful', @cb.post
@ -23,6 +23,7 @@ ThreadWatcher =
$.on $('.move > .close', @dialog), 'click', @toggleWatcher
$.on d, '4chanXInitFinished', @ready
switch g.VIEW
when 'index'
$.on d, 'IndexRefresh', @cb.onIndexRefresh
@ -147,6 +148,7 @@ ThreadWatcher =
fetchCount:
fetched: 0
fetching: 0
fetchAuto: ->
clearTimeout ThreadWatcher.timeout
return unless Conf['Auto Update Thread Watcher']
@ -158,6 +160,7 @@ ThreadWatcher =
ThreadWatcher.fetchAllStatus()
db.save()
ThreadWatcher.timeout = setTimeout ThreadWatcher.fetchAuto, interval
fetchAllStatus: ->
ThreadWatcher.db.forceSync()
ThreadWatcher.unreaddb.forceSync()
@ -166,6 +169,7 @@ ThreadWatcher =
for thread in threads
ThreadWatcher.fetchStatus thread
return
fetchStatus: (thread) ->
{boardID, threadID, data} = thread
return if data.isDead and !Conf['Show Unread Count']
@ -177,6 +181,7 @@ ThreadWatcher =
$.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json",
onloadend: ->
ThreadWatcher.parseStatus.call @, thread
parseStatus: ({boardID, threadID, data}) ->
{fetchCount} = ThreadWatcher
fetchCount.fetched++
@ -233,6 +238,7 @@ ThreadWatcher =
delete data.unread
delete data.quotingYou
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
getAll: ->
@ -269,8 +275,8 @@ ThreadWatcher =
div = $.el 'div'
fullID = "#{boardID}.#{threadID}"
div.dataset.fullID = fullID
$.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
$.addClass div, 'dead-thread' if data.isDead
$.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
$.addClass div, 'dead-thread' if data.isDead
if Conf['Show Unread Count']
$.addClass div, 'replies-unread' if data.unread
$.addClass div, 'replies-quoting-you' if data.quotingYou
@ -336,6 +342,7 @@ ThreadWatcher =
ThreadWatcher.rm boardID, threadID
else
ThreadWatcher.add thread
add: (thread) ->
data = {}
boardID = thread.board.ID
@ -350,6 +357,7 @@ ThreadWatcher =
ThreadWatcher.refresh()
if Conf['Show Unread Count']
ThreadWatcher.fetchStatus {boardID, threadID, data}
rm: (boardID, threadID) ->
ThreadWatcher.db.delete {boardID, threadID}
ThreadWatcher.refresh()
@ -423,6 +431,7 @@ ThreadWatcher =
@refreshers.push refresh.bind entry if refresh
@menu.addEntry entry
return
createSubEntry: (name, desc) ->
entry =
type: 'thread watcher'
@ -430,6 +439,6 @@ ThreadWatcher =
entry.el.title = desc
input = entry.el.firstElementChild
$.on input, 'change', $.cb.checked
$.on input, 'change', ThreadWatcher.refresh if name in ['Current Board', 'Show Unread Count']
$.on input, 'change', ThreadWatcher.refresh if name in ['Current Board', 'Show Unread Count']
$.on input, 'change', ThreadWatcher.fetchAuto if name in ['Show Unread Count', 'Auto Update Thread Watcher']
entry

View File

@ -20,6 +20,7 @@ Unread =
Thread.callbacks.push
name: 'Unread'
cb: @node
Post.callbacks.push
name: 'Unread'
cb: @addPost

View File

@ -1,5 +1,5 @@
Captcha.noscript =
lifetime: 2 * $.MINUTE
lifetime: 2 * $.MINUTE
iframeURL: '//www.google.com/recaptcha/api/fallback?k=<%= meta.recaptchaKey %>'
init: ->

View File

@ -113,6 +113,7 @@ QR =
return
if Conf['QR Shortcut']
$.rmClass $('.qr-shortcut'), 'disabled'
close: ->
if QR.req
QR.abort()
@ -129,6 +130,7 @@ QR =
QR.cooldown.auto = false
QR.status()
QR.captcha.destroy()
focus: ->
$.queueTask ->
return unless QR.nodes
@ -142,18 +144,22 @@ QR =
$.on d, 'scroll', QR.scrollLock
else
$.off d, 'scroll', QR.scrollLock
scrollLock: (e) ->
if d.activeElement and QR.nodes.el.contains(d.activeElement) and d.activeElement.nodeName is 'IFRAME'
window.scroll window.scrollX, QR.scrollY
else
$.off d, 'scroll', QR.scrollLock
hide: ->
d.activeElement.blur()
$.addClass QR.nodes.el, 'autohide'
QR.nodes.autohide.checked = true
unhide: ->
$.rmClass QR.nodes.el, 'autohide'
QR.nodes.autohide.checked = false
toggleHide: ->
if @checked
QR.hide()
@ -195,6 +201,7 @@ QR =
<% } %>
notifications: []
cleanNotifications: ->
for notification in QR.notifications
notification.close()
@ -449,7 +456,8 @@ QR =
dialog: ->
QR.nodes = nodes =
el: dialog = UI.dialog 'qr', 'top: 50px; right: 0px;', <%= importHTML('Features/QuickReply') %>
el: dialog = UI.dialog 'qr', 'top: 50px; right: 0px;',
<%= importHTML('Features/QuickReply') %>
setNode = (name, query) ->
nodes[name] = $ query, dialog
@ -478,7 +486,7 @@ QR =
setNode 'spoilerPar', '#qr-spoiler-label'
setNode 'status', '[type=submit]'
setNode 'fileInput', '[type=file]'
rules = $('ul.rules').textContent.trim()
match_min = rules.match(/.+smaller than (\d+)x(\d+).+/)
match_max = rules.match(/.+greater than (\d+)x(\d+).+/)
@ -512,7 +520,9 @@ QR =
nodes.spoiler.parentElement.hidden = true
if g.BOARD.ID is 'f' and g.VIEW isnt 'thread'
nodes.flashTag = $.el 'select', name: 'filetag'
nodes.flashTag = $.el 'select',
name: 'filetag'
$.extend nodes.flashTag, <%= html(
'<option value="0">Hentai</option>' +
'<option value="6">Porn</option>' +
@ -522,6 +532,7 @@ QR =
'<option value="5">Loop</option>' +
'<option value="4" selected>Other</option>'
) %>
nodes.flashTag.dataset.default = '4'
$.add nodes.form, nodes.flashTag
@ -533,8 +544,8 @@ QR =
$.on nodes.urlButton, 'click', QR.handleUrl
$.on nodes.addPost, 'click', -> new QR.post true
$.on nodes.form, 'submit', QR.submit
$.on nodes.fileRM, 'click', -> QR.selected.rmFile()
$.on nodes.fileExtras, 'click', (e) -> e.stopPropagation()
$.on nodes.fileRM, 'click', -> QR.selected.rmFile()
$.on nodes.fileExtras, 'click', (e) -> e.stopPropagation()
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
$.on nodes.fileInput, 'change', QR.handleFiles
@ -562,6 +573,7 @@ QR =
QR.status()
QR.cooldown.init()
QR.captcha.init()
$.add d.body, dialog
QR.captcha.setup()
@ -794,7 +806,7 @@ QR =
QR.close()
else
post.rm()
QR.captcha.setup (d.activeElement is QR.nodes.status)
QR.captcha.setup(d.activeElement is QR.nodes.status)
QR.cooldown.add req.uploadEndTime, threadID, postID
@ -802,6 +814,7 @@ QR =
"#{window.location.origin}/#{g.BOARD}/thread/#{threadID}"
else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index
"#{window.location.origin}/#{g.BOARD}/thread/#{threadID}#p#{postID}"
if URL
if Conf['Open Post in New Tab'] or postsCount
$.open URL

View File

@ -71,6 +71,7 @@ QR.post = class
(QR.posts[index-1] or QR.posts[index+1]).select()
QR.posts.splice index, 1
QR.status()
delete: ->
$.rm @nodes.el
URL.revokeObjectURL @URL
@ -103,10 +104,13 @@ QR.post = class
load: ->
# Load this post's values.
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename']
continue unless node = QR.nodes[name]
node.value = @[name] or node.dataset.default or null
(if @thread isnt 'new' then $.addClass else $.rmClass) QR.nodes.el, 'reply-to-thread'
@showFileData()
QR.characterCount()

View File

@ -9,6 +9,7 @@ QuoteThreading =
@enabled = true
@controls = $.el 'span',
<%= html('<label><input id="threadingControl" type="checkbox" checked> Threading</label>') %>
@threadNewLink = $.el 'span',
className: 'brackets-wrap threadnewlink'
hidden: true
@ -27,6 +28,7 @@ QuoteThreading =
Thread.callbacks.push
name: 'Quote Threading'
cb: @setThread
Post.callbacks.push
name: 'Quote Threading'
cb: @node

View File

@ -55,7 +55,6 @@ Quotify =
className: 'quotelink deadlink'
target: '_blank'
textContent: "#{quote}\u00A0(Dead)"
$.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
else