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}' + + '
&{thread.OP.info.commentHTML}
' + ) %> + + 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 × + + 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