Header = init: -> @menu = new UI.Menu 'header' @headerEl = $.el 'div', id: 'header' innerHTML: '
' headerBar = $('#header-bar', @headerEl) if $.get 'autohideHeaderBar', false $.addClass headerBar, 'autohide' menuButton = $.el 'a', className: 'menu-button' innerHTML: '[]' href: 'javascript:;' $.on menuButton, 'click', @menuToggle boardListButton = $.el 'span', className: 'show-board-list-button' innerHTML: '[+]' title: 'Toggle the board list.' $.on boardListButton, 'click', @toggleBoardList boardTitle = $.el 'a', className: 'board-name' innerHTML: "/#{g.BOARD}/ - ..." href: "/#{g.BOARD}/#{if g.VIEW is 'catalog' then 'catalog' else ''}" boardList = $.el 'span', className: 'board-list' hidden: true toggleBar = $.el 'div', id: 'toggle-header-bar' title: 'Toggle the header bar position.' $.on toggleBar, 'click', @toggleBar $.prepend headerBar, [menuButton, boardListButton, $.tn(' '), boardTitle, boardList, toggleBar] $.asap (-> d.body), -> $.prepend d.body, Header.headerEl $.asap (-> $.id 'boardNavDesktop'), @setBoardList setBoardList: -> if nav = $.id 'boardNavDesktop' if a = $ "a[href*='/#{g.BOARD}/']", nav a.className = 'current' $('.board-title', Header.headerEl).textContent = a.title $.add $('.board-list', Header.headerEl), Array::slice.call nav.childNodes toggleBoardList: -> node = @firstElementChild.firstChild if showBoardList = $.hasClass @, 'show-board-list-button' @className = 'hide-board-list-button' node.data = node.data.replace '+', '-' else @className = 'show-board-list-button' node.data = node.data.replace '-', '+' {headerEl} = Header $('.board-name', headerEl).hidden = showBoardList $('.board-list', headerEl).hidden = !showBoardList toggleBar: -> message = if isAutohiding = $.id('header-bar').classList.toggle 'autohide' 'The header bar will automatically hide itself.' else 'The header bar will remain visible.' new Notification 'info', message, 2 $.set 'autohideHeaderBar', isAutohiding menuToggle: (e) -> Header.menu.toggle e, @, g class Notification constructor: (@type, content, timeout) -> @el = $.el 'div', className: "notification #{type}" innerHTML: '×' $.on @el.firstElementChild, 'click', @close.bind @ if typeof content is 'string' content = $.tn content $.add @el.lastElementChild, content if timeout setTimeout @close.bind(@), timeout * $.SECOND el = @el $.ready -> $.add $.id('notifications'), el setType: (type) -> $.rmClass @el, @type $.addClass @el, type @type = type close: -> $.rm @el if @el.parentNode Settings = init: -> # 4chan X settings link link = $.el 'a', className: 'settings-link' textContent: '4chan X Settings' href: 'javascript:;' $.on link, 'click', Settings.open $.event 'AddMenuEntry', type: 'header' el: link # 4chan settings link link = $.el 'a', className: 'fourchan-settings-link' textContent: '4chan Settings' href: 'javascript:;' $.on link, 'click', -> $.id('settingsWindowLink').click() $.event 'AddMenuEntry', type: 'header' el: link open: -> !Conf['Disable 4chan\'s extension'] return unless Conf['Disable 4chan\'s extension'] settings = JSON.parse(localStorage.getItem '4chan-settings') or {} return if settings.disableAll settings.disableAll = true localStorage.setItem '4chan-settings', JSON.stringify settings open: -> Header.menu.close() # Here be settings Filter = filters: {} init: -> return if g.VIEW is 'catalog' or !Conf['Filter'] for key of Config.filter @filters[key] = [] for filter in Conf[key].split '\n' continue if filter[0] is '#' unless regexp = filter.match /\/(.+)\/(\w*)/ continue # Don't mix up filter flags with the regular expression. filter = filter.replace regexp[0], '' # Do not add this filter to the list if it's not a global one # and it's not specifically applicable to the current board. # Defaults to global. boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global' if boards isnt 'global' and not (g.BOARD.ID in boards.split ',') continue if key in ['uniqueID', 'MD5'] # MD5 filter will use strings instead of regular expressions. regexp = regexp[1] else try # Please, don't write silly regular expressions. regexp = RegExp regexp[1], regexp[2] catch err # I warned you, bro. new Notification 'warning', err.message, 60 continue # Filter OPs along with their threads, replies only, or both. # Defaults to replies only. op = filter.match(/[^t]op:(yes|no|only)/)?[1] or 'no' # Overrule the `Show Stubs` setting. # Defaults to stub showing. stub = switch filter.match(/stub:(yes|no)/)?[1] when 'yes' true when 'no' false else Conf['Stubs'] # Highlight the post, or hide it. # If not specified, the highlight class will be filter-highlight. # Defaults to post hiding. if hl = /highlight/.test filter hl = filter.match(/highlight:(\w+)/)?[1] or 'filter-highlight' # Put highlighted OP's thread on top of the board page or not. # Defaults to on top. top = filter.match(/top:(yes|no)/)?[1] or 'yes' top = top is 'yes' # Turn it into a boolean @filters[key].push @createFilter regexp, op, stub, hl, top # Only execute filter types that contain valid filters. unless @filters[key].length delete @filters[key] return unless Object.keys(@filters).length Post::callbacks.push name: 'Thread Hiding' cb: @node createFilter: (regexp, op, stub, hl, top) -> test = if typeof regexp is 'string' # MD5 checking (value) -> regexp is value else (value) -> regexp.test value settings = hide: !hl stub: stub class: hl top: top (value, isReply) -> if isReply and op is 'only' or !isReply and op is 'no' return false unless test value return false settings node: -> return if @isClone for key of Filter.filters value = Filter[key] @ # Continue if there's nothing to filter (no tripcode for example). continue if value is false for filter in Filter.filters[key] unless result = filter value, @isReply continue # Hide if result.hide if @isReply ReplyHiding.hide @, result.stub else if g.VIEW is 'index' ThreadHiding.hide @thread, result.stub else continue return # Highlight $.addClass @nodes.root, result.class if !@isReply and result.top and g.VIEW is 'index' # Put the highlighted OPs' thread on top of the board page... thisThread = @nodes.root.parentNode # ...before the first non highlighted thread. if firstThread = $ 'div[class="postContainer opContainer"]' unless firstThread is @nodes.root $.before firstThread.parentNode, [thisThread, thisThread.nextElementSibling] name: (post) -> if 'name' of post.info return post.info.name false uniqueID: (post) -> if 'uniqueID' of post.info return post.info.uniqueID false tripcode: (post) -> if 'tripcode' of post.info return post.info.tripcode false capcode: (post) -> if 'capcode' of post.info return post.info.capcode false email: (post) -> if 'email' of post.info return post.info.email false subject: (post) -> if 'subject' of post.info return post.info.subject or false false comment: (post) -> if 'comment' of post.info return post.info.comment false flag: (post) -> if 'flag' of post.info return post.info.flag false filename: (post) -> if post.file return post.file.name false dimensions: (post) -> if post.file and post.file.isImage return post.file.dimensions false filesize: (post) -> if post.file return post.file.size false MD5: (post) -> if post.file return post.file.MD5 false menu: init: -> return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Filter'] div = $.el 'div', textContent: 'Filter' entry = type: 'post' el: div open: (post) -> Filter.menu.post = post true subEntries: [] for type in [ ['Name', 'name'] ['Unique ID', 'uniqueID'] ['Tripcode', 'tripcode'] ['Capcode', 'capcode'] ['E-mail', 'email'] ['Subject', 'subject'] ['Comment', 'comment'] ['Flag', 'flag'] ['Filename', 'filename'] ['Image dimensions', 'dimensions'] ['Filesize', 'filesize'] ['Image MD5', 'MD5'] ] # Add a sub entry for each filter type. entry.subEntries.push Filter.menu.createSubEntry type[0], type[1] $.event 'AddMenuEntry', entry createSubEntry: (text, type) -> el = $.el 'a', href: 'javascript:;' textContent: text el.setAttribute 'data-type', type $.on el, 'click', Filter.menu.makeFilter return { el: el open: (post) -> value = Filter[type] post value isnt false } makeFilter: -> {type} = @dataset # Convert value -> regexp, unless type is MD5 value = Filter[type] Filter.menu.post re = if type in ['uniqueID', 'MD5'] then value else value.replace /// / | \\ | \^ | \$ | \n | \. | \( | \) | \{ | \} | \[ | \] | \? | \* | \+ | \| ///g, (c) -> if c is '\n' '\\n' else if c is '\\' '\\\\' else "\\#{c}" re = if type in ['uniqueID', 'MD5'] "/#{re}/" else "/^#{re}$/" unless Filter.menu.post.isReply re += ';op:yes' # Add a new line before the regexp unless the text is empty. save = $.get type, '' save = if save "#{save}\n#{re}" else re $.set type, save # Open the options and display & focus the relevant filter textarea. # Options.dialog() # select = $ 'select[name=filter]', $.id 'options' # select.value = type # $.event select, new Event 'change' # $.id('filter_tab').checked = true # ta = select.nextElementSibling # tl = ta.textLength # ta.setSelectionRange tl, tl # ta.focus() ThreadHiding = init: -> return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] @getHiddenThreads() @syncFromCatalog() @clean() Thread::callbacks.push name: 'Thread Hiding' cb: @node node: -> if data = ThreadHiding.hiddenThreads.threads[@] ThreadHiding.hide @, data.makeStub return unless Conf['Thread/Reply Hiding Buttons'] $.prepend @posts[@].nodes.root, ThreadHiding.makeButton @, 'hide' getHiddenThreads: -> hiddenThreads = $.get "hiddenThreads.#{g.BOARD}" unless hiddenThreads hiddenThreads = threads: {} lastChecked: Date.now() $.set "hiddenThreads.#{g.BOARD}", hiddenThreads ThreadHiding.hiddenThreads = hiddenThreads syncFromCatalog: -> # Sync hidden threads from the catalog into the index. hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} {threads} = ThreadHiding.hiddenThreads # Add threads that were hidden in the catalog. for threadID of hiddenThreadsOnCatalog continue if threadID of threads threads[threadID] = {} # Remove threads that were un-hidden in the catalog. for threadID of threads continue if threadID of threads delete threads[threadID] $.set "hiddenThreads.#{g.BOARD}", ThreadHiding.hiddenThreads clean: -> {hiddenThreads} = ThreadHiding {lastChecked} = hiddenThreads hiddenThreads.lastChecked = now = Date.now() return if lastChecked > now - $.DAY unless Object.keys(hiddenThreads.threads).length $.set "hiddenThreads.#{g.BOARD}", hiddenThreads return $.ajax "//api.4chan.org/#{g.BOARD}/catalog.json", onload: -> threads = {} for obj in JSON.parse @response for thread in obj.threads if thread.no of hiddenThreads.threads threads[thread.no] = hiddenThreads.threads[thread.no] hiddenThreads.threads = threads $.set "hiddenThreads.#{g.BOARD}", hiddenThreads menu: init: -> return if g.VIEW isnt 'index' or !Conf['Menu'] or !Conf['Thread Hiding'] div = $.el 'div', className: 'hide-thread-link' textContent: 'Hide thread' apply = $.el 'a', textContent: 'Apply' href: 'javascript:;' $.on apply, 'click', ThreadHiding.menu.hide makeStub = $.el 'label', innerHTML: " Make stub" $.event 'AddMenuEntry', type: 'post' el: div open: ({thread, isReply}) -> if isReply or thread.isHidden return false ThreadHiding.menu.thread = thread true subEntries: [el: apply; el: makeStub] hide: -> makeStub = $('input', @parentNode).checked {thread} = ThreadHiding.menu ThreadHiding.hide thread, makeStub ThreadHiding.saveHiddenState thread, makeStub Menu.close() makeButton: (thread, type) -> a = $.el 'a', className: "#{type}-thread-button" innerHTML: "[ #{if type is 'hide' then '-' else '+'} ]" href: 'javascript:;' $.on a, 'click', -> ThreadHiding.toggle thread a saveHiddenState: (thread, makeStub) -> # Get fresh hidden threads. hiddenThreads = ThreadHiding.getHiddenThreads() hiddenThreadsCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} if thread.isHidden hiddenThreads.threads[thread] = {makeStub} hiddenThreadsCatalog[thread] = true else delete hiddenThreads.threads[thread] delete hiddenThreadsCatalog[thread] $.set "hiddenThreads.#{g.BOARD}", hiddenThreads localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsCatalog toggle: (thread) -> if thread.isHidden ThreadHiding.show thread else ThreadHiding.hide thread ThreadHiding.saveHiddenState thread hide: (thread, makeStub=Conf['Stubs']) -> return if thread.hidden op = thread.posts[thread] threadRoot = op.nodes.root.parentNode threadRoot.hidden = thread.isHidden = true unless makeStub threadRoot.nextElementSibling.hidden = true #
" +
"
" +
"
'
else
''
closed =
if isClosed
'
'
else
''
container = $.el 'div',
id: "pc#{postID}"
className: "postContainer #{if isOP then 'op' else 'reply'}Container"
innerHTML: \
(if isOP then '' else "#{comment or ''}" + '
'
when '[/code]'
''
when '[moot]'
'