diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c95cd74..773f836d6 100644 --- a/CHANGELOG.md +++ b/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: + + - 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. diff --git a/Gruntfile.coffee b/Gruntfile.coffee index c97e8e155..64a932158 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -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' diff --git a/css/style.css b/css/style.css index f697b45e2..5abad3b16 100644 --- a/css/style.css +++ b/css/style.css @@ -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, diff --git a/html/General/Thread-catalog-view.html b/html/General/Thread-catalog-view.html new file mode 100644 index 000000000..cf87775c9 --- /dev/null +++ b/html/General/Thread-catalog-view.html @@ -0,0 +1,9 @@ + + +
+
+
+ #{postCount} / #{fileCount} / #{pageCount} +
+#{subject} +
#{comment}
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee index eee0dfae9..85c86d215 100644 --- a/src/Filtering/Filter.coffee +++ b/src/Filtering/Filter.coffee @@ -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 diff --git a/src/Filtering/ThreadHiding.coffee b/src/Filtering/ThreadHiding.coffee index 6d5635201..155a94be1 100644 --- a/src/Filtering/ThreadHiding.coffee +++ b/src/Filtering/ThreadHiding.coffee @@ -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 diff --git a/src/General/Build.coffee b/src/General/Build.coffee index 6fa1278b8..f5d81b137 100644 --- a/src/General/Build.coffee +++ b/src/General/Build.coffee @@ -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 + "
#{thread.OP.info.subject}
" + else + '' + comment = thread.OP.nodes.comment.innerHTML.replace /(
){2,}/g, '
' + + 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 diff --git a/src/General/CatalogThread.coffee b/src/General/CatalogThread.coffee new file mode 100644 index 000000000..ae8f832d0 --- /dev/null +++ b/src/General/CatalogThread.coffee @@ -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 = @ diff --git a/src/General/DataBoard.coffee b/src/General/DataBoard.coffee index aba14d68d..dc4319816 100644 --- a/src/General/DataBoard.coffee +++ b/src/General/DataBoard.coffee @@ -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/Index.coffee b/src/General/Index.coffee index 79a935099..1ce167006 100644 --- a/src/General/Index.coffee +++ b/src/General/Index.coffee @@ -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: ' Paged' } { el: $.el 'label', innerHTML: ' All threads' } + { el: $.el 'label', innerHTML: ' 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: -> diff --git a/src/General/Thread.coffee b/src/General/Thread.coffee index 410a4288a..8037f09d2 100644 --- a/src/General/Thread.coffee +++ b/src/General/Thread.coffee @@ -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() diff --git a/src/Images/AutoGIF.coffee b/src/Images/AutoGIF.coffee index 71ccc7d8e..a80481407 100644 --- a/src/Images/AutoGIF.coffee +++ b/src/Images/AutoGIF.coffee @@ -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 + diff --git a/src/Images/ImageHover.coffee b/src/Images/ImageHover.coffee index 9494b0c71..f205c941c 100644 --- a/src/Images/ImageHover.coffee +++ b/src/Images/ImageHover.coffee @@ -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 diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee index 3eac7ae4c..ab570ebb0 100644 --- a/src/Monitoring/ThreadWatcher.coffee +++ b/src/Monitoring/ThreadWatcher.coffee @@ -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