Merge branch 'catalog'

This commit is contained in:
ccd0 2014-09-20 21:17:04 -07:00
commit c47c02e95d
19 changed files with 570 additions and 119 deletions

View File

@ -113,6 +113,8 @@ Filter =
# Highlight
$.addClass @nodes.root, result.class
unless @highlights and result.class in @highlights
(@highlights or= []).push result.class
if !@isReply and result.top
@thread.isOnTop = true

View File

@ -1,6 +1,6 @@
ThreadHiding =
init: ->
return if g.VIEW isnt 'index' or !Conf['Thread Hiding Buttons'] and !Conf['Thread Hiding Link']
return if g.VIEW isnt 'index' or !Conf['Thread Hiding Buttons'] and !Conf['Thread Hiding Link'] and !Conf['JSON Navigation']
@db = new DataBoard 'hiddenThreads'
@syncCatalog()
@ -81,7 +81,7 @@ ThreadHiding =
el: div
order: 20
open: ({thread, isReply}) ->
if isReply or thread.isHidden
if isReply or thread.isHidden or Conf['JSON Navigation'] and Conf['Index Mode'] is 'catalog'
return false
ThreadHiding.menu.thread = thread
true
@ -188,6 +188,7 @@ ThreadHiding =
return if thread.isHidden
threadRoot = thread.OP.nodes.root.parentNode
thread.isHidden = true
Index.updateHideLabel() if Conf['JSON Navigation']
return threadRoot.hidden = true unless makeStub
@ -199,3 +200,4 @@ ThreadHiding =
delete thread.stub
threadRoot = thread.OP.nodes.root.parentNode
threadRoot.hidden = thread.isHidden = false
Index.updateHideLabel() if Conf['JSON Navigation']

View File

@ -1,4 +1,6 @@
Build =
staticPath: '//s.4cdn.org/image/'
gifIcon: if window.devicePixelRatio >= 2 then '@2x.gif' else '.gif'
initPixelRatio: window.devicePixelRatio
spoilerRange: {}
unescape: (text) ->
@ -289,11 +291,91 @@ Build =
nodes = [if OP then OP.nodes.root else Build.postFromObject data, board.ID]
if data.omitted_posts or !Conf['Show Replies'] and data.replies
[posts, files] = if Conf['Show Replies']
[data.omitted_posts, data.omitted_images]
# XXX data.omitted_images is not accurate.
[data.omitted_posts, data.images - data.last_replies.filter((data) -> !!data.ext).length]
else
# XXX data.images is not accurate.
[data.replies, data.omitted_images + data.last_replies.filter((data) -> !!data.ext).length]
[data.replies, data.images]
nodes.push Build.summary board.ID, data.no, posts, files
nodes
fullThread: (board, data) -> Build.postFromObject data, board.ID
catalogThread: (thread) ->
{staticPath, gifIcon} = Build
data = Index.liveThreadData[Index.liveThreadIDs.indexOf thread.ID]
if data.spoiler and !Conf['Reveal Spoiler Thumbnails']
src = "#{staticPath}spoiler"
if spoilerRange = Build.spoilerRange[thread.board]
# Randomize the spoiler image.
src += "-#{thread.board}" + Math.floor 1 + spoilerRange * Math.random()
src += '.png'
imgClass = 'spoiler-file'
else if data.filedeleted
src = "#{staticPath}filedeleted-res#{gifIcon}"
imgClass = 'deleted-file'
else if thread.OP.file
src = thread.OP.file.thumbURL
max = Math.max data.tn_w, data.tn_h
imgWidth = data.tn_w * 150 / max
imgHeight = data.tn_h * 150 / max
else
src = "#{staticPath}nofile.png"
imgClass = 'no-file'
thumb = if imgClass
<%= html('<img src="${src}" class="catalog-thumb ${imgClass}">') %>
else
<%= html('<img src="${src}" class="catalog-thumb" width="${imgWidth}" height="${imgHeight}">') %>
postCount = data.replies + 1
fileCount = data.images + !!data.ext
pageCount = Index.liveThreadIDs.indexOf(thread.ID) // Index.threadsNumPerPage + 1
subject = if thread.OP.info.subject
<%= html('<div class="subject">${thread.OP.info.subject}</div>') %>
else
<%= html('') %>
root = $.el 'div',
className: 'catalog-thread'
$.extend root, <%= html(
'<a href="/${thread.board}/thread/${thread.ID}">' +
'&{thumb}' +
'</a>' +
'<div class="catalog-stats" title="Post count / File count / Page count">' +
'<span class="post-count">${postCount}</span> / <span class="file-count">${fileCount}</span> / <span class="page-count">${pageCount}</span>' +
'<span class="catalog-icons"></span>' +
'</div>' +
'&{subject}' +
'<div class="comment">&{thread.OP.info.commentHTML}</div>'
) %>
root.dataset.fullID = thread.fullID
$.addClass root, 'pinned' if thread.isPinned
$.addClass root, thread.OP.highlights... if thread.OP.highlights
for quotelink in $$ '.quotelink, .deadlink', root.lastElementChild
$.replace quotelink, [quotelink.childNodes...]
for pp in $$ '.prettyprint', root.lastElementChild
$.replace pp, $.tn pp.textContent
for br in $$ 'br', root.lastElementChild when !br.previousSibling or br.previousSibling.nodeName is 'BR'
$.rm br
if thread.isSticky
$.add $('.catalog-icons', root), $.el 'img',
src: "#{staticPath}sticky#{gifIcon}"
className: 'stickyIcon'
title: 'Sticky'
if thread.isClosed
$.add $('.catalog-icons', root), $.el 'img',
src: "#{staticPath}closed#{gifIcon}"
className: 'closedIcon'
title: 'Closed'
if data.bumplimit
$.addClass $('.post-count', root), 'warning'
if data.imagelimit
$.addClass $('.file-count', root), 'warning'
root

