Main = init: -> # XXX Work around Pale Moon / old Firefox + GM 1.15 bug where script runs in iframe with wrong window.location. return if d.body and not $ 'title', d.head # XXX dwb userscripts extension reloads scripts run at document-start when replaceState/pushState is called. # XXX Firefox reinjects WebExtension content scripts when extension is updated / reloaded. try w = window w = (w.wrappedJSObject or w) if $.platform is 'crx' return if '<%= meta.name %> antidup' of w w['<%= meta.name %> antidup'] = true if location.hostname is 'www.google.com' $.get 'Captcha Fixes', true, ({'Captcha Fixes': enabled}) -> if enabled $.ready -> Captcha.fixes.init() return # Don't run inside ad iframes. try return if window.frameElement and window.frameElement.src in ['', 'about:blank'] # Detect multiple copies of 4chan X return if doc and $.hasClass(doc, 'fourchan-x') $.asap docSet, -> $.addClass doc, 'fourchan-x', 'seaweedchan' $.addClass doc, "ua-#{$.engine}" if $.engine $.on d, '4chanXInitFinished', -> if Main.expectInitFinished delete Main.expectInitFinished else new Notice 'error', 'Error: Multiple copies of 4chan X are enabled.' $.addClass doc, 'tainted' # Flatten default values from Config into Conf flatten = (parent, obj) -> if obj instanceof Array Conf[parent] = obj[0] else if typeof obj is 'object' for key, val of obj flatten key, val else # string or number Conf[parent] = obj return # XXX Remove document-breaking ad if location.hostname in ['boards.4chan.org', 'boards.4channel.org'] $.global -> fromCharCode0 = String.fromCharCode String.fromCharCode = -> if document.body String.fromCharCode = fromCharCode0 else if document.currentScript and not document.currentScript.src throw Error() fromCharCode0.apply @, arguments $.asap docSet, -> $.onExists doc, 'iframe[srcdoc]', $.rm flatten null, Config for db in DataBoard.keys Conf[db] = {} Conf['boardConfig'] = boards: {} Conf['archives'] = Redirect.archives Conf['selectedArchives'] = {} Conf['cooldowns'] = {} Conf['Index Sort'] = {} Conf["Last Long Reply Thresholds #{i}"] = {} for i in [0...2] Conf['siteProperties'] = '4chan.org': {software: 'yotsuba'} '4channel.org': {software: 'yotsuba'} '4cdn.org': {software: 'yotsuba'} # XXX old key names Conf['Except Archives from Encryption'] = false Conf['JSON Navigation'] = true Conf['Oekaki Links'] = true Conf['Show Name and Subject'] = false Conf['QR Shortcut'] = true Conf['Bottom QR Link'] = true Conf['Toggleable Thread Watcher'] = true Conf['siteSoftware'] = """ 4chan.org yotsuba 4channel.org yotsuba 4cdn.org yotsuba """ # Enforce JS whitelist if /\.4chan(?:nel)?\.org$/.test(location.hostname) and !$$('script:not([src])', d).filter((s) -> /this\[/.test(s.textContent)).length ($.getSync or $.get) {'jsWhitelist': Conf['jsWhitelist']}, ({jsWhitelist}) -> $.addCSP "script-src #{jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}" # Get saved values as items items = {} items[key] = undefined for key of Conf items['previousversion'] = undefined ($.getSync or $.get) items, (items) -> if !$.perProtocolSettings and /\.4chan(?:nel)?\.org$/.test(location.hostname) and (items['Redirect to HTTPS'] ? Conf['Redirect to HTTPS']) and location.protocol isnt 'https:' location.replace('https://' + location.host + location.pathname + location.search + location.hash) return $.asap docSet, -> # Don't hide the local storage warning behind a settings panel. if $.cantSet # pass # Fresh install else if !items.previousversion? Main.ready -> $.set 'previousversion', g.VERSION Settings.open() # Migrate old settings else if items.previousversion isnt g.VERSION Main.upgrade items # Combine default values with saved values for key, val of Conf Conf[key] = items[key] ? val Site.init Main.initFeatures upgrade: (items) -> {previousversion} = items changes = Settings.upgrade items, previousversion items.previousversion = changes.previousversion = g.VERSION $.set changes, -> if items['Show Updated Notifications'] ? true el = $.el 'span', <%= html(meta.name + ' has been updated to version ${g.VERSION}.') %> new Notice 'info', el, 15 initFeatures: -> {hostname, search} = location pathname = location.pathname.split /\/+/ g.BOARD = new Board pathname[1] unless hostname in ['www.4chan.org', 'www.4channel.org'] $.global -> document.documentElement.classList.add 'js-enabled' window.FCX = {} Main.jsEnabled = $.hasClass doc, 'js-enabled' switch hostname when 'www.4chan.org', 'www.4channel.org' $.onExists doc, 'body', -> $.addStyle CSS.www Captcha.replace.init() return when 'sys.4chan.org', 'sys.4channel.org' if pathname[2] is 'imgboard.php' if /\bmode=report\b/.test search Report.init() else if (match = search.match /\bres=(\d+)/) $.ready -> if Conf['404 Redirect'] and $.id('errmsg')?.textContent is 'Error: Specified thread does not exist.' Redirect.navigate 'thread', { boardID: g.BOARD.ID postID: +match[1] } else if pathname[2] is 'post' PostSuccessful.init() return if ImageHost.test hostname return unless pathname[2] and not /[sm]\.jpg$/.test(pathname[2]) $.asap (-> d.readyState isnt 'loading'), -> if Conf['404 Redirect'] and Site.is404?() Redirect.navigate 'file', { boardID: g.BOARD.ID filename: pathname[pathname.length - 1] } else if video = $ 'video' if Conf['Volume in New Tab'] Volume.setup video if Conf['Loop in New Tab'] video.loop = true video.controls = false video.play() ImageCommon.addControls video return return if Site.isAuxiliaryPage?() if pathname[2] in ['thread', 'res'] g.VIEW = 'thread' g.THREADID = +pathname[3].replace('.html', '') else if /^(?:catalog|archive)(?:\.html)?$/.test(pathname[2]) g.VIEW = pathname[2].replace('.html', '') else if /^(?:index|\d*)(?:\.html)?$/.test(pathname[2]) g.VIEW = 'index' else return g.threads = new SimpleDict() g.posts = new SimpleDict() # set up CSS when
is completely loaded $.onExists doc, 'body', Main.initStyle # c.time 'All initializations' for [name, feature] in Main.features continue if Site.disabledFeatures and name in Site.disabledFeatures # c.time "#{name} initialization" try feature.init() catch err Main.handleErrors message: "\"#{name}\" initialization crashed." error: err # finally # c.timeEnd "#{name} initialization" # c.timeEnd 'All initializations' $.ready Main.initReady initStyle: -> return if !Main.isThisPageLegit() # disable the mobile layout $('link[href*=mobile]', d.head)?.disabled = true doc.dataset.host = location.host $.addClass doc, "sw-#{Site.software}" $.addClass doc, if g.VIEW is 'thread' then 'thread-view' else g.VIEW $.onExists doc, '.ad-cnt, .adg-rects > .desktop', (ad) -> $.onExists ad, 'img, iframe', -> $.addClass doc, 'ads-loaded' $.addClass doc, 'autohiding-scrollbar' if Conf['Autohiding Scrollbar'] $.ready -> if d.body.clientHeight > doc.clientHeight and (window.innerWidth is doc.clientWidth) isnt Conf['Autohiding Scrollbar'] Conf['Autohiding Scrollbar'] = !Conf['Autohiding Scrollbar'] $.set 'Autohiding Scrollbar', Conf['Autohiding Scrollbar'] $.toggleClass doc, 'autohiding-scrollbar' $.addStyle CSS.boards, 'fourchanx-css' Main.bgColorStyle = $.el 'style', id: 'fourchanx-bgcolor-css' keyboard = false $.on d, 'mousedown', -> keyboard = false $.on d, 'keydown', (e) -> (keyboard = true if e.keyCode is 9) # tab window.addEventListener 'focus', (-> doc.classList.toggle 'keyboard-focus', keyboard), true Main.setClass() setClass: -> if g.VIEW is 'catalog' $.addClass doc, $.id('base-css').href.match(/catalog_(\w+)/)[1].replace('_new', '').replace /_+/g, '-' return style = 'yotsuba-b' mainStyleSheet = $ 'link[title=switch]', d.head styleSheets = $$ 'link[rel="alternate stylesheet"]', d.head setStyle = -> $.rmClass doc, style style = null for styleSheet in styleSheets if styleSheet.href is mainStyleSheet?.href style = styleSheet.title.toLowerCase().replace('new', '').trim().replace /\s+/g, '-' style = styleSheet.href.match(/[a-z]*(?=[^/]*$)/)[0] if style is '_special' style = null unless style in ['yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'photon', 'tomorrow', 'spooky'] break if style $.addClass doc, style $.rm Main.bgColorStyle else # Determine proper background color for dialogs if 4chan is using a special stylesheet. div = Site.bgColoredEl() div.style.position = 'absolute'; div.style.visibility = 'hidden'; $.add d.body, div bgColor = window.getComputedStyle(div).backgroundColor c.log(bgColor) $.rm div rgb = bgColor.match(/[\d.]+/g) # Use body background if reply background is transparent unless /^rgb\(/.test(bgColor) s = window.getComputedStyle(d.body) bgColor = "#{s.backgroundColor} #{s.backgroundImage} #{s.backgroundRepeat} #{s.backgroundPosition}" Main.bgColorStyle.textContent = """ .dialog, .suboption-list > div:last-of-type, :root.catalog-hover-expand .catalog-container:hover > .post { background: #{bgColor}; } .unread-mark-read { background-color: rgba(#{rgb[...3].join(', ')}, #{0.5*(rgb[3] || 1)}); } """ $.after $.id('fourchanx-css'), Main.bgColorStyle setStyle() return unless mainStyleSheet new MutationObserver(setStyle).observe mainStyleSheet, { attributes: true attributeFilter: ['href'] } initReady: -> if Site.is404?() if g.VIEW is 'thread' ThreadWatcher.set404 g.BOARD.ID, g.THREADID, -> if Conf['404 Redirect'] Redirect.navigate 'thread', boardID: g.BOARD.ID threadID: g.THREADID postID: +location.hash.match /\d+/ # post number or 0 , "/#{g.BOARD}/" return if Site.isIncomplete?() msg = $.el 'div', <%= html('The page didn't load completely.