{log} = console if console? config = '404 Redirect': [true, 'Redirect dead threads'] 'Anonymize': [false, 'Make everybody anonymous'] 'Auto Watch': [true, 'Automatically watch threads that you start (Firefox only)'] 'Comment Expansion': [true, 'Expand too long comments'] 'Image Auto-Gif': [false, 'Animate gif thumbnails'] 'Image Expansion': [true, 'Expand images'] 'Image Hover': [false, 'Show full image on mouseover'] 'Image Preloading': [false, 'Preload Images'] 'Keybinds': [false, 'Binds actions to keys'] 'Localize Time': [true, 'Show times based on your timezone'] 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'] 'Post in Title': [true, 'Show the op\'s post in the tab title'] 'Quick Reply': [true, 'Reply without leaving the page'] 'Quick Report': [true, 'Add quick report buttons'] 'Reply Hiding': [true, 'Hide single replies'] 'Reply Navigation': [false, 'Navigate to the beginning / end of a thread'] 'Sauce': [true, 'Add sauce to images'] 'Show Stubs': [true, 'Of hidden threads / replies'] 'Thread Expansion': [true, 'View all replies'] 'Thread Hiding': [true, 'Hide entire threads'] 'Thread Navigation': [true, 'Navigate to previous / next thread'] 'Thread Updater': [true, 'Update threads'] 'Thread Watcher': [true, 'Bookmark threads'] 'Unread Count': [true, 'Show unread post count in tab title'] #x-browser if typeof GM_deleteValue is 'undefined' window.GM_setValue = (name, value) -> value = (typeof value)[0] + value localStorage.setItem name, value window.GM_getValue = (name, defaultValue) -> unless value = localStorage.getItem name return defaultValue type = value[0] value = value[1..] switch type when 'b' value == 'true' when 'n' Number value else value window.GM_addStyle = (css) -> style = document.createElement 'style' style.type = 'text/css' style.textContent = css document.getElementsByTagName('head')[0].appendChild style window.GM_openInTab = (url) -> window.open url, "_blank" #dialog styling GM_addStyle ' div.dialog { border: 1px solid; } div.dialog > div.move { cursor: move; } label, a { cursor: pointer; } ' ui = dialog: (id, position, html) -> ui.el = el = document.createElement 'div' el.className = 'reply dialog' el.innerHTML = html el.id = id if typeof position is 'object' {left, top} = position else switch position when 'topleft' left = '0px' top = '0px' when 'topright' left = null top = '0px' when 'bottomleft' left = '0px' top = null when 'bottomright' left = null top = null when 'center' left = '50%' top = '25%' left = localStorage["#{id}Left"] or left top = localStorage["#{id}Top"] or top if left then el.style.left = left else el.style.right = '0px' if top then el.style.top = top else el.style.bottom = '0px' el.querySelector('div.move').addEventListener 'mousedown', ui.move, true el.querySelector('div.move a[name=close]')?.addEventListener 'click', (-> el.parentNode.removeChild(el)), true el move: (e) -> {el} = ui #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 = document.body.clientWidth - el.offsetWidth ui.height = document.body.clientHeight - el.offsetHeight document.addEventListener 'mousemove', ui.moveMove, true document.addEventListener 'mouseup', ui.moveEnd, true moveMove: (e) -> {el} = ui left = e.clientX - ui.dx if left < 20 then left = '0px' else if ui.width - left < 20 then left = '' right = if left then '' else '0px' el.style.left = left el.style.right = right top = e.clientY - ui.dy if top < 20 then top = '0px' else if ui.height - top < 20 then top = '' bottom = if top then '' else '0px' el.style.top = top el.style.bottom = bottom moveEnd: -> document.removeEventListener 'mousemove', ui.moveMove, true document.removeEventListener 'mouseup', ui.moveEnd, true {el} = ui #{id} = {el} = ui doesn't work {id} = el localStorage["#{id}Left"] = el.style.left localStorage["#{id}Top"] = el.style.top #convenience d = document g = null #globals #utility $ = (selector, root=d.body) -> root.querySelector selector $.extend = (object, properties) -> for key, val of properties object[key] = val object $.extend $, addClass: (el, className) -> el.className += ' ' + className removeClass: (el, className) -> el.className = el.className.replace ' ' + className, '' rm: (el) -> el.parentNode.removeChild el append: (parent, child) -> parent.appendChild child before: (root, el) -> root.parentNode.insertBefore el, root el: (tag, properties) -> el = d.createElement tag $.extend el, properties if properties el bind: (el, eventType, handler) -> el.addEventListener eventType, handler, true unbind: (el, eventType, handler) -> el.removeEventListener eventType, handler, true isDST: -> # XXX this should check for DST in NY ### http://en.wikipedia.org/wiki/Daylight_saving_time_in_the_United_States Since 2007, daylight saving time starts on the second Sunday of March and ends on the first Sunday of November, with all time changes taking place at 2:00 AM (0200) local time. ### date = new Date() month = date.getMonth() #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.getDate() - date.getDay() if month is 2 #before second sunday if sunday < 8 return false #during second sunday if sunday < 15 and date.getDay() is 0 if date.getHour() < 1 return false return true #after second sunday return true if month is 10 # before first sunday if sunday < 1 return true # during first sunday if sunday < 8 and date.getDay() is 0 if date.getHour() < 1 return true return false #after first sunday return false $$ = (selector, root=d.body) -> result = root.querySelectorAll selector node for node in result mv = (children..., parent) -> (parent.appendChild child) for child in children getConfig = (name) -> GM_getValue name, config[name][0] getTime = -> Math.floor(new Date().getTime() / 1000) hide = (el) -> el.style.display = 'none' inAfter = (root, el) -> root.parentNode.insertBefore el, root.nextSibling inBefore = (root, el) -> root.parentNode.insertBefore el, root rm = (el) -> el.parentNode.removeChild el replace = (root, el) -> root.parentNode.replaceChild el, root show = (el) -> el.style.display = '' slice = (arr, id) -> # the while loop is the only low-level loop left in coffeescript. # we need to use it to see the index. # would it be better to just use objects and the `delete` keyword? i = 0 l = arr.length while (i < l) if id == arr[i].id arr.splice i, 1 return arr i++ tn = (s) -> d.createTextNode s x = (path, root=d.body) -> d.evaluate(path, root, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null). singleNodeValue zeroPad = (n) -> if n < 10 then '0' + n else n #funks autoWatch = -> #TODO look for subject autoText = $('textarea', this).value.slice(0, 25) GM_setValue('autoText', "/#{g.BOARD}/ - #{autoText}") closeQR = -> div = @parentNode.parentNode rm div 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. GM_deleteValue("hiddenReplies/#{g.BOARD}/") GM_deleteValue("hiddenThreads/#{g.BOARD}/") @value = "hidden: 0" g.hiddenReplies = [] g.hiddenThreads = [] cooldown = -> submit = $ '#qr input[type=submit]' seconds = parseInt submit.value if seconds == 0 submit.disabled = false submit.value = 'Submit' auto = submit.previousSibling.lastChild if auto.checked $('#qr form').submit() #submit.click() doesn't work else submit.value = seconds - 1 window.setTimeout cooldown, 1000 editSauce = -> ta = $ '#options textarea' if ta.style.display then show ta else hide ta expandComment = (e) -> e.preventDefault() a = this href = a.getAttribute('href') r = new XMLHttpRequest() r.onload = -> onloadComment(@responseText, a, href) r.open('GET', href, true) r.send() g.xhrs.push { r: r, id: href.match(/\d+/)[0] } expandThread = -> id = x('preceding-sibling::input[1]', this).name span = this #close expanded thread if span.textContent[0] is '-' #goddamit moot num = if board is 'b' then 3 else 5 table = x "following::br[@clear][1]/preceding::table[#{num}]", span while (prev = table.previousSibling) and (prev.nodeName is 'TABLE') rm prev span.textContent = span.textContent.replace '-', '+' return span.textContent = span.textContent.replace '+', 'X Loading...' #load cache for xhr in g.xhrs if xhr.id == id #why can't we just xhr.r.onload()? onloadThread xhr.r.responseText, span return #create new request r = new XMLHttpRequest() r.onload = -> onloadThread @responseText, span r.open 'GET', "res/#{id}", true r.send() g.xhrs.push { r: r, id: id } getThread = -> threads = $$ 'div.thread' for thread, i in threads bottom = thread.getBoundingClientRect().bottom if bottom > 0 #we have not scrolled past return [thread, i] hideReply = (reply) -> if p = @parentNode reply = p.nextSibling g.hiddenReplies.push { id: reply.id timestamp: getTime() } GM_setValue("hiddenReplies/#{g.BOARD}/", JSON.stringify(g.hiddenReplies)) name = $('span.commentpostername', reply).textContent trip = $('span.postertrip', reply)?.textContent or '' table = x 'ancestor::table', reply hide table if getConfig 'Show Stubs' a = $.el 'a', textContent: "[ + ] #{name} #{trip}" className: 'pointer' $.bind a, 'click', showReply div = $.el 'div' mv a, div inBefore table, div hideThread = (div) -> if p = @parentNode div = p g.hiddenThreads.push { id: div.id timestamp: getTime() } GM_setValue("hiddenThreads/#{g.BOARD}/", JSON.stringify(g.hiddenThreads)) hide div if getConfig 'Show Stubs' if span = $ '.omittedposts', div num = Number(span.textContent.match(/\d+/)[0]) else num = 0 num += $$('table', div).length text = if num is 1 then "1 reply" else "#{num} replies" name = $('span.postername', div).textContent trip = $('span.postername + span.postertrip', div)?.textContent || '' a = $.el 'a', textContent: "[ + ] #{name}#{trip} (#{text})" className: 'pointer' $.bind a, 'click', showThread inBefore div, a imageHover = init: -> img = $.el 'img', id: 'iHover' hide img d.body.appendChild img g.callbacks.push imageHover.cb.node offset: x: 45 y: -120 cb: node: (root) -> thumbs = $$ 'img[md5]', root for thumb in thumbs thumb.addEventListener 'mouseover', imageHover.cb.mouseover, true mouseover: (e) -> {target, clientX, clientY} = e img = $ '#iHover' img.src = target.parentNode.href show img imageHover.winHeight = d.body.clientHeight imageHover.winWidth = d.body.clientWidth target.addEventListener 'mousemove', imageHover.cb.mousemove, true target.addEventListener 'mouseout', imageHover.cb.mouseout, true mousemove: (e) -> {clientX, clientY} = e img = $ '#iHover' imgHeight = img.offsetHeight top = clientY + imageHover.offset.y bot = top + imgHeight log bot, imageHover.winHeight img.style.top = if imageHover.winHeight < imgHeight or top < 0 '0px' else if bot > imageHover.winHeight imageHover.winHeight - imgHeight + 'px' else top + 'px' img.style.left = clientX + imageHover.offset.x mouseout: (e) -> {target} = e img = $ '#iHover' hide img img.src = null target.removeEventListener 'mousemove', imageHover.cb.mousemove, true target.removeEventListener 'mouseout', imageHover.cb.mouseout, true imageClick = (e) -> return if e.shiftKey or e.altKey or e.ctrlKey e.preventDefault() imageToggle this imageToggle = (image) -> # 'image' is actually the container thumb = image.firstChild cw = d.body.clientWidth ch = d.body.clientHeight imageType = $("#imageType").value if thumb.className is 'hide' imageThumb thumb else imageExpand thumb, cw, ch, imageType imageTypeChange = -> images = $$ 'img[md5] + img' cw = d.body.clientWidth ch = d.body.clientHeight imageType = @value for image in images imageResize cw, ch, imageType, image imageExpandClick = -> thumbs = $$ 'img[md5]' g.expand = @checked cw = d.body.clientWidth ch = d.body.clientHeight imageType = $("#imageType").value if @checked #expand for thumb in thumbs if thumb.className isnt 'hide' #hide the thumb and show image imageExpand thumb, cw, ch, imageType else #contract for thumb in thumbs if thumb.className is 'hide' #unhide thumb and remove image imageThumb thumb imageExpand = (thumb, cw, ch, imageType) -> thumb.className = 'hide' link = thumb.parentNode image = $.el 'img', src: link.href link.appendChild image imageResize cw, ch, imageType, image imageResize = (cw, ch, imageType, image) -> [_, iw, ih] = x("preceding::span[@class][1]/text()[2]", image) .textContent.match(/(\d+)x(\d+)/) iw = Number iw ih = Number ih switch imageType when 'full' image.removeAttribute 'style' return when 'fit width' if iw > cw image.style.width = '100%' image.style.margin = '0px' break when 'fit screen' ratio = Math.min cw/iw, ch/ih if ratio < 1 image.style.width = Math.floor ratio * iw image.style.margin = '0px' imageThumb = (thumb) -> thumb.className = '' rm thumb.nextSibling keydown = (e) -> kc = e.keyCode g.keyCode = kc g.char = String.fromCharCode kc keypress = (e) -> if d.activeElement.nodeName in ['TEXTAREA', 'INPUT'] keyModeInsert e else keyModeNormal e keyModeInsert = (e) -> kc = g.keyCode char = g.char if kc is 27 #escape rm $ '#qr' e.preventDefault() else if e.ctrlKey and char is "S" ta = d.activeElement 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 e.preventDefault() keyModeNormal = (e) -> return if e.ctrlKey or e.altKey char = g.char hash = location.hash switch char when "0" location.pathname = "/#{g.BOARD}" when "G" if e.shiftKey window.scrollTo 0, 99999 else window.scrollTo 0, 0 location.hash = '' when "I" if g.REPLY unless qrLink = $ 'td.replyhl span[id] a:not(:first-child)' qrLink = $ "span[id^=nothread] a:not(:first-child)" else [thread] = getThread() unless qrLink = $ 'td.replyhl span[id] a:not(:first-child)', thread qrLink = $ "span#nothread#{thread.id} a:not(:first-child)", thread if e.shiftKey $.append d.body, qr.dialog qrLink $('#qr textarea').focus() else # qrLink.click() doesn't work, so use this hack e = preventDefault: -> target: qrLink qr.cb.quote e when "J" if e.shiftKey if not g.REPLY then [root] = getThread() if td = $ 'td.replyhl', root td.className = 'reply' rect = td.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #you're visible next = x 'following::td[@class="reply"]', td 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', root for reply in replies top = reply.getBoundingClientRect().top if top > 0 reply.className = 'replyhl' break break when "K" if e.shiftKey if not g.REPLY then [root] = getThread() if td = $ 'td.replyhl', root td.className = 'reply' rect = td.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #you're 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', root replies.reverse() height = d.body.clientHeight for reply in replies bot = reply.getBoundingClientRect().bottom if bot < height reply.className = 'replyhl' break break when "M" if e.shiftKey $("#imageExpand").click() else if not g.REPLY then [root] = getThread() unless image = $ 'td.replyhl span.filesize ~ a[target]', root image = $ 'span.filesize ~ a[target]', root imageToggle image when "N" sign = if e.shiftKey then -1 else 1 scrollThread sign when "O" href = $("#{hash} ~ span[id] a:last-of-type").href if e.shiftKey location.href = href else GM_openInTab href when "U" updateNow() when "W" root = if g.REPLY then null else getThread()[0] watchButton = $ "span.filesize ~ img", root watch.call watchButton nodeInserted = (e) -> target = e.target if target.nodeName is 'TABLE' for callback in g.callbacks callback target else if target.id is 'recaptcha_challenge_field' and dialog = $ '#qr' $('#recaptcha_image img', dialog).src = "http://www.google.com/recaptcha/api/image?c=" + target.value $('#recaptcha_challenge_field', dialog).value = target.value onloadComment = (responseText, a, href) -> [_, op, id] = href.match /(\d+)#(\d+)/ [replies, opbq] = parseResponse responseText if id is op html = opbq.innerHTML else #css selectors don't like ids starting with numbers, # getElementById only works for root document. for reply in replies if reply.id == id html = $('blockquote', reply).innerHTML bq = x 'ancestor::blockquote', a bq.innerHTML = html onloadThread = (responseText, span) -> [replies, opbq] = parseResponse responseText span.textContent = span.textContent.replace 'X Loading...', '- ' #make sure all comments are fully expanded span.previousSibling.innerHTML = opbq.innerHTML while (next = span.nextSibling) and not next.clear#
rm next if next for reply in replies inBefore next, x('ancestor::table', reply) else#threading div = span.parentNode for reply in replies mv x('ancestor::table', reply), div changeCheckbox = -> GM_setValue @name, @checked changeValue = -> GM_setValue @name, @value options = -> if div = $ '#options' rm div return hiddenNum = g.hiddenReplies.length + g.hiddenThreads.length html = '
Options X
' for option, value of config description = value[1] checked = if getConfig option then "checked" else "" html += "
" html += "
Flavors
" html += "
" html += "
" html += "
" html += "
support
" html += '
Donate
' div = ui.dialog 'options', 'center', html for input in $$ 'input[type="checkbox"]', div input.addEventListener 'change', changeCheckbox, true $('a.sauce', div).addEventListener 'click', editSauce, true $('textarea', div).addEventListener 'change', changeValue, true $('input[type="button"]', div).addEventListener 'click', clearHidden, true mv div, d.body parseResponse = (responseText) -> body = $.el 'body', innerHTML: responseText replies = $$ 'td.reply', body opbq = $ 'blockquote', body return [replies, opbq] qr = init: -> g.callbacks.push qr.cb.node iframe = $.el 'iframe', name: 'iframe' $.append d.body, iframe $.bind iframe, 'load', qr.cb.load $.bind window, 'message', qr.cb.messageTop #hack - nuke id so it doesn't grab focus when reloading $('#recaptcha_response_field').id = '' autohide: set: -> $('#qr input[title=autohide]:not(:checked)')?.click() unset: -> $('#qr input[title=autohide]:checked')?.click() cb: autohide: (e) -> dialog = $ '#qr' if @checked $.addClass dialog, 'auto' else $.removeClass dialog, 'auto' load: (e) -> e.target.contentWindow.postMessage '', '*' messageIframe: (e) -> message = $('table b').firstChild.textContent e.source.postMessage message, '*' window.location = 'about:blank' messageTop: (e) -> {data} = e dialog = $ '#qr' if data is 'Post successful!' if dialog if getConfig 'Persistent QR' qr.refresh dialog else $.rm dialog else error = $.el 'span', className: 'error' textContent: data $.append dialog, error qr.autohide.unset() recaptchaReload() node: (root) -> quotes = $$ 'a.quotejs:not(:first-child)', root for quote in quotes $.bind quote, 'click', qr.cb.quote submit: (e) -> if span = @nextSibling $.rm span recaptcha = $('input[name=recaptcha_response_field]', this) if recaptcha.value qr.autohide.set() g.sage = $('#qr input[name=email]').value == 'sage' else e.preventDefault() span = $.el 'span', className: 'error' textContent: 'You forgot to type in the verification.' $.append @parentNode, span alert 'You forgot to type in the verification.' recaptcha.focus() quote: (e) -> e.preventDefault() {target} = e unless dialog = $ '#qr' dialog = qr.dialog target id = target.textContent text = ">>#{id}\n" selection = window.getSelection() if s = selection.toString() selectionID = x('preceding::input[@type="checkbox"][1]', selection.anchorNode)?.name if selectionID == id text += ">#{s}\n" ta = $ 'textarea', dialog ta.focus() ta.value += text refresh: (dialog) -> $('textarea', dialog).value = '' $('input[name=recaptcha_response_field]', dialog).value = '' # XXX file.value = '' doesn't work in opera f = $('input[type=file]', dialog).parentNode f.innerHTML = f.innerHTML submit = $ 'input[type=submit]', qr submit.value = if g.sage then 60 else 30 submit.disabled = true window.setTimeout cooldown, 1000 auto = submit.previousSibling.lastChild if auto.checked #unhide the qr so you know it's ready for the next item $('input[title=autohide]:checked', qr)?.click() dialog: (link) -> html = "
Quick Reply X
" dialog = ui.dialog 'qr', top: '0px', left: '0px', html el = $ 'input[title=autohide]', dialog $.bind el, 'click', qr.cb.autohide clone = $('form[name=post]').cloneNode(true) for script in $$ 'script', clone $.rm script clone.target = 'iframe' $.bind clone, 'submit', qr.cb.submit $.bind $('input[name=recaptcha_response_field]', clone), 'keydown', recaptchaListener if not g.REPLY #figure out which thread we're replying to xpath = 'preceding::span[@class="postername"][1]/preceding::input[1]' input = $.el 'input', type: 'hidden' name: 'resto' value: x(xpath, link).name $.append clone, input else if getConfig 'Persistent QR' submit = $ 'input[type=submit]', clone auto = $.el 'label', textContent: 'Auto' autobox = $.el 'input', type: 'checkbox' $.append auto, autobox $.before submit, auto $.append dialog, clone $.append d.body, dialog dialog.style.width = dialog.offsetWidth # lock dialog persist: -> $.append d.body, qr.dialog() qr.autohide.set() sys: -> $.bind window, 'message', qr.cb.messageIframe if recaptcha = $ '#recaptcha_response_field' # post reporting $.bind recaptcha, 'keydown', recaptchaListener if getConfig 'Auto Watch' html = $('b').innerHTML [_, thread, id] = html.match(//) if thread is '0' [_, board] = $('meta', d).content.match(/4chan.org\/(\w+)\//) g.watched[board] or= [] g.watched[board].push { id: id, text: GM_getValue 'autoText' } GM_setValue 'watched', JSON.stringify g.watched recaptchaListener = (e) -> if e.keyCode is 8 and @value is '' recaptchaReload() recaptchaReload = -> window.location = 'javascript:Recaptcha.reload()' redirect = -> switch g.BOARD when 'a', 'g', 'lit', 'sci', 'tv' url = "http://green-oval.net/cgi-board.pl/#{g.BOARD}/thread/#{g.THREAD_ID}" when 'cgl', 'jp', 'm', 'tg' url = "http://archive.easymodo.net/cgi-board.pl/#{g.BOARD}/thread/#{g.THREAD_ID}" when '3', 'adv', 'an', 'c', 'ck', 'co', 'fa', 'fit', 'int', 'k', 'mu', 'n', 'o', 'p', 'po', 'soc', 'sp', 'toy', 'trv', 'v', 'vp', 'x' url = "http://archive.no-ip.org/#{g.BOARD}/thread/#{g.THREAD_ID}" else url = "http://boards.4chan.org/#{g.BOARD}" location.href = url replyNav = -> if g.REPLY window.location = if @textContent is '▲' then '#navtop' else '#navbot' else direction = if @textContent is '▲' then 'preceding' else 'following' op = x("#{direction}::span[starts-with(@id, 'nothread')][1]", this).id window.location = "##{op}" report = -> input = x('preceding-sibling::input[1]', this) input.click() $('input[value="Report"]').click() input.click() scrollThread = (count) -> [thread, idx] = getThread() top = thread.getBoundingClientRect().top if idx is 0 and top > 1 #we haven't scrolled to the first thread idx = -1 if count < 0 and top < -1 #we've started scrolling past this thread, # but now want to read from the beginning count++ temp = idx + count if temp < 0 hash = '' else if temp > 9 hash = 'p9' else hash = "p#{temp}" location.hash = hash showReply = -> div = @parentNode table = div.nextSibling show table rm div id = $('td.reply, td.replyhl', table).id slice g.hiddenReplies, id GM_setValue "hiddenReplies/#{g.BOARD}/", JSON.stringify(g.hiddenReplies) showThread = -> div = @nextSibling show div hide this id = div.id slice g.hiddenThreads, id GM_setValue("hiddenThreads/#{g.BOARD}/", JSON.stringify(g.hiddenThreads)) stopPropagation = (e) -> e.stopPropagation() threadF = (current) -> div = $.el 'div', className: 'thread' a = $.el 'a', textContent: '[ - ]' className: 'pointer' $.bind a, 'click', hideThread mv a, div inBefore current, div while (!current.clear)#
mv current, div current = div.nextSibling mv current, div current = div.nextSibling id = $('input[value="delete"]', div).name div.id = id #check if we should hide the thread for hidden in g.hiddenThreads if id == hidden.id hideThread(div) current = current.nextSibling.nextSibling if current.nodeName isnt 'CENTER' threadF(current) request = (url, callback) -> r = new XMLHttpRequest() r.onload = callback r.open 'get', url, true r.send() r updateCallback = -> count = $ '#updater #count' timer = $ '#updater #timer' if @status is 404 count.textContent = 404 count.className = 'error' timer.textContent = '' clearInterval g.interval for input in $$ 'input[type=submit]' input.disabled = true input.value = 404 s = '' if getConfig 'Unread Count' then s += "(#{g.replies.length}) " s += "/#{g.BOARD}/ - 404" d.title = s g.dead = true updateFavicon() return body = $.el 'body', innerHTML: @responseText replies = $$ 'td.reply', body root = $('br[clear]') if reply = $ 'td.reply, td.replyhl', root.previousElementSibling id = Number reply.id else id = 0 arr = [] while (reply = replies.pop()) and (Number reply.id > id) arr.push reply if g.verbose l = arr.length count.textContent = "+#{l}" count.className = if l > 0 then 'new' else '' #insert replies in order, so backlinks resolve while reply = arr.pop() table = x 'ancestor::table', reply inBefore root, table timer.textContent = -1 * GM_getValue 'Interval', 10 updateFavicon = -> len = g.replies.length if g.dead if len > 0 href = g.favDeadHalo else href = g.favDead else if len > 0 href = g.favHalo else href = g.favDefault favicon = $ 'link[rel="shortcut icon"]', d clone = favicon.cloneNode true clone.href = href replace favicon, clone updateTime = -> span = $ '#updater #timer' time = Number span.textContent if ++time is 0 updateNow() else if time > 10 time = 0 g.req.abort() updateNow() if g.verbose count = $ '#updater #count' count.textContent = 'retry' count.className = '' else span.textContent = time updateTitle = -> len = g.replies.length d.title = d.title.replace /\d+/, len updateFavicon() updateAuto = -> span = $ '#updater #timer' if @checked span.textContent = -1 * GM_getValue 'Interval', 10 g.interval = window.setInterval updateTime, 1000 else span.textContent = 'Thread Updater' clearInterval g.interval updateInterval = -> unless num = Number @value num = 10 @value = num GM_setValue 'Interval', num span = $ '#updater #timer' if 0 > Number span.textContent span.textContent = -1 * num updateNow = -> url = location.href + '?' + new Date().getTime() # fool the cache g.req = request url, updateCallback $("#updater #timer").textContent = 0 updateVerbose = -> g.verbose = @checked timer = $ '#updater #timer' if @checked timer.hidden = false else timer.hidden = true $("#updater #count").textContent = 'Thread Updater' updaterMake = -> html = "
Thread Updater
" html += "
" html += "
" html += "
" html += "
" html += "
" div = ui.dialog 'updater', 'bottomright', html for input in $$ 'input[type=checkbox]', div input.addEventListener 'click', changeCheckbox, true name = input.name if name is 'autoL' input.checked = GM_getValue 'autoG', true else input.checked = GM_getValue name, true switch name when 'autoL' input.addEventListener 'click', updateAuto, true when 'verbose' input.addEventListener 'click', updateVerbose, true unless g.verbose = GM_getValue 'verbose', true $("#timer", div).hidden = true interval = $ 'input[name=interval]', div interval.value = GM_getValue 'Interval', 10 interval.addEventListener 'change', updateInterval, true $('input[type=button]', div).addEventListener 'click', updateNow, true d.body.appendChild div if GM_getValue 'autoG' then updateAuto.call $("input[name=autoL]", div) watch = -> id = @nextSibling.name if @src is g.favEmpty @src = g.favDefault text = "/#{g.BOARD}/ - " + x('following-sibling::blockquote', this).textContent.slice(0,25) g.watched[g.BOARD] or= [] g.watched[g.BOARD].push { id: id, text: text } else @src = g.favEmpty g.watched[g.BOARD] = slice(g.watched[g.BOARD], id) GM_setValue('watched', JSON.stringify(g.watched)) watcherUpdate() watcherUpdate = -> div = $.el 'div' for board of g.watched for thread in g.watched[board] a = $.el 'a', textContent: 'X' className: 'pointer' $.bind a, 'click', watchX link = $.el 'a', textContent: thread.text href: "/#{board}/res/#{thread.id}" mv a, tn(' '), link, $.el('br'), div old = $('#watcher div:last-child') replace(old, div) watchX = -> [board, _, id] = @nextElementSibling. getAttribute('href').substring(1).split('/') g.watched[board] = slice(g.watched[board], id) GM_setValue('watched', JSON.stringify(g.watched)) watcherUpdate() if input = $("input[name=\"#{id}\"]") favicon = input.previousSibling favicon.src = g.favEmpty #main g = callbacks: [] expand: false favDead: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEUAAAAAAAD/AAA9+90tAAAAAXRSTlMAQObYZgAAADtJREFUCB0FwUERxEAIALDszMG730PNSkBEBSECoU0AEPe0mly5NWprRUcDQAdn68qtkVsj3/84z++CD5u7CsnoBJoaAAAAAElFTkSuQmCC' favDeadHalo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAWUlEQVR4XrWSAQoAIAgD/f+njSApsTqjGoTQ5oGWPJMOOs60CzsWwIwz1I4PUIYh+WYEMGQ6I/txw91kP4oA9BdwhKp1My4xQq6e8Q9ANgDJjOErewFiNesV2uGSfGv1/HYAAAAASUVORK5CYII=' favDefault: $('link[rel="shortcut icon"]', d)?.href or '' #no favicon in `post successful` page favEmpty: 'http://static.4chan.org/image/favicon-dis.ico' flavors: [ 'http://regex.info/exif.cgi?url=' 'http://iqdb.org/?url=' 'http://saucenao.com/search.php?db=999&url=' 'http://tineye.com/search?url=' ].join '\n' watched: JSON.parse(GM_getValue('watched', '{}')) xhrs: [] g.favHalo = if /ws/.test g.favDefault then 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAZklEQVR4XrWRQQoAIQwD+6L97j7Ih9WTQQxhDqJQCk4Mranuvqod6LgwawSqSuUmWSPw/UNlJlnDAmA2ARjABLYj8ZyCzJHHqOg+GdAKZmKPIQUzuYrxicHqEgHzP9g7M0+hj45sAnRWxtPj3zSPAAAAAElFTkSuQmCC' else 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEUAAABmzDP///8AAABet0i+AAAAAXRSTlMAQObYZgAAAExJREFUeF4tyrENgDAMAMFXKuQswQLBG3mOlBnFS1gwDfIYLpEivvjq2MlqjmYvYg5jWEzCwtDSQlwcXKCVLrpFbvLvvSf9uZJ2HusDtJAY7Tkn1oYAAAAASUVORK5CYII=' pathname = location.pathname.substring(1).split('/') [g.BOARD, temp] = pathname if temp is 'res' g.REPLY = temp g.THREAD_ID = pathname[2] else g.PAGENUM = parseInt(temp) || 0 g.hiddenThreads = JSON.parse(GM_getValue("hiddenThreads/#{g.BOARD}/", '[]')) g.hiddenReplies = JSON.parse(GM_getValue("hiddenReplies/#{g.BOARD}/", '[]')) tzOffset = (new Date()).getTimezoneOffset() / 60 # GMT -8 is given as +480; would GMT +8 be -480 ? g.chanOffset = 5 - tzOffset# 4chan = EST = GMT -5 if $.isDST() then g.chanOffset -= 1 lastChecked = GM_getValue('lastChecked', 0) now = getTime() DAY = 24 * 60 * 60 if lastChecked < now - 1*DAY cutoff = now - 7*DAY while g.hiddenThreads.length if g.hiddenThreads[0].timestamp > cutoff break g.hiddenThreads.shift() while g.hiddenReplies.length if g.hiddenReplies[0].timestamp > cutoff break g.hiddenReplies.shift() GM_setValue("hiddenThreads/#{g.BOARD}/", JSON.stringify(g.hiddenThreads)) GM_setValue("hiddenReplies/#{g.BOARD}/", JSON.stringify(g.hiddenReplies)) GM_setValue('lastChecked', now) GM_addStyle ' #iHover { position: fixed; } #options textarea { height: 100px; width: 500px; } #updater { position: fixed; text-align: right; } #updater input[type=text] { width: 50px; } #updater:not(:hover) { border: none; background: transparent; } #updater:not(:hover) > div:not(.move) { display: none; } #watcher { position: absolute; } #watcher > div.move { text-decoration: underline; padding: 5px 5px 0 5px; } #watcher > div:last-child { padding: 0 5px 5px 5px; } span.error { color: red; } #qr.auto:not(:hover) form { display: none; } #qr span.error { position: absolute; bottom: 0; left: 0; } #qr { position: fixed; } #qr > div { text-align: right; } #qr > form > div, /* ad */ #qr td.rules { display: none; } #options { position: fixed; padding: 5px; text-align: right; } form[name=delform] a img { border: 0px; float: left; margin: 0px 20px; } iframe { display: none; } span.navlinks { position: absolute; right: 5px; } span.navlinks > a { font-size: 16px; text-decoration: none; } .hide { display: none; } .new { background: lime; } ' if location.hostname is 'sys.4chan.org' qr.sys() return if navtopr = $ '#navtopr a' a = $.el 'a', textContent: '4chan X' className: 'pointer' $.bind a, 'click', options replace navtopr, a navbotr = $ '#navbotr a' a = $.el 'a', textContent: '4chan X' className: 'pointer' $.bind a, 'click', options replace navbotr, a else if getConfig('404 Redirect') and d.title is '4chan - 404' redirect() else return #hack to tab from comment straight to recaptcha for el in $$ '#recaptcha_table a' el.tabIndex = 1 recaptcha = $ '#recaptcha_response_field' recaptcha.addEventListener('keydown', recaptchaListener, true) scroll = -> height = d.body.clientHeight for reply, i in g.replies bottom = reply.getBoundingClientRect().bottom if bottom > height #post is not completely read break if i is 0 then return g.replies = g.replies[i..] updateTitle() #major features if getConfig 'Image Expansion' delform = $ 'form[name=delform]' expand = $.el 'div', innerHTML: " " imageType = GM_getValue 'imageType', 'full' for option in $$("option", expand) if option.textContent is imageType option.selected = true break $("select", expand).addEventListener 'change', changeValue, true $("select", expand).addEventListener 'change', imageTypeChange, true $("input", expand).addEventListener 'click', imageExpandClick, true inBefore delform.firstChild, expand g.callbacks.push (root) -> thumbs = $$ 'img[md5]', root for thumb in thumbs thumb.parentNode.addEventListener 'click', imageClick, true if g.expand then imageToggle thumb.parentNode if getConfig 'Image Hover' imageHover.init() if getConfig 'Image Auto-Gif' g.callbacks.push (root) -> thumbs = $$ 'img[md5]', root for thumb in thumbs src = thumb.parentNode.href if /gif$/.test src thumb.src = src if getConfig 'Localize Time' g.callbacks.push (root) -> spans = $$ 'span[id^=no]', root for span in spans s = span.previousSibling [_, month, day, year, hour, min_sec] = s.textContent.match /(\d+)\/(\d+)\/(\d+)\(\w+\)(\d+):(\S+)/ year = "20#{year}" month -= 1 #months start at 0 hour = g.chanOffset + Number hour date = new Date year, month, day, hour year = date.getFullYear() - 2000 month = zeroPad date.getMonth() + 1 day = zeroPad date.getDate() hour = zeroPad date.getHours() dotw = [ 'Sun' 'Mon' 'Tue' 'Wed' 'Thu' 'Fri' 'Sat' ][date.getDay()] s.textContent = " #{month}/#{day}/#{year}(#{dotw})#{hour}:#{min_sec} " if getConfig 'Sauce' g.callbacks.push (root) -> spans = $$ 'span.filesize', root prefixes = GM_getValue('flavors', g.flavors).split '\n' names = (prefix.match(/(\w+)\./)[1] for prefix in prefixes) for span in spans suffix = $('a', span).href i = 0; l = names.length while i < l link = $.el 'a', textContent: names[i] href: prefixes[i] + suffix mv tn(' '), link, span i++ if getConfig 'Reply Hiding' g.callbacks.push (root) -> tds = $$('td.doubledash', root) for td in tds a = $.el 'a', textContent: '[ - ]' className: 'pointer' $.bind a, 'click', hideReply replace(td.firstChild, a) next = td.nextSibling id = next.id for obj in g.hiddenReplies if obj.id is id hideReply(next) if getConfig 'Quick Reply' qr.init() if getConfig 'Quick Report' g.callbacks.push (root) -> arr = $$('span[id^=no]', root) for el in arr a = $.el 'a', textContent: '[ ! ]' className: 'pointer' $.bind a, 'click', report inAfter el, a inAfter el, tn(' ') if getConfig 'Thread Watcher' #create watcher html = '
Thread Watcher
' watcher = ui.dialog 'watcher', top: '50px', left: '0px', html mv watcher, d.body watcherUpdate() #add buttons threads = g.watched[g.BOARD] || [] #normal, threading inputs = $$('form > input[value="delete"], div > input[value="delete"]') for input in inputs id = input.name src = (-> for thread in threads if id is thread.id return g.favDefault g.favEmpty )() img = $.el 'img', src: src className: 'pointer' $.bind img, 'click', watch inBefore input, img if getConfig 'Anonymize' g.callbacks.push (root) -> names = $$('span.postername, span.commentpostername', root) for name in names name.innerHTML = 'Anonymous' trips = $$('span.postertrip', root) for trip in trips if trip.parentNode.nodeName is 'A' rm trip.parentNode else rm trip if getConfig 'Reply Navigation' g.callbacks.push (root) -> arr = $$('span[id^=norep]', root) for el in arr span = $.el 'span' up = $.el 'a', textContent: '▲' className: 'pointer' $.bind up, 'click', replyNav down = $.el 'a', textContent: '▼' className: 'pointer' $.bind down, 'click', replyNav mv tn(' '), up, tn(' '), down, span inAfter el, span if getConfig 'Keybinds' d.addEventListener 'keydown', keydown, true d.addEventListener 'keypress', keypress, true if g.REPLY if getConfig 'Image Preloading' g.callbacks.push (root) -> thumbs = $$ 'img[md5]', root for thumb in thumbs parent = thumb.parentNode el = $.el 'img', src: parent.href if getConfig 'Thread Updater' updaterMake() if getConfig('Quick Reply') and getConfig 'Persistent QR' qr.persist() if getConfig 'Post in Title' unless text = $('span.filetitle').textContent text = $('blockquote').textContent if text d.title = "/#{g.BOARD}/ - #{text}" if getConfig 'Unread Count' g.replies = [] d.title = '(0) ' + d.title window.addEventListener 'scroll', scroll, true g.callbacks.push (root) -> g.replies = g.replies.concat $$ 'td.reply, td.replyhl', root updateTitle() else #not reply if getConfig 'Thread Hiding' delform = $('form[name=delform]') start = $ 'form[name=delform] > *' start = start.nextSibling if getConfig 'Image Expansion' #skip over image expansion dialog #don't confuse other scripts d.addEventListener('DOMNodeInserted', stopPropagation, true) threadF start d.removeEventListener('DOMNodeInserted', stopPropagation, true) if getConfig 'Auto Watch' $('form[name="post"]').addEventListener('submit', autoWatch, true) if getConfig 'Thread Navigation' arr = $$ 'div > span.filesize, form > span.filesize' l1 = arr.length - 1 for el, i in arr span = $.el 'span', className: 'navlinks' id: 'p' + i if i textContent = '▲' href = "#p#{i - 1}" else if g.PAGENUM textContent = '◀' href = "#{g.PAGENUM - 1}#p0" else textContent = '▲' href = "#navtop" up = $.el 'a', className: 'pointer' textContent: textContent href: href if i < l1 textContent = '▼' href = "#p#{i + 1}" else textContent = '▶' href = "#{g.PAGENUM + 1}#p0" down = $.el 'a', className: 'pointer' textContent: textContent href: href mv up, tn(' '), down, span inBefore el, span if location.hash is '#p0' window.location = window.location if getConfig 'Thread Expansion' omitted = $$('span.omittedposts') for span in omitted a = $.el 'a', className: 'pointer omittedposts' textContent: "+ #{span.textContent}" $.bind a, 'click', expandThread replace(span, a) if getConfig 'Comment Expansion' as = $$('span.abbr a') for a in as a.addEventListener('click', expandComment, true) callback() for callback in g.callbacks d.body.addEventListener('DOMNodeInserted', nodeInserted, true)