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 re
$.set type, save, cb $.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: -> quickFilterMD5: ->
post = Get.postFromNode @ post = Get.postFromNode @
return unless post.file files = post.files.filter((f) -> f.MD5)
Filter.addFilter 'MD5', "/#{post.file.MD5}/" return unless files.length
filter = files.map((f) -> "/#{f.MD5}/").join('\n')
Filter.addFilter 'MD5', filter
origin = post.origin or post origin = post.origin or post
if origin.isReply if origin.isReply
PostHiding.hide origin PostHiding.hide origin
else if g.VIEW is 'index' else if g.VIEW is 'index'
ThreadHiding.hide origin.thread ThreadHiding.hide origin.thread
# If post is still visible, give an indication that the MD5 was filtered. {notice} = Filter.quickFilterMD5
if post.nodes.post.getBoundingClientRect().height if notice
new Notice 'info', 'MD5 filtered.', 2 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) -> escape: (value) ->
value.replace /// value.replace ///
@ -346,13 +392,4 @@ Filter =
).join('\n') ).join('\n')
Filter.addFilter type, res, -> Filter.addFilter type, res, ->
# Open the settings and display & focus the relevant filter textarea. Filter.showFilters type
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()

View File

@ -145,7 +145,7 @@ ThreadHiding =
a a
makeStub: (thread, root) -> 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 numReplies += +summary.textContent.match /\d+/ if summary = $ g.SITE.selectors.summary, root
a = ThreadHiding.makeButton thread, 'show' a = ThreadHiding.makeButton thread, 'show'

View File

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

View File

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

View File

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

View File

