config = main: Enhancing: '404 Redirect': [true, 'Redirect dead threads and images'] 'Keybinds': [true, 'Binds actions to keys'] 'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time'] 'Report Button': [true, 'Add report buttons'] 'Comment Expansion': [true, 'Expand too long comments'] 'Thread Expansion': [true, 'View all replies'] 'Index Navigation': [true, 'Navigate to previous / next thread'] 'Reply Navigation': [false, 'Navigate to top / bottom of thread'] 'Check for Updates': [true, 'Check for updated versions of 4chan X'] Filtering: 'Anonymize': [false, 'Make everybody anonymous'] 'Filter': [false, 'Self-moderation placebo'] 'Filter OPs': [false, 'Filter OPs along with their threads'] 'Reply Hiding': [true, 'Hide single replies'] 'Thread Hiding': [true, 'Hide entire threads'] 'Show Stubs': [true, 'Of hidden threads / replies'] Imaging: 'Image Auto-Gif': [false, 'Animate gif thumbnails'] 'Image Expansion': [true, 'Expand images'] 'Image Hover': [false, 'Show full image on mouseover'] 'Sauce': [true, 'Add sauce to images'] 'Reveal Spoilers': [false, 'Replace spoiler thumbnails by the original thumbnail'] Monitoring: 'Thread Updater': [true, 'Update threads. Has more options in its own dialog.'] 'Unread Count': [true, 'Show unread post count in tab title'] 'Post in Title': [true, 'Show the op\'s post in the tab title'] 'Thread Stats': [true, 'Display reply and image count'] 'Thread Watcher': [true, 'Bookmark threads'] 'Auto Watch': [true, 'Automatically watch threads that you start'] 'Auto Watch Reply': [false, 'Automatically watch threads that you reply to'] Posting: 'Auto Noko': [true, 'Always redirect to your post'] 'Cooldown': [true, 'Prevent `flood detected` errors'] 'Quick Reply': [true, 'Reply without leaving the page'] 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'] 'Auto Hide QR': [true, 'Automatically auto-hide the quick reply when posting'] 'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting'] Quoting: 'Quote Backlinks': [true, 'Add quote backlinks'] 'OP Backlinks': [false, 'Add backlinks to the OP'] 'Quote Highlighting': [true, 'Highlight the previewed post'] 'Quote Inline': [true, 'Show quoted post inline on quote click'] 'Quote Preview': [true, 'Show quote content on hover'] 'Indicate OP quote': [true, 'Add \'(OP)\' to OP quotes'] 'Indicate Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes'] 'Forward Hiding': [true, 'Hide original posts of inlined backlinks'] filter: name: '' tripcode: '' email: '' subject: '' comment: '' filename: '' filesize: '' md5: '' flavors: [ 'http://iqdb.org/?url=' 'http://google.com/searchbyimage?image_url=' '#http://tineye.com/search?url=' '#http://saucenao.com/search.php?db=999&url=' '#http://3d.iqdb.org/?url=' '#http://regex.info/exif.cgi?imgurl=' '#http://imgur.com/upload?url=' '#http://ompldr.org/upload?url1=' ].join '\n' time: '%m/%d/%y(%a)%H:%M' backlink: '>>%id' favicon: 'ferongr' hotkeys: close: 'Esc' spoiler: 'ctrl+s' openQR: 'i' openEmptyQR: 'I' submit: 'alt+s' nextReply: 'J' previousReply: 'K' nextThread: 'n' previousThread: 'p' nextPage: 'L' previousPage: 'H' zero: '0' openThreadTab: 'o' openThread: 'O' expandThread: 'e' watch: 'w' hide: 'x' expandImages: 'm' expandAllImages: 'M' update: 'u' unreadCountTo0: 'z' updater: checkbox: 'Scrolling': [false, 'Scroll updated posts into view. Only enabled at bottom of page.'] 'Scroll BG': [false, 'Scroll background tabs'] 'Verbose': [true, 'Show countdown timer, new post count'] 'Auto Update': [true, 'Automatically fetch new posts'] 'Interval': 30 # XXX Chrome can't into {log} = console # XXX GreaseMonkey can't into console.log.bind log = console.log.bind? console # flatten the config conf = {} (flatten = (parent, obj) -> if obj.length #array if typeof obj[0] is 'boolean' conf[parent] = obj[0] else conf[parent] = obj else if typeof obj is 'object' for key, val of obj flatten key, val else #constant conf[parent] = obj ) null, config NAMESPACE = '4chan_x.' VERSION = '2.23.7' SECOND = 1000 MINUTE = 60*SECOND HOUR = 60*MINUTE DAY = 24*HOUR engine = /WebKit|Presto|Gecko/.exec(navigator.userAgent)[0].toLowerCase() d = document g = callbacks: [] ui = dialog: (id, position, html) -> el = d.createElement 'div' el.className = 'reply dialog' el.innerHTML = html el.id = id el.style.cssText = if saved = localStorage["#{NAMESPACE}#{id}.position"] then saved else position el.querySelector('div.move')?.addEventListener 'mousedown', ui.dragstart, false el dragstart: (e) -> #prevent text selection e.preventDefault() ui.el = el = @parentNode d.addEventListener 'mousemove', ui.drag, false d.addEventListener 'mouseup', ui.dragend, false #distance from pointer to el edge is constant; calculate it here. # XXX opera reports el.offsetLeft / el.offsetTop as 0 rect = el.getBoundingClientRect() ui.dx = e.clientX - rect.left ui.dy = e.clientY - rect.top #factor out el from document dimensions ui.width = d.body.clientWidth - el.offsetWidth ui.height = d.body.clientHeight - el.offsetHeight drag: (e) -> left = e.clientX - ui.dx top = e.clientY - ui.dy left = if left < 10 then 0 else if ui.width - left < 10 then null else left top = if top < 10 then 0 else if ui.height - top < 10 then null else top right = if left is null then 0 else null bottom = if top is null then 0 else null #using null instead of '' is 4% faster #these 4 statements are 40% faster than 1 style.cssText {style} = ui.el style.top = top style.right = right style.bottom = bottom style.left = left dragend: -> #$ coffee -bpe '{a} = {b} = c' #var a, b; #a = (b = c.b, c).a; {el} = ui localStorage["#{NAMESPACE}#{el.id}.position"] = el.style.cssText d.removeEventListener 'mousemove', ui.drag, false d.removeEventListener 'mouseup', ui.dragend, false hover: (e) -> {clientX, clientY} = e {el} = ui {style} = el {clientHeight, clientWidth} = d.body height = el.offsetHeight top = clientY - 120 style.top = if clientHeight < height or top < 0 0 else if top + height > clientHeight clientHeight - height else top if clientX < clientWidth - 400 style.left = clientX + 45 style.right = null else style.left = null style.right = clientWidth - clientX + 45 hoverend: -> ui.el.parentNode.removeChild ui.el ### loosely follows the jquery api: http://api.jquery.com/ not chainable ### $ = (selector, root=d.body) -> root.querySelector selector $.extend = (object, properties) -> for key, val of properties object[key] = val object $.extend $, id: (id) -> d.getElementById id globalEval: (code) -> script = $.el 'script', textContent: "(#{code})()" $.add d.head, script $.rm script ajax: (url, cb, opts={}) -> {type, event, headers} = opts type or= 'get' event or= 'onload' r = new XMLHttpRequest() r.open type, url, true for key, val of headers r.setRequestHeader key, val r[event] = cb r.send() r cache: (url, cb) -> if req = $.cache.requests[url] if req.readyState is 4 cb.call req else req.callbacks.push cb else req = $.ajax url, (-> cb.call @ for cb in @callbacks) req.callbacks = [cb] $.cache.requests[url] = req cb: checked: -> $.set @name, @checked conf[@name] = @checked value: -> $.set @name, @value conf[@name] = @value addStyle: (css) -> style = $.el 'style', textContent: css $.add d.head, style style x: (path, root=d.body) -> d.evaluate(path, root, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null). singleNodeValue tn: (s) -> d.createTextNode s replace: (root, el) -> root.parentNode.replaceChild el, root addClass: (el, className) -> el.classList.add className removeClass: (el, className) -> el.classList.remove className rm: (el) -> el.parentNode.removeChild el add: (parent, children...) -> for child in children parent.appendChild child prepend: (parent, child) -> parent.insertBefore child, parent.firstChild after: (root, el) -> root.parentNode.insertBefore el, root.nextSibling before: (root, el) -> root.parentNode.insertBefore el, root el: (tag, properties) -> el = d.createElement tag $.extend el, properties if properties el on: (el, eventType, handler) -> el.addEventListener eventType, handler, false off: (el, eventType, handler) -> el.removeEventListener eventType, handler, false isDST: -> ### http://en.wikipedia.org/wiki/Eastern_Time_Zone Its UTC time offset is −5 hrs (UTC−05) during standard time and −4 hrs (UTC−04) during daylight saving time. Since 2007, the local time changes at 02:00 EST to 03:00 EDT on the second Sunday in March and returns at 02:00 EDT to 01:00 EST on the first Sunday in November, in the U.S. as well as in Canada. 0200 EST (UTC-05) = 0700 UTC 0200 EDT (UTC-04) = 0600 UTC ### D = new Date() date = D.getUTCDate() day = D.getUTCDay() hours = D.getUTCHours() month = D.getUTCMonth() #this is the easy part if month < 2 or 10 < month return false if 2 < month < 10 return true # (sunday's date) = (today's date) - (number of days past sunday) # date is not zero-indexed sunday = date - day if month is 2 #before second sunday if sunday < 8 return false #during second sunday if sunday < 15 and day is 0 if hours < 7 return false return true #after second sunday return true #month is 10 # before first sunday if sunday < 1 return true # during first sunday if sunday < 8 and day is 0 if hours < 6 return true return false #after first sunday return false $.cache.requests = {} if GM_deleteValue? $.extend $, delete: (name) -> name = NAMESPACE + name GM_deleteValue name get: (name, defaultValue) -> name = NAMESPACE + name if value = GM_getValue name JSON.parse value else defaultValue openInTab: (url) -> GM_openInTab url set: (name, value) -> name = NAMESPACE + name # for `storage` events localStorage[name] = JSON.stringify value GM_setValue name, JSON.stringify value else $.extend $, delete: (name) -> name = NAMESPACE + name delete localStorage[name] get: (name, defaultValue) -> name = NAMESPACE + name if value = localStorage[name] JSON.parse value else defaultValue openInTab: (url) -> window.open url, "_blank" set: (name, value) -> name = NAMESPACE + name localStorage[name] = JSON.stringify value #load values from localStorage for key, val of conf conf[key] = $.get key, val $$ = (selector, root=d.body) -> Array::slice.call root.querySelectorAll selector filter = regexps: {} callbacks: [] init: -> for key of config.filter unless m = conf[key].match /^\/.+\/\w*$/gm continue @regexps[key] = [] for filter in m f = filter.match /^\/(.+)\/(\w*)$/ try @regexps[key].push RegExp f[1], f[2] catch e alert e.message #only execute what's filterable @callbacks.push @[key] g.callbacks.push @node node: (root) -> unless root.className if filter.callbacks.some((callback) -> callback root) replyHiding.hideHide $ 'td:not([nowrap])', root else if root.className is 'op' and not g.REPLY and conf['Filter OPs'] if filter.callbacks.some((callback) -> callback root) threadHiding.hideHide root.parentNode test: (key, value) -> filter.regexps[key].some (regexp) -> regexp.test value name: (root) -> name = if root.className is 'op' then $ '.postername', root else $ '.commentpostername', root filter.test 'name', name.textContent tripcode: (root) -> if trip = $ '.postertrip', root filter.test 'tripcode', trip.textContent email: (root) -> if mail = $ '.linkmail', root filter.test 'email', mail.href subject: (root) -> sub = if root.className is 'op' then $ '.filetitle', root else $ '.replytitle', root filter.test 'subject', sub.textContent comment: (root) -> filter.test 'comment', ($.el 'a', innerHTML: $('blockquote', root).innerHTML.replace /
/g, '\n').textContent filename: (root) -> if file = $ '.filesize span', root filter.test 'filename', file.title filesize: (root) -> if img = $ 'img[md5]', root filter.test 'filesize', img.alt md5: (root) -> if img = $ 'img[md5]', root filter.test 'md5', img.getAttribute('md5') strikethroughQuotes = init: -> g.callbacks.push (root) -> return if root.className is 'inline' for quote in $$ '.quotelink', root if el = $.id quote.hash[1..] if el.parentNode.parentNode.parentNode.hidden $.addClass quote, 'filtered' expandComment = init: -> for a in $$ '.abbr a' $.on a, 'click', expandComment.expand expand: (e) -> e.preventDefault() [_, threadID, replyID] = @href.match /(\d+)#(\d+)/ @textContent = "Loading #{replyID}..." threadID = @pathname.split('/').pop() or $.x('ancestor::div[@class="thread"]/div', @).id a = @ $.cache @pathname, (-> expandComment.parse @, a, threadID, replyID) parse: (req, a, threadID, replyID) -> if req.status isnt 200 a.textContent = "#{req.status} #{req.statusText}" return body = $.el 'body', innerHTML: req.responseText if threadID is replyID #OP bq = $ 'blockquote', body else #css selectors don't like ids starting with numbers, # getElementById only works for root document. for reply in $$ 'td[id]', body if reply.id == replyID bq = $ 'blockquote', reply break for quote in $$ '.quotelink', bq if quote.getAttribute('href') is quote.hash quote.pathname = "/#{g.BOARD}/res/#{threadID}" if quote.hash[1..] is threadID quote.innerHTML += ' (OP)' if conf['Quote Preview'] $.on quote, 'mouseover', quotePreview.mouseover $.on quote, 'mousemove', ui.hover $.on quote, 'mouseout', quotePreview.mouseout if conf['Quote Inline'] $.on quote, 'click', quoteInline.toggle $.replace a.parentNode.parentNode, bq expandThread = init: -> for span in $$ '.omittedposts' a = $.el 'a', textContent: "+ #{span.textContent}" className: 'omittedposts' href: 'javascript:;' $.on a, 'click', expandThread.cb.toggle $.replace span, a cb: toggle: -> thread = @parentNode expandThread.toggle thread toggle: (thread) -> threadID = thread.firstChild.id pathname = "/#{g.BOARD}/res/#{threadID}" a = $ '.omittedposts', thread switch a.textContent[0] when '+' $('.op .container', thread)?.innerHTML = '' a.textContent = a.textContent.replace '+', 'X Loading...' $.cache pathname, (-> expandThread.parse @, pathname, thread, a) when 'X' a.textContent = a.textContent.replace 'X Loading...', '+' #FIXME this will kill all callbacks $.cache[pathname].abort() when '-' a.textContent = a.textContent.replace '-', '+' #goddamit moot num = switch g.BOARD when 'b' then 3 when 't' then 1 else 5 table = $.x "following::br[@clear]/preceding::table[#{num}]", a while (prev = table.previousSibling) and (prev.nodeName is 'TABLE') $.rm prev for backlink in $$ '.op a.backlink' $.rm backlink if !$.id backlink.hash[1..] parse: (req, pathname, thread, a) -> if req.status isnt 200 a.textContent = "#{req.status} #{req.statusText}" $.off a, 'click', expandThread.cb.toggle return a.textContent = a.textContent.replace 'X Loading...', '-' body = $.el 'body', innerHTML: req.responseText frag = d.createDocumentFragment() for reply in $$ '.reply', body for quote in $$ '.quotelink', reply if (href = quote.getAttribute('href')) is quote.hash #add pathname to normal quotes quote.pathname = pathname else if href isnt quote.href #fix x-thread links, not x-board ones quote.href = "res/#{href}" link = $ '.quotejs', reply link.href = "res/#{thread.firstChild.id}##{reply.id}" link.nextSibling.href = "res/#{thread.firstChild.id}#q#{reply.id}" $.add frag, reply.parentNode.parentNode.parentNode # eat everything, then replace with fresh full posts while (next = a.nextSibling) and not next.clear #br[clear] $.rm next br = next $.before br, frag replyHiding = init: -> g.callbacks.push (root) -> return unless dd = $ '.doubledash', root dd.className = 'replyhider' a = $.el 'a', textContent: '[ - ]' href: 'javascript:;' $.on a, 'click', replyHiding.cb.hide $.replace dd.firstChild, a reply = dd.nextSibling id = reply.id if id of g.hiddenReplies replyHiding.hide reply cb: hide: -> reply = @parentNode.nextSibling replyHiding.hide reply show: -> div = @parentNode table = div.nextSibling replyHiding.show table $.rm div hide: (reply) -> replyHiding.hideHide reply id = reply.id for quote in $$ ".quotelink[href='##{id}'], .backlink[href='##{id}']" $.addClass quote, 'filtered' g.hiddenReplies[id] = Date.now() $.set "hiddenReplies/#{g.BOARD}/", g.hiddenReplies hideHide: (reply) -> table = reply.parentNode.parentNode.parentNode return if table.hidden #already hidden by filter table.hidden = true if conf['Show Stubs'] name = $('.commentpostername', reply).textContent trip = $('.postertrip', reply)?.textContent or '' a = $.el 'a', textContent: "[ + ] #{name} #{trip}" href: 'javascript:;' $.on a, 'click', replyHiding.cb.show div = $.el 'div', className: 'stub' $.add div, a $.before table, div show: (table) -> table.hidden = false id = $('td[id]', table).id for quote in $$ ".quotelink[href='##{id}'], .backlink[href='##{id}']" $.removeClass quote, 'filtered' delete g.hiddenReplies[id] $.set "hiddenReplies/#{g.BOARD}/", g.hiddenReplies keybinds = init: -> for node in $$ '[accesskey]' node.removeAttribute 'accesskey' $.on d, 'keydown', keybinds.keydown keydown: (e) -> updater.focus = true return if e.target.nodeName in ['TEXTAREA', 'INPUT'] and not e.altKey and not e.ctrlKey and not (e.keyCode is 27) return unless key = keybinds.keyCode e thread = nav.getThread() switch key when conf.close if o = $ '#overlay' $.rm o else if qr.el qr.close() when conf.spoiler ta = e.target return unless ta.nodeName is 'TEXTAREA' value = ta.value selStart = ta.selectionStart selEnd = ta.selectionEnd valStart = value[0...selStart] + '[spoiler]' valMid = value[selStart...selEnd] valEnd = '[/spoiler]' + value[selEnd..] ta.value = valStart + valMid + valEnd range = valStart.length + valMid.length ta.setSelectionRange range, range when conf.zero window.location = "/#{g.BOARD}/0#0" when conf.openEmptyQR keybinds.qr thread when conf.nextReply keybinds.hl.next thread when conf.previousReply keybinds.hl.prev thread when conf.expandAllImages keybinds.img thread, true when conf.openThread keybinds.open thread when conf.expandThread expandThread.toggle thread when conf.openQR keybinds.qr thread, true when conf.expandImages keybinds.img thread when conf.nextThread nav.next() when conf.openThreadTab keybinds.open thread, true when conf.previousThread nav.prev() when conf.update updater.update() when conf.watch watcher.toggle thread when conf.hide threadHiding.toggle thread when conf.nextPage $('input[value=Next]')?.click() when conf.previousPage $('input[value=Previous]')?.click() when conf.submit if qr.el qr.submit.call $ 'form', qr.el else $('.postarea form').submit() when conf.unreadCountTo0 unread.replies = [] unread.updateTitle() Favicon.update() else return e.preventDefault() keyCode: (e) -> key = switch kc = e.keyCode when 8 '' when 27 'Esc' when 37 'Left' when 38 'Up' when 39 'Right' when 40 'Down' when 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90 #0-9, A-Z c = String.fromCharCode kc if e.shiftKey then c else c.toLowerCase() else null if key if e.altKey then key = 'alt+' + key if e.ctrlKey then key = 'ctrl+' + key key img: (thread, all) -> if all $("#imageExpand").click() else root = $('td.replyhl', thread) or thread thumb = $ 'img[md5]', root imgExpand.toggle thumb.parentNode qr: (thread, quote) -> if quote qr.quote.call $ '.quotejs + a', $('.replyhl', thread) or thread else unless qr.el qr.dialog '', thread?.firstChild.id $('textarea', qr.el).focus() open: (thread, tab) -> id = thread.firstChild.id url = "http://boards.4chan.org/#{g.BOARD}/res/#{id}" if tab $.openInTab url else location.href = url hl: next: (thread) -> if td = $ 'td.replyhl', thread td.className = 'reply' rect = td.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #you're fully visible next = $.x 'following::td[@class="reply"]', td return if $.x('ancestor::div[@class="thread"]', next) isnt thread rect = next.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #and so is the next next.className = 'replyhl' return replies = $$ 'td.reply', thread for reply in replies top = reply.getBoundingClientRect().top if top > 0 reply.className = 'replyhl' return prev: (thread) -> if td = $ 'td.replyhl', thread td.className = 'reply' rect = td.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #you're fully visible prev = $.x 'preceding::td[@class="reply"][1]', td rect = prev.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #and so is the prev prev.className = 'replyhl' return replies = $$ 'td.reply', thread replies.reverse() height = d.body.clientHeight for reply in replies bot = reply.getBoundingClientRect().bottom if bot < height reply.className = 'replyhl' return nav = # ◀ ▶ init: -> span = $.el 'span', id: 'navlinks' prev = $.el 'a', textContent: '▲' href: 'javascript:;' next = $.el 'a', textContent: '▼' href: 'javascript:;' $.on prev, 'click', nav.prev $.on next, 'click', nav.next $.add span, prev, $.tn(' '), next $.add d.body, span prev: -> nav.scroll -1 next: -> nav.scroll +1 threads: [] getThread: (full) -> nav.threads = $$ 'div.thread:not([hidden])' for thread, i in nav.threads rect = thread.getBoundingClientRect() {bottom} = rect if bottom > 0 #we have not scrolled past if full return [thread, i, rect] return thread return null scroll: (delta) -> if g.REPLY if delta is -1 window.scrollTo 0,0 else window.scrollTo 0, d.body.scrollHeight return [thread, i, rect] = nav.getThread true {top} = rect #unless we're not at the beginning of the current thread # (and thus wanting to move to beginning) # or we're above the first thread and don't want to skip it unless (delta is -1 and Math.ceil(top) < 0) or (delta is +1 and top > 1) i += delta if i is -1 if g.PAGENUM is 0 window.scrollTo 0, 0 else window.location = "#{g.PAGENUM - 1}#0" return if delta is +1 # if we're at the last thread, or we're at the bottom of the page. # kind of hackish, what we really need to do is make nav.getThread smarter. if i is nav.threads.length or (innerHeight + pageYOffset == d.body.scrollHeight) if $ 'table.pages input[value="Next"]' window.location = "#{g.PAGENUM + 1}#0" return #TODO sfx {top} = nav.threads[i].getBoundingClientRect() window.scrollBy 0, top options = init: -> home = $ '#navtopr a' a = $.el 'a', textContent: '4chan X' href: 'javascript:;' $.on a, 'click', options.dialog $.replace home, a home = $ '#navbotr a' a = $.el 'a', textContent: '4chan X' href: 'javascript:;' $.on a, 'click', options.dialog $.replace home, a unless $.get 'firstrun' options.dialog() $.set 'firstrun', true dialog: -> dialog = ui.dialog 'options', '', '
4chan X | ' + VERSION + ' | Issues
| | | |

