Merge pull request #1405 from MayhemYDG/index
Index navigation improvements - EP04
This commit is contained in:
commit
20ceb570e1
12
CHANGELOG.md
12
CHANGELOG.md
@ -1,3 +1,15 @@
|
||||
- More index navigation improvements:
|
||||
- New index mode: `catalog`
|
||||
- When in catalog mode, use `Shift+Click` to hide, and `Alt+Click` to pin threads.
|
||||
- Existing features affect the catalog mode such as:
|
||||
<ul>
|
||||
<li> Filter (hiding, highlighting)
|
||||
<li> Thread Hiding
|
||||
<li> Linkify
|
||||
<li> Auto-GIF
|
||||
<li> Image Hover
|
||||
</ul>
|
||||
- Support for the official catalog will be removed in the future, once the catalog mode for the index is deemed satisfactory.
|
||||
- Added `Original filename` variable to Sauce panel.
|
||||
- Added a `Reset Settings` button in the settings.
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ module.exports = (grunt) ->
|
||||
# <--|
|
||||
'src/General/Board.coffee'
|
||||
'src/General/Thread.coffee'
|
||||
'src/General/CatalogThread.coffee'
|
||||
'src/General/Post.coffee'
|
||||
'src/General/Clone.coffee'
|
||||
'src/General/DataBoard.coffee'
|
||||
|
||||
@ -401,6 +401,58 @@ a[href="javascript:;"] {
|
||||
.summary {
|
||||
text-decoration: none;
|
||||
}
|
||||
.catalog-mode {
|
||||
text-align: center;
|
||||
}
|
||||
.catalog-thread {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-top: 5px;
|
||||
width: 165px;
|
||||
max-height: 320px;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
.catalog-thread > a {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.thumb {
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, .25);
|
||||
}
|
||||
.thunb.spoiler-file {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.thumb.deleted-file {
|
||||
width: 127px;
|
||||
height: 13px;
|
||||
padding: 20px 11px;
|
||||
}
|
||||
.thumb.no-file {
|
||||
width: 77px;
|
||||
height: 13px;
|
||||
padding: 20px 36px;
|
||||
}
|
||||
.thread-icons {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
}
|
||||
.thread-stats {
|
||||
cursor: help;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: .8;
|
||||
margin-top: 1px;
|
||||
float: none;
|
||||
}
|
||||
.catalog-thread .subject {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Announcement Hiding */
|
||||
:root.hide-announcement #globalMessage,
|
||||
@ -597,6 +649,10 @@ a.hide-announcement {
|
||||
.filter-highlight > .reply {
|
||||
box-shadow: -5px 0 rgba(255, 0, 0, .5);
|
||||
}
|
||||
.pinned .thumb,
|
||||
.filter-highlight .thumb {
|
||||
border: 2px solid rgba(255, 0, 0, .5);
|
||||
}
|
||||
|
||||
/* Thread & Reply Hiding */
|
||||
.hide-thread-button,
|
||||
|
||||
9
html/General/Thread-catalog-view.html
Normal file
9
html/General/Thread-catalog-view.html
Normal file
@ -0,0 +1,9 @@
|
||||
<a href="/#{thread.board}/res/#{thread.ID}" target="_blank">
|
||||
<img src="#{src}" class="thumb #{imgClass or ''}" #{if imgWidth then "width='#{imgWidth}'"} #{if imgHeight then "height='#{imgHeight}'"}>
|
||||
<div class="thread-icons"></div>
|
||||
</a>
|
||||
<div class="thread-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>
|
||||
</div>
|
||||
#{subject}
|
||||
<div class="comment">#{comment}</div>
|
||||
@ -110,6 +110,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
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
ThreadHiding =
|
||||
init: ->
|
||||
return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] and !Conf['Thread Hiding Link']
|
||||
return if g.VIEW isnt 'index'
|
||||
|
||||
@db = new DataBoard 'hiddenThreads'
|
||||
@syncCatalog()
|
||||
$.on d, 'IndexBuild', @onIndexBuild
|
||||
$.on d, 'IndexRefresh', @onIndexRefresh
|
||||
Thread.callbacks.push
|
||||
name: 'Thread Hiding'
|
||||
cb: @node
|
||||
@ -15,8 +15,8 @@ ThreadHiding =
|
||||
return unless Conf['Thread Hiding']
|
||||
$.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide'
|
||||
|
||||
onIndexBuild: ({detail: nodes}) ->
|
||||
for root, i in nodes by 2
|
||||
onIndexRefresh: ->
|
||||
for root, i in Index.nodes by 2
|
||||
thread = Get.threadFromRoot root
|
||||
continue unless thread.isHidden
|
||||
unless thread.stub
|
||||
|
||||
@ -254,9 +254,69 @@ Build =
|
||||
[posts, files] = if Conf['Show Replies']
|
||||
[data.omitted_posts, data.omitted_images]
|
||||
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
|
||||
|
||||
$.add root, nodes
|
||||
root
|
||||
catalogThread: (thread) ->
|
||||
{staticPath, gifIcon} = Build
|
||||
data = Index.liveThreadData[Index.liveThreadIDs.indexOf thread.ID]
|
||||
|
||||
if data.spoiler and !Conf['Reveal Spoilers']
|
||||
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'
|
||||
|
||||
postCount = data.replies + 1
|
||||
fileCount = data.images + !!data.ext
|
||||
pageCount = Math.floor Index.liveThreadIDs.indexOf(thread.ID) / Index.threadsNumPerPage
|
||||
|
||||
subject = if thread.OP.info.subject
|
||||
"<div class='subject'>#{thread.OP.info.subject}</div>"
|
||||
else
|
||||
''
|
||||
comment = thread.OP.nodes.comment.innerHTML.replace /(<br>){2,}/g, '<br>'
|
||||
|
||||
root = $.el 'div',
|
||||
className: 'catalog-thread'
|
||||
innerHTML: <%= importHTML('General/Thread-catalog-view') %>
|
||||
|
||||
root.dataset.fullID = thread.fullID
|
||||
$.addClass root, 'pinned' if thread.isPinned
|
||||
$.addClass root, thread.OP.highlights... if thread.OP.highlights
|
||||
|
||||
for quotelink in $$ '.quotelink', root.lastElementChild
|
||||
$.replace quotelink, [quotelink.childNodes...]
|
||||
|
||||
if thread.isSticky
|
||||
$.add $('.thread-icons', root), $.el 'img',
|
||||
src: "#{staticPath}sticky#{gifIcon}"
|
||||
className: 'stickyIcon'
|
||||
title: 'Sticky'
|
||||
if thread.isClosed
|
||||
$.add $('.thread-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
|
||||
|
||||
14
src/General/CatalogThread.coffee
Normal file
14
src/General/CatalogThread.coffee
Normal file
@ -0,0 +1,14 @@
|
||||
class CatalogThread
|
||||
@callbacks = []
|
||||
toString: -> @ID
|
||||
|
||||
constructor: (root, @thread) ->
|
||||
@ID = @thread.ID
|
||||
@board = @thread.board
|
||||
@nodes =
|
||||
root: root
|
||||
thumb: $ '.thumb', root
|
||||
postCount: $ '.post-count', root
|
||||
fileCount: $ '.file-count', root
|
||||
pageCount: $ '.page-count', root
|
||||
@thread.catalogView = @
|
||||
@ -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]
|
||||
|
||||
@ -1,7 +1,25 @@
|
||||
Index =
|
||||
init: ->
|
||||
if g.VIEW is 'catalog'
|
||||
$.ready ->
|
||||
span = $.el 'a',
|
||||
href: '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md'
|
||||
textContent: 'Support for the official catalog to be removed'
|
||||
title: '<%= meta.name %> now has a "catalog" Index mode.'
|
||||
className: 'btn-wrap warning'
|
||||
target: '_blank'
|
||||
$.add $.id('info'), span
|
||||
|
||||
return if g.VIEW isnt 'index' or g.BOARD.ID is 'f'
|
||||
|
||||
@db = new DataBoard 'pinnedThreads'
|
||||
Thread.callbacks.push
|
||||
name: 'Thread Pinning'
|
||||
cb: @threadNode
|
||||
CatalogThread.callbacks.push
|
||||
name: 'Catalog Features'
|
||||
cb: @catalogNode
|
||||
|
||||
@button = $.el 'a',
|
||||
className: 'index-refresh-shortcut fa fa-refresh'
|
||||
title: 'Refresh Index'
|
||||
@ -14,6 +32,7 @@ Index =
|
||||
subEntries: [
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="paged"> Paged' }
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="all pages"> All threads' }
|
||||
{ el: $.el 'label', innerHTML: '<input type=radio name="Index Mode" value="catalog"> Catalog' }
|
||||
]
|
||||
for label in modeEntry.subEntries
|
||||
input = label.el.firstChild
|
||||
@ -68,6 +87,7 @@ Index =
|
||||
$.addClass doc, 'index-loading'
|
||||
@update()
|
||||
@root = $.el 'div', className: 'board'
|
||||
Index.cb.rootClass()
|
||||
@pagelist = $.el 'div',
|
||||
className: 'pagelist'
|
||||
hidden: true
|
||||
@ -100,8 +120,44 @@ Index =
|
||||
$.asap (-> $('.pagelist') or d.readyState isnt 'loading'), ->
|
||||
$.replace $('.pagelist'), Index.pagelist
|
||||
|
||||
threadNode: ->
|
||||
return unless data = Index.db.get {boardID: @board.ID, threadID: @ID}
|
||||
@pin() if data.isPinned
|
||||
catalogNode: ->
|
||||
$.on @nodes.thumb, 'click', Index.onClick
|
||||
onClick: (e) ->
|
||||
return if e.button isnt 0
|
||||
root = @parentNode.parentNode
|
||||
thread = g.threads[root.dataset.fullID]
|
||||
if e.shiftKey
|
||||
$.rm root
|
||||
ThreadHiding.hide thread
|
||||
ThreadHiding.saveHiddenState thread
|
||||
else if e.altKey
|
||||
Index.togglePin thread
|
||||
else
|
||||
return
|
||||
e.preventDefault()
|
||||
togglePin: (thread) ->
|
||||
if thread.isPinned
|
||||
thread.unpin()
|
||||
Index.db.delete
|
||||
boardID: thread.board.ID
|
||||
threadID: thread.ID
|
||||
else
|
||||
thread.pin()
|
||||
Index.db.set
|
||||
boardID: thread.board.ID
|
||||
threadID: thread.ID
|
||||
val: isPinned: thread.isPinned
|
||||
Index.sort()
|
||||
Index.buildIndex()
|
||||
|
||||
cb:
|
||||
rootClass: ->
|
||||
(if Conf['Index Mode'] is 'catalog' then $.addClass else $.rmClass) Index.root, 'catalog-mode'
|
||||
mode: ->
|
||||
Index.cb.rootClass()
|
||||
Index.togglePagelist()
|
||||
Index.buildIndex()
|
||||
sort: ->
|
||||
@ -289,6 +345,8 @@ Index =
|
||||
Index.nodes.push threadRoot, $.el 'hr'
|
||||
if thread = g.BOARD.threads[threadData.no]
|
||||
thread.setPage Math.floor i / Index.threadsNumPerPage
|
||||
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
|
||||
@ -334,6 +392,16 @@ Index =
|
||||
|
||||
Main.handleErrors errors if errors
|
||||
Main.callbackNodes Post, posts
|
||||
buildCatalogViews: ->
|
||||
threads = Index.sortedNodes
|
||||
.filter((n, i) -> !(i % 2))
|
||||
.map((threadRoot) -> Get.threadFromRoot threadRoot)
|
||||
.filter (thread) -> !thread.isHidden
|
||||
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: ->
|
||||
switch Conf['Index Sort']
|
||||
when 'bump'
|
||||
@ -368,16 +436,19 @@ Index =
|
||||
Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
|
||||
return
|
||||
buildIndex: ->
|
||||
if Conf['Index Mode'] is 'paged'
|
||||
pageNum = Index.getCurrentPage()
|
||||
nodesPerPage = Index.threadsNumPerPage * 2
|
||||
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
|
||||
else
|
||||
nodes = Index.sortedNodes
|
||||
switch Conf['Index Mode']
|
||||
when 'paged'
|
||||
pageNum = Index.getCurrentPage()
|
||||
nodesPerPage = Index.threadsNumPerPage * 2
|
||||
nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
|
||||
when 'catalog'
|
||||
nodes = Index.buildCatalogViews()
|
||||
else
|
||||
nodes = Index.sortedNodes
|
||||
$.rmAll Index.root
|
||||
Index.buildReplies nodes if Conf['Show Replies']
|
||||
$.event 'IndexBuild', nodes
|
||||
Index.buildReplies nodes if Conf['Show Replies'] and Conf['Index Mode'] isnt 'catalog'
|
||||
$.add Index.root, nodes
|
||||
$.event 'IndexBuild', nodes
|
||||
|
||||
isSearching: false
|
||||
clearSearch: ->
|
||||
|
||||
@ -10,13 +10,21 @@ class Thread
|
||||
@postLimit = false
|
||||
@fileLimit = false
|
||||
|
||||
@OP = null
|
||||
@catalogView = null
|
||||
|
||||
g.threads[@fullID] = board.threads[@] = @
|
||||
|
||||
setPage: (pageNum) ->
|
||||
icon = $ '.page-num', @OP.nodes.post
|
||||
for key in ['title', 'textContent']
|
||||
icon[key] = icon[key].replace /\d+/, pageNum
|
||||
return
|
||||
@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}"
|
||||
return if @[name] is status
|
||||
@ -25,20 +33,33 @@ class Thread
|
||||
typeLC = type.toLowerCase()
|
||||
unless status
|
||||
$.rm $ ".#{typeLC}Icon", @OP.nodes.info
|
||||
$.rm $ ".#{typeLC}Icon", @catalogView 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"
|
||||
root = if type is 'Closed' and @isSticky
|
||||
$ '.stickyIcon', @OP.nodes.info
|
||||
else if g.VIEW is 'index'
|
||||
$ '.page-num', @OP.nodes.info
|
||||
$ '.page-num', @OP.nodes.info
|
||||
else
|
||||
$ '[title="Quote this post"]', @OP.nodes.info
|
||||
$.after root, [$.tn(' '), icon]
|
||||
|
||||
return unless @catalogView
|
||||
root = $ '.thread-icons', @catalogView
|
||||
(if type is 'Sticky' and @isClosed then $.prepend else $.add) root, icon.cloneNode()
|
||||
|
||||
pin: ->
|
||||
@isOnTop = @isPinned = true
|
||||
$.addClass @catalogView.nodes.root, 'pinned' if @catalogView
|
||||
unpin: ->
|
||||
@isOnTop = @isPinned = false
|
||||
$.rmClass @catalogView.nodes.root, 'pinned' if @catalogView
|
||||
|
||||
kill: ->
|
||||
@isDead = true
|
||||
@timeOfDeath = Date.now()
|
||||
|
||||
@ -5,6 +5,9 @@ AutoGIF =
|
||||
Post.callbacks.push
|
||||
name: 'Auto-GIF'
|
||||
cb: @node
|
||||
CatalogThread.callbacks.push
|
||||
name: 'Auto-GIF'
|
||||
cb: @catalogNode
|
||||
node: ->
|
||||
return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage
|
||||
{thumb, URL} = @file
|
||||
@ -13,8 +16,17 @@ AutoGIF =
|
||||
# Revealed spoilers do not have height/width set, this fixes auto-gifs dimensions.
|
||||
{style} = thumb
|
||||
style.maxHeight = style.maxWidth = if @isReply then '125px' else '250px'
|
||||
AutoGIF.replaceThumbnail thumb, URL
|
||||
catalogNode: ->
|
||||
{OP} = @thread
|
||||
return unless OP.file?.isImage
|
||||
{URL} = OP.file
|
||||
return unless /gif$/.test URL
|
||||
AutoGIF.replaceThumbnail @nodes.thumb, URL
|
||||
replaceThumbnail: (thumb, URL) ->
|
||||
gif = $.el 'img'
|
||||
$.on gif, 'load', ->
|
||||
# Replace the thumbnail once the GIF has finished loading.
|
||||
thumb.src = URL
|
||||
gif.src = URL
|
||||
|
||||
|
||||
@ -5,11 +5,20 @@ ImageHover =
|
||||
Post.callbacks.push
|
||||
name: 'Image Hover'
|
||||
cb: @node
|
||||
CatalogThread.callbacks.push
|
||||
name: 'Image Hover'
|
||||
cb: @catalogNode
|
||||
node: ->
|
||||
return unless @file?.isImage
|
||||
$.on @file.thumb, 'mouseover', ImageHover.mouseover
|
||||
catalogNode: ->
|
||||
return unless @thread.OP.file?.isImage
|
||||
$.on @nodes.thumb, 'mouseover', ImageHover.mouseover
|
||||
mouseover: (e) ->
|
||||
post = Get.postFromNode @
|
||||
post = if $.hasClass @, 'thumb'
|
||||
g.posts[@parentNode.parentNode.dataset.fullID]
|
||||
else
|
||||
Get.postFromNode @
|
||||
el = $.el 'img',
|
||||
id: 'ihover'
|
||||
src: post.file.URL
|
||||
|
||||
@ -72,9 +72,8 @@ ThreadWatcher =
|
||||
else if Conf['Auto Watch Reply']
|
||||
ThreadWatcher.add board.threads[threadID]
|
||||
onIndexRefresh: ->
|
||||
{db} = ThreadWatcher
|
||||
boardID = g.BOARD.ID
|
||||
for threadID, data of db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads
|
||||
for threadID, data of ThreadWatcher.db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads
|
||||
if Conf['Auto Prune']
|
||||
ThreadWatcher.db.delete {boardID, threadID}
|
||||
else
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user