@ -561,10 +561,13 @@ Settings =
$.id('lastarchivecheck').textContent = 'never' $.id('lastarchivecheck').textContent = 'never'
items = {} 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] items[name] = Conf[name]
input = inputs[name] event = if (
event = if name in ['archiveLists', 'archiveAutoUpdate', 'QR.personas', 'favicon', 'usercss'] then 'change' else 'input' 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, $.cb[if input.type is 'checkbox' then 'checked' else 'value']
$.on input, event, Settings[name] if name of Settings $.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> <button id="update-archives">Update now</button> Last updated: <time id="lastarchivecheck"></time> <label><input type="checkbox" name="archiveAutoUpdate"> Auto-update</label>
</fieldset> </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> <fieldset>
<legend>Override 4chan Image Host</legend> <legend>Override 4chan Image Host</legend>
<div>Change 4chan image links to this domain. Leave blank for no change.</div> <div>Change 4chan image links to this domain. Leave blank for no change.</div>
@ -173,3 +183,9 @@
</div> </div>
<textarea hidden name="jsWhitelist" class="field" spellcheck="false"></textarea> <textarea hidden name="jsWhitelist" class="field" spellcheck="false"></textarea>
</fieldset> </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 = Banner =
banners: `<%= JSON.stringify(readJSON('banners.json')) %>`
init: -> init: ->
if Conf['Custom Board Titles'] if Conf['Custom Board Titles']
@db = new DataBoard 'customTitles', null, true @db = new DataBoard 'customTitles', null, true
@ -44,7 +42,7 @@ Banner =
cb: cb:
toggle: -> toggle: ->
unless Banner.choices?.length unless Banner.choices?.length
Banner.choices = Banner.banners.slice() Banner.choices = Conf['knownBanners'].split(',').slice()
i = Math.floor(Banner.choices.length * Math.random()) i = Math.floor(Banner.choices.length * Math.random())
banner = Banner.choices.splice i, 1 banner = Banner.choices.splice i, 1
$('img', @parentNode).src = "//s.4cdn.org/image/title/#{banner}" $('img', @parentNode).src = "//s.4cdn.org/image/title/#{banner}"

View File

@ -13,10 +13,11 @@ CatalogLinks =
link.href = CatalogLinks.index() link.href = CatalogLinks.index()
when "/#{g.BOARD}/catalog" when "/#{g.BOARD}/catalog"
link.href = CatalogLinks.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 = link.parentNode.cloneNode true
catalogLink.firstElementChild.textContent = '<%= meta.name %> Catalog' link2 = catalogLink.firstElementChild
catalogLink.firstElementChild.href = CatalogLinks.catalog() link2.href = catalogURL
link2.textContent = if link2.hostname is location.hostname then '<%= meta.name %> Catalog' else 'External Catalog'
$.after link.parentNode, [$.tn(' '), catalogLink] $.after link.parentNode, [$.tn(' '), catalogLink]
return return
@ -57,33 +58,63 @@ CatalogLinks =
setLinks: (list) -> setLinks: (list) ->
return unless (CatalogLinks.enabled ? Conf['Catalog Links']) and 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) for a in $$('a:not([data-only])', list)
continue if ( {siteID, boardID} = a.dataset
a.hostname not in ['boards.4chan.org', 'boards.4channel.org', 'catalog.neet.tv'] or unless siteID and boardID
!(board = a.pathname.split('/')[1]) or {siteID, boardID, VIEW} = Site.parseURL a
board in ['f', 'status', '4chan'] or continue unless (
a.pathname.split('/')[2] is 'archive' or siteID and boardID and
$.hasClass a, 'external' 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 board = {siteID, boardID}
# conditions where External Catalog has been disabled between switches. url = if Conf['Header catalog links'] then CatalogLinks.catalog(board) else Get.url('index', board)
a.href = if Conf['Header catalog links'] then CatalogLinks.catalog(board) else "//#{BoardConfig.domain(board)}/#{board}/" if url
a.href = url
if a.dataset.indexOptions and a.hostname in ['boards.4chan.org', 'boards.4channel.org'] and a.pathname.split('/')[2] is '' if a.dataset.indexOptions and url.split('#')[0] is Get.url('index', board)
a.href += (if a.hash then '/' else '#') + a.dataset.indexOptions a.href += (if a.hash then '/' else '#') + a.dataset.indexOptions
return return
catalog: (board=g.BOARD.ID) -> externalParse: ->
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'] CatalogLinks.externalList = {}
"//catalog.neet.tv/#{board}/" for line in Conf['externalCatalogURLs'].split '\n'
else if Conf['JSON Index'] and Conf['Use <%= meta.name %> Catalog'] continue if line[0] is '#'
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" url = line.split(';')[0]
else boards = Filter.parseBoards(line.match(/;boards:([^;]+)/)?[1] or '*')
"//#{BoardConfig.domain(board)}/#{board}/catalog" 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) -> external: ({siteID, boardID}) ->
if Conf['JSON Index'] and board isnt 'f' CatalogLinks.externalParse() unless CatalogLinks.externalList
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 = (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 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 return if @ isnt status.req # aborted
delete status.req delete status.req
ExpandThread.parse @, thread, a ExpandThread.parse @, thread, a
status.numReplies = $$(g.SITE.selectors.replyOriginal, thread.nodes.root).length
contract: (thread, a, threadRoot) -> contract: (thread, a, threadRoot) ->
status = ExpandThread.statuses[thread] status = ExpandThread.statuses[thread]
@ -69,15 +70,7 @@ ExpandThread =
return return
replies = $$ '.thread > .replyContainer', threadRoot replies = $$ '.thread > .replyContainer', threadRoot
if !Conf['JSON Index'] or Conf['Show Replies'] replies = replies[...(-status.numReplies)] if status.numReplies
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]
postsCount = 0 postsCount = 0
filesCount = 0 filesCount = 0
for reply in replies for reply in replies

View File

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

View File

@ -39,22 +39,24 @@ Nav =
Nav.scroll +1 Nav.scroll +1
getThread: -> getThread: ->
return $ '.board' if $.hasClass doc, 'catalog-mode' return g.threads["#{g.BOARD}.#{g.THREADID}"].nodes.root if g.VIEW is 'thread'
for threadRoot in $$ '.thread' return if $.hasClass doc, 'catalog-mode'
for threadRoot in $$ g.SITE.selectors.thread
thread = Get.threadFromRoot threadRoot thread = Get.threadFromRoot threadRoot
continue if thread.isHidden and !thread.stub continue if thread.isHidden and !thread.stub
if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past
return threadRoot return threadRoot
return $ '.board' return
scroll: (delta) -> scroll: (delta) ->
d.activeElement?.blur() d.activeElement?.blur()
thread = Nav.getThread() thread = Nav.getThread()
return unless thread
axis = if delta is +1 axis = if delta is +1
'following' 'following'
else else
'preceding' '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, # Unless we're not at the beginning of the current thread,
# and thus wanting to move to beginning, # and thus wanting to move to beginning,
# or we're above the first thread and don't want to skip it. # or we're above the first thread and don't want to skip it.

View File

@ -1,6 +1,7 @@
ThreadUpdater = ThreadUpdater =
init: -> init: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Updater'] 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. # 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. # 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 $.rm nodes.flag
delete nodes.flag delete nodes.flag
if g.BOARD.ID is 'pol' if g.BOARD.config.troll_flags
flag = QR.flags() flag = QR.flags()
flag.dataset.name = 'flag' flag.dataset.name = 'flag'
flag.dataset.default = '0' flag.dataset.default = '0'

View File

@ -92,7 +92,8 @@ QuoteYou =
cb: cb:
seek: (type) -> 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') unless QuoteYou.lastRead and doc.contains(QuoteYou.lastRead) and $.hasClass(QuoteYou.lastRead, 'quotesYou')
if not (post = 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] QuoteYou.cb.scroll posts[if type is 'following' then 0 else posts.length - 1]
scroll: (root) -> scroll: (root) ->
post = $ '.post', root post = Get.postFromRoot root
if !post.getBoundingClientRect().height if !post.nodes.post.getBoundingClientRect().height
return false return false
else else
QuoteYou.lastRead = root QuoteYou.lastRead = root
location.href = "##{post.id}" location.href = Get.url('post', post)
Header.scrollTo post Header.scrollTo post.nodes.post
$.addClass post, 'highlight' 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 return true

View File

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

View File

@ -1,7 +1,12 @@
Post.Clone = class extends Post Post.Clone = class extends Post
isClone: true 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'] for key in ['ID', 'postID', 'threadID', 'boardID', 'siteID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply']
# Copy or point to the origin's key value. # Copy or point to the origin's key value.
@[key] = @origin[key] @[key] = @origin[key]

View File

@ -825,6 +825,10 @@ Config =
lastarchivecheck: 0 lastarchivecheck: 0
archiveAutoUpdate: true 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: """ boardnav: """
[ toggle-all ] [ toggle-all ]
a-replace a-replace
@ -1177,3 +1181,5 @@ Config =
fourchanImageHost: 'i.4cdn.org' fourchanImageHost: 'i.4cdn.org'
hiddenPSAList: [{}] hiddenPSAList: [{}]
knownBanners: '<%= readJSON('banners.json').join(',') %>'

View File

@ -9,9 +9,15 @@
} }
/* 4chan style fixes */ /* 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; 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 */ /* Header */
:root.photon #header-bar.dialog { :root.photon #header-bar.dialog {

