Settings = init: -> # Appchan X settings link link = $.el 'a', id: 'appchanOptions' className: 'settings-link' href: 'javascript:;' $.on link, 'click', Settings.open $.asap (-> d.body), -> return unless Main.isThisPageLegit() # Wait for #boardNavMobile instead of #boardNavDesktop, # it might be incomplete otherwise. $.asap (-> $.id 'boardNavMobile'), -> $.prepend $.id('navtopright'), [$.tn(' ['), link, $.tn('] ')] $.get 'previousversion', null, (item) -> if previous = item['previousversion'] return if previous is g.VERSION # Avoid conflicts between sync'd newer versions # and out of date extension on this device. prev = previous.match(/\d+/g).map Number curr = g.VERSION.match(/\d+/g).map Number return unless prev[0] <= curr[0] and prev[1] <= curr[1] and prev[2] <= curr[2] changelog = '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' el = $.el 'span', innerHTML: "<%= meta.name %> has been updated to version #{g.VERSION}." new Notification 'info', el, 30 else $.on d, '4chanXInitFinished', Settings.open $.set lastupdate: Date.now() previousversion: g.VERSION Settings.addSection 'Style', Settings.style Settings.addSection 'Themes', Settings.themes Settings.addSection 'Mascots', Settings.mascots Settings.addSection 'Script', Settings.main Settings.addSection 'Filter', Settings.filter Settings.addSection 'Sauce', Settings.sauce Settings.addSection 'Rice', Settings.rice Settings.addSection 'Keybinds', Settings.keybinds $.on d, 'AddSettingsSection', Settings.addSection $.on d, 'OpenSettings', (e) -> Settings.open e.detail return if Conf['Enable 4chan\'s Extension'] settings = JSON.parse(localStorage.getItem '4chan-settings') or {} return if settings.disableAll settings.disableAll = true localStorage.setItem '4chan-settings', JSON.stringify settings open: (openSection) -> if Conf['editMode'] is "theme" if confirm "Opening the options dialog will close and discard any theme changes made with the theme editor." ThemeTools.close() return if Conf['editMode'] is "mascot" if confirm "Opening the options dialog will close and discard any mascot changes made with the mascot editor." MascotTools.close() return return if Settings.overlay $.event 'CloseMenu' Settings.dialog = dialog = $.el 'div', id: 'appchanx-settings' class: 'dialog' innerHTML: """
""" Settings.overlay = overlay = $.el 'div', id: 'overlay' links = [] for section in Settings.sections link = $.el 'a', className: "tab-#{section.hyphenatedTitle}" textContent: section.title href: 'javascript:;' $.on link, 'click', Settings.openSection.bind section links.push link sectionToOpen = link if section.title is openSection $.add $('.sections-list', dialog), links (if sectionToOpen then sectionToOpen else links[0]).click() $.on $('.close', dialog), 'click', Settings.close $.on overlay, 'click', Settings.close d.body.style.width = "#{d.body.clientWidth}px" $.addClass d.body, 'unscroll' $.add d.body, [overlay, dialog] close: -> return unless Settings.dialog d.body.style.removeProperty 'width' $.rmClass d.body, 'unscroll' $.rm Settings.overlay $.rm Settings.dialog delete Settings.overlay delete Settings.dialog sections: [] addSection: (title, open) -> if typeof title isnt 'string' {title, open} = title.detail hyphenatedTitle = title.toLowerCase().replace /\s+/g, '-' Settings.sections.push {title, hyphenatedTitle, open} openSection: (mode)-> if selected = $ '.tab-selected', Settings.dialog $.rmClass selected, 'tab-selected' $.addClass $(".tab-#{@hyphenatedTitle}", Settings.dialog), 'tab-selected' section = $ 'section', Settings.dialog $.rmAll section section.className = "section-#{@hyphenatedTitle}" @open section, mode section.scrollTop = 0 main: (section) -> section.innerHTML = """

