Merge branch 'next'

This commit is contained in:
ccd0 2019-07-24 15:12:11 -07:00
commit 61954985bf
30 changed files with 413 additions and 224 deletions

View File

@ -246,18 +246,64 @@ Filter =
re
$.set type, save, cb
removeFilters: (type, res, cb) ->
$.get type, Conf[type], (item) ->
save = item[type]
res = res.map(Filter.escape).join('|')
save = save.replace RegExp("(?:$\n|^)(?:#{res})$", 'mg'), ''
$.set type, save, cb
showFilters: (type) ->
# Open the settings and display & focus the relevant filter textarea.
Settings.open 'Filter'
section = $ '.section-container'
select = $ 'select[name=filter]', section
select.value = type
Settings.selectFilter.call select
$.onExists section, 'textarea', (ta) ->
tl = ta.textLength
ta.setSelectionRange tl, tl
ta.focus()
quickFilterMD5: ->
post = Get.postFromNode @
return unless post.file
Filter.addFilter 'MD5', "/#{post.file.MD5}/"
files = post.files.filter((f) -> f.MD5)
return unless files.length
filter = files.map((f) -> "/#{f.MD5}/").join('\n')
Filter.addFilter 'MD5', filter
origin = post.origin or post
if origin.isReply
PostHiding.hide origin
else if g.VIEW is 'index'
ThreadHiding.hide origin.thread
# If post is still visible, give an indication that the MD5 was filtered.
if post.nodes.post.getBoundingClientRect().height
new Notice 'info', 'MD5 filtered.', 2
{notice} = Filter.quickFilterMD5
if notice
notice.filters.push filter
notice.posts.push origin
$('span', notice.el).textContent = "#{notice.filters.length} MD5s filtered."
else
msg = $.el 'div',
<%= html('<span>MD5 filtered.</span> [<a href="javascript:;">show</a>] [<a href="javascript:;">undo</a>]') %>
notice = Filter.quickFilterMD5.notice = new Notice 'info', msg, undefined, ->
delete Filter.quickFilterMD5.notice
notice.filters = [filter]
notice.posts = [origin]
links = $$ 'a', msg
$.on links[0], 'click', Filter.quickFilterCB.show.bind(notice)
$.on links[1], 'click', Filter.quickFilterCB.undo.bind(notice)
quickFilterCB:
show: ->
Filter.showFilters 'MD5'
@close()
undo: ->
Filter.removeFilters 'MD5', @filters
for post in @posts
if post.isReply
PostHiding.show post
else if g.VIEW is 'index'
ThreadHiding.show post.thread
@close()
escape: (value) ->
value.replace ///
@ -346,13 +392,4 @@ Filter =
).join('\n')
Filter.addFilter type, res, ->
# Open the settings and display & focus the relevant filter textarea.
Settings.open 'Filter'
section = $ '.section-container'
select = $ 'select[name=filter]', section
select.value = type
Settings.selectFilter.call select
$.onExists section, 'textarea', (ta) ->
tl = ta.textLength
ta.setSelectionRange tl, tl
ta.focus()
Filter.showFilters type

View File

@ -145,7 +145,7 @@ ThreadHiding =
a
makeStub: (thread, root) ->
numReplies = $$(g.SITE.selectors.postContainer + g.SITE.selectors.relative.replyPost, root).length
numReplies = $$(g.SITE.selectors.replyOriginal, root).length
numReplies += +summary.textContent.match /\d+/ if summary = $ g.SITE.selectors.summary, root
a = ThreadHiding.makeButton thread, 'show'

View File

