Merge branch 'zixaphir-merge'

This brings various changes from @zixaphir's Appchan X into this fork:
- Fix updating of the preview text in the advanced settings panel.
- Close menus on scroll (fixes post menus not scrolling with their posts).
- Add "none" option (don't open any section) to the "OpenSettings" event.
- Give "Heaven" a white ID color.
- Some settings description improvements.
- Refactoring and minor changes.

Some changes are being deferred for now, but are intended to be added in future commits:
- addition of src/Miscellaneous/Flash.coffee
- rewrite of src/Quotelinks/QuoteBacklinks.coffee
- refactoring of various quote link features into QuoteMarkers.coffee (including change in QuoteInline.qiQuote)
- adding dataset.fullID for simpler Get.threadFromRoot and Get.postFromRoot
- "Except Archives" -> "Exempt Archives" setting change

The changes to Callbacks.execute in src/General/lib/callbacks.class and
elimination of the corresponding methods in Main.coffee may be incorporated
in the future, but will need to be rewritten to preserve the existing
25-posts-at-a-time functionality to prevent hanging on long threads.

The post/thread hiding changes from
https://github.com/MayhemYDG/4chan-x/pull/1457
are being rejected, at least for now, due to breaking thread stubs.
This commit is contained in:
ccd0 2015-02-06 23:47:17 -08:00
commit 12bc5d1612
37 changed files with 187 additions and 182 deletions

View File

@ -1,6 +1,6 @@
Anonymize =
init: ->
return if g.VIEW not in ['index', 'thread', 'archive'] or !Conf['Anonymize']
return unless g.VIEW in ['index', 'thread', 'archive'] and Conf['Anonymize']
return @archive() if g.VIEW is 'archive'
Post.callbacks.push

View File

@ -1,7 +1,7 @@
Filter =
filters: {}
init: ->
return if g.VIEW not in ['index', 'thread'] or !Conf['Filter']
return unless g.VIEW in ['index', 'thread'] and Conf['Filter']
unless Conf['Filtered Backlinks']
$.addClass doc, 'hide-backlinks'
@ -100,10 +100,8 @@ Filter =
node: ->
return if @isClone or @isFetchedQuote
for key of Filter.filters
value = Filter[key] @
for key of Filter.filters when (value = Filter[key] @) isnt false
# Continue if there's nothing to filter (no tripcode for example).
continue if value is false
for filter in Filter.filters[key] when result = filter value, @isReply
# Hide
@ -171,7 +169,7 @@ Filter =
menu:
init: ->
return if g.VIEW not in ['index', 'thread'] or !Conf['Menu'] or !Conf['Filter']
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Filter']
div = $.el 'div',
textContent: 'Filter'

View File

@ -22,10 +22,9 @@ Recursive =
rm: (recursive, post) ->
return unless obj = Recursive.recursives[post.fullID]
for rec, i in obj.recursives
if rec is recursive
obj.recursives.splice i, 1
obj.args.splice i, 1
for rec, i in obj.recursives when rec is recursive
obj.recursives.splice i, 1
obj.args.splice i, 1
return
apply: (recursive, post, args...) ->

View File

@ -1,13 +1,12 @@
Build =
staticPath: '//s.4cdn.org/image/'
gifIcon: if window.devicePixelRatio >= 2 then '@2x.gif' else '.gif'
initPixelRatio: window.devicePixelRatio
spoilerRange: {}
unescape: (text) ->
return text unless text?
text.replace(/<[^>]*>/g, '').replace /&(amp|#039|quot|lt|gt);/g, (c) ->
{'&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>'}[c]
shortFilename: (filename, isReply) ->
shortFilename: (filename) ->
threshold = 30
ext = filename.match(/\.?[^\.]*$/)[0]
if filename.length - ext.length > threshold
@ -15,8 +14,8 @@ Build =
else
filename
thumbRotate: do ->
n = 0
-> n = (n + 1) % 2
t = 0
-> t = (if t then 0 else 1)
sameThread: (boardID, threadID) ->
g.VIEW is 'thread' and g.BOARD.ID is boardID and g.THREADID is +threadID
postURL: (boardID, threadID, postID) ->
@ -83,8 +82,7 @@ Build =
name or= ''
subject or= ''
isOP = postID is threadID
retina = if Build.initPixelRatio >= 2 then '@2x' else ''
{staticPath, gifIcon} = Build
### Name Block ###
@ -92,19 +90,19 @@ Build =
when 'admin', 'admin_highlight'
capcodeClass = ' capcodeAdmin'
capcodeStart = <%= html(' <strong class="capcode hand id_admin" title="Highlight posts by the Administrator">## Admin</strong>') %>
capcodeIcon = <%= html(' <img src="//s.4cdn.org/image/adminicon${retina}.gif" alt="Admin Icon" title="This user is the 4chan Administrator." class="identityIcon retina">') %>
capcodeIcon = <%= html(' <img src="${staticPath}adminicon${gifIcon}" alt="Admin Icon" title="This user is the 4chan Administrator." class="identityIcon retina">') %>
when 'mod'
capcodeClass = ' capcodeMod'
capcodeStart = <%= html(' <strong class="capcode hand id_mod" title="Highlight posts by Moderators">## Mod</strong>') %>
capcodeIcon = <%= html(' <img src="//s.4cdn.org/image/modicon${retina}.gif" alt="Mod Icon" title="This user is a 4chan Moderator." class="identityIcon retina">') %>
capcodeIcon = <%= html(' <img src="${staticPath}modicon${gifIcon}" alt="Mod Icon" title="This user is a 4chan Moderator." class="identityIcon retina">') %>
when 'developer'
capcodeClass = ' capcodeDeveloper'
capcodeStart = <%= html(' <strong class="capcode hand id_developer" title="Highlight posts by Developers">## Developer</strong>') %>
capcodeIcon = <%= html(' <img src="//s.4cdn.org/image/developericon${retina}.gif" alt="Developer Icon" title="This user is a 4chan Developer." class="identityIcon retina">') %>
capcodeIcon = <%= html(' <img src="${staticPath}developericon${gifIcon}" alt="Developer Icon" title="This user is a 4chan Developer." class="identityIcon retina">') %>
when 'manager'
capcodeClass = ' capcodeManager'
capcodeStart = <%= html(' <strong class="capcode hand id_manager" title="Highlight posts by Managers">## Manager</strong>') %>
capcodeIcon = <%= html(' <img src="//s.4cdn.org/image/managericon${retina}.gif" alt="Manager Icon" title="This user is a 4chan Manager." class="identityIcon retina">') %>
capcodeIcon = <%= html(' <img src="${staticPath}managericon${gifIcon}" alt="Manager Icon" title="This user is a 4chan Manager." class="identityIcon retina">') %>
else
capcodeClass = ''
capcodeStart = <%= html('') %>
@ -155,7 +153,7 @@ Build =
icons = for type in ['Sticky', 'Closed', 'Archived'] when o["is#{type}"] and !(type is 'Closed' and o.isArchived)
typeLC = type.toLowerCase()
<%= html(' <img src="//s.4cdn.org/image/${typeLC}${retina}.gif" alt="${type}" title="${type}" class="${typeLC}Icon retina">') %>
<%= html(' <img src="${staticPath}${typeLC}${gifIcon}" alt="${type}" title="${type}" class="${typeLC}Icon retina">') %>
replyLink = if isOP and g.VIEW is 'index'
<%= html(' &nbsp; <span>[<a href="/${boardID}/thread/${threadID}" class="replylink">Reply</a>]</span>') %>
@ -181,7 +179,7 @@ Build =
fileCont = if file?.isDeleted
<%= html(
'<span class="fileThumb">' +
'<img src="//s.4cdn.org/image/filedeleted-res${retina}.gif" alt="File deleted." class="fileDeletedRes retina">' +
'<img src="${staticPath}filedeleted-res${gifIcon}" alt="File deleted." class="fileDeletedRes retina">' +
'</span>'
) %>
else if file and boardID is 'f'
@ -196,9 +194,9 @@ Build =
shortFilename = 'Spoiler Image'
if spoilerRange = Build.spoilerRange[boardID]
# Randomize the spoiler image.
fileThumb = "//s.4cdn.org/image/spoiler-#{boardID}#{Math.floor 1 + spoilerRange * Math.random()}.png"
fileThumb = "#{staticPath}spoiler-#{boardID}#{Math.floor 1 + spoilerRange * Math.random()}.png"
else
fileThumb = '//s.4cdn.org/image/spoiler.png'
fileThumb = "#{staticPath}spoiler.png"
file.twidth = file.theight = 100
else
shortFilename = Build.shortFilename file.name, !isOP

View File

@ -53,7 +53,7 @@ Config =
]
'Comment Expansion': [
true
'Add buttons to expand too long comments.'
'Expand Comments that are too long to display on the index. Not applicable with JSON Navigation.'
]
'File Info Formatting': [
true
@ -118,6 +118,11 @@ Config =
true
'Convert text into links where applicable.'
]
'Link Title': [
true
'Replace the link of a supported site with its actual title. Currently Supported: YouTube, Vimeo, SoundCloud, and Github gists'
1
]
'Embedding': [
true
'Embed supported services. Note: Some services don\'t work on HTTPS.'
@ -133,11 +138,6 @@ Config =
'Embed content in a frame that remains in place when the page is scrolled.'
2
]
'Link Title': [
true
'Replace the link of a supported site with its actual title. Currently Supported: YouTube, Vimeo, SoundCloud, and Github gists'
1
]
'Filtering':
'Anonymize': [
@ -384,8 +384,8 @@ Config =
'All-in-one form to reply, create threads, automate dumping and more.'
]
'QR Shortcut': [
true,
'Adds a small [QR] link in the header.'
true
'Add a shortcut to the header to toggle the QR.'
1
]
'Persistent QR': [
@ -598,7 +598,7 @@ Config =
]
'Auto Prune': [
false
'Automatically prune dead threads.'
'Automatically remove dead threads.'
]
'Show Unread Count': [
true

View File

@ -9,6 +9,11 @@ g =
FAQ: '<%= meta.faq %>'
CHANGELOG: '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md'
boards: {}
E = (text) ->
(text+'').replace /[&"'<>]/g, (x) ->
{'&': '&amp;', "'": '&#039;', '"': '&quot;', '<': '&lt;', '>': '&gt;'}[x]
E = do ->
str = {'&': '&amp;', "'": '&#039;', '"': '&quot;', '<': '&lt;', '>': '&gt;'}
r = String::replace
regex = /[&"'<>]/g
fn = (x) ->
str[x]
(text) -> r.call text, regex, fn

View File

@ -3,8 +3,8 @@ Main =
if location.hostname is 'www.google.com'
return $.ready -> Captcha.noscript.initFrame()
g.threads = new SimpleDict
g.posts = new SimpleDict
g.threads = new SimpleDict()
g.posts = new SimpleDict()
pathname = location.pathname.split '/'
g.BOARD = new Board pathname[1]
@ -90,6 +90,7 @@ Main =
error: err
# finally
# c.timeEnd "#{name} initialization"
# c.timeEnd 'All initializations'
$.ready Main.initReady
@ -163,19 +164,19 @@ Main =
Settings.open()
$.set 'previousversion', g.VERSION
return unless Conf['Show Support Message']
<% if (type === 'userscript') { %>
GMver = GM_info.version.split '.'
for v, i in "<%= meta.min.greasemonkey %>".split '.'
continue if v is GMver[i]
(v < GMver[i]) or new Notice 'warning', "Your version of Greasemonkey is outdated (v#{GM_info.version} instead of v<%= meta.min.greasemonkey %> minimum) and <%= meta.name %> may not operate correctly.", 30
break
<% } %>
if Conf['Show Support Message']
<% if (type === 'userscript') { %>
GMver = GM_info.version.split '.'
for v, i in "<%= meta.min.greasemonkey %>".split '.'
continue if v is GMver[i]
(v < GMver[i]) or new Notice 'warning', "Your version of Greasemonkey is outdated (v#{GM_info.version} instead of v<%= meta.min.greasemonkey %> minimum) and <%= meta.name %> may not operate correctly.", 30
break
<% } %>
try
localStorage.getItem '4chan-settings'
catch err
new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to operate properly.', 30
try
localStorage.getItem '4chan-settings'
catch err
new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to operate properly.', 30
initThread: ->
if board = $ '.board'

View File

@ -10,14 +10,16 @@ Settings =
Header.addShortcut link
Settings.addSection 'Main', Settings.main
Settings.addSection 'Filter', Settings.filter
Settings.addSection 'Sauce', Settings.sauce
Settings.addSection 'Advanced', Settings.advanced
Settings.addSection 'Keybinds', Settings.keybinds
add = @addSection
add 'Main', @main
add 'Filter', @filter
add 'Sauce', @sauce
add 'Advanced', @advanced
add 'Keybinds', @keybinds
$.on d, 'AddSettingsSection', Settings.addSection
$.on d, 'OpenSettings', (e) -> Settings.open e.detail
$.on d, 'OpenSettings', (e) -> Settings.open e.detail
if Conf['Disable Native Extension']
settings = JSON.parse(localStorage.getItem '4chan-settings') or {}
@ -26,22 +28,22 @@ Settings =
localStorage.setItem '4chan-settings', JSON.stringify settings
open: (openSection) ->
return if Settings.dialog
return if Settings.overlay
$.event 'CloseMenu'
Settings.overlay = overlay = $.el 'div',
id: 'overlay'
Settings.dialog = dialog = $.el 'div',
id: 'fourchanx-settings'
className: 'dialog'
$.extend dialog, <%= importHTML('Settings/Settings') %>
$('a[href$="/CHANGELOG.md"]', dialog).textContent = g.VERSION
$.on $('.export', Settings.dialog), 'click', Settings.export
$.on $('.import', Settings.dialog), 'click', Settings.import
$.on $('.reset', Settings.dialog), 'click', Settings.reset
$.on $('input', Settings.dialog), 'change', Settings.onImport
Settings.overlay = overlay = $.el 'div',
id: 'overlay'
$.on $('.export', dialog), 'click', Settings.export
$.on $('.import', dialog), 'click', Settings.import
$.on $('.reset', dialog), 'click', Settings.reset
$.on $('input', dialog), 'change', Settings.onImport
links = []
for section in Settings.sections
@ -54,7 +56,7 @@ Settings =
sectionToOpen = link if section.title is openSection
links.pop()
$.add $('.sections-list', dialog), links
(if sectionToOpen then sectionToOpen else links[0]).click()
(if sectionToOpen then sectionToOpen else links[0]).click() unless openSection is 'none'
$.on $('.close', dialog), 'click', Settings.close
$.on overlay, 'click', Settings.close
@ -103,8 +105,9 @@ Settings =
div = $.el 'div',
<%= html('<label><input type="checkbox" name="${key}">${key}</label><span class="description">: ${description}</span>') %>
input = $ 'input', div
$.on input, 'change', $.cb.checked
$.on input, 'change', -> @parentNode.parentNode.dataset.checked = @checked
$.on input, 'change', ->
@parentNode.parentNode.dataset.checked = @checked
$.cb.checked.call @
items[key] = Conf[key]
inputs[key] = input
level = arr[2] or 0
@ -160,7 +163,7 @@ Settings =
<% } %>
a.click()
import: ->
$('input', @parentNode).click()
$('input[type=file]', @parentNode).click()
onImport: ->
return unless file = @files[0]
@ -305,17 +308,21 @@ Settings =
items = {}
inputs = {}
for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss']
input = $ "[name=#{name}]", section
input = $ "[name='#{name}']", section
items[name] = Conf[name]
inputs[name] = input
event = if name in ['favicon', 'usercss']
'change'
else
'input'
$.on input, event, $.cb.value
if name is 'usercss'
$.on input, 'change', $.cb.value
else if name is 'favicon'
$.on input, 'change', $.cb.value
$.on input, 'change', Settings[name]
else
$.on input, 'input', $.cb.value
$.on input, 'input', Settings[name]
# Quick Reply Personas
ta = $ '.personafield', section
$.get 'QR.personas', Conf['QR.personas'], (item) ->
ta.value = item['QR.personas']
$.on ta, 'change', $.cb.value
@ -325,7 +332,6 @@ Settings =
input = inputs[key]
input.value = val
continue if key is 'usercss'
$.on input, event, Settings[key]
Settings[key].call input
return
@ -484,6 +490,7 @@ Settings =
inputs[key] = input
$.on input, 'keydown', Settings.keybind
$.add tbody, tr
$.get items, (items) ->
for key, val of items
inputs[key].value = val

View File

@ -65,8 +65,7 @@ UI = do ->
$.addClass lastToggledButton, 'active'
$.on d, 'click', @close
$.on d, 'CloseMenu', @close
$.one d, 'click scroll CloseMenu', @close
$.add button, menu
# Position
@ -121,7 +120,6 @@ UI = do ->
$.rmClass lastToggledButton, 'active'
currentMenu = null
lastToggledButton = null
$.off d, 'click CloseMenu', @close
findNextEntry: (entry, direction) ->
entries = [entry.parentNode.children...]
@ -162,6 +160,7 @@ UI = do ->
onFocus: (e) =>
e.stopPropagation()
@focus e.target
focus: (entry) ->
while focused = $.x 'parent::*/child::*[contains(@class,"focused")]', entry
$.rmClass focused, 'focused'
@ -376,7 +375,6 @@ UI = do ->
$.add label, [input, $.tn " #{text}"]
label
return {
dialog: dialog
Menu: Menu

View File

@ -6,7 +6,9 @@
<a href="javascript:;" class="gal-close">×</a>
</span>
<a class="gal-name" target="_blank"></a>
<span class="gal-count"><span class="count"></span> / <span class="total"></span></span>
<span class="gal-count">
<span class="count"></span> / <span class="total"></span>
</span>
<div class="gal-prev"></div>
<div class="gal-image">
<a href="javascript:;"><img></a>

View File

@ -10,9 +10,9 @@
</div>
<form>
<div class=persona>
<input name=name data-name=name list="list-name" placeholder=Name class=field size=1>
<input name=name data-name=name list="list-name" placeholder=Name class=field size=1>
<input name=email data-name=email list="list-email" placeholder=Options class=field size=1>
<input name=sub data-name=sub list="list-sub" placeholder=Subject class=field size=1>
<input name=sub data-name=sub list="list-sub" placeholder=Subject class=field size=1>
</div>
<div class=textarea>
<textarea data-name=com placeholder=Comment class=field></textarea>

View File

@ -13,7 +13,7 @@
</fieldset>
<fieldset>
<legend>Custom Board Navigation</span></legend>
<legend>Custom Board Navigation</legend>
<div><textarea name=boardnav class=field spellcheck=false></textarea></div>
<span class=note>New lines will be converted into spaces.</span><br><br>
<div class=note>In the following examples for /g/, <code>g</code> can be changed to a different board ID (<code>a</code>, <code>b</code>, etc...), the current board (<code>current</code>), or the Twitter link (<code>@</code>).</div>

View File

@ -215,7 +215,7 @@ $.one = (el, events, handler) ->
$.event = (event, detail, root=d) ->
<% if (type === 'userscript') { %>
if detail? and typeof cloneInto is 'function'
detail = cloneInto detail, document.defaultView
detail = cloneInto detail, d.defaultView
<% } %>
root.dispatchEvent new CustomEvent event, {bubbles: true, detail}

View File

@ -2,7 +2,7 @@ class Board
toString: -> @ID
constructor: (@ID) ->
@threads = new SimpleDict
@posts = new SimpleDict
@threads = new SimpleDict()
@posts = new SimpleDict()
g.boards[@] = @
g.boards[@] = @

View File

@ -1,5 +1,5 @@
class CatalogThread
@callbacks = new Callbacks 'CatalogThread'
@callbacks = new Callbacks 'Catalog Thread'
toString: -> @ID
constructor: (root, @thread) ->

View File

@ -1,11 +1,11 @@
class Connection
constructor: (@target, @origin, @cb) ->
$.on window, 'message', @onMessage.bind @
$.on window, 'message', @onMessage
send: (data) ->
send: (data) =>
@target.postMessage "#{g.NAMESPACE}#{JSON.stringify data}", @origin
onMessage: (e) ->
onMessage: (e) =>
return unless e.source is @target and
e.origin is @origin and
typeof e.data is 'string' and

View File

@ -17,7 +17,7 @@ class Fetcher
clone = post.addClone @context, ($.hasClass @root, 'dialog')
Main.callbackNodes Clone, [clone]
# Get rid of the side arrows.
# Get rid of the side arrows/stubs.
{nodes} = clone
$.rmAll nodes.root
$.add nodes.root, nodes.post
@ -36,13 +36,14 @@ class Fetcher
{status} = req
unless status in [200, 304]
# The thread can die by the time we check a quote.
unless @archivedPost()
$.addClass @root, 'warning'
@root.textContent =
if status is 404
"Thread No.#{@threadID} 404'd."
else
"Error #{req.statusText} (#{req.status})."
return if @archivedPost()
$.addClass @root, 'warning'
@root.textContent =
if status is 404
"Thread No.#{@threadID} 404'd."
else
"Error #{req.statusText} (#{req.status})."
return
{posts} = req.response
@ -60,9 +61,10 @@ class Fetcher
return
# The post can be deleted by the time we check a quote.
unless @archivedPost()
$.addClass @root, 'warning'
@root.textContent = "Post No.#{@postID} was not found."
return if @archivedPost()
$.addClass @root, 'warning'
@root.textContent = "Post No.#{@postID} was not found."
return
board = g.boards[@boardID] or

View File

@ -234,10 +234,9 @@ class Post
for clone in @clones
clone.resurrect()
for quotelink in Get.allQuotelinksLinkingTo @
if $.hasClass quotelink, 'deadlink'
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
$.rmClass quotelink, 'deadlink'
for quotelink in Get.allQuotelinksLinkingTo @ when $.hasClass quotelink, 'deadlink'
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
$.rmClass quotelink, 'deadlink'
return
collect: ->

View File

@ -4,7 +4,7 @@ class Thread
constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}"
@posts = new SimpleDict
@posts = new SimpleDict()
@isDead = false
@isHidden = false
@isOnTop = false

View File

@ -1,6 +1,6 @@
ArchiveLink =
init: ->
return if g.VIEW not in ['index', 'thread'] or !Conf['Menu'] or !Conf['Archive Link']
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Archive Link']
div = $.el 'div',
textContent: 'Archive'

View File

@ -1,6 +1,6 @@
DeleteLink =
init: ->
return if g.VIEW not in ['index', 'thread'] or !Conf['Menu'] or !Conf['Delete Link']
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Delete Link']
div = $.el 'div',
className: 'delete-link'

View File

@ -1,6 +1,6 @@
DownloadLink =
init: ->
return if g.VIEW not in ['index', 'thread'] or !Conf['Menu'] or !Conf['Download Link']
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Download Link']
a = $.el 'a',
className: 'download-link'

View File

@ -1,6 +1,6 @@
Menu =
init: ->
return if g.VIEW not in ['index', 'thread'] or !Conf['Menu']
return unless g.VIEW in ['index', 'thread'] and Conf['Menu']
@button = $.el 'a',
className: 'menu-button'

View File

@ -1,6 +1,6 @@
ReportLink =
init: ->
return if g.VIEW not in ['index', 'thread'] or !Conf['Menu'] or !Conf['Report Link']
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Report Link']
a = $.el 'a',
className: 'report-link'

View File

@ -16,16 +16,15 @@ Banner =
if g.BOARD.ID isnt 'f' and g.VIEW is 'thread' and Conf['Remove Thread Excerpt']
Banner.setTitle children[1].textContent
i = 0
while child = children[i++]
if i is 1
for child, i in children
if i is 0
child.title = "Click to change"
$.on child, 'click', Banner.cb.toggle
continue
if Conf['Custom Board Titles']
Banner.custom(child).title = "Ctrl/\u2318+click to edit board #{if i is 3
Banner.custom(child).title = "Ctrl/\u2318+click to edit board #{if i is 2
'sub'
else
''}title"

View File

@ -13,5 +13,5 @@ CustomCSS =
update: ->
unless @style
@addStyle()
return @addStyle()
@style.textContent = Conf['usercss']

View File

@ -1,6 +1,6 @@
ExpandComment =
init: ->
return if g.VIEW isnt 'index' or !Conf['Comment Expansion']
return if g.VIEW isnt 'index' or !Conf['Comment Expansion'] or Conf['JSON Navigation']
@callbacks.push Fourchan.code if g.BOARD.ID is 'g'
@callbacks.push Fourchan.math if g.BOARD.ID is 'sci'

View File

@ -1,28 +1,26 @@
IDColor =
init: ->
return if g.VIEW not in ['index', 'thread'] or not Conf['Color User IDs']
@ids = {}
return unless g.VIEW in ['index', 'thread'] and Conf['Color User IDs']
@ids = {
Heaven: [0, 0, 0, '#fff']
}
Post.callbacks.push
name: 'Color User IDs'
cb: @node
node: ->
return if @isClone or not uid = @info.uniqueID
span = $ '.hand', @nodes.uniqueID
return unless span and span.nodeName is 'SPAN'
rgb = IDColor.compute uid
return if @isClone or !((uid = @info.uniqueID) and (span = $ 'span.hand', @nodes.uniqueID))
rgb = IDColor.ids[uid] or IDColor.compute uid
# Style the damn node.
{style} = span
style.color = rgb[3]
style.backgroundColor = "rgb(#{rgb[0]},#{rgb[1]},#{rgb[2]})"
$.addClass span, 'painted'
span.title = 'Highlight posts by this ID'
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
@ -35,7 +33,7 @@ IDColor =
]
# 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
rgb.push if (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) > 125
'#000'
else
'#fff'

View File

@ -46,7 +46,7 @@ Keybinds =
else if (notifications = $$ '.notification').length
for notification in notifications
$('.close', notification).click()
else if QR.nodes and !QR.nodes.el.hidden and window.getComputedStyle(QR.nodes.form).display isnt 'none'
else if QR.nodes and not (QR.nodes.el.hidden or window.getComputedStyle(QR.nodes.form).display is 'none')
if Conf['Persistent QR']
QR.hide()
else
@ -139,19 +139,19 @@ Keybinds =
Index.cycleSortType()
# Thread Navigation
when Conf['Next thread']
return if g.VIEW isnt 'index' or !threadRoot
return unless g.VIEW is 'index' and threadRoot
Nav.scroll +1
when Conf['Previous thread']
return if g.VIEW isnt 'index' or !threadRoot
return unless g.VIEW is 'index' and threadRoot
Nav.scroll -1
when Conf['Expand thread']
return if g.VIEW isnt 'index' or !threadRoot
return unless g.VIEW is 'index' and threadRoot
ExpandThread.toggle thread
when Conf['Open thread']
return if g.VIEW isnt 'index' or !threadRoot
return unless g.VIEW is 'index' and threadRoot
Keybinds.open thread
when Conf['Open thread tab']
return if g.VIEW isnt 'index' or !threadRoot
return unless g.VIEW is 'index' and threadRoot
Keybinds.open thread, true
# Reply Navigation
when Conf['Next reply']

View File

@ -1,6 +1,6 @@
Time =
init: ->
return if g.VIEW not in ['index', 'thread'] or !Conf['Time Formatting']
return unless g.VIEW in ['index', 'thread'] and Conf['Time Formatting']
Post.callbacks.push
name: 'Time Formatting'

View File

@ -7,7 +7,7 @@ MarkNewIPs =
node: ->
MarkNewIPs.ipCount = @ipCount
MarkNewIPs.postIDs = @posts.keys.map (x) -> +x
MarkNewIPs.postIDs = (+x for x in @posts.keys)
$.on d, 'ThreadUpdate', MarkNewIPs.onUpdate
onUpdate: (e) ->
@ -35,11 +35,7 @@ MarkNewIPs =
suffix = if (ipCount // 10) % 10 is 1
'th'
else
switch ipCount % 10
when 1 then 'st'
when 2 then 'nd'
when 3 then 'rd'
else 'th'
['st', 'nd', 'rd'][ipCount % 10 - 1] or 'th' # fuck switches
counter = $.el 'span',
className: 'ip-counter'
textContent: "(#{ipCount})"

View File

@ -84,5 +84,5 @@ ThreadStats =
ThreadStats.pageCountEl.textContent = page.page
(if page.page is @response.length then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning'
# Thread data may be stale (modification date given < time of last post). If so, try again on next thread update.
ThreadStats.lastPageUpdate = new Date thread.last_modified * 1000
ThreadStats.lastPageUpdate = new Date thread.last_modified * $.SECOND
return

View File

@ -12,7 +12,7 @@ ThreadUpdater =
@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'
$.ready =>
$.ready ->
$.add d.body, sc
@checkPostCount = 0
@ -120,11 +120,11 @@ ThreadUpdater =
-> not d.hidden
autoUpdate: (e) ->
ThreadUpdater.count ThreadUpdater.isUpdating = @checked
interval: ->
interval: (e) ->
val = parseInt @value, 10
if val < 1 then val = 1
ThreadUpdater.interval = @value = val
$.cb.value.call @
$.cb.value.call @ if e
load: (e) ->
{req} = ThreadUpdater
switch req.status
@ -239,6 +239,7 @@ ThreadUpdater =
ThreadUpdater.req?.abort()
ThreadUpdater.req = $.ajax "//a.4cdn.org/#{ThreadUpdater.thread.board}/thread/#{ThreadUpdater.thread}.json",
onloadend: ThreadUpdater.cb.load
timeout: $.MINUTE
,
whenModified: true
@ -344,7 +345,7 @@ ThreadUpdater =
$.event 'ThreadUpdate',
404: false
threadID: ThreadUpdater.thread.fullID
newPosts: posts.map (post) -> post.fullID
newPosts: (post.fullID for post in posts)
postCount: OP.replies + 1
fileCount: OP.images + (!!ThreadUpdater.thread.OP.file and !ThreadUpdater.thread.OP.file.isDead)
ipCount: OP.unique_ips

View File

@ -14,7 +14,7 @@ ThreadWatcher =
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild
@refreshButton = $ '.move > .refresh', @dialog
@refreshButton = $ '.refresh', @dialog
@unreaddb = Unread.db or new DataBoard 'lastReadPosts'
$.on d, 'QRPostSuccessful', @cb.post
@ -36,13 +36,6 @@ ThreadWatcher =
ThreadWatcher.fetchAuto()
Post.callbacks.push
name: 'Thread Watcher'
cb: @node
CatalogThread.callbacks.push
name: 'Thread Watcher'
cb: @catalogNode
if g.VIEW is 'index' and Conf['JSON Navigation'] and Conf['Menu'] and g.BOARD.ID isnt 'f'
Menu.menu.addEntry
el: $.el 'a', href: 'javascript:;'
@ -60,6 +53,13 @@ ThreadWatcher =
$.on @el, 'click', @cb
true
Post.callbacks.push
name: 'Thread Watcher'
cb: @node
CatalogThread.callbacks.push
name: 'Thread Watcher'
cb: @catalogNode
isWatched: (thread) ->
ThreadWatcher.db?.get {boardID: thread.board.ID, threadID: thread.ID}

View File

@ -1,20 +1,21 @@
Unread =
init: ->
return if g.VIEW isnt 'thread' or
!Conf['Unread Count'] and
!Conf['Unread Favicon'] and
!Conf['Unread Line'] and
!Conf['Scroll to Last Read Post'] and
!Conf['Thread Watcher'] and
!Conf['Desktop Notifications'] and
!Conf['Quote Threading']
return unless g.VIEW is 'thread' and (
Conf['Unread Count'] or
Conf['Unread Favicon'] or
Conf['Unread Line'] or
Conf['Scroll to Last Read Post'] or
Conf['Thread Watcher'] or
Conf['Desktop Notifications'] or
Conf['Quote Threading']
)
@db = new DataBoard 'lastReadPosts', @sync
@hr = $.el 'hr',
id: 'unread-line'
@posts = new Set
@postsQuotingYou = new Set
@order = new RandomAccessList
@posts = new Set()
@postsQuotingYou = new Set()
@order = new RandomAccessList()
@position = null
Thread.callbacks.push

View File

@ -9,6 +9,7 @@ Captcha.noscript =
container = $.el 'div',
className: 'captcha-img'
title: 'Reload reCAPTCHA'
input = $.el 'input',
className: 'captcha-input field'
title: 'Verification'
@ -103,7 +104,7 @@ Captcha.noscript =
$.off input, 'focus click', @cb.focus
if QR.nodes.el.getBoundingClientRect().bottom > doc.clientHeight
QR.nodes.el.style.top = null
QR.nodes.el.style.top = ''
QR.nodes.el.style.bottom = '0px'
destroy: ->

View File

@ -13,6 +13,17 @@ QR =
noscript = Conf['Force Noscript Captcha'] or !doc.dataset.jsEnabled
@captcha = Captcha[if noscript then 'noscript' else 'v2']
$.on d, '4chanXInitFinished', @initReady
window.addEventListener 'focus', @focus, true
window.addEventListener 'blur', @focus, true
# We don't receive blur events from captcha iframe.
$.on d, 'click', @focus
Post.callbacks.push
name: 'Quick Reply'
cb: @node
if Conf['QR Shortcut']
@shortcut = sc = $.el 'a',
className: 'qr-shortcut fa fa-comment-o disabled'
@ -35,17 +46,6 @@ QR =
# Prevent unnecessary loading of fallback iframe.
$.onExists doc, '#postForm noscript', true, $.rm
$.on d, '4chanXInitFinished', @initReady
window.addEventListener 'focus', @focus, true
window.addEventListener 'blur', @focus, true
# We don't receive blur events from captcha iframe.
$.on d, 'click', @focus
Post.callbacks.push
name: 'Quick Reply'
cb: @node
initReady: ->
$.off d, '4chanXInitFinished', @initReady
QR.postingIsEnabled = !!$.id 'postForm'
@ -318,7 +318,7 @@ QR =
QR.open()
QR.handleFiles files
$.addClass QR.nodes.el, 'dump'
handleUrl: ->
url = prompt 'Enter a URL:'
return if url is null
@ -429,7 +429,6 @@ QR =
if (e.ctrlKey or e.metaKey) and e.type is 'click'
$.addClass QR.nodes.filename, 'edit'
QR.nodes.filename.focus()
return $.on QR.nodes.filename, 'blur', -> $.rmClass QR.nodes.filename, 'edit'
return if e.target.nodeName is 'INPUT' or (e.keyCode and e.keyCode not in [32, 13]) or e.ctrlKey
e.preventDefault()
QR.nodes.fileInput.click()
@ -437,11 +436,11 @@ QR =
generatePostableThreadsList: ->
return unless QR.nodes
list = QR.nodes.thread
options = [list.firstChild]
options = [list.firstElementChild]
for thread in g.BOARD.threads.keys
options.push $.el 'option',
value: thread
textContent: "Thread No.#{thread}"
textContent: "No.#{thread}"
val = list.value
$.rmAll list
$.add list, options
@ -548,6 +547,7 @@ QR =
$.on nodes.urlButton, 'click', QR.handleUrl
$.on nodes.addPost, 'click', -> new QR.post true
$.on nodes.form, 'submit', QR.submit
$.on nodes.filename, 'blur', -> $.rmClass @, 'edit'
$.on nodes.fileRM, 'click', -> QR.selected.rmFile()
$.on nodes.fileExtras, 'click', (e) -> e.stopPropagation()
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
@ -665,7 +665,7 @@ QR =
QR.status()
QR.error $.el 'span',
<%= html(
'${g.NAME} encountered an error while posting. ' +
meta.name + ' encountered an error while posting. ' +
'[<a href="//4chan.org/banned" target="_blank">Banned?</a>] ' +
'[<a href="${g.FAQ}#what-does-4chan-x-encountered-an-error-while-posting-please-try-again-mean" target="_blank">More info</a>]'
) %>