diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index c043d40f4..aad1494b7 100755
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -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
diff --git a/src/Filtering/ThreadHiding.coffee b/src/Filtering/ThreadHiding.coffee
index e0ed48f41..31e4c130f 100755
--- a/src/Filtering/ThreadHiding.coffee
+++ b/src/Filtering/ThreadHiding.coffee
@@ -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']
diff --git a/src/General/Build.coffee b/src/General/Build.coffee
index e9c77f3a4..d0af8afdb 100755
--- a/src/General/Build.coffee
+++ b/src/General/Build.coffee
@@ -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('
') %>
+ else
+ <%= html('
') %>
+
+ 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('
${thread.OP.info.subject}
') %>
+ else
+ <%= html('') %>
+
+ root = $.el 'div',
+ className: 'catalog-thread'
+ $.extend root, <%= html(
+ '' +
+ '&{thumb}' +
+ '' +
+ '' +
+ '${postCount} / ${fileCount} / ${pageCount}' +
+ '' +
+ '
' +
+ '&{subject}' +
+ ''
+ ) %>
+
+ 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
diff --git a/src/General/Config.coffee b/src/General/Config.coffee
index 45408e983..2f995e035 100755
--- a/src/General/Config.coffee
+++ b/src/General/Config.coffee
@@ -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
diff --git a/src/General/Header.coffee b/src/General/Header.coffee
index ec293a3f4..c5ee2a4fc 100755
--- a/src/General/Header.coffee
+++ b/src/General/Header.coffee
@@ -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 '@'
diff --git a/src/General/Index.coffee b/src/General/Index.coffee
index cee9b5120..03d9a6f7c 100644
--- a/src/General/Index.coffee
+++ b/src/General/Index.coffee
@@ -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(' Paged') %> }
{ el: $.el 'label', <%= html(' Infinite scrolling') %> }
{ el: $.el 'label', <%= html(' All threads') %> }
+ { el: $.el 'label', <%= html(' 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(' Bump order') %> }
- { el: $.el 'label', <%= html(' Last reply') %> }
- { el: $.el 'label', <%= html(' Creation date') %> }
- { el: $.el 'label', <%= html(' Reply count') %> }
- { el: $.el 'label', <%= html(' 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) ->
diff --git a/src/General/Main.coffee b/src/General/Main.coffee
index 89942ddde..3bbe98561 100755
--- a/src/General/Main.coffee
+++ b/src/General/Main.coffee
@@ -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]
diff --git a/src/General/css/style.css b/src/General/css/style.css
index e516baa88..a328ca8e6 100755
--- a/src/General/css/style.css
+++ b/src/General/css/style.css
@@ -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 {
diff --git a/src/General/html/Features/Index-navlinks.html b/src/General/html/Features/Index-navlinks.html
index 4ba933836..11f3dff85 100644
--- a/src/General/html/Features/Index-navlinks.html
+++ b/src/General/html/Features/Index-navlinks.html
@@ -1,6 +1,15 @@
-Return
-Catalog
+Return
+Catalog
Bottom
×
+ — [Show]
+
diff --git a/src/General/lib/catalogthread.class b/src/General/lib/catalogthread.class
new file mode 100644
index 000000000..564f62bfb
--- /dev/null
+++ b/src/General/lib/catalogthread.class
@@ -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 = @
diff --git a/src/General/lib/classes.coffee b/src/General/lib/classes.coffee
index eba1788af..3b0b36558 100755
--- a/src/General/lib/classes.coffee
+++ b/src/General/lib/classes.coffee
@@ -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') %>
\ No newline at end of file
+<%= grunt.file.read('src/General/lib/simpledict.class') %>
diff --git a/src/General/lib/databoard.class b/src/General/lib/databoard.class
index 3545b5469..f0567d78d 100755
--- a/src/General/lib/databoard.class
+++ b/src/General/lib/databoard.class
@@ -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]
diff --git a/src/General/lib/post.class b/src/General/lib/post.class
index 2e9c89dec..fcc16f490 100755
--- a/src/General/lib/post.class
+++ b/src/General/lib/post.class
@@ -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.
#
-> \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
diff --git a/src/General/lib/thread.class b/src/General/lib/thread.class
index f5c4efd71..0b5273c5f 100755
--- a/src/General/lib/thread.class
+++ b/src/General/lib/thread.class
@@ -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()
diff --git a/src/Images/FappeTyme.coffee b/src/Images/FappeTyme.coffee
index a46b704aa..a540410b1 100755
--- a/src/Images/FappeTyme.coffee
+++ b/src/Images/FappeTyme.coffee
@@ -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]
diff --git a/src/Menu/Menu.coffee b/src/Menu/Menu.coffee
index cd89585ee..7b19153b2 100755
--- a/src/Menu/Menu.coffee
+++ b/src/Menu/Menu.coffee
@@ -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
diff --git a/src/Miscellaneous/CatalogLinks.coffee b/src/Miscellaneous/CatalogLinks.coffee
index 09f36ed06..cff23dfe8 100755
--- a/src/Miscellaneous/CatalogLinks.coffee
+++ b/src/Miscellaneous/CatalogLinks.coffee
@@ -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
diff --git a/src/Miscellaneous/Fourchan.coffee b/src/Miscellaneous/Fourchan.coffee
index 0014eb823..1e70fc2ce 100755
--- a/src/Miscellaneous/Fourchan.coffee
+++ b/src/Miscellaneous/Fourchan.coffee
@@ -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/
diff --git a/src/Miscellaneous/Keybinds.coffee b/src/Miscellaneous/Keybinds.coffee
index 93b4bdf26..0293783f5 100755
--- a/src/Miscellaneous/Keybinds.coffee
+++ b/src/Miscellaneous/Keybinds.coffee
@@ -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