@ -1,4 +1,6 @@
Get =
url: (type, IDs, args...) ->
g.sites[IDs.siteID]?.urls[type] IDs, args...
threadExcerpt: (thread) ->
{OP} = thread
excerpt = ("/#{decodeURIComponent thread.board.ID}/ - ") + (

View File

@ -234,7 +234,10 @@ Header =
href: "/#{g.BOARD.ID}/"
textContent: text or g.BOARD.ID
className: 'current'
if /-catalog/.test(t)
if /-index/.test(t)
a.dataset.only = 'index'
else if /-catalog/.test(t)
a.dataset.only = 'catalog'
a.href += 'catalog.html'
else if /-(archive|expired)/.test(t)
a = a.firstChild # Its text node.
@ -263,9 +266,10 @@ Header =
text or boardID
if m = t.match /-(index|catalog)/
unless boardID is 'f' and m[1] is 'catalog'
urlIC = CatalogLinks[m[1]] {siteID: '4chan.org', boardID}
if urlIC
a.dataset.only = m[1]
a.href = CatalogLinks[m[1]] boardID
a.href = urlIC
$.addClass a, 'catalog' if m[1] is 'catalog'
else
return a.firstChild # Its text node.

View File

@ -2,14 +2,17 @@ Index =
showHiddenThreads: false
changed: {}
enabledOn: ({siteID, boardID}) ->
Conf['JSON Index'] and g.sites[siteID].software is 'yotsuba' and boardID isnt 'f'
init: ->
return unless g.VIEW is 'index' and g.BOARD.ID isnt 'f'
return unless g.VIEW is 'index'
# For IndexRefresh events
$.one d, '4chanXInitFinished', @cb.initFinished
$.on d, 'PostsInserted', @cb.postsInserted
return unless Conf['JSON Index']
return unless @enabledOn g.BOARD
@enabled = true
@ -197,7 +200,7 @@ Index =
menu:
init: ->
return if g.VIEW isnt 'index' or !Conf['JSON Index'] or !Conf['Menu'] or !Conf['Thread Hiding Link'] or g.BOARD.ID is 'f'
return unless g.VIEW is 'index' and Conf['Menu'] and Conf['Thread Hiding Link'] and Index.enabledOn(g.BOARD)
Menu.menu.addEntry
el: $.el 'a',

View File

@ -561,10 +561,13 @@ Settings =
$.id('lastarchivecheck').textContent = 'never'
items = {}
for name in ['archiveLists', 'archiveAutoUpdate', 'fourchanImageHost', 'captchaLanguage', 'captchaServiceDomain', 'boardnav', 'time', 'timeLocale', 'backlink', 'pastedname', 'fileInfo', 'QR.personas', 'favicon', 'usercss', 'customCooldown', 'jsWhitelist']
for name, input of inputs when name not in ['captchaServiceKey', 'Interval', 'Custom CSS']
items[name] = Conf[name]
input = inputs[name]
event = if name in ['archiveLists', 'archiveAutoUpdate', 'QR.personas', 'favicon', 'usercss'] then 'change' else 'input'
event = if (
input.nodeName is 'SELECT' or
input.type in ['checkbox', 'radio'] or
(input.nodeName is 'TEXTAREA' and name not of Settings)
) then 'change' else 'input'
$.on input, event, $.cb[if input.type is 'checkbox' then 'checked' else 'value']
$.on input, event, Settings[name] if name of Settings

View File

@ -19,6 +19,16 @@
<button id="update-archives">Update now</button> Last updated: <time id="lastarchivecheck"></time> <label><input type="checkbox" name="archiveAutoUpdate"> Auto-update</label>
</fieldset>
<fieldset>
<legend>External Catalog</legend>
<div class="warning" data-feature="External Catalog"><code>External Catalog</code> is disabled. This will be used only as a fallback.</div>
<div>
URLs of external catalog sites, where <code>%board</code> is to be replaced by the board name.<br>
Each URL should be followed by <code>;boards:</code> and optionally <code>;exclude:</code> and a list of supported/excluded boards in the format explained in the Filter guide.
</div>
<textarea hidden name="externalCatalogURLs" class="field" spellcheck="false"></textarea>
</fieldset>
<fieldset>
<legend>Override 4chan Image Host</legend>
<div>Change 4chan image links to this domain. Leave blank for no change.</div>
@ -173,3 +183,9 @@
</div>
<textarea hidden name="jsWhitelist" class="field" spellcheck="false"></textarea>
</fieldset>
<fieldset>
<legend>Known Banners</legend>
<div>List of known banners, used for click-to-change feature.</div>
<textarea hidden name="knownBanners" class="field" spellcheck="false"></textarea>
</fieldset>

View File

@ -1,6 +1,4 @@
Banner =
banners: `<%= JSON.stringify(readJSON('banners.json')) %>`
init: ->
if Conf['Custom Board Titles']
@db = new DataBoard 'customTitles', null, true
@ -44,7 +42,7 @@ Banner =
cb:
toggle: ->
unless Banner.choices?.length
Banner.choices = Banner.banners.slice()
Banner.choices = Conf['knownBanners'].split(',').slice()
i = Math.floor(Banner.choices.length * Math.random())
banner = Banner.choices.splice i, 1
$('img', @parentNode).src = "//s.4cdn.org/image/title/#{banner}"

View File

@ -13,10 +13,11 @@ CatalogLinks =
link.href = CatalogLinks.index()
when "/#{g.BOARD}/catalog"
link.href = CatalogLinks.catalog()
if g.VIEW is 'catalog' and Conf['JSON Index'] and Conf['Use <%= meta.name %> Catalog']
if g.VIEW is 'catalog' and (catalogURL = CatalogLinks.catalog()) isnt g.SITE.urls.catalog?(g.BOARD)
catalogLink = link.parentNode.cloneNode true
catalogLink.firstElementChild.textContent = '<%= meta.name %> Catalog'
catalogLink.firstElementChild.href = CatalogLinks.catalog()
link2 = catalogLink.firstElementChild
link2.href = catalogURL
link2.textContent = if link2.hostname is location.hostname then '<%= meta.name %> Catalog' else 'External Catalog'
$.after link.parentNode, [$.tn(' '), catalogLink]
return
@ -57,33 +58,63 @@ CatalogLinks =
setLinks: (list) ->
return unless (CatalogLinks.enabled ? Conf['Catalog Links']) and list
# do not transform links unless they differ from the expected value at most by this tail
tail = /(?:index)?(?:\.\w+)?$/
for a in $$('a:not([data-only])', list)
continue if (
a.hostname not in ['boards.4chan.org', 'boards.4channel.org', 'catalog.neet.tv'] or
!(board = a.pathname.split('/')[1]) or
board in ['f', 'status', '4chan'] or
a.pathname.split('/')[2] is 'archive' or
$.hasClass a, 'external'
)
{siteID, boardID} = a.dataset
unless siteID and boardID
{siteID, boardID, VIEW} = Site.parseURL a
continue unless (
siteID and boardID and
VIEW in ['index', 'catalog'] and
(a.dataset.indexOptions or a.href.replace(tail, '') is Get.url(VIEW, {siteID, boardID}).replace(tail, ''))
)
$.extend a.dataset, {siteID, boardID}
# Href is easier than pathname because then we don't have
# conditions where External Catalog has been disabled between switches.
a.href = if Conf['Header catalog links'] then CatalogLinks.catalog(board) else "//#{BoardConfig.domain(board)}/#{board}/"
if a.dataset.indexOptions and a.hostname in ['boards.4chan.org', 'boards.4channel.org'] and a.pathname.split('/')[2] is ''
a.href += (if a.hash then '/' else '#') + a.dataset.indexOptions
board = {siteID, boardID}
url = if Conf['Header catalog links'] then CatalogLinks.catalog(board) else Get.url('index', board)
if url
a.href = url
if a.dataset.indexOptions and url.split('#')[0] is Get.url('index', board)
a.href += (if a.hash then '/' else '#') + a.dataset.indexOptions
return
catalog: (board=g.BOARD.ID) ->
if Conf['External Catalog'] and board in ['3', 'a', 'adv', 'an', 'asp', 'biz', 'c', 'cgl', 'ck', 'cm', 'co', 'diy', 'f', 'fa', 'fit', 'g', 'gd', 'his', 'i', 'int', 'jp', 'k', 'lgbt', 'lit', 'm', 'mlp', 'mu', 'n', 'news', 'o', 'out', 'p', 'po', 'pol', 's4s', 'sci', 'sp', 'tg', 'toy', 'trv', 'tv', 'v', 'vg', 'vip', 'vp', 'vr', 'w', 'wg', 'wsg', 'wsr', 'x']
"//catalog.neet.tv/#{board}/"
else if Conf['JSON Index'] and Conf['Use <%= meta.name %> Catalog']
if location.hostname in ['boards.4chan.org', 'boards.4channel.org'] and g.BOARD.ID is board and g.VIEW is 'index' then '#catalog' else "//#{BoardConfig.domain(board)}/#{board}/#catalog"
else
"//#{BoardConfig.domain(board)}/#{board}/catalog"
externalParse: ->
CatalogLinks.externalList = {}
for line in Conf['externalCatalogURLs'].split '\n'
continue if line[0] is '#'
url = line.split(';')[0]
boards = Filter.parseBoards(line.match(/;boards:([^;]+)/)?[1] or '*')
excludes = Filter.parseBoards(line.match(/;exclude:([^;]+)/)?[1]) or {}
for board of boards
unless excludes[board] or excludes[board.split('/')[0] + '/*']
CatalogLinks.externalList[board] = url
return
index: (board=g.BOARD.ID) ->
if Conf['JSON Index'] and board isnt 'f'
if location.hostname in ['boards.4chan.org', 'boards.4channel.org'] and g.BOARD.ID is board and g.VIEW is 'index' then '#index' else "//#{BoardConfig.domain(board)}/#{board}/#index"
external: ({siteID, boardID}) ->
CatalogLinks.externalParse() unless CatalogLinks.externalList
external = (CatalogLinks.externalList["#{siteID}/#{boardID}"] or CatalogLinks.externalList["#{siteID}/*"])
if external then external.replace(/%board/g, boardID) else undefined
jsonIndex: (board, hash) ->
if g.SITE.ID is board.siteID and g.BOARD.ID is board.boardID and g.VIEW is 'index'
hash
else
"//#{BoardConfig.domain(board)}/#{board}/"
Get.url('index', board) + hash
catalog: (board=g.BOARD) ->
if Conf['External Catalog'] and (external = CatalogLinks.external board)
external
else if Index.enabledOn(board) and Conf['Use <%= meta.name %> Catalog']
CatalogLinks.jsonIndex board, '#catalog'
else if (nativeCatalog = Get.url 'catalog', board)
nativeCatalog
else
CatalogLinks.external board
index: (board=g.BOARD) ->
if Index.enabledOn(board)
CatalogLinks.jsonIndex board, '#index'
else
Get.url 'index', board

View File

@ -58,6 +58,7 @@ ExpandThread =
return if @ isnt status.req # aborted
delete status.req
ExpandThread.parse @, thread, a
status.numReplies = $$(g.SITE.selectors.replyOriginal, thread.nodes.root).length
contract: (thread, a, threadRoot) ->
status = ExpandThread.statuses[thread]
@ -69,15 +70,7 @@ ExpandThread =
return
replies = $$ '.thread > .replyContainer', threadRoot
if !Conf['JSON Index'] or Conf['Show Replies']
num = if thread.isSticky
1
else switch g.BOARD.ID
# XXX boards config
when 'b', 'vg', 'bant' then 3
when 't' then 1
else 5
replies = replies[...-num]
replies = replies[...(-status.numReplies)] if status.numReplies
postsCount = 0
filesCount = 0
for reply in replies

View File

@ -21,14 +21,9 @@ Keybinds =
{target} = e
if target.nodeName in ['INPUT', 'TEXTAREA']
return unless /(Esc|Alt|Ctrl|Meta|Shift\+\w{2,})/.test(key) and not /^Alt\+(\d|Up|Down|Left|Right)$/.test(key)
unless (
g.VIEW not in ['index', 'thread'] or
g.VIEW is 'index' and Conf['JSON Index'] and Conf['Index Mode'] is 'catalog' or
g.VIEW is 'index' and g.BOARD.ID is 'f'
)
if g.VIEW in ['index', 'thread']
threadRoot = Nav.getThread()
if op = $ '.op', threadRoot
thread = Get.postFromNode(op).thread
thread = Get.threadFromRoot threadRoot
switch key
# QR & Options
when Conf['Toggle board list']
@ -93,10 +88,10 @@ Keybinds =
when Conf['Update']
switch g.VIEW
when 'thread'
return unless Conf['Thread Updater']
return unless ThreadUpdater.enabled
ThreadUpdater.update()
when 'index'
return unless Conf['JSON Index'] and g.BOARD.ID isnt 'f'
return unless Index.enabled
Index.update()
else
return
@ -118,10 +113,11 @@ Keybinds =
# Images
when Conf['Expand image']
return unless ImageExpand.enabled and threadRoot
Keybinds.img threadRoot
post = Get.postFromNode Keybinds.post threadRoot
ImageExpand.toggle post if post.file
when Conf['Expand images']
return unless ImageExpand.enabled and threadRoot
Keybinds.img threadRoot, true
return unless ImageExpand.enabled
ImageExpand.cb.toggleAll()
when Conf['Open Gallery']
return unless Gallery.enabled
Gallery.cb.toggle()
@ -133,47 +129,51 @@ Keybinds =
FappeTyme.toggle 'werk'
# Board Navigation
when Conf['Front page']
if Conf['JSON Index'] and g.VIEW is 'index' and g.BOARD.ID isnt 'f'
if Index.enabled
Index.userPageNav 1
else
location.href = "/#{g.BOARD}/"
when Conf['Open front page']
$.open "#{location.origin}/#{g.BOARD}/"
when Conf['Next page']
return unless g.VIEW is 'index' and g.BOARD.ID isnt 'f'
if Conf['JSON Index']
return unless g.VIEW is 'index' and !g.SITE.isOnePage?(g.BOARD)
if Index.enabled
return unless Conf['Index Mode'] in ['paged', 'infinite']
$('.next button', Index.pagelist).click()
else
if form = $ '.next form'
location.href = form.action
$(g.SITE.selectors.nav.next)?.click()
when Conf['Previous page']
return unless g.VIEW is 'index' and g.BOARD.ID isnt 'f'
if Conf['JSON Index']
return unless g.VIEW is 'index' and !g.SITE.isOnePage?(g.BOARD)
if Index.enabled
return unless Conf['Index Mode'] in ['paged', 'infinite']
$('.prev button', Index.pagelist).click()
else
if form = $ '.prev form'
location.href = form.action
$(g.SITE.selectors.nav.prev)?.click()
when Conf['Search form']
return unless g.VIEW is 'index' and g.BOARD.ID isnt 'f'
searchInput = if Conf['JSON Index'] then Index.searchInput else $.id('search-box')
return unless g.VIEW is 'index'
searchInput = if Index.enabled
Index.searchInput
else if g.SITE.selectors.searchBox
$ g.SITE.selectors.searchBox
else
undefined
return unless searchInput
Header.scrollToIfNeeded searchInput
searchInput.focus()
when Conf['Paged mode']
return unless Conf['JSON Index'] and g.BOARD.ID isnt 'f'
return unless Index.enabledOn(g.BOARD)
location.href = if g.VIEW is 'index' then '#paged' else "/#{g.BOARD}/#paged"
when Conf['Infinite scrolling mode']
return unless Conf['JSON Index'] and g.BOARD.ID isnt 'f'
return unless Index.enabledOn(g.BOARD)
location.href = if g.VIEW is 'index' then '#infinite' else "/#{g.BOARD}/#infinite"
when Conf['All pages mode']
return unless Conf['JSON Index'] and g.BOARD.ID isnt 'f'
return unless Index.enabledOn(g.BOARD)
location.href = if g.VIEW is 'index' then '#all-pages' else "/#{g.BOARD}/#all-pages"
when Conf['Open catalog']
return if g.BOARD.ID is 'f'
location.href = CatalogLinks.catalog()
return unless (catalog = CatalogLinks.catalog())
location.href = catalog
when Conf['Cycle sort type']
return unless Conf['JSON Index'] and g.VIEW is 'index' and g.BOARD.ID isnt 'f'
return unless Index.enabled
Index.cycleSortType()
# Thread Navigation
when Conf['Next thread']
@ -269,7 +269,11 @@ Keybinds =
key
post: (thread) ->
$('.post.highlight', thread) or $('.op', thread)
s = g.SITE.selectors
(
$("#{s.postContainer}#{s.highlightable.reply}.#{g.SITE.classes.highlight}", thread) or
$("#{if g.SITE.isOPContainerThread then s.thread else s.postContainer}#{s.highlightable.op}", thread)
)
qr: (thread) ->
QR.open()
@ -309,49 +313,44 @@ Keybinds =
""
else "sage"
img: (thread, all) ->
if all
ImageExpand.cb.toggleAll()
else
post = Get.postFromNode Keybinds.post thread
ImageExpand.toggle post if post.file
open: (thread, tab) ->
return if g.VIEW isnt 'index'
url = "/#{thread.board}/thread/#{thread}"
url = Get.url 'thread', thread
if tab
$.open location.origin + url
$.open url
else
location.href = url
hl: (delta, thread) ->
postEl = $ '.reply.highlight', thread
replySelector = "#{g.SITE.selectors.postContainer}#{g.SITE.selectors.highlightable.reply}"
{highlight} = g.SITE.classes
postEl = $ "#{replySelector}.#{highlight}", thread
unless delta
$.rmClass postEl, 'highlight' if postEl
$.rmClass postEl, highlight if postEl
return
if postEl
{height} = postEl.getBoundingClientRect()
if Header.getTopOf(postEl) >= -height and Header.getBottomOf(postEl) >= -height # We're at least partially visible
root = postEl.parentNode
{root} = Get.postFromNode(postEl).nodes
axis = if delta is +1
'following'
else
'preceding'
return if not (next = $.x "#{axis}-sibling::div[contains(@class,'replyContainer') and not(@hidden) and not(child::div[@class='stub'])][1]/child::div[contains(@class,'reply')]", root)
return unless (next = $.x "#{axis}-sibling::#{g.SITE.xpath.replyContainer}[not(@hidden) and not(child::div[@class='stub'])][1]", root)
next = $ replySelector, next unless next.matches(replySelector)
Header.scrollToIfNeeded next, delta is +1
@focus next
$.rmClass postEl, 'highlight'
$.addClass next, highlight
$.rmClass postEl, highlight
return
$.rmClass postEl, 'highlight'
$.rmClass postEl, highlight
replies = $$ '.reply', thread
replies = $$ replySelector, thread
replies.reverse() if delta is -1
for reply in replies
if delta is +1 and Header.getTopOf(reply) > 0 or delta is -1 and Header.getBottomOf(reply) > 0
@focus reply
$.addClass reply, highlight
return
focus: (post) ->
$.addClass post, 'highlight'
return

View File

@ -39,22 +39,24 @@ Nav =
Nav.scroll +1
getThread: ->
return $ '.board' if $.hasClass doc, 'catalog-mode'
for threadRoot in $$ '.thread'
return g.threads["#{g.BOARD}.#{g.THREADID}"].nodes.root if g.VIEW is 'thread'
return if $.hasClass doc, 'catalog-mode'
for threadRoot in $$ g.SITE.selectors.thread
thread = Get.threadFromRoot threadRoot
continue if thread.isHidden and !thread.stub
if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past
return threadRoot
return $ '.board'
return
scroll: (delta) ->
d.activeElement?.blur()
thread = Nav.getThread()
return unless thread
axis = if delta is +1
'following'
else
'preceding'
if next = $.x "#{axis}-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread
if next = $.x "#{axis}-sibling::#{g.SITE.xpath.thread}[not(@hidden)][1]", thread
# 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.

View File

@ -1,6 +1,7 @@
ThreadUpdater =
init: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Updater']
@enabled = true
# Chromium won't play audio created in an inactive tab until the tab has been focused, so set it up now.
# XXX Sometimes the loading stalls in Firefox, esp. when opening in private browsing window followed by normal window.

View File

@ -628,7 +628,7 @@ QR =
$.rm nodes.flag
delete nodes.flag
if g.BOARD.ID is 'pol'
if g.BOARD.config.troll_flags
flag = QR.flags()
flag.dataset.name = 'flag'
flag.dataset.default = '0'

View File

@ -92,7 +92,8 @@ QuoteYou =
cb:
seek: (type) ->
$.rmClass highlight, 'highlight' if highlight = $ '.highlight'
{highlight} = g.SITE.classes
$.rmClass highlighted, highlight if (highlighted = $ ".#{highlight}")
unless QuoteYou.lastRead and doc.contains(QuoteYou.lastRead) and $.hasClass(QuoteYou.lastRead, 'quotesYou')
if not (post = QuoteYou.lastRead = $ '.quotesYou')
@ -111,12 +112,16 @@ QuoteYou =
QuoteYou.cb.scroll posts[if type is 'following' then 0 else posts.length - 1]
scroll: (root) ->
post = $ '.post', root
if !post.getBoundingClientRect().height
post = Get.postFromRoot root
if !post.nodes.post.getBoundingClientRect().height
return false
else
QuoteYou.lastRead = root
location.href = "##{post.id}"
Header.scrollTo post
$.addClass post, 'highlight'
location.href = Get.url('post', post)
Header.scrollTo post.nodes.post
if post.isReply
sel = "#{g.SITE.selectors.postContainer}#{g.SITE.selectors.highlightable.reply}"
node = post.nodes.root
node = $ sel, node unless node.matches(sel)
$.addClass node, g.SITE.classes.highlight
return true

View File

@ -11,7 +11,7 @@ class Callbacks
@keys.push name unless @[name]
@[name] = cb
execute: (node, keys=@keys, force) ->
execute: (node, keys=@keys, force=false) ->
return if node.callbacksExecuted and !force
node.callbacksExecuted = true
for name in keys

View File

@ -1,7 +1,12 @@
Post.Clone = class extends Post
isClone: true
constructor: (@origin, @context, contractThumb) ->
constructor: ->
that = Object.create(Post.Clone.prototype)
that.construct arguments...
return that
construct: (@origin, @context, contractThumb) ->
for key in ['ID', 'postID', 'threadID', 'boardID', 'siteID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply']
# Copy or point to the origin's key value.
@[key] = @origin[key]

View File

@ -825,6 +825,10 @@ Config =
lastarchivecheck: 0
archiveAutoUpdate: true
externalCatalogURLs: """
//catalog.neet.tv/%board/;boards:4chan.org:3,a,adv,an,asp,biz,c,cgl,ck,cm,co,diy,f,fa,fit,g,gd,his,i,int,jp,k,lgbt,lit,m,mlp,mu,n,news,o,out,p,po,pol,s4s,sci,sp,tg,toy,trv,tv,v,vg,vip,vp,vr,w,wg,wsg,wsr,x
"""
boardnav: """
[ toggle-all ]
a-replace
@ -1177,3 +1181,5 @@ Config =
fourchanImageHost: 'i.4cdn.org'
hiddenPSAList: [{}]
knownBanners: '<%= readJSON('banners.json').join(',') %>'

View File

@ -9,9 +9,15 @@
}
/* 4chan style fixes */
:root.photon.sw-yotsuba #arc-list tr:nth-of-type(odd) span.quote {
:root.photon #arc-list tr:nth-of-type(odd) span.quote {
color: #C0E17A;
}
:root.photon.highlight-you .quotesYou$site$highlightable$reply {
border-left: 3px solid rgba(221, 0, 0, .8) !important;
}
:root.photon.highlight-own .yourPost$site$highlightable$reply {
border-left: 3px dashed rgba(221, 0, 0, .8) !important;
}
/* Header */
:root.photon #header-bar.dialog {

View File

@ -9,9 +9,15 @@
}
/* 4chan style fixes */
:root.spooky.sw-yotsuba #arc-list span.quote {
:root.spooky #arc-list span.quote {
color: #634C2C;
}
:root.spooky.highlight-you .quotesYou$site$highlightable$reply {
border-left: 3px solid rgba(145, 182, 214, .8) !important;
}
:root.spooky.highlight-own .yourPost$site$highlightable$reply {
border-left: 3px dashed rgba(145, 182, 214, .8) !important;
}
/* Header */
:root.spooky #header-bar.dialog {
@ -67,16 +73,16 @@
:root.spooky .qphl {
outline: 2px solid rgba(145, 182, 214, .8);
}
:root.spooky.highlight-you .quotesYou$site$relative$opHighlight,
:root.spooky.highlight-you .quotesYou$site$relative$replyPost {
:root.spooky.highlight-you .quotesYou$site$highlightable$op,
:root.spooky.highlight-you .quotesYou$site$highlightable$reply {
border-left: 3px solid rgba(145, 182, 214, .8);
}
:root.spooky.highlight-own .yourPost$site$relative$opHighlight,
:root.spooky.highlight-own .yourPost$site$relative$replyPost {
:root.spooky.highlight-own .yourPost$site$highlightable$op,
:root.spooky.highlight-own .yourPost$site$highlightable$reply {
border-left: 3px dashed rgba(145, 182, 214, .8);
}
:root.spooky .filter-highlight$site$relative$opHighlight,
:root.spooky .filter-highlight$site$relative$replyPost {
:root.spooky .filter-highlight$site$highlightable$op,
:root.spooky .filter-highlight$site$highlightable$reply {
box-shadow: inset 5px 0 rgba(145, 182, 214, .5);
}
:root.spooky.highlight-own .yourPost > $site$sideArrows,

View File

@ -658,7 +658,9 @@ div[data-checked="false"] > .suboption-list {
.section-advanced textarea {
height: 150px;
}
.section-advanced textarea[name="archiveLists"] {
.section-advanced textarea[name="archiveLists"],
.section-advanced textarea[name="externalCatalogURLs"],
.section-advanced textarea[name="knownBanners"] {
height: 75px;
}
.section-advanced .archive-cell {
@ -1383,8 +1385,8 @@ input[name="Default Volume"] {
margin: 0px;
}
/* Fappe and Werk Tyme */
:root.fappeTyme $site$relative$replyOriginal.noFile,
:root.fappeTyme $site$relative$replyOriginal.noFile + br {
:root.fappeTyme $site$replyOriginal.noFile,
:root.fappeTyme $site$replyOriginal.noFile + br {
display: none;
}
:root.werkTyme $site$thumbLink,
@ -1438,16 +1440,16 @@ input[name="Default Volume"] {
.qphl {
outline: 2px solid rgba(216, 94, 49, .8);
}
:root.highlight-you .quotesYou$site$relative$opHighlight,
:root.highlight-you .quotesYou$site$relative$replyPost {
:root.highlight-you .quotesYou$site$highlightable$op,
:root.highlight-you .quotesYou$site$highlightable$reply {
border-left: 3px solid rgba(221, 0, 0, .8);
}
:root.highlight-own .yourPost$site$relative$opHighlight,
:root.highlight-own .yourPost$site$relative$replyPost {
:root.highlight-own .yourPost$site$highlightable$op,
:root.highlight-own .yourPost$site$highlightable$reply {
border-left: 3px dashed rgba(221, 0, 0, .8);
}
.filter-highlight$site$relative$opHighlight,
.filter-highlight$site$relative$replyPost {
.filter-highlight$site$highlightable$op,
.filter-highlight$site$highlightable$reply {
box-shadow: inset 5px 0 rgba(221, 0, 0, .5);
}
:root.highlight-own .yourPost > $site$sideArrows,
@ -1455,9 +1457,9 @@ input[name="Default Volume"] {
.filter-highlight > $site$sideArrows {
color: rgba(221, 0, 0, .8);
}
:root.highlight-own .yourPost$site$relative$opHighlight::after,
:root.highlight-you .quotesYou$site$relative$opHighlight::after,
.filter-highlight$site$relative$opHighlight::after {
:root.highlight-own .yourPost$site$highlightable$op::after,
:root.highlight-you .quotesYou$site$highlightable$op::after,
.filter-highlight$site$highlightable$op::after {
content: "";
display: block;
clear: both;
@ -1466,7 +1468,7 @@ input[name="Default Volume"] {
:root.werkTyme .catalog-thread.filter-highlight:not(:hover),
:root.werkTyme:not(.catalog-hover-expand) .catalog-thread.filter-highlight,
:root.werkTyme.catalog-hover-expand .catalog-thread.filter-highlight > .catalog-container:hover > .catalog-post,
:root.catalog $site$catalog$thread.filter-highlight$site$relative$catalogHighlight {
:root.catalog $site$catalog$thread.filter-highlight$site$highlightable$catalog {
box-shadow: 0 0 3px 3px rgba(255, 0, 0, .5);
}
:root:not(.werkTyme) .catalog-thread.watched .catalog-thumb,

View File

@ -5,9 +5,15 @@
}
/* 4chan style fixes */
:root.tomorrow.sw-yotsuba #arc-list span.quote {
:root.tomorrow #arc-list span.quote {
color: #B5BD68;
}
:root.tomorrow.highlight-you .quotesYou$site$highlightable$reply {
border-left: 3px solid rgba(145, 182, 214, .8) !important;
}
:root.tomorrow.highlight-own .yourPost$site$highlightable$reply {
border-left: 3px dashed rgba(145, 182, 214, .8) !important;
}
/* Header */
:root.tomorrow #header-bar.dialog {
@ -63,16 +69,16 @@
:root.tomorrow .qphl {
outline: 2px solid rgba(145, 182, 214, .8);
}
:root.tomorrow.highlight-you .quotesYou$site$relative$opHighlight,
:root.tomorrow.highlight-you .quotesYou$site$relative$replyPost {
:root.tomorrow.highlight-you .quotesYou$site$highlightable$op,
:root.tomorrow.highlight-you .quotesYou$site$highlightable$reply {
border-left: 3px solid rgba(145, 182, 214, .8);
}
:root.tomorrow.highlight-own .yourPost$site$relative$opHighlight,
:root.tomorrow.highlight-own .yourPost$site$relative$replyPost {
:root.tomorrow.highlight-own .yourPost$site$highlightable$op,
:root.tomorrow.highlight-own .yourPost$site$highlightable$reply {
border-left: 3px dashed rgba(145, 182, 214, .8);
}
:root.tomorrow .filter-highlight$site$relative$opHighlight,
:root.tomorrow .filter-highlight$site$relative$replyPost {
:root.tomorrow .filter-highlight$site$highlightable$op,
:root.tomorrow .filter-highlight$site$highlightable$reply {
box-shadow: inset 5px 0 rgba(145, 182, 214, .5);
}
:root.tomorrow.highlight-own .yourPost > $site$sideArrows,

View File

@ -8,6 +8,14 @@
border-color: #98E;
}
/* 4chan style fixes */
:root.yotsuba-b.highlight-you .quotesYou$site$highlightable$reply {
border-left: 3px solid rgba(221, 0, 0, .8) !important;
}
:root.yotsuba-b.highlight-own .yourPost$site$highlightable$reply {
border-left: 3px dashed rgba(221, 0, 0, .8) !important;
}
/* Header */
:root.yotsuba-b #header-bar.dialog {
background-color: rgba(214,218,240,0.98);

View File

@ -8,6 +8,14 @@
border-color: #EA8;
}
/* 4chan style fixes */
:root.yotsuba.highlight-you .quotesYou$site$highlightable$reply {
border-left: 3px solid rgba(221, 0, 0, .8) !important;
}
:root.yotsuba.highlight-own .yourPost$site$highlightable$reply {
border-left: 3px dashed rgba(221, 0, 0, .8) !important;
}
/* Header */
:root.yotsuba #header-bar.dialog {
background-color: rgba(240,224,214,0.98);

View File

@ -123,11 +123,30 @@ Main =
<%= html(meta.name + ' has been updated to <a href="' + meta.changelog + '" target="_blank">version ${g.VERSION}</a>.') %>
new Notice 'info', el, 15
initFeatures: ->
{hostname, search} = location
pathname = location.pathname.split /\/+/
g.BOARD = new Board pathname[1] unless hostname in ['www.4chan.org', 'www.4channel.org']
parseURL: (site=g.SITE, url=location) ->
r = {}
return r if !site
r.siteID = site.ID
return r if site.isBoardlessPage?(url)
pathname = url.pathname.split /\/+/
r.boardID = pathname[1]
if site.isFileURL(url)
r.VIEW = 'file'
else if site.isAuxiliaryPage?(url)
# pass
else if pathname[2] in ['thread', 'res']
r.VIEW = 'thread'
r.threadID = r.THREADID = +pathname[3].replace(/\.\w+$/, '')
else if /^(?:catalog|archive)(?:\.\w+)?$/.test(pathname[2])
r.VIEW = pathname[2].replace(/\.\w+$/, '')
else if /^(?:index|\d*)(?:\.\w+)?$/.test(pathname[2])
r.VIEW = 'index'
r
initFeatures: ->
$.global ->
document.documentElement.classList.add 'js-enabled'
window.FCX = {}
@ -136,29 +155,17 @@ Main =
# XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
$.ajaxPageInit?()
switch hostname
when 'www.4chan.org', 'www.4channel.org'
$.onExists doc, 'body', -> $.addStyle CSS.www
Captcha.replace.init()
return
when 'sys.4chan.org', 'sys.4channel.org'
if pathname[2] is 'imgboard.php'
if /\bmode=report\b/.test search
Report.init()
else if (match = search.match /\bres=(\d+)/)
$.ready ->
if Conf['404 Redirect'] and $.id('errmsg')?.textContent is 'Error: Specified thread does not exist.'
Redirect.navigate 'thread', {
boardID: g.BOARD.ID
postID: +match[1]
}
else if pathname[2] is 'post'
PostSuccessful.init()
return
$.extend g, Main.parseURL()
g.BOARD = new Board g.boardID if g.boardID
if g.SITE.isFileURL()
if !g.VIEW
g.SITE.initAuxiliary?()
return
if g.VIEW is 'file'
$.asap (-> d.readyState isnt 'loading'), ->
if g.SITE.software is 'yotsuba' and Conf['404 Redirect'] and g.SITE.is404?()
pathname = location.pathname.split /\/+/
Redirect.navigate 'file', {
boardID: g.BOARD.ID
filename: pathname[pathname.length - 1]
@ -173,18 +180,6 @@ Main =
ImageCommon.addControls video
return
return if g.SITE.isAuxiliaryPage?()
if pathname[2] in ['thread', 'res']
g.VIEW = 'thread'
g.THREADID = +pathname[3].replace(/\.\w+$/, '')
else if /^(?:catalog|archive)(?:\.\w+)?$/.test(pathname[2])
g.VIEW = pathname[2].replace(/\.\w+$/, '')
else if /^(?:index|\d*)(?:\.\w+)?$/.test(pathname[2])
g.VIEW = 'index'
else
return
g.threads = new SimpleDict()
g.posts = new SimpleDict()

View File

@ -4,12 +4,10 @@ SW.tinyboard =
threadModTimeIgnoresSage: true
disabledFeatures: [
'Index Generator'
'Resurrect Quotes'
'Quick Reply Personas'
'Quick Reply'
'Cooldown'
'Index Generator (Menu)'
'Report Link'
'Delete Link'
'Edit Link'
@ -47,6 +45,9 @@ SW.tinyboard =
urls:
thread: ({siteID, boardID, threadID}) -> "#{Conf['siteProperties'][siteID]?.root or "http://#{siteID}/"}#{boardID}/res/#{threadID}.html"
post: ({postID}) -> "##{postID}"
index: ({siteID, boardID}) -> "#{Conf['siteProperties'][siteID]?.root or "http://#{siteID}/"}#{boardID}/"
catalog: ({siteID, boardID}) -> "#{Conf['siteProperties'][siteID]?.root or "http://#{siteID}/"}#{boardID}/catalog.html"
threadJSON: ({siteID, boardID, threadID}) ->
root = Conf['siteProperties'][siteID]?.root
if root then "#{root}#{boardID}/res/#{threadID}.json" else ''
@ -68,6 +69,7 @@ SW.tinyboard =
summary: '.omitted'
postContainer: 'div[id^="reply_"]:not(.hidden)' # postContainer is thread for OP
opBottom: '.op'
replyOriginal: 'div[id^="reply_"]:not(.hidden)'
infoRoot: '.intro'
info:
subject: '.subject'
@ -90,11 +92,10 @@ SW.tinyboard =
thumb: 'a > .post-image'
thumbLink: '.file > a'
multifile: '.files > .file'
relative:
opHighlight: ' > .op'
replyPost: '.reply'
replyOriginal: 'div[id^="reply_"]:not(.hidden)'
catalogHighlight: ' > .thread'
highlightable:
op: ' > .op'
reply: '.reply'
catalog: ' > .thread'
comment: '.body'
spoiler: '.spoiler'
quotelink: 'a[onclick^="highlightReply("]'
@ -106,10 +107,17 @@ SW.tinyboard =
boardListBottom: '.boardlist.bottom'
styleSheet: '#stylesheet'
psa: '.blotter'
nav:
prev: '.pages > form > [value=Previous]'
next: '.pages > form > [value=Next]'
classes:
highlight: 'highlighted'
xpath:
thread: 'div[starts-with(@id,"thread_")]'
postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]'
thread: 'div[starts-with(@id,"thread_")]'
postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]'
replyContainer: 'div[starts-with(@id,"reply_")]'
regexp:
quotelink:
@ -154,8 +162,8 @@ SW.tinyboard =
bgColoredEl: ->
$.el 'div', className: 'post reply'
isFileURL: ->
/\/src\/[^\/]+/.test(location.pathname)
isFileURL: (url) ->
/\/src\/[^\/]+/.test(url.pathname)
parseNodes: (post, nodes) ->
# Add vichan's span.poster_id around the ID if not already present.

View File

@ -4,6 +4,9 @@ SW.yotsuba =
urls:
thread: ({boardID, threadID}) -> "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/thread/#{threadID}"
post: ({postID}) -> "#p#{postID}"
index: ({boardID}) -> "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/"
catalog: ({boardID}) -> if boardID is 'f' then undefined else "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/catalog"
threadJSON: ({boardID, threadID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/thread/#{threadID}.json"
threadsListJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/threads.json"
archiveListJSON: ({boardID}) -> if BoardConfig.isArchived(boardID) then "#{location.protocol}//a.4cdn.org/#{boardID}/archive.json" else ''
@ -14,8 +17,9 @@ SW.yotsuba =
thumb: ({boardID}, filename) ->
"#{location.protocol}//#{ImageHost.thumbHost()}/#{boardID}/#{filename}"
isPrunedByAge: ({boardID}) -> boardID is 'f'
isPrunedByAge: ({boardID}) -> boardID is 'f'
areMD5sDeferred: ({boardID}) -> boardID is 'f'
isOnePage: ({boardID}) -> boardID is 'f'
noAudio: ({boardID}) -> BoardConfig.noAudio(boardID)
selectors:
@ -24,6 +28,7 @@ SW.yotsuba =
threadDivider: '.board > hr'
summary: '.summary'
postContainer: '.postContainer'
replyOriginal: '.replyContainer:not([data-clone])'
sideArrows: 'div.sideArrows'
post: '.post'
infoRoot: '.postInfo'
@ -50,11 +55,10 @@ SW.yotsuba =
link: '.fileText > a'
thumb: 'a.fileThumb > [data-md5]'
thumbLink: 'a.fileThumb'
relative:
opHighlight: '.opContainer'
replyPost: ' > .reply'
replyOriginal: '.replyContainer:not([data-clone])'
catalogHighlight: ''
highlightable:
op: '.opContainer'
reply: ' > .reply'
catalog: ''
comment: '.postMessage'
spoiler: 's'
quotelink: ':not(pre) > .quotelink' # XXX https://github.com/4chan/4chan-JS/issues/77: 4chan currently creates quote links inside [code] tags; ignore them
@ -67,10 +71,18 @@ SW.yotsuba =
styleSheet: 'link[title=switch]'
psa: '#globalMessage'
psaTop: '#globalToggle'
searchBox: '#search-box'
nav:
prev: '.prev > form > [type=submit]'
next: '.next > form > [type=submit]'
classes:
highlight: 'highlight'
xpath:
thread: 'div[contains(concat(" ",@class," ")," thread ")]'
postContainer: 'div[contains(@class,"postContainer")]'
thread: 'div[contains(concat(" ",@class," ")," thread ")]'
postContainer: 'div[contains(@class,"postContainer")]'
replyContainer: 'div[contains(@class,"replyContainer")]'
regexp:
quotelink:
@ -105,11 +117,36 @@ SW.yotsuba =
isIncomplete: ->
return g.VIEW in ['index', 'thread'] and not $('.board + *')
isAuxiliaryPage: ->
location.hostname not in ['boards.4chan.org', 'boards.4channel.org']
isBoardlessPage: (url) ->
url.hostname in ['www.4chan.org', 'www.4channel.org']
isFileURL: ->
ImageHost.test(location.hostname)
isAuxiliaryPage: (url) ->
url.hostname not in ['boards.4chan.org', 'boards.4channel.org']
isFileURL: (url) ->
ImageHost.test(url.hostname)
initAuxiliary: ->
switch location.hostname
when 'www.4chan.org', 'www.4channel.org'
$.onExists doc, 'body', -> $.addStyle CSS.www
Captcha.replace.init()
return
when 'sys.4chan.org', 'sys.4channel.org'
pathname = location.pathname.split /\/+/
if pathname[2] is 'imgboard.php'
if /\bmode=report\b/.test location.search
Report.init()
else if (match = location.search.match /\bres=(\d+)/)
$.ready ->
if Conf['404 Redirect'] and $.id('errmsg')?.textContent is 'Error: Specified thread does not exist.'
Redirect.navigate 'thread', {
boardID: g.BOARD.ID
postID: +match[1]
}
else if pathname[2] is 'post'
PostSuccessful.init()
return
scriptData: ->
for script in $$ 'script:not([src])', d.head

View File

@ -6,14 +6,10 @@ Site =
init: (cb) ->
$.extend Conf['siteProperties'], Site.defaultProperties
{hostname} = location
while hostname and hostname not of Conf['siteProperties']
hostname = hostname.replace(/^[^.]*\.?/, '')
if hostname
hostname = canonical if (canonical = Conf['siteProperties'][hostname].canonical)
if Conf['siteProperties'][hostname].software of SW
@set hostname
cb()
hostname = Site.resolve()
if hostname and Conf['siteProperties'][hostname].software of SW
@set hostname
cb()
$.onExists doc, 'body', =>
for software of SW when (changes = SW[software].detect?())
changes.software = software
@ -31,6 +27,18 @@ Site =
return
return
resolve: (url=location) ->
{hostname} = url
while hostname and hostname not of Conf['siteProperties']
hostname = hostname.replace(/^[^.]*\.?/, '')
if hostname
hostname = canonical if (canonical = Conf['siteProperties'][hostname].canonical)
hostname
parseURL: (url) ->
siteID = Site.resolve url
Main.parseURL g.sites[siteID], url
set: (hostname) ->
for ID, properties of Conf['siteProperties']
continue if properties.canonical

View File

@ -15,5 +15,5 @@ for ext in ['jpg', 'png', 'gif']:
print(banner, status)
if status == 200:
banners.append(banner)
with open('src/Miscellaneous/Banner/banners.json', 'w') as f:
with open('src/config/banners.json', 'w') as f:
f.write(json.dumps(banners))