""" $.on $('.export', section), 'click', Settings.export $.on $('.import', section), 'click', Settings.import $.on $('input', section), 'change', Settings.onImport items = {} inputs = {} for key, obj of Config.main fs = $.el 'fieldset', innerHTML: "#{key}" for key, arr of obj description = arr[1] div = $.el 'div', innerHTML: "#{description}" input = $ 'input', div $.on $('label', div), 'mouseover', Settings.mouseover $.on input, 'change', $.cb.checked items[key] = Conf[key] inputs[key] = input $.add fs, div Rice.nodes fs $.add section, fs $.get items, (items) -> for key, val of items inputs[key].checked = val return div = $.el 'div', innerHTML: ": Clear manually-hidden threads and posts on all boards. Refresh the page to apply." button = $ 'button', div hiddenNum = 0 $.get 'hiddenThreads', boards: {}, (item) -> for ID, board of item.hiddenThreads.boards for ID, thread of board hiddenNum++ button.textContent = "Hidden: #{hiddenNum}" $.get 'hiddenPosts', boards: {}, (item) -> for ID, board of item.hiddenPosts.boards for ID, thread of board for ID, post of thread hiddenNum++ button.textContent = "Hidden: #{hiddenNum}" $.on button, 'click', -> @textContent = 'Hidden: 0' $.get 'hiddenThreads', boards: {}, (item) -> for boardID of item.hiddenThreads.boards localStorage.removeItem "4chan-hide-t-#{boardID}" $.delete ['hiddenThreads', 'hiddenPosts'] $.after $('input[name="Stubs"]', section).parentNode.parentNode, div div = $.el 'div', innerHTML: ": Unhide the navigation bar." unhide = $ 'button', div $.on unhide, 'click', -> Header.setBarPosition.call textContent: "sticky top" $.after $('input[name="Check for Updates"]', section).parentNode.parentNode, div export: (now, data) -> unless typeof now is 'number' now = Date.now() data = version: g.VERSION date: now Conf['WatchedThreads'] = {} for db in DataBoards Conf[db] = boards: {} # Make sure to export the most recent data. $.get Conf, (Conf) -> data.Conf = Conf Settings.export now, data return a = $.el 'a', className: 'warning' textContent: 'Save me!' download: "<%= meta.name %> v#{g.VERSION}-#{now}.json" href: "data:application/json;base64,#{btoa unescape encodeURIComponent JSON.stringify data, null, 2}" target: '_blank' if $.engine isnt 'gecko' a.click() return # XXX Firefox won't let us download automatically. p = $ '.imp-exp-result', Settings.dialog $.rmAll p $.add p, a import: -> @nextElementSibling.click() onImport: -> return unless file = @files[0] output = @parentNode.nextElementSibling unless confirm 'Your current settings will be entirely overwritten, are you sure?' output.textContent = 'Import aborted.' return reader = new FileReader() reader.onload = (e) -> try data = JSON.parse e.target.result Settings.loadSettings data if confirm 'Import successful. Refresh now?' window.location.reload() catch err output.textContent = 'Import failed due to an error.' c.error err.stack reader.readAsText file loadSettings: (data) -> version = data.version.split '.' if version[0] is '2' data = Settings.convertSettings data, # General confs 'Disable 4chan\'s extension': '' 'Catalog Links': '' 'Reply Navigation': '' 'Show Stubs': 'Stubs' 'Image Auto-Gif': 'Auto-GIF' 'Expand From Current': '' 'Unread Favicon': 'Unread Tab Icon' 'Post in Title': 'Thread Excerpt' 'Auto Hide QR': '' 'Open Reply in New Tab': '' 'Remember QR size': '' 'Quote Inline': 'Quote Inlining' 'Quote Preview': 'Quote Previewing' 'Indicate OP quote': 'Mark OP Quotes' 'Indicate Cross-thread Quotes': 'Mark Cross-thread Quotes' # filter 'uniqueid': 'uniqueID' 'mod': 'capcode' 'country': 'flag' 'md5': 'MD5' # keybinds 'openEmptyQR': 'Open empty QR' 'openQR': 'Open QR' 'openOptions': 'Open settings' 'close': 'Close' 'spoiler': 'Spoiler tags' 'code': 'Code tags' 'submit': 'Submit QR' 'watch': 'Watch' 'update': 'Update' 'unreadCountTo0': '' 'expandAllImages': 'Expand images' 'expandImage': 'Expand image' 'zero': 'Front page' 'nextPage': 'Next page' 'previousPage': 'Previous page' 'nextThread': 'Next thread' 'previousThread': 'Previous thread' 'expandThread': 'Expand thread' 'openThreadTab': 'Open thread' 'openThread': 'Open thread tab' 'nextReply': 'Next reply' 'previousReply': 'Previous reply' 'hide': 'Hide' # updater 'Scrolling': 'Auto Scroll' 'Verbose': '' data.Conf.sauces = data.Conf.sauces.replace /\$\d/g, (c) -> switch c when '$1' '%TURL' when '$2' '%URL' when '$3' '%MD5' when '$4' '%board' else c for key, val of Config.hotkeys continue unless key of data.Conf data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) -> "Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}" data.Conf.WatchedThreads = data.WatchedThreads $.set data.Conf convertSettings: (data, map) -> for prevKey, newKey of map data.Conf[newKey] = data.Conf[prevKey] if newKey delete data.Conf[prevKey] data filter: (section) -> section.innerHTML = """
""" select = $ 'select', section $.on select, 'change', Settings.selectFilter Settings.selectFilter.call select selectFilter: -> div = @nextElementSibling if (name = @value) isnt 'guide' $.rmAll div ta = $.el 'textarea', name: name className: 'field' spellcheck: false $.get name, Conf[name], (item) -> ta.value = item[name] $.on ta, 'change', $.cb.value $.add div, ta return div.innerHTML = """
Filter is disabled.

Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string `weeaboo`, case-insensitive.
MD5 filtering uses exact string matching, not regular expressions.