View File

@ -5,6 +5,10 @@ Config =
true
'Replace the board index with a dynamically generated one supporting searching, sorting, and infinite scrolling.'
]
'Use 4chan X Catalog': [
false
'Link to 4chan X\'s catalog instead of the native 4chan one.'
]
'Catalog Links': [
true
'Add toggle link in header menu to turn Navigation links into links to each board\'s catalog.'
@ -553,6 +557,7 @@ http://iqdb.org/?url=%TURL
Index:
'Index Mode': 'paged'
'Previous Index Mode': 'paged'
'Index Sort': 'bump'
'Show Replies': true
'Anchor Hidden Threads': true

View File

@ -197,7 +197,7 @@ Header =
if Conf['External Catalog']
a.href = CatalogLinks.external board
else
a.href += 'catalog'
a.href += if Conf['JSON Navigation'] and Conf['Use 4chan X Catalog'] then '#catalog' else 'catalog'
$.addClass a, 'catalog'
$.addClass a, 'navSmall' if board is '@'

View File

@ -1,9 +1,27 @@
Index =
showHiddenThreads: false
init: ->
return if g.BOARD.ID is 'f' or g.VIEW isnt 'index' or !Conf['JSON Navigation']
return if g.BOARD.ID is 'f' or !Conf['JSON Navigation']
if g.VIEW is 'thread' and Conf['Use 4chan X Catalog']
$.ready ->
for link in $$ '.navLinks.desktop a' when link.pathname is "/#{g.BOARD}/catalog"
link.href = "/#{g.BOARD}/#catalog"
return if g.VIEW isnt 'index'
@board = "#{g.BOARD}"
@db = new DataBoard 'pinnedThreads'
Thread.callbacks.push
name: 'Thread Pinning'
cb: @threadNode
CatalogThread.callbacks.push
name: 'Catalog Features'
cb: @catalogNode
if Conf['Use 4chan X Catalog'] and Conf['Index Mode'] is 'catalog'
Index.setMode Conf['Previous Index Mode']
@cb.popstate()
@button = $.el 'a',
className: 'index-refresh-shortcut fa fa-refresh'
title: 'Refresh'
@ -18,28 +36,17 @@ Index =
{ el: $.el 'label', <%= html('<input type="radio" name="Index Mode" value="paged"> Paged') %> }
{ el: $.el 'label', <%= html('<input type="radio" name="Index Mode" value="infinite"> Infinite scrolling') %> }
{ el: $.el 'label', <%= html('<input type="radio" name="Index Mode" value="all pages"> All threads') %> }
{ el: $.el 'label', <%= html('<input type="radio" name="Index Mode" value="catalog"> Catalog') %> }
]
open: ->
for label in @subEntries
input = label.el.firstChild
input.checked = Conf['Index Mode'] is input.value
true
for label in modeEntry.subEntries
input = label.el.firstChild
input.checked = Conf['Index Mode'] is input.value
$.on input, 'change', $.cb.value
$.on input, 'change', @cb.mode
sortEntry =
el: $.el 'span', textContent: 'Sort by'
subEntries: [
{ el: $.el 'label', <%= html('<input type="radio" name="Index Sort" value="bump"> Bump order') %> }
{ el: $.el 'label', <%= html('<input type="radio" name="Index Sort" value="lastreply"> Last reply') %> }
{ el: $.el 'label', <%= html('<input type="radio" name="Index Sort" value="birth"> Creation date') %> }
{ el: $.el 'label', <%= html('<input type="radio" name="Index Sort" value="replycount"> Reply count') %> }
{ el: $.el 'label', <%= html('<input type="radio" name="Index Sort" value="filecount"> File count') %> }
]
for label in sortEntry.subEntries
input = label.el.firstChild
input.checked = Conf['Index Sort'] is input.value
$.on input, 'change', $.cb.value
$.on input, 'change', @cb.sort
repliesEntry = el: UI.checkbox 'Show Replies', ' Show replies'
anchorEntry = el: UI.checkbox 'Anchor Hidden Threads', ' Anchor hidden threads'
refNavEntry = el: UI.checkbox 'Refreshed Navigation', ' Refreshed navigation'
@ -59,20 +66,20 @@ Index =
el: $.el 'span',
textContent: 'Index Navigation'
order: 98
subEntries: [repliesEntry, anchorEntry, refNavEntry, modeEntry, sortEntry]
subEntries: [repliesEntry, anchorEntry, refNavEntry, modeEntry]
$.addClass doc, 'index-loading'
@root = $.el 'div', className: 'board'
@pagelist = $.el 'div',
className: 'pagelist'
hidden: true
$.addClass doc, 'index-loading', "#{Conf['Index Mode'].replace /\ /g, '-'}-mode"
@root = $.el 'div', className: 'board'
@pagelist = $.el 'div', className: 'pagelist'
$.extend @pagelist, <%= importHTML('Features/Index-pagelist') %>
@navLinks = $.el 'div',
className: 'navLinks'
$('.cataloglink a', @pagelist).href = if Conf['Use 4chan X Catalog'] then '#catalog' else "/#{g.BOARD}/catalog"
@navLinks = $.el 'div', className: 'navLinks'
$.extend @navLinks, <%= importHTML('Features/Index-navlinks') %>
$('.returnlink a', @navLinks).href = "//boards.4chan.org/#{g.BOARD}/"
$('.cataloglink a', @navLinks).href = "//boards.4chan.org/#{g.BOARD}/catalog"
$('.returnlink a', @navLinks).href = if Conf['Use 4chan X Catalog'] then '#index' else "/#{g.BOARD}/"
$('.cataloglink a', @navLinks).href = if Conf['Use 4chan X Catalog'] then '#catalog' else "/#{g.BOARD}/catalog"
@searchInput = $ '#index-search', @navLinks
@hideLabel = $ '#hidden-label', @navLinks
@selectSort = $ '#index-sort', @navLinks
@currentPage = @getCurrentPage()
$.on window, 'popstate', @cb.popstate
@ -80,6 +87,10 @@ Index =
$.on @pagelist, 'click', @cb.pageNav
$.on @searchInput, 'input', @onSearchInput
$.on $('#index-search-clear', @navLinks), 'click', @clearSearch
$.on $('#hidden-toggle a', @navLinks), 'click', @cb.toggleHiddenThreads
@selectSort.value = Conf[@selectSort.name]
$.on @selectSort, 'change', $.cb.value
$.on @selectSort, 'change', @cb.sort
@update()
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
@ -129,10 +140,93 @@ Index =
new Notice 'info', "Last page reached.", 2
setTimeout reset, 3 * $.SECOND
menu:
init: ->
return if g.VIEW isnt 'index' or !Conf['JSON Navigation'] or !Conf['Menu'] or g.BOARD.ID is 'f'
Menu.menu.addEntry
el: $.el 'a', href: 'javascript:;'
order: 5
open: ({thread}) ->
return false if Conf['Index Mode'] isnt 'catalog'
@el.textContent = if thread.isHidden
'Unhide thread'
else
'Hide thread'
$.off @el, 'click', @cb if @cb
@cb = ->
$.event 'CloseMenu'
Index.toggleHide thread
$.on @el, 'click', @cb
true
Menu.menu.addEntry
el: $.el 'a', href: 'javascript:;'
order: 6
open: ({thread}) ->
return false if Conf['Index Mode'] isnt 'catalog'
@el.textContent = if thread.isPinned
'Unpin thread'
else
'Pin thread'
$.off @el, 'click', @cb if @cb
@cb = ->
$.event 'CloseMenu'
Index.togglePin thread
$.on @el, 'click', @cb
true
threadNode: ->
return unless Index.db.get {boardID: @board.ID, threadID: @ID}
@pin()
catalogNode: ->
$.on @nodes.thumb.parentNode, 'click', Index.onClick
onClick: (e) ->
return if e.button isnt 0
thread = g.threads[@parentNode.dataset.fullID]
if e.shiftKey
Index.toggleHide thread
else if e.altKey
Index.togglePin thread
else
return
e.preventDefault()
toggleHide: (thread) ->
$.rm thread.catalogView.nodes.root
if Index.showHiddenThreads
ThreadHiding.show thread
return unless ThreadHiding.db.get {boardID: thread.board.ID, threadID: thread.ID}
# Don't save when un-hiding filtered threads.
else
ThreadHiding.hide thread
ThreadHiding.saveHiddenState thread
togglePin: (thread) ->
data =
boardID: thread.board.ID
threadID: thread.ID
if thread.isPinned
thread.unpin()
Index.db.delete data
else
thread.pin()
data.val = true
Index.db.set data
Index.sort()
Index.buildIndex()
cb:
mode: ->
Index.togglePagelist()
toggleHiddenThreads: ->
$('#hidden-toggle a', Index.navLinks).textContent = if Index.showHiddenThreads = !Index.showHiddenThreads
'Hide'
else
'Show'
Index.sort()
Index.buildIndex()
mode: ->
Index.setMode @value
Index.pushState Conf['Index Mode'], Index.currentPage
Index.buildIndex()
Index.setPage()
sort: ->
Index.sort()
Index.buildIndex()
@ -140,9 +234,31 @@ Index =
Index.buildThreads()
Index.sort()
Index.buildIndex()
hashchange: (e) ->
switch command = location.hash[1..]
when 'paged', 'infinite', 'all-pages', 'catalog'
mode = command.replace /-/g, ' '
when 'index'
mode = Conf['Previous Index Mode']
if mode
Index.setMode mode
history.replaceState {mode}, '', if Index.currentPage is 1 then './' else Index.currentPage
if e
# hash change, not call from init
Index.buildIndex()
Index.setPage()
return
history.replaceState {mode: Conf['Index Mode']}, ''
popstate: (e) ->
unless e?.state
# page load or hash change
return Index.cb.hashchange.call @, e
{mode} = e.state
pageNum = Index.getCurrentPage()
Index.pageLoad pageNum if Index.currentPage isnt pageNum
unless Conf['Index Mode'] is mode and Index.currentPage is pageNum
Index.setMode mode
Index.buildIndex()
Index.setPage()
pageNav: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
switch e.target.nodeName
@ -158,23 +274,36 @@ Index =
Index.userPageNav +a.pathname.split('/')[2] or 1
scrollToIndex: ->
Header.scrollToIfNeeded Index.root
Header.scrollToIfNeeded Index.navLinks
getCurrentPage: ->
+window.location.pathname.split('/')[2] or 1
if Conf['Index Mode'] in ['all pages', 'catalog']
1
else
+window.location.pathname.split('/')[2] or 1
userPageNav: (pageNum) ->
history.pushState null, '', if pageNum is 1 then './' else pageNum
if Conf['Refreshed Navigation'] and Conf['Index Mode'] isnt 'all pages'
Index.pushState Conf['Index Mode'], pageNum
if Conf['Refreshed Navigation']
Index.update pageNum
else
return if Index.currentPage is pageNum
Index.pageLoad pageNum
pushState: (mode, pageNum) ->
history.pushState {mode}, '', if pageNum is 1 then './' else pageNum
pageLoad: (pageNum) ->
Index.currentPage = pageNum
return if Conf['Index Mode'] is 'all pages'
Index.buildIndex()
Index.setPage()
Index.scrollToIndex()
setMode: (mode) ->
$.rmClass doc, "#{Conf['Index Mode'].replace /\ /g, '-'}-mode"
$.addClass doc, "#{mode.replace /\ /g, '-'}-mode"
Conf['Index Mode'] = mode
$.set 'Index Mode', mode
Index.currentPage = Index.getCurrentPage()
if mode not in ['catalog', Conf['Previous Index Mode']]
Conf['Previous Index Mode'] = mode
$.set 'Previous Index Mode', mode
getPagesNum: ->
if Index.isSearching
@ -183,8 +312,6 @@ Index =
Index.pagesNum
getMaxPageNum: ->
Math.max 1, Index.getPagesNum()
togglePagelist: ->
Index.pagelist.hidden = Conf['Index Mode'] isnt 'paged'
buildPagelist: ->
pagesRoot = $ '.pages', Index.pagelist
maxPageNum = Index.getMaxPageNum()
@ -197,7 +324,6 @@ Index =
nodes.push $.tn('['), a, $.tn '] '
$.rmAll pagesRoot
$.add pagesRoot, nodes
Index.togglePagelist()
setPage: (pageNum) ->
pageNum or= Index.getCurrentPage()
maxPageNum = Index.getMaxPageNum()
@ -221,7 +347,21 @@ Index =
$.before a, strong
$.add strong, a
update: (pageNum, forceReparse) ->
updateHideLabel: ->
hiddenCount = 0
for threadID, thread of g.BOARD.threads when thread.isHidden
hiddenCount++ if thread.ID in Index.liveThreadIDs
unless hiddenCount
Index.hideLabel.hidden = true
Index.cb.toggleHiddenThreads() if Index.showHiddenThreads
return
Index.hideLabel.hidden = false
$('#hidden-count', Index.navLinks).textContent = if hiddenCount is 1
'1 hidden thread'
else
"#{hiddenCount} hidden threads"
update: (pageNum) ->
return unless navigator.onLine
delete Index.pageNum
Index.req?.abort()
@ -241,7 +381,7 @@ Index =
onabort: onload
onloadend: onload
,
whenModified: !forceReparse
whenModified: true
$.addClass Index.button, 'fa-spin'
load: (e, pageNum) ->
@ -317,6 +457,8 @@ Index =
try
threadRoot = Build.thread g.BOARD, threadData
if thread = g.BOARD.threads[threadData.no]
thread.setCount 'post', threadData.replies + 1, threadData.bumplimit
thread.setCount 'file', threadData.images + !!threadData.ext, threadData.imagelimit
thread.setStatus 'Sticky', !!threadData.sticky
thread.setStatus 'Closed', !!threadData.closed
else
@ -338,6 +480,7 @@ Index =
$.nodes Index.nodes
Main.callbackNodes Thread, threads
Main.callbackNodes Post, posts
Index.updateHideLabel()
$.event 'IndexRefresh'
buildReplies: (threadRoots) ->
@ -365,6 +508,16 @@ Index =
Main.handleErrors errors if errors
Main.callbackNodes Post, posts
buildCatalogViews: ->
threads = Index.sortedNodes
.map((threadRoot) -> Get.threadFromRoot threadRoot)
.filter (thread) -> !thread.isHidden isnt Index.showHiddenThreads
catalogThreads = []
for thread in threads when !thread.catalogView
catalogThreads.push new CatalogThread Build.catalogThread(thread), thread
Main.callbackNodes CatalogThread, catalogThreads
threads.map (thread) -> thread.catalogView.nodes.root
sort: ->
{liveThreadIDs, liveThreadData} = Index
sortedThreadIDs = {
@ -388,7 +541,7 @@ Index =
# Sticky threads
Index.sortOnTop (thread) -> thread.isSticky
# Highlighted threads
Index.sortOnTop((thread) -> thread.isOnTop) if Conf['Filter']
Index.sortOnTop (thread) -> thread.isOnTop or thread.isPinned
# Non-hidden threads
Index.sortOnTop((thread) -> !thread.isHidden) if Conf['Anchor Hidden Threads']
@ -400,14 +553,20 @@ Index =
Index.sortedNodes = topNodes.concat(bottomNodes)
buildIndex: ->
if Conf['Index Mode'] isnt 'all pages'
nodes = Index.buildSinglePage Index.getCurrentPage()
else
nodes = Index.sortedNodes
switch Conf['Index Mode']
when 'all pages'
nodes = Index.sortedNodes
when 'catalog'
nodes = Index.buildCatalogViews()
else
nodes = Index.buildSinglePage Index.getCurrentPage()
$.rmAll Index.root
$.rmAll Header.hover
Index.buildReplies nodes if Conf['Show Replies']
Index.buildStructure nodes
if Conf['Index Mode'] is 'catalog'
$.add Index.root, nodes
else
Index.buildReplies nodes if Conf['Show Replies']
Index.buildStructure nodes
buildSinglePage: (pageNum) ->
nodesPerPage = Index.threadsNumPerPage
@ -439,12 +598,8 @@ Index =
return unless Index.searchInput.dataset.searching
pageNum = Index.pageBeforeSearch
delete Index.pageBeforeSearch
<% if (type === 'userscript') { %>
# XXX https://github.com/greasemonkey/greasemonkey/issues/1571
Index.searchInput.removeAttribute 'data-searching'
<% } else { %>
delete Index.searchInput.dataset.searching
<% } %>
Index.sort()
# Go to the last available page if we were past the limit.
pageNum = Math.min pageNum, Index.getMaxPageNum() if Conf['Index Mode'] isnt 'all pages'
@ -453,7 +608,7 @@ Index =
Index.buildIndex()
Index.setPage()
else
history.pushState null, '', if pageNum is 1 then './' else pageNum
Index.pushState Conf['Index Mode'], pageNum
Index.pageLoad pageNum
querySearch: (query) ->

View File

@ -293,6 +293,7 @@ Main =
['Strike-through Quotes', QuoteStrikeThrough]
['Quick Reply', QR]
['Menu', Menu]
['Index Generator (Menu)', Index.menu]
['Report Link', ReportLink]
['Thread Hiding (Menu)', ThreadHiding.menu]
['Reply Hiding (Menu)', PostHiding.menu]

View File

@ -471,7 +471,11 @@ hr + div.center:not(.ad-cnt):not(.topad):not(.middlead):not(.bottomad) {
/* Index */
:root.index-loading .navLinks,
:root.index-loading .board,
:root.index-loading .pagelist {
:root.index-loading .pagelist,
:root.infinite-mode .pagelist,
:root.all-pages-mode .pagelist,
:root.catalog-mode .pagelist,
:root:not(.catalog-mode) #hidden-label {
display: none;
}
#index-search {
@ -485,7 +489,10 @@ hr + div.center:not(.ad-cnt):not(.topad):not(.middlead):not(.bottomad) {
}
#index-search-clear {
color: gray;
margin-left: -1em;
display: inline-block;
position: relative;
left: -1em;
width: 0;
}
<% if (type === 'crx') { %>
/* ``::-webkit-*'' selectors break selector lists on Firefox. */
@ -494,10 +501,101 @@ hr + div.center:not(.ad-cnt):not(.topad):not(.middlead):not(.bottomad) {
#index-search:not([data-searching]) + #index-search-clear {
display: none;
}
#index-sort {
float: right;
}
.summary {
text-decoration: none;
}
/* Catalog */
:root.catalog-mode .board {
text-align: center;
}
.catalog-thread {
display: -webkit-inline-flex;
display: inline-flex;
text-align: left;
-webkit-flex-direction: column;
flex-direction: column;
-webkit-align-items: center;
align-items: center;
width: 165px;
margin: 0 2px 5px;
max-height: 320px;
word-wrap: break-word;
vertical-align: top;
}
.catalog-thread > a {
flex-shrink: 0;
-webkit-flex-shrink: 0;
position: relative;
}
.catalog-thumb {
max-width: 150px;
max-height: 150px;
border-radius: 2px;
box-shadow: 0 0 5px rgba(0, 0, 0, .25);
}
.catalog-thumb:not(.deleted-file):not(.no-file) {
min-width: 30px;
min-height: 30px;
}
.catalog-thumb.spoiler-file {
width: 100px;
height: 100px;
}
.catalog-thumb.deleted-file {
width: 127px;
height: 13px;
padding: 20px 11px;
}
.catalog-thumb.no-file {
width: 77px;
height: 13px;
padding: 20px 36px;
}
.catalog-icons > img,
.catalog-stats > .menu-button {
width: 1em;
height: 1em;
margin: 0;
vertical-align: text-top;
padding-left: 2px;
}
.catalog-stats > .menu-button {
text-align: center;
font-weight: normal;
}
.catalog-stats > .menu-button > i::before {
line-height: 11px;
}
.catalog-stats {
-webkit-flex-shrink: 0;
flex-shrink: 0;
cursor: help;
font-size: 10px;
font-weight: 700;
margin-top: 2px;
}
.catalog-thread > .subject {
-webkit-flex-shrink: 0;
flex-shrink: 0;
-webkit-align-self: stretch;
align-self: stretch;
font-weight: 700;
line-height: 1;
text-align: center;
}
.catalog-thread > .comment {
-webkit-flex-shrink: 1;
flex-shrink: 1;
-webkit-align-self: stretch;
align-self: stretch;
overflow: hidden;
text-align: center;
}
/* Announcement Hiding */
:root.hide-announcement #globalMessage {
display: none;
@ -752,9 +850,21 @@ span.hide-announcement {
display: none;
}
/* Werk Tyme */
:root.werkTyme .postContainer:not(.noFile) .fileThumb {
:root.werkTyme .postContainer:not(.noFile) .fileThumb,
:root.werkTyme .catalog-thumb:not(.deleted-file):not(.no-file),
:root:not(.werkTyme) .werkTyme-filename {
display: none;
}
.werkTyme-filename {
font-weight: bold;
}
:root.werkTyme .catalog-thread > a {
text-align: center;
}
.pinned .werkTyme-filename,
.filter-highlight .werkTyme-filename {
border: 2px solid rgba(255, 0, 0, .5);
}
/* Index/Reply Navigation */
#navlinks {
@ -762,6 +872,9 @@ span.hide-announcement {
top: 25px;
right: 10px;
}
:root.catalog-mode #navlinks {
display: none;
}
/* Filter */
.opContainer.filter-highlight {
@ -770,6 +883,10 @@ span.hide-announcement {
.filter-highlight > .reply {
box-shadow: -5px 0 rgba(255, 0, 0, .5);
}
.pinned .catalog-thumb,
.filter-highlight .catalog-thumb {
border: 2px solid rgba(255, 0, 0, .5);
}
/* Spoiler text */
:root.reveal-spoilers s {

View File

@ -1,6 +1,15 @@
<span class="brackets-wrap returnlink"><a href="javascript:;">Return</a></span>
<span class="brackets-wrap cataloglink"><a href="javascript:;">Catalog</a></span>
<span class="brackets-wrap returnlink"><a href="./">Return</a></span>
<span class="brackets-wrap cataloglink"><a href="./catalog">Catalog</a></span>
<span class="brackets-wrap bottomlink"><a href="#bottom">Bottom</a></span>
<span class="brackets-wrap" id="index-last-refresh"><time title="Last index refresh">...</time></span>
<input type="search" id="index-search" class="field" placeholder="Search">
<a id="index-search-clear" href="javascript:;" title="Clear search">×</a>
<span id="hidden-label" hidden> &mdash; <span id="hidden-count"></span> <span id="hidden-toggle">[<a href="javascript:;">Show</a>]</span></span>
<select id="index-sort" name="Index Sort">
<option disabled>Index Sort</option>
<option value="bump">Bump order</option>
<option value="lastreply">Last reply</option>
<option value="birth">Creation date</option>
<option value="replycount">Reply count</option>
<option value="filecount">File count</option>
</select>

View File

@ -0,0 +1,16 @@
class CatalogThread
@callbacks = new Callbacks 'CatalogThread'
toString: -> @ID
constructor: (root, @thread) ->
@ID = @thread.ID
@board = @thread.board
@nodes =
root: root
thumb: $ '.catalog-thumb', root
icons: $ '.catalog-icons', root
postCount: $ '.post-count', root
fileCount: $ '.file-count', root
pageCount: $ '.page-count', root
comment: $ '.comment', root
@thread.catalogView = @

View File

@ -1,9 +1,10 @@
<%= grunt.file.read('src/General/lib/callbacks.class') %>
<%= grunt.file.read('src/General/lib/board.class') %>
<%= grunt.file.read('src/General/lib/thread.class') %>
<%= grunt.file.read('src/General/lib/catalogthread.class') %>
<%= grunt.file.read('src/General/lib/post.class') %>
<%= grunt.file.read('src/General/lib/clone.class') %>
<%= grunt.file.read('src/General/lib/databoard.class') %>
<%= grunt.file.read('src/General/lib/notice.class') %>
<%= grunt.file.read('src/General/lib/randomaccesslist.class') %>
<%= grunt.file.read('src/General/lib/simpledict.class') %>
<%= grunt.file.read('src/General/lib/simpledict.class') %>

View File

@ -1,5 +1,5 @@
class DataBoard
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
@keys = ['pinnedThreads', 'hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
constructor: (@key, sync, dontClean) ->
@data = Conf[key]

View File

@ -78,6 +78,9 @@ class Post
@parseQuotes()
@parseFile that
@isDead = false
@isHidden = false
@clones = []
g.posts.push @fullID, thread.posts.push @, board.posts.push @, @
@kill() if that.isArchived
@ -85,6 +88,7 @@ class Post
parseComment: ->
# Merge text nodes and remove empty ones.
@nodes.comment.normalize()
# Get the comment's text.
# <br> -> \n
# Remove:
@ -97,7 +101,11 @@ class Post
for node in $$ '.abbr, .exif, b', bq
$.rm node
@info.comment = @nodesToText bq
# Hide spoilers.
# Save cleaned comment HTML.
@info.commentHTML = <%= html('&{bq}') %>
# Get the comment's text with spoilers hidden.
spoilers = $$ 's', bq
@info.commentSpoilered = if spoilers.length
for node in spoilers
@ -177,17 +185,14 @@ class Post
$.rmClass node, 'desktop'
return
kill: (file, now) ->
now or= new Date()
kill: (file) ->
if file
return if @file.isDead
@file.isDead = true
@file.timeOfDeath = now
$.addClass @nodes.root, 'deleted-file'
else
return if @isDead
@isDead = true
@timeOfDeath = now
$.addClass @nodes.root, 'deleted-post'
unless strong = $ 'strong.warning', @nodes.info
@ -199,7 +204,7 @@ class Post
return if @isClone
for clone in @clones
clone.kill file, now
clone.kill file
return if file
# Get quotelinks/backlinks to this post
@ -212,7 +217,6 @@ class Post
# giving us false-positive dead posts.
resurrect: ->
delete @isDead
delete @timeOfDeath
$.rmClass @nodes.root, 'deleted-post'
strong = $ 'strong.warning', @nodes.info
# no false-positive files

View File

@ -5,12 +5,19 @@ class Thread
constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}"
@posts = new SimpleDict
@isDead = false
@isHidden = false
@isOnTop = false
@isPinned = false
@isSticky = false
@isClosed = false
@isArchived = false
@postLimit = false
@fileLimit = false
@OP = null
@catalogView = null
g.threads.push @fullID, board.threads.push @, @
setPage: (pageNum) ->
@ -20,6 +27,12 @@ class Thread
$.after $('a[title="Reply to this post"]', info), [$.tn(' '), icon]
icon.title = "This thread is on page #{pageNum} in the original index."
icon.textContent = "[#{pageNum}]"
@catalogView.nodes.pageCount.textContent = pageNum if @catalogView
setCount: (type, count, reachedLimit) ->
return unless @catalogView
el = @catalogView.nodes["#{type}Count"]
el.textContent = count
(if reachedLimit then $.addClass else $.rmClass) el, 'warning'
setStatus: (type, status) ->
name = "is#{type}"
@ -37,21 +50,31 @@ class Thread
unless status
$.rm icon.previousSibling
$.rm icon
$.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView
return
icon = $.el 'img',
src: "//s.4cdn.org/image/#{typeLC}#{if window.devicePixelRatio >= 2 then '@2x' else ''}.gif"
src: "#{Build.staticPath}#{typeLC}#{Build.gifIcon}"
alt: type
title: type
className: "#{typeLC}Icon retina"
root = if type isnt 'Sticky' and @isSticky
$ '.stickyIcon', @OP.nodes.info
else
$('.page-num', @OP.nodes.info) or $('[title="Reply to this post"]', @OP.nodes.info)
$('.page-num', @OP.nodes.info) or $('[title="Reply to this post"]', @OP.nodes.info)
$.after root, [$.tn(' '), icon]
return unless @catalogView
(if type is 'Sticky' and @isClosed then $.prepend else $.add) @catalogView.nodes.icons, icon.cloneNode()
pin: ->
@isPinned = true
$.addClass @catalogView.nodes.root, 'pinned' if @catalogView
unpin: ->
@isPinned = false
$.rmClass @catalogView.nodes.root, 'pinned' if @catalogView
kill: ->
@isDead = true
@timeOfDeath = Date.now()
collect: ->
@posts.forEach (post) -> post.collect()

View File

@ -20,10 +20,22 @@ FappeTyme =
name: 'Fappe Tyme'
cb: @node
CatalogThread.callbacks.push
name: 'Werk Tyme'
cb: @catalogNode
node: ->
return if @file
$.addClass @nodes.root, "noFile"
catalogNode: ->
{file} = @thread.OP
return if !file
filename = $.el 'div',
textContent: file.name
className: 'werkTyme-filename'
$.add @nodes.thumb.parentNode, filename
cb:
set: (type) ->
FappeTyme[type].checked = Conf[type]

View File

@ -11,18 +11,22 @@ Menu =
Post.callbacks.push
name: 'Menu'
cb: @node
CatalogThread.callbacks.push
name: 'Menu'
cb: @catalogNode
node: ->
if @isClone
$.on $('.menu-button', @nodes.info), 'click', Menu.toggle
Menu.makeButton @, $('.menu-button', @nodes.info)
return
$.add @nodes.info, Menu.makeButton()
$.add @nodes.info, Menu.makeButton @
makeButton: ->
clone = Menu.button.cloneNode true
$.on clone, 'click', Menu.toggle
clone
catalogNode: ->
post = g.threads[@thread.fullID].OP
$.after @nodes.icons, Menu.makeButton post
toggle: (e) ->
post = Get.postFromNode @
Menu.menu.toggle e, @, post
makeButton: (post, button) ->
button or= Menu.button.cloneNode true
$.on button, 'click', (e) ->
Menu.menu.toggle e, @, post
button

View File

@ -24,14 +24,17 @@ CatalogLinks =
CatalogLinks.set @checked
set: (useCatalog) ->
path = if useCatalog then 'catalog' else ''
path = if useCatalog
if Conf['JSON Navigation'] and Conf['Use 4chan X Catalog'] then '#catalog' else 'catalog'
else
''
generateURL = if useCatalog and Conf['External Catalog']
CatalogLinks.external
else
(board) -> a.href = "/#{board}/#{path}"
for a in $$ """#board-list a:not(.catalog), #boardNavDesktopFoot a"""
for a in $$ """#board-list a:not([data-only]), #boardNavDesktopFoot a"""
continue if a.hostname not in ['boards.4chan.org', 'catalog.neet.tv', '4index.gropes.us'] or
!(board = a.pathname.split('/')[1]) or
board in ['f', 'status', '4chan'] or

View File

@ -21,7 +21,7 @@ Fourchan =
if (!jsMath) return;
if (jsMath.loaded) {
// process one post
jsMath.ProcessBeforeShowing(document.getElementById(e.detail));
jsMath.ProcessBeforeShowing(e.target);
} else if (jsMath.Autoload && jsMath.Autoload.checked) {
// load jsMath and process whole document
jsMath.Autoload.Script.Push('ProcessBeforeShowing', [null]);
@ -32,6 +32,9 @@ Fourchan =
Post.callbacks.push
name: 'Parse /sci/ math'
cb: @math
CatalogThread.callbacks.push
name: 'Parse /sci/ math'
cb: @math
code: ->
return if @isClone
apply = (e) ->
@ -44,8 +47,8 @@ Fourchan =
return
math: ->
return if (@isClone and doc.contains @origin.nodes.root) or !$ '.math', @nodes.comment
$.asap (=> doc.contains @nodes.post), =>
$.event 'jsmath', @nodes.post.id, window
$.asap (=> doc.contains @nodes.comment), =>
$.event 'jsmath', null, @nodes.comment
parseThread: (threadID, offset, limit) ->
# Fix /sci/
# Fix /g/

View File

@ -21,21 +21,21 @@ Keybinds =
{target} = e
if target.nodeName in ['INPUT', 'TEXTAREA']
return unless /(Esc|Alt|Ctrl|Meta|Shift\+\w{2,})/.test key
unless g.VIEW is 'catalog'
unless g.VIEW is 'catalog' or g.VIEW is 'index' and Conf['JSON Navigation'] and Conf['Index Mode'] is 'catalog'
threadRoot = Nav.getThread()
if op = $ '.op', threadRoot
thread = Get.postFromNode(op).thread
switch key
# QR & Options
when Conf['Toggle board list']
if Conf['Custom Board Navigation']
Header.toggleBoardList()
return unless Conf['Custom Board Navigation']
Header.toggleBoardList()
when Conf['Toggle header']
Header.toggleBarVisibility()
when Conf['Open empty QR']
Keybinds.qr()
when Conf['Open QR']
return if g.VIEW is 'catalog'
return unless threadRoot
Keybinds.qr threadRoot
when Conf['Open settings']
Settings.open()
@ -45,11 +45,13 @@ Keybinds =
else if (notifications = $$ '.notification').length
for notification in notifications
$('.close', notification).click()
else if QR.nodes
else if QR.nodes and !QR.nodes.el.hidden
if Conf['Persistent QR']
QR.hide()
else
QR.close()
else
return
when Conf['Spoiler tags']
return if target.nodeName isnt 'TEXTAREA'
Keybinds.tags 'spoiler', target
@ -63,25 +65,31 @@ Keybinds =
return if target.nodeName isnt 'TEXTAREA'
Keybinds.tags 'math', target
when Conf['Toggle sage']
Keybinds.sage() if QR.nodes
return unless QR.nodes and !QR.nodes.el.hidden
Keybinds.sage()
when Conf['Submit QR']
QR.submit() if QR.nodes and !QR.status()
return unless QR.nodes and !QR.nodes.el.hidden
QR.submit() if !QR.status()
# Index/Thread related
when Conf['Update']
switch g.VIEW
when 'thread'
ThreadUpdater.update() if Conf['Thread Updater']
return unless Conf['Thread Updater']
ThreadUpdater.update()
when 'index'
if Conf['JSON Navigation'] then Index.update()
return unless Conf['JSON Navigation']
Index.update()
else
return
when Conf['Watch']
return if g.VIEW is 'catalog'
return unless thread
ThreadWatcher.toggle thread
# Images
when Conf['Expand image']
return if g.VIEW is 'catalog'
return unless threadRoot
Keybinds.img threadRoot
when Conf['Expand images']
return if g.VIEW is 'catalog'
return unless threadRoot
Keybinds.img threadRoot, true
when Conf['Open Gallery']
return if g.VIEW is 'catalog'
@ -95,6 +103,10 @@ Keybinds =
# Board Navigation
when Conf['Front page']
if Conf['JSON Navigation'] and g.VIEW is 'index'
if Conf['Use 4chan X Catalog'] and Conf['Index Mode'] is 'catalog'
window.location = '#index'
return
return unless Conf['Index Mode'] in ['paged', 'infinite']
Index.userPageNav 1
else
window.location = "/#{g.BOARD}/"
@ -103,16 +115,16 @@ Keybinds =
when Conf['Next page']
return unless g.VIEW is 'index'
if Conf['JSON Navigation']
if Conf['Index Mode'] isnt 'all pages'
$('.next button', Index.pagelist).click()
return unless Conf['Index Mode'] in ['paged', 'infinite']
$('.next button', Index.pagelist).click()
else
if form = $ '.next form'
window.location = form.action
when Conf['Previous page']
return unless g.VIEW is 'index'
if Conf['JSON Navigation']
if Conf['Index Mode'] isnt 'all pages'
$('.prev button', Index.pagelist).click()
return unless Conf['Index Mode'] in ['paged', 'infinite']
$('.prev button', Index.pagelist).click()
else
if form = $ '.prev form'
window.location = form.action
@ -125,45 +137,45 @@ Keybinds =
if Conf['External Catalog']
window.location = CatalogLinks.external(g.BOARD.ID)
else
window.location = "/#{g.BOARD}/catalog"
window.location = "/#{g.BOARD}/" + if Conf['JSON Navigation'] and Conf['Use 4chan X Catalog'] then '#catalog' else 'catalog'
# Thread Navigation
when Conf['Next thread']
return if g.VIEW isnt 'index'
return if g.VIEW isnt 'index' or !threadRoot
Nav.scroll +1
when Conf['Previous thread']
return if g.VIEW isnt 'index'
return if g.VIEW isnt 'index' or !threadRoot
Nav.scroll -1
when Conf['Expand thread']
return if g.VIEW isnt 'index'
return if g.VIEW isnt 'index' or !threadRoot
ExpandThread.toggle thread
when Conf['Open thread']
return if g.VIEW isnt 'index'
return if g.VIEW isnt 'index' or !threadRoot
Keybinds.open thread
when Conf['Open thread tab']
return if g.VIEW isnt 'index'
return if g.VIEW isnt 'index' or !threadRoot
Keybinds.open thread, true
# Reply Navigation
when Conf['Next reply']
return if g.VIEW is 'catalog'
return unless threadRoot
Keybinds.hl +1, threadRoot
when Conf['Previous reply']
return if g.VIEW is 'catalog'
return unless threadRoot
Keybinds.hl -1, threadRoot
when Conf['Deselect reply']
return if g.VIEW is 'catalog'
return unless threadRoot
Keybinds.hl 0, threadRoot
when Conf['Hide']
return if g.VIEW is 'catalog'
return unless thread
ThreadHiding.toggle thread if ThreadHiding.db
when Conf['Previous Post Quoting You']
return if g.VIEW is 'catalog'
return unless threadRoot
QuoteYou.cb.seek 'preceding'
when Conf['Next Post Quoting You']
return if g.VIEW is 'catalog'
return unless threadRoot
QuoteYou.cb.seek 'following'
<% if (tests_enabled) { %>
when 't'
return if g.VIEW is 'catalog'
return unless threadRoot
BuildTest.testAll()
<% } %>
else