Sauce is disabled.
Filter is disabled.
Use regular expressions, one per line.
For example, /weeaboo/i will filter posts containing `weeaboo` case-insensitive.

Name:

Tripcode:

E-mail:

Subject:

Comment:

Filename:

Filesize:

Image MD5:

Quote Backlinks are disabled.
Time Formatting is disabled.
Unread Count is disabled.
Unread favicons
Keybinds are disabled.
ActionsKeybinds
Close Options or QR
Quick spoiler
Open QR with post number inserted
Open QR without post number inserted
Submit post
Select next reply
Select previous reply
See next thread
See previous thread
Jump to the next page
Jump to the previous page
Jump to page 0
Open thread in current tab
Open thread in new tab
Expand thread
Watch thread
Hide thread
Expand selected image
Expand all images
Update now
Reset the unread count to 0
' #main for key, obj of config.main ul = $.el 'ul', textContent: key for key, arr of obj checked = if conf[key] then 'checked' else '' description = arr[1] li = $.el 'li', innerHTML: ": #{description}" $.on $('input', li), 'click', $.cb.checked $.add ul, li $.add $('#main_tab + div', dialog), ul hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} hiddenNum = Object.keys(g.hiddenReplies).length + Object.keys(hiddenThreads).length li = $.el 'li', innerHTML: " : Forget all hidden posts. Useful if you accidentally hide a post and have `Show Stubs` disabled." $.on $('button', li), 'click', options.clearHidden $.add $('ul:nth-child(2)', dialog), li #filter & sauce for ta in $$ 'textarea', dialog ta.textContent = conf[ta.name] $.on ta, 'change', $.cb.value #rice (back = $ '[name=backlink]', dialog).value = conf['backlink'] (time = $ '[name=time]', dialog).value = conf['time'] $.on back, 'keyup', $.cb.value $.on back, 'keyup', options.backlink $.on time, 'keyup', $.cb.value $.on time, 'keyup', options.time favicon = $ 'select', dialog for option in favicon.options if option.textContent is conf['favicon'] option.selected = true break $.on favicon, 'change', $.cb.value $.on favicon, 'change', options.favicon #keybinds for input in $$ '#keybinds_tab + div input', dialog input.type = 'text' input.value = conf[input.name] $.on input, 'keydown', options.keybind #indicate if the settings require a feature to be enabled indicators = {} for indicator in $$ '.error', dialog key = indicator.firstChild.textContent indicator.hidden = conf[key] indicators[key] = indicator $.on $("[name='#{key}']", dialog), 'click', -> indicators[@name].hidden = @checked overlay = $.el 'div', id: 'overlay' $.on overlay, 'click', -> $.rm overlay $.on dialog, 'click', (e) -> e.stopPropagation() $.add overlay, dialog $.add d.body, overlay options.backlink.call back options.time.call time options.favicon.call favicon clearHidden: -> #'hidden' might be misleading; it's the number of IDs we're *looking* for, # not the number of posts actually hidden on the page. $.delete "hiddenReplies/#{g.BOARD}/" $.delete "hiddenThreads/#{g.BOARD}/" @textContent = "hidden: 0" g.hiddenReplies = {} keybind: (e) -> return if e.keyCode is 9 e.preventDefault() e.stopPropagation() return unless (key = keybinds.keyCode e)? @value = key $.cb.value.call @ time: -> Time.foo() Time.date = new Date() $('#timePreview').textContent = Time.funk Time backlink: -> $('#backlinkPreview').textContent = conf['backlink'].replace /%id/, '123456789' favicon: -> Favicon.switch() Favicon.update() if g.REPLY and conf['Unread Count'] @nextElementSibling.innerHTML = " " cooldown = #TODO merge into qr init: -> if match = location.search.match /cooldown=(\d+)/ [_, time] = match $.set g.BOARD+'/cooldown', time if $.get(g.BOARD+'/cooldown', 0) < time cooldown.start() if Date.now() < $.get g.BOARD+'/cooldown', 0 $.on window, 'storage', (e) -> cooldown.start() if e.key is "#{NAMESPACE}#{g.BOARD}/cooldown" $('.postarea form').action += '?cooldown' if g.REPLY start: -> cooldown.duration = Math.ceil ($.get(g.BOARD+'/cooldown', 0) - Date.now()) / 1000 return unless cooldown.duration > 0 for submit in $$ '#com_submit' submit.value = cooldown.duration submit.disabled = true setTimeout cooldown.cb, 1000 cb: -> submits = $$ '#com_submit' if --cooldown.duration setTimeout cooldown.cb, 1000 for submit in submits submit.value = cooldown.duration else for submit in submits submit.disabled = false submit.value = 'Submit' qr.autoPost() qr = # TODO # error handling / logging # persistent captcha # rm Recaptcha # email reverts init: -> g.callbacks.push qr.node $.on $('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', qr.captchaNode qr.captchaTime = Date.now() qr.spoiler = if $('.postarea label') then '' else '' qr.acceptFiles = $('.rules').textContent.match(/: (.+) /)[1].replace /\w+/g, (type) -> switch type when 'JPG' 'image/JPEG' when 'PDF' 'application/' + type else 'image/' + type iframe = $.el 'iframe', name: 'iframe' hidden: true $.add d.body, iframe #hack - nuke id so it doesn't grab focus when reloading $('#recaptcha_response_field').id = '' attach: -> fileDiv = $.el 'div', innerHTML: "X" $.on fileDiv.firstChild, 'change', qr.validateFileSize $.on fileDiv.lastChild, 'click', (-> $.rm @parentNode) $.add $('#files', qr.el), fileDiv attachNext: -> fileDiv = $.rm $('#files div', qr.el) file = fileDiv.firstChild oldFile = $ '#qr_form input[type=file]', qr.el $.replace oldFile, file autoPost: -> if qr.el and $('#auto', qr.el).checked qr.submit.call $ 'form', qr.el captchaNode: (e) -> return unless qr.el val = e.target.value $('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + val qr.challenge = val qr.captchaTime = Date.now() captchaKeydown: (e) -> return unless e.keyCode is 13 and @value #enter, captcha filled $('#auto', qr.el).checked = true if cooldown.duration #enable autoposting captchas = $.get 'captchas', [] captchas.push challenge: qr.challenge response: @value time: qr.captchaTime $.set 'captchas', captchas $('#captchas', qr.el).textContent = captchas.length + ' captchas' Recaptcha.reload() @value = '' if !$('textarea', qr.el).value and !$('input[type=file]', qr.el).files.length e.preventDefault() close: -> $.rm qr.el qr.el = null dialog: (link) -> submitValue = $('#com_submit').value submitDisabled = if $('#com_submit').disabled then 'disabled' else '' #FIXME inlined cross-thread quotes THREAD_ID = g.THREAD_ID or $.x('ancestor::div[@class="thread"]/div', link).id qr.challenge = $('#recaptcha_challenge_field').value html = " X
Quick Reply
#{qr.spoiler}
#{$.get('captchas', []).length} captchas
attach another file
" qr.el = ui.dialog 'qr', 'top: 0; right: 0;', html c = d.cookie $('input[name=name]', qr.el).value = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else '' $('input[name=email]', qr.el).value = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else '' $('input[name=pwd]', qr.el).value = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value $.on $('input[name=name]', qr.el), 'mousedown', (e) -> e.stopPropagation() $.on $('input[name=upfile]', qr.el), 'change', qr.validateFileSize $.on $('#close', qr.el), 'click', qr.close $.on $('form', qr.el), 'submit', qr.submit $.on $('#attach', qr.el), 'click', qr.attach $.on $('img', qr.el), 'click', Recaptcha.reload $.on $('#dummy', qr.el), 'keydown', Recaptcha.listener $.on $('#dummy', qr.el), 'keydown', qr.captchaKeydown $.add d.body, qr.el message: (data) -> $('iframe[name=iframe]').src = 'about:blank' fileCount = $('#files', qr.el).childElementCount tc = data.textContent unless /successful!|uploaded!$/.test tc # error message, not a successful post if tc is undefined data.textContent = "Connection error with sys.4chan.org." $.extend $('#error', qr.el), data $('#recaptcha_response_field', qr.el).value = '' $('#autohide', qr.el).checked = false if tc is 'You seem to have mistyped the verification.' setTimeout qr.autoPost, 1000 else if tc is 'Error: Duplicate file entry detected.' and fileCount $('textarea', qr.el).value += '\n' + tc + ' ' + data.href qr.attachNext() setTimeout qr.autoPost, 1000 return if qr.el if g.REPLY and (conf['Persistent QR'] or fileCount) qr.refresh() if fileCount qr.attachNext() else qr.close() if conf['Cooldown'] duration = if qr.sage then 60 else 30 $.set g.BOARD+'/cooldown', Date.now() + duration * 1000 cooldown.start() node: (root) -> quote = $ 'a.quotejs:not(:first-child)', root $.on quote, 'click', qr.quote postInvalid: -> content = $('textarea', qr.el).value or $('input[type=file]', qr.el).files.length return 'Error: No text entered.' unless content ### captchas expire after 30 minutes, see window.RecaptchaState.timeout. cutoff 5 minutes before then, b/c posting takes time. ### cutoff = Date.now() - 25*MINUTE captchas = $.get 'captchas', [] while captcha = captchas.shift() if captcha.time > cutoff break $.set 'captchas', captchas $('#captchas', qr.el).textContent = captchas.length + ' captchas' unless captcha dummy = $ '#dummy', qr.el return 'You forgot to type in the verification' unless response = dummy.value captcha = challenge: qr.challenge response: response dummy.value = '' Recaptcha.reload() $('#recaptcha_challenge_field', qr.el).value = captcha.challenge $('#recaptcha_response_field', qr.el).value = captcha.response false quote: (e) -> e.preventDefault() if e if qr.el $('#autohide', qr.el).checked = false else qr.dialog @ id = @textContent text = ">>#{id}\n" selection = window.getSelection() if s = selection.toString() selectionID = $.x('ancestor-or-self::blockquote/preceding-sibling::input', selection.anchorNode)?.name if selectionID is id s = s.replace /\n/g, '\n>' text += ">#{s}\n" ta = $ 'textarea', qr.el caretPos = ta.selectionStart #replace selection for text ta.value = ta.value.slice(0, caretPos) + text + ta.value.slice(ta.selectionEnd, ta.value.length) ta.focus() #move the caret to the end of the new quote ta.selectionEnd = ta.selectionStart = caretPos + text.length + 1*(engine is 'presto') refresh: -> $('[name=sub]', qr.el).value = '' $('[name=email]', qr.el).value = if m = d.cookie.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else '' $('[name=com]', qr.el).value = '' $('[name=recaptcha_response_field]', qr.el).value = '' $('[name=spoiler]', qr.el)?.checked = false unless conf['Remember Spoiler'] # XXX opera doesn't allow resetting file inputs w/ file.value = '' oldFile = $ '[type=file]', qr.el newFile = $.el 'input', type: 'file', name: 'upfile', accept: qr.acceptFiles $.replace oldFile, newFile submit: (e) -> #XXX `e` won't exist if we're here from `qr.submit.call form`. if msg = qr.postInvalid() e.preventDefault?() alert msg if msg is 'You forgot to type in the verification.' $('#dummy', qr.el).focus() return if conf['Auto Watch Reply'] and conf['Thread Watcher'] if g.REPLY and $('img.favicon').src is Favicon.empty watcher.watch null, g.THREAD_ID else id = $('input[name=resto]', qr.el).value op = $.id id if $('img.favicon', op).src is Favicon.empty watcher.watch op, id if !e then @submit() $('#error', qr.el).textContent = '' $('#autohide', qr.el).checked = true if conf['Auto Hide QR'] qr.sage = /sage/i.test $('input[name=email]', @).value sys: -> if recaptcha = $ '#recaptcha_response_field' #post reporting $.on recaptcha, 'keydown', Recaptcha.listener return ### http://code.google.com/p/chromium/issues/detail?id=20773 Let content scripts see other frames (instead of them being undefined) To access the parent, we have to break out of the sandbox and evaluate in the global context. ### $.globalEval -> data = {} if node = document.querySelector('td b')?.firstChild data.textContent = node.textContent data.href = node.href if node.href parent.postMessage data, '*' c = $('b')?.lastChild return unless c and c.nodeType is 8 #comment node [_, thread, id] = c.textContent.match(/thread:(\d+),no:(\d+)/) {search} = location cooldown = /cooldown/.test search noko = /noko/ .test search sage = /sage/ .test search watch = /watch/ .test search url = "http://boards.4chan.org/#{g.BOARD}" if watch and thread is '0' url += "/res/#{id}?watch" else if noko url += '/res/' url += if thread is '0' then id else thread if cooldown duration = Date.now() + (if sage then 60 else 30) * 1000 url += '?cooldown=' + duration if noko url += '#' + id window.location = url validateFileSize: (e) -> return unless @files[0].size > $('input[name=MAX_FILE_SIZE]').value file = $.el 'input', type: 'file', name: 'upfile', accept: qr.acceptFiles $.on file, 'change', qr.validateFileSize $.replace @, file $('#error', qr.el).textContent = 'Error: File too large.' alert 'Error: File too large.' Recaptcha = init: -> #hack to tab from comment straight to recaptcha for el in $$ '#recaptcha_table a' el.tabIndex = 1 $.on $('#recaptcha_response_field'), 'keydown', Recaptcha.listener listener: (e) -> if e.keyCode is 8 and @value is '' # backspace to reload Recaptcha.reload() reload: -> window.location = 'javascript:Recaptcha.reload()' threading = init: -> threading.thread $('body > form').firstChild op: (node) -> op = $.el 'div', className: 'op' $.before node, op while node.nodeName isnt 'BLOCKQUOTE' $.add op, node node = op.nextSibling $.add op, node #add the blockquote op.id = $('input', op).name op thread: (node) -> node = threading.op node return if g.REPLY div = $.el 'div', className: 'thread' $.before node, div while node.nodeName isnt 'HR' $.add div, node node = div.nextSibling node = node.nextElementSibling #skip text node #{N,}SFW unless node.align or node.nodeName is 'CENTER' threading.thread node threadHiding = init: -> hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} for thread in $$ '.thread' op = thread.firstChild a = $.el 'a', textContent: '[ - ]' href: 'javascript:;' $.on a, 'click', threadHiding.cb.hide $.prepend op, a if op.id of hiddenThreads threadHiding.hideHide thread cb: hide: -> thread = @parentNode.parentNode threadHiding.hide thread show: -> thread = @parentNode.parentNode threadHiding.show thread toggle: (thread) -> if /\bstub\b/.test(thread.className) or thread.hidden threadHiding.show thread else threadHiding.hide thread hide: (thread) -> threadHiding.hideHide thread id = thread.firstChild.id hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} hiddenThreads[id] = Date.now() $.set "hiddenThreads/#{g.BOARD}/", hiddenThreads hideHide: (thread) -> if conf['Show Stubs'] return if /stub/.test thread.className #already hidden by filter if span = $ '.omittedposts', thread num = Number span.textContent.match(/\d+/)[0] else num = 0 num += $$('table', thread).length text = if num is 1 then "1 reply" else "#{num} replies" name = $('.postername', thread).textContent trip = $('.postername + .postertrip', thread)?.textContent or '' a = $.el 'a', textContent: "[ + ] #{name}#{trip} (#{text})" href: 'javascript:;' $.on a, 'click', threadHiding.cb.show div = $.el 'div', className: 'block' $.add div, a $.add thread, div $.addClass thread, 'stub' else thread.hidden = true thread.nextSibling.hidden = true show: (thread) -> $.rm $ 'div.block', thread $.removeClass thread, 'stub' thread.hidden = false thread.nextSibling.hidden = false id = thread.firstChild.id hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} delete hiddenThreads[id] $.set "hiddenThreads/#{g.BOARD}/", hiddenThreads updater = init: -> #thread closed return unless $ 'form[name=post]' if conf['Scrolling'] if conf['Scroll BG'] updater.focus = true else $.on window, 'focus', (-> updater.focus = true) $.on window, 'blur', (-> updater.focus = false) html = "
-#{conf['Interval']}
" {checkbox} = config.updater for name of checkbox title = checkbox[name][1] checked = if conf[name] then 'checked' else '' html += "
" checked = if conf['Auto Update'] then 'checked' else '' html += "
" dialog = ui.dialog 'updater', 'bottom: 0; right: 0;', html updater.count = $ '#count', dialog updater.timer = $ '#timer', dialog updater.br = $ 'br[clear]' for input in $$ 'input', dialog if input.type is 'checkbox' $.on input, 'click', $.cb.checked $.on input, 'click', -> conf[@name] = @checked if input.name is 'Verbose' $.on input, 'click', updater.cb.verbose updater.cb.verbose.call input else if input.name is 'Auto Update This' $.on input, 'click', updater.cb.autoUpdate updater.cb.autoUpdate.call input else if input.name is 'Interval' $.on input, 'change', -> conf['Interval'] = @value = parseInt(@value, 10) or conf['Interval'] $.on input, 'change', $.cb.value else if input.type is 'button' $.on input, 'click', updater.update $.add d.body, dialog updater.retryCoef = 10 updater.lastModified = 0 cb: verbose: -> if conf['Verbose'] updater.count.textContent = '+0' updater.timer.hidden = false else $.extend updater.count, className: '' textContent: 'Thread Updater' updater.timer.hidden = true autoUpdate: -> if @checked updater.timeoutID = setTimeout updater.timeout, 1000 else clearTimeout updater.timeoutID update: -> if @status is 404 updater.timer.textContent = '' updater.count.textContent = 404 updater.count.className = 'error' clearTimeout updater.timeoutID for input in $$ '#com_submit' input.disabled = true input.value = 404 d.title = d.title.match(/^.+-/)[0] + ' 404' g.dead = true Favicon.update() return updater.retryCoef = 10 updater.timer.textContent = '-' + conf['Interval'] ### Status Code 304: Not modified By sending the `If-Modified-Since` header we get a proper status code, and no response. This saves bandwidth for both the user and the servers, avoid unnecessary computation, and won't load images and scripts when parsing the response. ### if @status is 304 if conf['Verbose'] updater.count.textContent = '+0' updater.count.className = null return updater.lastModified = @getResponseHeader 'Last-Modified' body = $.el 'body', innerHTML: @responseText #this only works on Chrome because of cross origin policy if $('title', body).textContent is '4chan - Banned' updater.count.textContent = 'Banned' updater.count.className = 'error' return id = $('td[id]', updater.br.previousElementSibling)?.id or 0 frag = d.createDocumentFragment() for reply in $$('.reply', body).reverse() if reply.id <= id #make sure to not insert older posts break $.prepend frag, reply.parentNode.parentNode.parentNode #table newPosts = frag.childNodes.length scroll = conf['Scrolling'] && updater.focus && newPosts && (d.body.scrollHeight - d.body.clientHeight - window.scrollY < 20) if conf['Verbose'] updater.count.textContent = '+' + newPosts if newPosts is 0 updater.count.className = null else updater.count.className = 'new' $.before updater.br, frag if scroll scrollTo 0, d.body.scrollHeight timeout: -> updater.timeoutID = setTimeout updater.timeout, 1000 n = 1 + Number updater.timer.textContent if n is 0 updater.update() else if n is updater.retryCoef updater.retryCoef += 10 * (updater.retryCoef < 120) updater.retry() else updater.timer.textContent = n retry: -> updater.count.textContent = 'Retry' updater.count.className = '' updater.update() update: -> updater.timer.textContent = 0 updater.request?.abort() #fool the cache url = location.pathname + '?' + Date.now() updater.request = $.ajax url, updater.cb.update, headers: 'If-Modified-Since': updater.lastModified watcher = init: -> html = '
Thread Watcher
' watcher.dialog = ui.dialog 'watcher', 'top: 50px; left: 0px;', html $.add d.body, watcher.dialog #add watch buttons inputs = $$ '.op input' for input in inputs favicon = $.el 'img', className: 'favicon' $.on favicon, 'click', watcher.cb.toggle $.before input, favicon #populate watcher, display watch buttons watcher.refresh() if conf['Auto Watch'] unless g.REPLY $('.postarea form').action += '?watch' else if /watch/.test(location.search) and $('img.favicon').src is Favicon.empty watcher.watch null, g.THREAD_ID $.on window, 'storage', (e) -> watcher.refresh() if e.key is "#{NAMESPACE}watched" refresh: -> watched = $.get 'watched', {} frag = d.createDocumentFragment() for board of watched for id, props of watched[board] x = $.el 'a', textContent: 'X' href: 'javascript:;' $.on x, 'click', watcher.cb.x link = $.el 'a', props link.title = link.textContent div = $.el 'div' $.add div, x, $.tn(' '), link $.add frag, div for div in $$ 'div:not(.move)', watcher.dialog $.rm div $.add watcher.dialog, frag watchedBoard = watched[g.BOARD] or {} for favicon in $$ 'img.favicon' id = favicon.nextSibling.name if id of watchedBoard favicon.src = Favicon.default else favicon.src = Favicon.empty cb: toggle: -> watcher.toggle @parentNode x: -> [board, _, id] = @nextElementSibling .getAttribute('href')[1..].split('/') watcher.unwatch board, id toggle: (thread) -> favicon = $ 'img.favicon', thread id = favicon.nextSibling.name if favicon.src == Favicon.empty watcher.watch thread, id else # favicon.src == Favicon.default watcher.unwatch g.BOARD, id unwatch: (board, id) -> watched = $.get 'watched', {} delete watched[board][id] $.set 'watched', watched watcher.refresh() watch: (thread, id) -> text = getTitle thread props = href: "/#{g.BOARD}/res/#{id}" textContent: text watched = $.get 'watched', {} watched[g.BOARD] or= {} watched[g.BOARD][id] = props $.set 'watched', watched watcher.refresh() anonymize = init: -> g.callbacks.push (root) -> name = $ '.commentpostername, .postername', root name.textContent = 'Anonymous' if trip = $ '.postertrip', root if trip.parentNode.nodeName is 'A' $.rm trip.parentNode else $.rm trip sauce = init: -> return unless sauce.prefixes = conf['flavors'].match /^[^#].+$/gm sauce.names = sauce.prefixes.map (prefix) -> prefix.match(/(\w+)\./)[1] g.callbacks.push (root) -> return if root.className is 'inline' or not span = $ '.filesize', root suffix = $('a', span).href for prefix, i in sauce.prefixes link = $.el 'a', textContent: sauce.names[i] href: prefix + suffix target: '_blank' $.add span, $.tn(' '), link revealSpoilers = init: -> g.callbacks.push (root) -> return if not (img = $ 'img[alt^=Spoiler]', root) or root.className is 'inline' img.removeAttribute 'height' img.removeAttribute 'width' [_, board, imgID] = img.parentNode.href.match /(\w+)\/src\/(\d+)/ img.src = "http://0.thumbs.4chan.org/#{board}/thumb/#{imgID}s.jpg" Time = init: -> Time.foo() # GMT -8 is given as +480; would GMT +8 be -480 ? chanOffset = 5 - new Date().getTimezoneOffset() / 60 # 4chan = EST = GMT -5 chanOffset-- if $.isDST() @parse = if Date.parse '10/11/11(Tue)18:53' is 1318351980000 (node) -> new Date Date.parse(node.textContent) + chanOffset*HOUR else # Firefox and Opera do not parse 4chan's time format correctly (node) -> [_, month, day, year, hour, min] = node.textContent.match /(\d+)\/(\d+)\/(\d+)\(\w+\)(\d+):(\d+)/ year = "20#{year}" month -= 1 #months start at 0 hour = chanOffset + Number hour new Date year, month, day, hour, min g.callbacks.push Time.node node: (root) -> return if root.className is 'inline' node = if posttime = $('.posttime', root) then posttime else $('span[id]', root).previousSibling Time.date = Time.parse node time = $.el 'time', textContent: ' ' + Time.funk(Time) + ' ' $.replace node, time foo: -> code = conf['time'].replace /%([A-Za-z])/g, (s, c) -> if c of Time.formatters "' + Time.formatters.#{c}() + '" else s Time.funk = Function 'Time', "return '#{code}'" day: [ 'Sunday' 'Monday' 'Tuesday' 'Wednesday' 'Thursday' 'Friday' 'Saturday' ] month: [ 'January' 'February' 'March' 'April' 'May' 'June' 'July' 'August' 'September' 'October' 'November' 'December' ] zeroPad: (n) -> if n < 10 then '0' + n else n formatters: a: -> Time.day[Time.date.getDay()][...3] A: -> Time.day[Time.date.getDay()] b: -> Time.month[Time.date.getMonth()][...3] B: -> Time.month[Time.date.getMonth()] d: -> Time.zeroPad Time.date.getDate() e: -> Time.date.getDate() H: -> Time.zeroPad Time.date.getHours() I: -> Time.zeroPad Time.date.getHours() % 12 or 12 k: -> Time.date.getHours() l: -> Time.date.getHours() % 12 or 12 m: -> Time.zeroPad Time.date.getMonth() + 1 M: -> Time.zeroPad Time.date.getMinutes() p: -> if Time.date.getHours() < 12 then 'AM' else 'PM' P: -> if Time.date.getHours() < 12 then 'am' else 'pm' y: -> Time.date.getFullYear() - 2000 getTitle = (thread) -> el = $ '.filetitle', thread if not el.textContent el = $ 'blockquote', thread if not el.textContent el = $ '.postername', thread span = $.el 'span', innerHTML: el.innerHTML.replace /
/g, ' ' "/#{g.BOARD}/ - #{span.textContent}" titlePost = init: -> d.title = getTitle() quoteBacklink = init: -> format = conf['backlink'].replace /%id/, "' + id + '" quoteBacklink.funk = Function 'id', "return'#{format}'" g.callbacks.push (root) -> return if /\binline\b/.test root.className quotes = {} for quote in $$ '.quotelink', root #don't process >>>/b/ if qid = quote.hash[1..] #duplicate quotes get overwritten quotes[qid] = quote # op or reply id = $('input', root).name a = $.el 'a', href: "##{id}" className: if root.hidden then 'filtered backlink' else 'backlink' textContent: quoteBacklink.funk id for qid of quotes continue unless el = $.id qid #don't backlink the op continue if el.className is 'op' and !conf['OP Backlinks'] link = a.cloneNode true if conf['Quote Preview'] $.on link, 'mouseover', quotePreview.mouseover $.on link, 'mousemove', ui.hover $.on link, 'mouseout', quotePreview.mouseout if conf['Quote Inline'] $.on link, 'click', quoteInline.toggle unless (container = $ '.container', el) and container.parentNode is el container = $.el 'span', className: 'container' root = $('.reportbutton', el) or $('span[id]', el) $.after root, container $.add container, $.tn(' '), link quoteInline = init: -> g.callbacks.push (root) -> for quote in $$ '.quotelink, .backlink', root continue unless quote.hash quote.removeAttribute 'onclick' $.on quote, 'click', quoteInline.toggle toggle: (e) -> return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 e.preventDefault() id = @hash[1..] if /\binlined\b/.test @className quoteInline.rm @, id else return if $.x "ancestor::*[@id='#{id}']", @ quoteInline.add @, id @classList.toggle 'inlined' add: (q, id) -> root = if q.parentNode.nodeName is 'FONT' then q.parentNode else if q.nextSibling then q.nextSibling else q if el = $.id id inline = quoteInline.table id, el.innerHTML if g.REPLY and conf['Unread Count'] and (i = unread.replies.indexOf el.parentNode.parentNode.parentNode) isnt -1 unread.replies.splice i, 1 unread.updateTitle() Favicon.update() if /\bbacklink\b/.test q.className $.after q.parentNode, inline $.addClass $.x('ancestor::table', el), 'forwarded' if conf['Forward Hiding'] return $.after root, inline else inline = $.el 'td', className: 'reply inline' id: "i#{id}" innerHTML: "Loading #{id}..." $.after root, inline {pathname} = q threadID = pathname.split('/').pop() $.cache pathname, (-> quoteInline.parse @, pathname, id, threadID, inline) rm: (q, id) -> #select the corresponding table or loading td table = $.x "following::*[@id='i#{id}']", q $.rm table return unless conf['Forward Hiding'] for inlined in $$ '.backlink.inlined', table $.removeClass $.x('ancestor::table', $.id inlined.hash[1..]), 'forwarded' if /\bbacklink\b/.test q.className $.removeClass $.x('ancestor::table', $.id id), 'forwarded' parse: (req, pathname, id, threadID, inline) -> return unless inline.parentNode if req.status isnt 200 inline.innerHTML = "#{req.status} #{req.statusText}" return body = $.el 'body', innerHTML: req.responseText if id is threadID #OP op = threading.op $('body > form', body).firstChild html = op.innerHTML else for reply in $$ 'td.reply', body if reply.id == id html = reply.innerHTML break newInline = quoteInline.table id, html for quote in $$ '.quotelink', newInline if (href = quote.getAttribute('href')) is quote.hash #add pathname to normal quotes quote.pathname = pathname else if !g.REPLY and href isnt quote.href #fix x-thread links, not x-board ones quote.href = "res/#{href}" link = $ '.quotejs', newInline link.href = "#{pathname}##{id}" link.nextSibling.href = "#{pathname}#q#{id}" $.addClass newInline, 'crossquote' $.replace inline, newInline table: (id, html) -> $.el 'table', className: 'inline' id: "i#{id}" innerHTML: "#{html}" quotePreview = init: -> g.callbacks.push (root) -> for quote in $$ '.quotelink, .backlink', root continue unless quote.hash $.on quote, 'mouseover', quotePreview.mouseover $.on quote, 'mousemove', ui.hover $.on quote, 'mouseout', quotePreview.mouseout mouseover: (e) -> qp = ui.el = $.el 'div', id: 'qp' className: 'reply' $.add d.body, qp id = @hash[1..] if el = $.id id qp.innerHTML = el.innerHTML $.addClass el, 'qphl' if conf['Quote Highlighting'] if /\bbacklink\b/.test @className replyID = $.x('preceding-sibling::input', @parentNode).name for quote in $$ '.quotelink', qp if quote.hash[1..] is replyID quote.className = 'forwardlink' else qp.innerHTML = "Loading #{id}..." threadID = @pathname.split('/').pop() or $.x('ancestor::div[@class="thread"]/div', @).id $.cache @pathname, (-> quotePreview.parse @, id, threadID) ui.hover e mouseout: -> $.removeClass el, 'qphl' if el = $.id @hash[1..] ui.hoverend() parse: (req, id, threadID) -> return unless (qp = ui.el) and (qp.innerHTML is "Loading #{id}...") if req.status isnt 200 qp.innerHTML = "#{req.status} #{req.statusText}" return body = $.el 'body', innerHTML: req.responseText if id is threadID #OP op = threading.op $('body > form', body).firstChild html = op.innerHTML else for reply in $$ 'td.reply', body if reply.id == id html = reply.innerHTML break qp.innerHTML = html Time.node qp quoteOP = init: -> g.callbacks.push (root) -> return if root.className is 'inline' tid = g.THREAD_ID or $.x('ancestor::div[contains(@class,"thread")]/div', root).id for quote in $$ '.quotelink', root if quote.hash[1..] is tid quote.innerHTML += ' (OP)' quoteDR = init: -> g.callbacks.push (root) -> return if root.className is 'inline' tid = g.THREAD_ID or $.x('ancestor::div[contains(@class,"thread")]/div', root).id for quote in $$ '.quotelink', root #if quote leads to a different thread id and is located on the same board (index 0) if quote.pathname.indexOf("res/#{tid}") is -1 and !quote.pathname.indexOf "/#{g.BOARD}/res" quote.innerHTML += ' (Cross-thread)' reportButton = init: -> g.callbacks.push (root) -> if not a = $ '.reportbutton', root span = $ 'span[id]', root a = $.el 'a', className: 'reportbutton' innerHTML: '[ ! ]' href: 'javascript:;' $.after span, a $.after span, $.tn(' ') $.on a, 'click', reportButton.report report: -> url = "http://sys.4chan.org/#{g.BOARD}/imgboard.php?mode=report&no=#{$.x('preceding-sibling::input', @).name}" id = Date.now() set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200" window.open url, id, set threadStats = init: -> threadStats.posts = 1 threadStats.images = if $ '.op img[md5]' then 1 else 0 html = "
#{threadStats.posts} / #{threadStats.images}
" dialog = ui.dialog 'stats', 'bottom: 0; left: 0;', html dialog.className = 'dialog' threadStats.postcountEl = $ '#postcount', dialog threadStats.imagecountEl = $ '#imagecount', dialog $.add d.body, dialog g.callbacks.push threadStats.node node: (root) -> return if root.className threadStats.postcountEl.textContent = ++threadStats.posts if $ 'img[md5]', root threadStats.imagecountEl.textContent = ++threadStats.images if threadStats.images > 150 threadStats.imagecountEl.className = 'error' unread = init: -> unread.replies = [] d.title = '(0) ' + d.title $.on window, 'scroll', unread.scroll g.callbacks.push unread.node node: (root) -> return if root.hidden or root.className unread.replies.push root unread.updateTitle() if unread.replies.length is 1 Favicon.update() scroll: -> updater.focus = true height = d.body.clientHeight for reply, i in unread.replies {bottom} = reply.getBoundingClientRect() if bottom > height #post is not completely read break return if i is 0 unread.replies = unread.replies[i..] unread.updateTitle() if unread.replies.length is 0 Favicon.update() updateTitle: -> d.title = d.title.replace /\d+/, unread.replies.length Favicon = init: -> favicon = $ 'link[rel="shortcut icon"]', d.head favicon.type = 'image/x-icon' {href} = favicon @SFW = /ws.ico$/.test href @default = href @switch() switch: -> switch conf['favicon'] when 'ferongr' @unreadDead = 'data:unreadDead;base64,R0lGODlhEAAQAOMHAOgLAnMFAL8AAOgLAukMA/+AgP+rq////////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw==' @unreadSFW = 'data:unreadSFW;base64,R0lGODlhEAAQAOMHAADX8QBwfgC2zADX8QDY8nnl8qLp8v///////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw==' @unreadNSFW = 'data:unreadNSFW;base64,R0lGODlhEAAQAOMHAFT+ACh5AEncAFT+AFX/Acz/su7/5v///////////////////////////////////yH5BAEKAAcALAAAAAAQABAAAARZ8MhJ6xwDWIBv+AM1fEEIBIVRlNKYrtpIECuGzuwpCLg974EYiXUYkUItjGbC6VQ4omXFiKROA6qSy0A8nAo9GS3YCswIWnOvLAi0be23Z1QtdSUaqXcviQAAOw==' when 'xat-' @unreadDead = 'data:unreadDead;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA2ElEQVQ4y61TQQrCMBDMQ8WDIEV6LbT2A4og2Hq0veo7fIAH04dY9N4xmyYlpGmI2MCQTWYy3Wy2DAD7B2wWAzWgcTgVeZKlZRxHNYFi2jM18oBh0IcKtC6ixf22WT4IFLs0owxswXu9egm0Ls6bwfCFfNsJYJKfqoEkd3vgUgFVLWObtzNgVKyruC+ljSzr5OEnBzjvjcQecaQhbZgBb4CmGQw+PoMkTUtdbd8VSEPakcGxPOcsoIgUKy0LecY29BmdBrqRfjIwZ93KLs5loHvBnL3cLH/jF+C/+z5dgUysAAAAAElFTkSuQmCC' @unreadSFW = 'data:unreadSFW;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA30lEQVQ4y2P4//8/AyWYgSoGQMF/GJ7Y11VVUVoyKTM9ey4Ig9ggMWQ1YA1IBvzXm34YjkH8mPyJB+Nqlp8FYRAbmxoMF6ArSNrw6T0Qf8Amh9cFMEWVR/7/A+L/uORxhgEIt5/+/3/2lf//5wAxiI0uj+4CBlBgxVUvOwtydgXQZpDmi2/+/7/0GmIQSAwkB1IDUkuUAZeABlx+g2zAZ9wGlAOjChba+LwAUgNSi2HA5Am9VciBhSsQQWyoWgZiovEDsdGI1QBYQiLJAGQalpSxyWEzAJYWkGm8clTJjQCZ1hkoVG0CygAAAABJRU5ErkJggg==' @unreadNSFW = 'data:unreadNSFW;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA4ElEQVQ4y2P4//8/AyWYgSoGQMF/GJ7YNbGqrKRiUnp21lwQBrFBYshqwBqQDPifdsYYjkH8mInxB+OWx58FYRAbmxoMF6ArKPmU9B6IP2CTw+sCmKKe/5X/gPg/LnmcYQDCs/63/1/9fzYQzwGz0eXRXcAACqy4ZfFnQc7u+V/xD6T55v+LQHwJbBBIDCQHUgNSS5QBt4Cab/2/jDDgMx4DykrKJ8FCG58XQGpAajEMmNw7uQo5sHAFIogNVctATDR+IDYasRoAS0gkGYBMw5IyNjlsBsDSAjKNV44quREAx58Mr9vt5wQAAAAASUVORK5CYII=' when 'Mayhem' @unreadDead = 'data:unreadDead;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABIUlEQVQ4jZ2ScWuDMBDFgw4pIkU0WsoQkWAYIkXZH4N9/+/V3dmfXSrKYIFHwt17j8vdGWNMIkgFuaDgzgQnwRs4EQs5KdolUQtagRN0givEDBTEOjgtGs0Zq8F7cKqqusVxrMQLaDUWcjBSrXkn8gs51tpJSWLk9b3HUa0aNIL5gPBR1/V4kJvR7lTwl8GmAm1Gf9+c3S+89qBHa8502AsmSrtBaEBPbIbj0ah2madlNAPEccdgJDfAtWifBjqWKShRBT6KoiH8QlEUn/qt0CCjnNdmPUwmFWzj9Oe6LpKuZXcwqq88z78Pch3aZU3dPwwc2sWlfZKCW5tWluV8kGvXClLm6dYN4/aUqfCbnEOzNDGhGZbNargvxCzvMGfRJD8UaDVvgkzo6QAAAABJRU5ErkJggg==' @unreadSFW = 'data:unreadSFW;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABCElEQVQ4jZ2S4crCMAxF+0OGDJEPKYrIGKOsiJSx/fJRfSAfTJNyKqXfiuDg0C25N2RJjTGmEVrhTzhw7oStsIEtsVzT4o2Jo9ALThiEM8IdHIgNaHo8mjNWg6/ske8bohPo+63QOLzmooHp8fyAICBSQkVz0QKdsFQEV6WSW/D+7+BbgbIDHcb4Kp61XyjyI16zZ8JemGltQtDBSGxB4/GoN+7TpkkjDCsFArm0IYv3U0BbnYtf8BCy+JytsE0X6VyuKhPPK/GAJ14kvZZDZVV3pZIb8MZr6n4o4PDGKn0S5SdDmyq5PnXQsk+Xbhinp03FFzmHJw6xYRiWm9VxnohZ3vOcxdO8ARmXRvbWdtzQAAAAAElFTkSuQmCC' @unreadNSFW = 'data:unreadNSFW;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABCklEQVQ4jZ2S0WrDMAxF/TBCCKWMYhZKCSGYmFJMSNjD/mhf239qJXNcjBdTWODgRLpXKJKNMaYROuFTOHEehFb4gJZYrunwxsSXMApOmIQzwgOciE1oRjyaM1aDj+yR7xuiHvT9VmgcXnPRwO/9+wWCgEgJFc1FCwzCVhFclUpuw/u3g3cFyg50GPOjePZ+ocjPeM2RCXthpbUFwQAzsQ2Nx6PeuE+bJo0w7BQI5NKGLN5XAW11LX7BQ8jia7bCLl2kc7mqTLzuxAOeeJH0Wk6VVf0oldyEN15T948CDm+sMiZRfjK0pZIbUwcd+3TphnF62lR8kXN44hAbhmG5WQNnT8zynucsnuYJhFpBfkMzqD4AAAAASUVORK5CYII=' when 'Original' @unreadDead = 'data:unreadDead;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs=' @unreadSFW = 'data:unreadSFW;base64,R0lGODlhEAAQAKECAAAAAC6Xw////////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs=' @unreadNSFW = 'data:unreadNSFW;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAMALAAAAAAQABAAAAI/nI95wsqygIRxDgGCBhTrwF3Zxowg5H1cSopS6FrGQ82PU1951ckRmYKJVCXizLRC9kAnT0aIiR6lCFT1cigAADs=' when 'None' @unreadDead = @dead @unreadSFW = 'http://static.4chan.org/image/favicon-ws.ico' @unreadNSFW = 'http://static.4chan.org/image/favicon.ico' @unread = if @SFW then @unreadSFW else @unreadNSFW empty: 'data:image/gif;base64,R0lGODlhEAAQAJEAAAAAAP///9vb2////yH5BAEAAAMALAAAAAAQABAAAAIvnI+pq+D9DBAUoFkPFnbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==' dead: 'data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP8AAP///////yH5BAEKAAIALAAAAAAQABAAAAIvlI+pq+D9DAgUoFkPDlbs7lFZKIJOJJ3MyraoB14jFpOcVMpzrnF3OKlZYsMWowAAOw==' update: -> l = unread.replies.length favicon = $ 'link[rel="shortcut icon"]', d.head favicon.href = if g.dead if l @unreadDead else @dead else if l @unread else @default #`favicon.href = href` doesn't work on Firefox #`favicon.href = href` isn't enough on Opera #Opera won't always update the favicon if the href do not change if engine isnt 'webkit' clone = favicon.cloneNode true favicon.href = null $.replace favicon, clone redirect = init: -> url = # waiting for https://github.com/FoOlRulez/FoOlFuuka/issues/11 if location.hostname is 'images.4chan.org' redirect.image g.BOARD, location.pathname.split('/')[3] else if /^\d+$/.test g.THREAD_ID redirect.thread() location.href = url if url image: (board, filename) -> #board must be given, the image can originate from a cross-quote switch board when 'a', 'jp', 'm', 'tg', 'tv', 'u' "http://archivethumb.foolz.us/board/#{board}/img/#{filename}" thread: -> switch g.BOARD when 'a', 'jp', 'm', 'tg', 'tv', 'u' "http://archive.foolz.us/#{g.BOARD}/thread/#{g.THREAD_ID}/" when 'lit' "http://fuuka.warosu.org/#{g.BOARD}/thread/#{g.THREAD_ID}" when 'diy', 'g', 'sci' "http://archive.installgentoo.net/#{g.BOARD}/thread/#{g.THREAD_ID}" when '3', 'adv', 'an', 'ck', 'co', 'fa', 'fit', 'int', 'k', 'mu', 'n', 'o', 'p', 'po', 'pol', 'r9k', 'soc', 'sp', 'toy', 'trv', 'v', 'vp', 'x' "http://archive.no-ip.org/#{g.BOARD}/thread/#{g.THREAD_ID}" else "http://boards.4chan.org/#{g.BOARD}" imgHover = init: -> g.callbacks.push (root) -> return unless thumb = $ 'img[md5]', root $.on thumb, 'mouseover', imgHover.mouseover $.on thumb, 'mousemove', ui.hover $.on thumb, 'mouseout', ui.hoverend mouseover: -> ui.el = $.el 'img' id: 'ihover' src: @parentNode.href $.add d.body, ui.el imgGif = init: -> g.callbacks.push (root) -> return if root.hidden or !thumb = $ 'img[md5]', root src = thumb.parentNode.href if /gif$/.test src thumb.src = src imgExpand = init: -> g.callbacks.push imgExpand.node imgExpand.dialog() node: (root) -> return unless thumb = $ 'img[md5]', root a = thumb.parentNode $.on a, 'click', imgExpand.cb.toggle if imgExpand.on and !root.hidden and root.className isnt 'inline' imgExpand.expand a.firstChild cb: toggle: (e) -> return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 e.preventDefault() imgExpand.toggle @ all: -> imgExpand.on = @checked if imgExpand.on #expand for thumb in $$ '.op > a > img[md5]:last-child, table:not([hidden]) img[md5]:last-child' imgExpand.expand thumb else #contract for thumb in $$ 'img[md5][hidden]' imgExpand.contract thumb typeChange: -> switch @value when 'full' klass = '' when 'fit width' klass = 'fitwidth' when 'fit height' klass = 'fitheight' when 'fit screen' klass = 'fitwidth fitheight' $('body > form').className = klass if /\bfitheight\b/.test klass $.on window, 'resize', imgExpand.resize unless imgExpand.style imgExpand.style = $.addStyle '' imgExpand.resize() else if imgExpand.style $.off window, 'resize', imgExpand.resize toggle: (a) -> thumb = a.firstChild if thumb.hidden imgExpand.contract thumb else imgExpand.expand thumb contract: (thumb) -> thumb.hidden = false $.rm thumb.nextSibling expand: (thumb, url) -> a = thumb.parentNode img = $.el 'img', src: if url then url else a.href if engine is 'gecko' and a.parentNode.className isnt 'op' filesize = $.x('preceding-sibling::span[@class="filesize"]', a).textContent max = filesize.match /(\d+)x/ img.style.maxWidth = "#{max[1]}px" $.on img, 'error', imgExpand.error if conf['404 Redirect'] thumb.hidden = true $.add a, img error: -> thumb = @previousSibling imgExpand.contract thumb src = @src.split '/' if url = redirect.image src[3], src[5] imgExpand.expand thumb, url #navigator.online is not x-browser/os yet else if engine is 'webkit' req = $.ajax @src, (-> setTimeout imgExpand.retry, 10000, thumb if @status isnt 404 ), type: 'head', event: 'onreadystatechange' #Firefox returns a status code of 0 because of the same origin policy #Oprah doesn't send any request else unless g.dead setTimeout imgExpand.retry, 10000, thumb retry: (thumb) -> imgExpand.expand thumb unless thumb.hidden dialog: -> controls = $.el 'div', id: 'imgControls' innerHTML: " " imageType = $.get 'imageType', 'full' select = $ 'select', controls for option in select.options if option.textContent is imageType option.selected = true break imgExpand.cb.typeChange.call select $.on select, 'change', $.cb.value $.on select, 'change', imgExpand.cb.typeChange $.on $('input', controls), 'click', imgExpand.cb.all form = $ 'body > form' $.prepend form, controls resize: -> imgExpand.style.innerHTML = ".fitheight [md5] + img {max-height:#{d.body.clientHeight}px;}" Main = init: -> pathname = location.pathname[1..].split('/') [g.BOARD, temp] = pathname if temp is 'res' g.REPLY = true g.THREAD_ID = pathname[2] else g.PAGENUM = parseInt(temp) or 0 if location.hostname is 'sys.4chan.org' if /interactive|complete/.test d.readyState qr.sys() else $.on d, 'DOMContentLoaded', qr.sys return $.on window, 'message', Main.message now = Date.now() if conf['Check for Updates'] and $.get('lastUpdate', 0) < now - 6*HOUR update = -> $.off d, 'DOMContentLoaded', update $.add d.head, $.el 'script', src: 'https://raw.github.com/mayhemydg/4chan-x/master/latest.js' if /interactive|complete/.test d.readyState update() else $.on d, 'DOMContentLoaded', update $.set 'lastUpdate', now g.hiddenReplies = $.get "hiddenReplies/#{g.BOARD}/", {} if $.get('lastChecked', 0) < now - 1*DAY $.set 'lastChecked', now cutoff = now - 7*DAY hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} for id, timestamp of hiddenThreads if timestamp < cutoff delete hiddenThreads[id] for id, timestamp of g.hiddenReplies if timestamp < cutoff delete g.hiddenReplies[id] $.set "hiddenThreads/#{g.BOARD}/", hiddenThreads $.set "hiddenReplies/#{g.BOARD}/", g.hiddenReplies #major features if conf['Filter'] filter.init() if conf['Reply Hiding'] replyHiding.init() if conf['Filter'] or conf['Reply Hiding'] strikethroughQuotes.init() if conf['Anonymize'] anonymize.init() if conf['Time Formatting'] Time.init() if conf['Sauce'] sauce.init() if conf['Image Auto-Gif'] imgGif.init() if conf['Image Hover'] imgHover.init() if conf['Report Button'] reportButton.init() if conf['Quote Inline'] quoteInline.init() if conf['Quote Preview'] quotePreview.init() if conf['Quote Backlinks'] quoteBacklink.init() if conf['Indicate OP quote'] quoteOP.init() if conf['Indicate Cross-thread Quotes'] quoteDR.init() if /interactive|complete/.test d.readyState Main.onLoad() else $.on d, 'DOMContentLoaded', Main.onLoad onLoad: -> $.off d, 'DOMContentLoaded', Main.onLoad if conf['404 Redirect'] and d.title is '4chan - 404' redirect.init() return if not $('#navtopr') or location.hostname is 'images.4chan.org' return $.addClass d.body, engine $.addStyle Main.css threading.init() Favicon.init() #recaptcha may be blocked, eg by noscript if (form = $ 'form[name=post]') and (canPost = !!$ '#recaptcha_response_field') Recaptcha.init() if g.REPLY and conf['Auto Watch Reply'] and conf['Thread Watcher'] $.on form, 'submit', -> if $('img.favicon').src is Favicon.empty watcher.watch null, g.THREAD_ID #major features if conf['Auto Noko'] and canPost form.action += '?noko' if conf['Cooldown'] and canPost cooldown.init() if conf['Image Expansion'] imgExpand.init() if conf['Reveal Spoilers'] and $('.postarea label') revealSpoilers.init() if conf['Quick Reply'] qr.init() if conf['Thread Watcher'] watcher.init() if conf['Keybinds'] keybinds.init() if conf['Reply Navigation'] or conf['Index Navigation'] nav.init() if g.REPLY if conf['Thread Updater'] updater.init() if conf['Thread Stats'] threadStats.init() if conf['Post in Title'] titlePost.init() if conf['Unread Count'] unread.init() if conf['Quick Reply'] and conf['Persistent QR'] and canPost qr.dialog() if conf['Auto Hide QR'] $('#autohide', qr.el).checked = true else #not reply if conf['Thread Hiding'] threadHiding.init() if conf['Thread Expansion'] expandThread.init() if conf['Comment Expansion'] expandComment.init() nodes = $$ '.op, a + table' for callback in g.callbacks try for node in nodes callback node catch err alert err $.on $('form[name=delform]'), 'DOMNodeInserted', Main.node options.init() message: (e) -> {origin, data} = e if origin is 'http://sys.4chan.org' qr.message data else if data.version and data.version isnt VERSION and confirm 'An updated version of 4chan X is available, would you like to install it now?' window.location = "https://raw.github.com/mayhemydg/4chan-x/#{data.version}/4chan_x.user.js" node: (e) -> {target} = e return unless target.nodeName is 'TABLE' for callback in g.callbacks try callback target catch err #nothing css: ' /* dialog styling */ div.dialog { border: 1px solid; } div.dialog > div.move { cursor: move; } label, a, .favicon, #qr img { cursor: pointer; } a[href="javascript:;"] { text-decoration: none; } .thread.stub > :not(.block), #content > [name=tab]:not(:checked) + div, #updater:not(:hover) > :not(.move), #qp > input, #qp .inline, .forwarded { display: none; } .new { background: lime; } .error { color: red; } #error { cursor: default; } #error[href] { cursor: pointer; } td.replyhider { vertical-align: top; } .filesize + br + a { float: left; pointer-events: none; } [md5], [md5] + img { pointer-events: all; } .fitwidth [md5] + img { max-width: 100%; } .gecko > .fitwidth [md5] + img, .presto > .fitwidth [md5] + img { width: 100%; } #qp, #ihover { position: fixed; } #ihover { max-height: 100%; max-width: 75%; } #navlinks { font-size: 16px; position: fixed; top: 25px; right: 5px; } #overlay { position: fixed; top: 0; right: 0; left: 0; bottom: 0; text-align: center; background: rgba(0,0,0,.5); } #overlay::after { content: ""; display: inline-block; height: 100%; vertical-align: middle; } #options { display: inline-block; padding: 5px; text-align: left; vertical-align: middle; width: 500px; } #credits { float: right; } #options ul { list-style: none; padding: 0; } #options label { text-decoration: underline; } #content > div { height: 450px; overflow: auto; } #content textarea { margin: 0; min-height: 100px; resize: vertical; width: 100%; } #flavors { height: 100%; } #qr { position: fixed; max-height: 100%; overflow-x: hidden; overflow-y: auto; } #qr > div.move { text-align: right; } #qr input[name=name] { float: left; } #qr_form { clear: left; } #qr_form, #qr #com_submit, #qr input[name=upfile] { margin: 0; } #qr textarea { width: 100%; height: 125px; } #qr #close, #qr #autohide { float: right; } #qr:not(:hover) > #autohide:checked ~ .autohide { height: 0; overflow: hidden; } /* http://stackoverflow.com/questions/2610497/change-an-inputs-html5-placeholder-color-with-css */ #qr input::-webkit-input-placeholder { color: grey; } #qr input:-moz-placeholder { color: grey; } /* qr reCAPTCHA */ #qr img { border: 1px solid #AAA; } #updater { position: fixed; text-align: right; } #updater input[type=text] { width: 50px; } #updater:not(:hover) { border: none; background: transparent; } #stats { border: none; position: fixed; } #watcher { position: absolute; } #watcher > div { overflow: hidden; padding-right: 5px; padding-left: 5px; text-overflow: ellipsis; max-width: 200px; white-space: nowrap; } #watcher > div.move { text-decoration: underline; padding-top: 5px; } #watcher > div:last-child { padding-bottom: 5px; } #qp { border: 1px solid; padding-bottom: 5px; } .qphl { outline: 2px solid rgba(216, 94, 49, .7); } .inlined { opacity: .5; } .inline td.reply { background-color: rgba(255, 255, 255, 0.15); border: 1px solid rgba(128, 128, 128, 0.5); } .filetitle, .replytitle, .postername, .commentpostername, .postertrip { background: none; } .filtered { text-decoration: line-through; } #files > input { display: block; } ' Main.init()