View File

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

View File

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

View File

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

View File

@ -8,6 +8,14 @@
border-color: #98E; 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 */ /* Header */
:root.yotsuba-b #header-bar.dialog { :root.yotsuba-b #header-bar.dialog {
background-color: rgba(214,218,240,0.98); background-color: rgba(214,218,240,0.98);

View File

@ -8,6 +8,14 @@
border-color: #EA8; 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 */ /* Header */
:root.yotsuba #header-bar.dialog { :root.yotsuba #header-bar.dialog {
background-color: rgba(240,224,214,0.98); 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>.') %> <%= html(meta.name + ' has been updated to <a href="' + meta.changelog + '" target="_blank">version ${g.VERSION}</a>.') %>
new Notice 'info', el, 15 new Notice 'info', el, 15
initFeatures: -> parseURL: (site=g.SITE, url=location) ->
{hostname, search} = location r = {}
pathname = location.pathname.split /\/+/
g.BOARD = new Board pathname[1] unless hostname in ['www.4chan.org', 'www.4channel.org']
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 -> $.global ->
document.documentElement.classList.add 'js-enabled' document.documentElement.classList.add 'js-enabled'
window.FCX = {} window.FCX = {}
@ -136,29 +155,17 @@ Main =
# XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638 # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
$.ajaxPageInit?() $.ajaxPageInit?()
switch hostname $.extend g, Main.parseURL()
when 'www.4chan.org', 'www.4channel.org' g.BOARD = new Board g.boardID if g.boardID
$.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
if g.SITE.isFileURL() if !g.VIEW
g.SITE.initAuxiliary?()
return
if g.VIEW is 'file'
$.asap (-> d.readyState isnt 'loading'), -> $.asap (-> d.readyState isnt 'loading'), ->
if g.SITE.software is 'yotsuba' and Conf['404 Redirect'] and g.SITE.is404?() if g.SITE.software is 'yotsuba' and Conf['404 Redirect'] and g.SITE.is404?()
pathname = location.pathname.split /\/+/
Redirect.navigate 'file', { Redirect.navigate 'file', {
boardID: g.BOARD.ID boardID: g.BOARD.ID
filename: pathname[pathname.length - 1] filename: pathname[pathname.length - 1]
@ -173,18 +180,6 @@ Main =
ImageCommon.addControls video ImageCommon.addControls video
return 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.threads = new SimpleDict()
g.posts = new SimpleDict() g.posts = new SimpleDict()

View File

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

View File

@ -4,6 +4,9 @@ SW.yotsuba =
urls: urls:
thread: ({boardID, threadID}) -> "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/thread/#{threadID}" 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" threadJSON: ({boardID, threadID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/thread/#{threadID}.json"
threadsListJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/threads.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 '' 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) -> thumb: ({boardID}, filename) ->
"#{location.protocol}//#{ImageHost.thumbHost()}/#{boardID}/#{filename}" "#{location.protocol}//#{ImageHost.thumbHost()}/#{boardID}/#{filename}"
isPrunedByAge: ({boardID}) -> boardID is 'f' isPrunedByAge: ({boardID}) -> boardID is 'f'
areMD5sDeferred: ({boardID}) -> boardID is 'f' areMD5sDeferred: ({boardID}) -> boardID is 'f'
isOnePage: ({boardID}) -> boardID is 'f'
noAudio: ({boardID}) -> BoardConfig.noAudio(boardID) noAudio: ({boardID}) -> BoardConfig.noAudio(boardID)
selectors: selectors:
@ -24,6 +28,7 @@ SW.yotsuba =
threadDivider: '.board > hr' threadDivider: '.board > hr'
summary: '.summary' summary: '.summary'
postContainer: '.postContainer' postContainer: '.postContainer'
replyOriginal: '.replyContainer:not([data-clone])'
sideArrows: 'div.sideArrows' sideArrows: 'div.sideArrows'
post: '.post' post: '.post'
infoRoot: '.postInfo' infoRoot: '.postInfo'
@ -50,11 +55,10 @@ SW.yotsuba =
link: '.fileText > a' link: '.fileText > a'
thumb: 'a.fileThumb > [data-md5]' thumb: 'a.fileThumb > [data-md5]'
thumbLink: 'a.fileThumb' thumbLink: 'a.fileThumb'
relative: highlightable:
opHighlight: '.opContainer' op: '.opContainer'
replyPost: ' > .reply' reply: ' > .reply'
replyOriginal: '.replyContainer:not([data-clone])' catalog: ''
catalogHighlight: ''
comment: '.postMessage' comment: '.postMessage'
spoiler: 's' 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 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]' styleSheet: 'link[title=switch]'
psa: '#globalMessage' psa: '#globalMessage'
psaTop: '#globalToggle' psaTop: '#globalToggle'
searchBox: '#search-box'
nav:
prev: '.prev > form > [type=submit]'
next: '.next > form > [type=submit]'
classes:
highlight: 'highlight'
xpath: xpath:
thread: 'div[contains(concat(" ",@class," ")," thread ")]' thread: 'div[contains(concat(" ",@class," ")," thread ")]'
postContainer: 'div[contains(@class,"postContainer")]' postContainer: 'div[contains(@class,"postContainer")]'
replyContainer: 'div[contains(@class,"replyContainer")]'
regexp: regexp:
quotelink: quotelink:
@ -105,11 +117,36 @@ SW.yotsuba =
isIncomplete: -> isIncomplete: ->
return g.VIEW in ['index', 'thread'] and not $('.board + *') return g.VIEW in ['index', 'thread'] and not $('.board + *')
isAuxiliaryPage: -> isBoardlessPage: (url) ->
location.hostname not in ['boards.4chan.org', 'boards.4channel.org'] url.hostname in ['www.4chan.org', 'www.4channel.org']
isFileURL: -> isAuxiliaryPage: (url) ->
ImageHost.test(location.hostname) 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: -> scriptData: ->
for script in $$ 'script:not([src])', d.head for script in $$ 'script:not([src])', d.head

View File

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

View File

@ -15,5 +15,5 @@ for ext in ['jpg', 'png', 'gif']:
print(banner, status) print(banner, status)
if status == 200: if status == 200:
banners.append(banner) 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)) f.write(json.dumps(banners))