""" sauce: (section) -> section.innerHTML = """
Sauce is disabled.
Lines starting with a # will be ignored.
You can specify a display text by appending ;text:[text] to the URL.
""" sauce = $ 'textarea', section $.get 'sauces', Conf['sauces'], (item) -> sauce.value = item['sauces'] $.on sauce, 'change', $.cb.value rice: (section) -> section.innerHTML = """
Custom Board Navigation is disabled.
In the following, board can translate to a board ID (a, b, etc...), the current board (current), or the Status/Twitter link (status, @).
Board link: board
Title link: board-title
Full text link: board-full
Custom text link: board-text:"VIP Board"
Index-only link: board-index
Catalog-only link: board-catalog
Combinations are possible: board-index-text:"VIP Index"
Full board list toggle: toggle-all
Time Formatting is disabled.
:
Supported format specifiers:
Day: %a, %A, %d, %e
Month: %m, %b, %B
Year: %y
Hour: %k, %H, %l, %I, %p, %P
Minute: %M
Second: %S
Quote Backlinks formatting is disabled.
:
File Info Formatting is disabled.
:
Link: %l (truncated), %L (untruncated), %T (Unix timestamp)
Original file name: %n (truncated), %N (untruncated), %t (Unix timestamp)
Spoiler indicator: %p
Size: %B (Bytes), %K (KB), %M (MB), %s (4chan default)
Resolution: %r (Displays 'PDF' for PDF files)
Unread Tab Icon is disabled.
Custom CSS
""" items = {} inputs = {} for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss'] input = $ "[name='#{name}']", section items[name] = Conf[name] inputs[name] = input event = if ['favicon', 'usercss'].contains name 'change' else 'input' $.on input, event, $.cb.value $.get items, (items) -> for key, val of items input = inputs[key] input.value = val unless 'usercss' is name $.on input, event, Settings[key] Settings[key].call input return Rice.nodes section $.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss $.on $.id('apply-css'), 'click', Settings.usercss boardnav: -> Header.generateBoardList @value time: -> funk = Time.createFunc @value @nextElementSibling.textContent = funk Time, new Date() backlink: -> @nextElementSibling.textContent = Conf['backlink'].replace /%id/, '123456789' fileInfo: -> data = isReply: true file: URL: '//images.4chan.org/g/src/1334437723720.jpg' name: 'd9bb2efc98dd0df141a94399ff5880b7.jpg' size: '276 KB' sizeInBytes: 276 * 1024 dimensions: '1280x720' isImage: true isSpoiler: true funk = FileInfo.createFunc @value @nextElementSibling.innerHTML = funk FileInfo, data favicon: -> Favicon.switch() Unread.update() if g.VIEW is 'thread' and Conf['Unread Tab Icon'] @nextElementSibling.innerHTML = """ """ togglecss: -> if $('textarea', @parentNode.parentNode).disabled = !@checked CustomCSS.rmStyle() else CustomCSS.addStyle() $.cb.checked.call @ usercss: -> CustomCSS.update() keybinds: (section) -> section.innerHTML = """
Keybinds are disabled.
Allowed keys: a-z, 0-9, Ctrl, Shift, Alt, Meta, Enter, Esc, Up, Down, Right, Left.
Press Backspace to disable a keybind.
ActionsKeybinds
""" tbody = $ 'tbody', section items = {} inputs = {} for key, arr of Config.hotkeys tr = $.el 'tr', innerHTML: "#{arr[1]}" input = $ 'input', tr input.name = key input.spellcheck = false items[key] = Conf[key] inputs[key] = input $.on input, 'keydown', Settings.keybind Rice.nodes tr $.add tbody, tr $.get items, (items) -> for key, val of items inputs[key].value = val return keybind: (e) -> return if e.keyCode is 9 # tab e.preventDefault() e.stopPropagation() return unless (key = Keybinds.keyCode e)? @value = key $.cb.value.call @ style: (section) -> nodes = $.frag() items = {} inputs = {} for key, obj of Config.style fs = $.el 'fieldset', innerHTML: "#{key}" for key, arr of obj [value, description, type] = arr div = $.el 'div', className: 'styleoption' if type if type is 'text' div.innerHTML = "
#{key}
#{description}
" input = $ "input", div else html = "
#{key}
#{description}
" div.innerHTML = html input = $ "select", div else div.innerHTML = "
#{description}" input = $ 'input', div input.bool = true items[key] = Conf[key] inputs[key] = input $.on $('.option', div), 'mouseover', Settings.mouseover $.on input, 'change', Settings.change $.add fs, div $.add nodes, fs $.get items, (items) -> for key, val of items input = inputs[key] if input.bool input.checked = val Rice.checkbox input else input.value = val if input.nodeName is 'SELECT' Rice.select input $.add section, nodes change: -> $.cb[if @bool then 'checked' else 'value'].call @ Style.addStyle() themes: (section, mode) -> if typeof mode isnt 'string' mode = 'default' parentdiv = $.el 'div', id: "themeContainer" suboptions = $.el 'div', className: "suboptions" id: "themes" keys = Object.keys(Themes) keys.sort() cb = Settings.cb.theme if mode is "default" for name in keys theme = Themes[name] unless theme["Deleted"] div = $.el 'div', className: "theme #{if name is Conf['theme'] then 'selectedtheme' else ''}" id: name innerHTML: "
#{name} #{theme['Author']} (SAGE) #{theme['Author Tripcode']} No.27583594 >>edit >>export >>delete
>>27582902
Post content is right here.

Selected

" div.style.backgroundColor = theme['Background Color'] $.on $('a.edit', div), 'click', cb.edit $.on $('a.export', div), 'click', cb.export $.on $('a.delete', div), 'click', cb.delete $.on div, 'click', cb.select $.add suboptions, div div = $.el 'div', id: 'addthemes' innerHTML: " New Theme / Import Theme / Import from 4chan SS / Import from Oneechan / Undelete Theme " $.on $("#newtheme", div), 'click', -> ThemeTools.init "untitled" Settings.close() $.on $("#import", div), 'click', -> @nextSibling.click() $.on $("#importbutton", div), 'change', (e) -> ThemeTools.importtheme "appchan", e $.on $("#OCimport", div), 'click', -> @nextSibling.click() $.on $("#OCimportbutton", div), 'change', (e) -> ThemeTools.importtheme "oneechan", e $.on $("#SSimportbutton", div), 'change', (e) -> ThemeTools.importtheme "SS", e $.on $("#SSimport", div), 'click', -> @nextSibling.click() $.on $('#tUndelete', div), 'click', -> $.rm $.id "themeContainer" Settings.openSection themes, 'undelete' else for name in keys theme = Themes[name] if theme["Deleted"] div = $.el 'div', id: name className: theme innerHTML: "
#{name} #{theme['Author']} (SAGE) #{theme['Author Tripcode']} No.27583594
>>27582902
I forgive you for using VLC to open me. ;__;
" $.on div, 'click', cb.restore $.add suboptions, div div = $.el 'div', id: 'addthemes' innerHTML: "Return" $.on $('a', div), 'click', -> $.rm $.id "themeContainer" Settings.openSection themes $.add parentdiv, suboptions $.add parentdiv, div $.add section, parentdiv mouseover: (e) -> mouseover = $.el 'div', id: 'mouseover' className: 'dialog' $.add Header.hover, mouseover mouseover.innerHTML = @nextElementSibling.innerHTML UI.hover root: @ el: mouseover latestEvent: e endEvents: 'mouseout' asapTest: -> true close: true return mascots: (section, mode) -> categories = {} menu = [] cb = Settings.cb.mascot if typeof mode isnt 'string' mode = 'default' parentdiv = $.el "div", id: "mascotContainer" suboptions = $.el "div", className: "suboptions" mascotHide = $.el "div", id: "mascot_hide" className: "reply" innerHTML: "Hide Categories
" keys = Object.keys Mascots keys.sort() if mode is 'default' # Create a keyed Unordered List Element and hide option for each mascot category. for name in MascotTools.categories categories[name] = $.el "div", className: "mascots" id: name if Conf["Hidden Categories"].contains name categories[name].hidden = true header = $.el "h3", className: "mascotHeader" textContent: name menu.push option = $.el "label", name: name innerHTML: "#{name}" $.on $('input', option), 'change', Settings.cb.mascotCategory $.add categories[name], header $.add suboptions, categories[name] for name in keys continue if Conf["Deleted Mascots"].contains name mascot = Mascots[name] mascotEl = $.el 'div', className: 'mascot' id: name innerHTML: "
#{name.replace /_/g, " "}
" if Conf[g.MASCOTSTRING].contains name $.addClass mascotEl, 'enabled' $.on $('.edit', mascotEl), 'click', cb.edit $.on $('.delete', mascotEl), 'click', cb.delete $.on $('.export', mascotEl), 'click', cb.export $.on mascotEl, 'click', cb.select if MascotTools.categories.contains mascot.category $.add categories[mascot.category], mascotEl else $.add categories[MascotTools.categories[0]], mascotEl $.add $('div', mascotHide), menu batchmascots = $.el 'div', id: "mascots_batch" innerHTML: " Clear All / Select All / Add Mascot / Import Mascot / Undelete Mascots / Get More Mascots! " $.on $('#clear', batchmascots), 'click', -> enabledMascots = JSON.parse(JSON.stringify(Conf[g.MASCOTSTRING])) for name in enabledMascots $.rmClass $.id(name), 'enabled' $.set g.MASCOTSTRING, Conf[g.MASCOTSTRING] = [] $.on $('#selectAll', batchmascots), 'click', -> for name, mascot of Mascots unless Conf["Hidden Categories"].contains(mascot.category) or Conf[g.MASCOTSTRING].contains(name) or Conf["Deleted Mascots"].contains(name) $.addClass $.id(name), 'enabled' Conf[g.MASCOTSTRING].push name $.set g.MASCOTSTRING, Conf[g.MASCOTSTRING] $.on $('#createNew', batchmascots), 'click', -> MascotTools.dialog() Settings.close() $.on $("#importMascot", batchmascots), 'click', -> @nextSibling.click() $.on $("#importMascotButton", batchmascots), 'change', (e) -> MascotTools.importMascot e $.on $('#undelete', batchmascots), 'click', -> unless Conf["Deleted Mascots"].length > 0 alert "No mascots have been deleted." return $.rm $.id "mascotContainer" Settings.mascotTab.dialog Settings.el, 'undelete' else categories = $.el "div", className: "mascots" id: name for name in keys continue unless Conf["Deleted Mascots"].contains name mascot = Mascots[name] mascotEl = $.el 'div', className: 'mascot' id: name innerHTML: "
#{name.replace /_/g, " "}
" $.on mascotEl, 'click', Settings.cb.mascot.restore $.add categories, mascotEl $.add suboptions, categories batchmascots = $.el 'div', id: "mascots_batch" innerHTML: "Return" $.on $('#return', batchmascots), 'click', -> $.rm $.id "mascotContainer" Settings.section 'mascots' $.add parentdiv, [suboptions, batchmascots, mascotHide] Rice.nodes parentdiv $.add section, parentdiv cb: mascot: category: -> if $.id(@name).hidden = @checked Conf["Hidden Categories"].push @name # Gather all names of enabled mascots in the hidden category in every context it could be enabled. for type in ["Enabled Mascots", "Enabled Mascots sfw", "Enabled Mascots nsfw"] setting = Conf[type] i = setting.length test = type is g.MASCOTSTRING while i-- name = setting[i] continue unless Mascots[name].category is @name setting.remove name continue unless test $.rmClass $.id(name), 'enabled' $.set type, setting else Conf["Hidden Categories"].remove @name $.set "Hidden Categories", Conf["Hidden Categories"] edit: (e) -> e.stopPropagation() MascotTools.dialog @name Settings.close() delete: (e) -> e.stopPropagation() if confirm "Are you sure you want to delete \"#{@name}\"?" if Conf['mascot'] is @name MascotTools.init() for type in ["Enabled Mascots", "Enabled Mascots sfw", "Enabled Mascots nsfw"] Conf[type].remove @name $.set type, Conf[type] Conf["Deleted Mascots"].push @name $.set "Deleted Mascots", Conf["Deleted Mascots"] $.rm $.id @name export: (e) -> e.stopPropagation() exportMascot = Mascots[@name] exportMascot['Mascot'] = @name exportedMascot = "data:application/json," + encodeURIComponent(JSON.stringify(exportMascot)) if window.open exportedMascot, "_blank" return else if confirm "Your popup blocker is preventing Appchan X from exporting this theme. Would you like to open the exported theme in this window?" window.location exportedMascot restore: -> if confirm "Are you sure you want to restore \"#{@id}\"?" Conf["Deleted Mascots"].remove @id $.set "Deleted Mascots", Conf["Deleted Mascots"] $.rm @ select: -> if Conf[g.MASCOTSTRING].remove @id if Conf['mascot'] is @id MascotTools.init() else Conf[g.MASCOTSTRING].push @id MascotTools.init @id $.toggleClass @, 'enabled' $.set g.MASCOTSTRING, Conf[g.MASCOTSTRING] theme: select: -> if currentTheme = $.id(Conf['theme']) $.rmClass currentTheme, 'selectedtheme' if Conf["NSFW/SFW Themes"] $.set "theme_#{g.TYPE}", @id else $.set "theme", @id Conf['theme'] = @id $.addClass @, 'selectedtheme' Style.addStyle() edit: (e) -> e.preventDefault() e.stopPropagation() ThemeTools.init @name Settings.close() export: (e) -> e.preventDefault() e.stopPropagation() exportTheme = Themes[@name] exportTheme['Theme'] = @name exportedTheme = "data:application/json," + encodeURIComponent(JSON.stringify(exportTheme)) if window.open exportedTheme, "_blank" return else if confirm "Your popup blocker is preventing Appchan X from exporting this theme. Would you like to open the exported theme in this window?" window.location exportedTheme delete: (e) -> e.preventDefault() e.stopPropagation() container = $.id @name unless container.previousSibling or container.nextSibling alert "Cannot delete theme (No other themes available)." return if confirm "Are you sure you want to delete \"#{@name}\"?" if @name is Conf['theme'] if settheme = container.previousSibling or container.nextSibling Conf['theme'] = settheme.id $.addClass settheme, 'selectedtheme' $.set 'theme', Conf['theme'] Themes[@name]["Deleted"] = true $.get "userThemes", {}, -> userThemes = items['userThemes'] userThemes[@name] = Themes[@name] $.set 'userThemes', userThemes $.rm container restore: -> if confirm "Are you sure you want to restore \"#{@id}\"?" Themes[@id]["Deleted"] = false $.get "userThemes", {}, (item) -> userThemes = item["userThemes"] userThemes[@id] = Themes[@id] $.set 'userThemes', userThemes $.rm @