diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 7888933c0..923e59c14 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -17,6 +17,7 @@ module.exports = (grunt) -> 'lib/$.coffee' 'lib/polyfill.coffee' 'src/appchan.coffee' + 'src/settings.coffee' 'src/features.coffee' 'src/qr.coffee' 'src/report.coffee' diff --git a/appchan-x.user.js b/appchan-x.user.js index 98172e9df..c665c103f 100644 --- a/appchan-x.user.js +++ b/appchan-x.user.js @@ -20,7 +20,7 @@ // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAElBMVEX///8EZgR8ulSk0oT///8EAgQ1A88mAAAAAXRSTlMAQObYZgAAAIpJREFUeF6t0sENwjAMhWF84N4H6gAYMUBkdQMYwfuvwmstEeD4kl892P0OaaWcpga2/K0SGII1HNBXARgu7veoY3ANd+esgMHZIz85u0EABrbms3pl/bkC1Tn5ihGOfQwqHeZ/FdYdirEMgCG2ZAQWDTL0m9FvjAhcvoGNAK2gZhGYYX9+ZgFm9gaiNmNkMENY4QAAAABJRU5ErkJggg== // ==/UserScript== -/* appchan x - Version 2.0.0 - 2013-04-08 +/* appchan x - Version 2.0.0 - 2013-04-09 * http://zixaphir.github.com/appchan-x/ * * Copyright (c) 2009-2011 James Campos @@ -4862,251 +4862,6 @@ } }; - Header = { - init: function() { - this.bar = $.el('div', { - id: 'notifications' - }); - this.shortcuts = $.el('span', { - id: 'shortcuts' - }); - this.hover = $.el('div', { - id: 'hoverUI' - }); - $.asap((function() { - return d.body; - }), function() { - if (!Main.isThisPageLegit()) { - return; - } - return $.asap((function() { - return $.id('boardNavMobile'); - }), Header.setBoardList); - }); - return $.ready(function() { - return $.add(d.body, Header.hover); - }); - }, - setBoardList: function() { - var a, btn, customBoardList, fullBoardList, nav; - - Header.nav = nav = $.id('boardNavDesktop'); - if (a = $("a[href*='/" + g.BOARD + "/']", nav)) { - a.className = 'current'; - } - fullBoardList = $.el('span', { - id: 'full-board-list', - hidden: true - }); - customBoardList = $.el('span', { - id: 'custom-board-list' - }); - $.add(fullBoardList, __slice.call(nav.childNodes)); - $.add(nav, [customBoardList, fullBoardList, Header.shortcuts, $('#navtopright', fullBoardList)]); - $.add(d.body, Header.bar); - if (Conf['Custom Board Navigation']) { - Header.generateBoardList(Conf['boardnav']); - $.sync('boardnav', Header.generateBoardList); - btn = $.el('span', { - className: 'hide-board-list-button', - innerHTML: '[ - ]\u00A0' - }); - $.on(btn, 'click', Header.toggleBoardList); - return $.prepend(fullBoardList, btn); - } else { - $.rm($('#custom-board-list', nav)); - return fullBoardList.hidden = false; - } - }, - generateBoardList: function(text) { - var as, list, nodes; - - list = $('#custom-board-list', Header.nav); - list.innerHTML = null; - if (!text) { - return; - } - as = $$('#full-board-list a', Header.nav).slice(0, -2); - nodes = text.match(/[\w@]+(-(all|title|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map(function(t) { - var a, board, m, _i, _len; - - if (/^[^\w@]/.test(t)) { - return $.tn(t); - } - if (/^toggle-all/.test(t)) { - a = $.el('a', { - className: 'show-board-list-button', - textContent: (t.match(/-text:"(.+)"/) || [null, '+'])[1], - href: 'javascript:;' - }); - $.on(a, 'click', Header.toggleBoardList); - return a; - } - board = /^current/.test(t) ? g.BOARD.ID : t.match(/^[^-]+/)[0]; - for (_i = 0, _len = as.length; _i < _len; _i++) { - a = as[_i]; - if (a.textContent === board) { - a = a.cloneNode(true); - if (/-title/.test(t)) { - a.textContent = a.title; - } else if (/-full/.test(t)) { - a.textContent = "/" + board + "/ - " + a.title; - } else if (/-(index|catalog|text)/.test(t)) { - if (m = t.match(/-(index|catalog)/)) { - a.setAttribute('data-only', m[1]); - a.href = "//boards.4chan.org/" + board + "/"; - if (m[1] === 'catalog') { - a.href += 'catalog'; - } - } - if (m = t.match(/-text:"(.+)"/)) { - a.textContent = m[1]; - } - } else if (board === '@') { - $.addClass(a, 'navSmall'); - } - return a; - } - } - return $.tn(t); - }); - return $.add(list, nodes); - }, - toggleBoardList: function() { - var custom, full, nav, showBoardList; - - nav = Header.nav; - custom = $('#custom-board-list', nav); - full = $('#full-board-list', nav); - showBoardList = !full.hidden; - custom.hidden = !showBoardList; - return full.hidden = showBoardList; - }, - addShortcut: function(el) { - var shortcut; - - shortcut = $.el('span', { - className: 'shortcut' - }); - $.add(shortcut, [$.tn(' ['), el, $.tn(']')]); - return $.add(Header.shortcuts, shortcut); - } - }; - - Notification = (function() { - var add, close; - - function Notification(type, content, timeout) { - this.timeout = timeout; - this.add = add.bind(this); - this.close = close.bind(this); - this.el = $.el('div', { - innerHTML: '×
' - }); - this.el.style.opacity = 0; - this.setType(type); - $.on(this.el.firstElementChild, 'click', this.close); - if (typeof content === 'string') { - content = $.tn(content); - } - $.add(this.el.lastElementChild, content); - $.ready(this.add); - } - - Notification.prototype.setType = function(type) { - return this.el.className = "notification " + type; - }; - - add = function() { - if (d.hidden) { - $.on(d, 'visibilitychange', this.add); - return; - } - $.off(d, 'visibilitychange', this.add); - $.add($.id('notifications'), this.el); - this.el.clientHeight; - this.el.style.opacity = 1; - if (this.timeout) { - return setTimeout(this.close, this.timeout * $.SECOND); - } - }; - - close = function() { - if (this.el.parentNode) { - return $.rm(this.el); - } - }; - - return Notification; - - })(); - - CatalogLinks = { - init: function() { - var el; - - $.ready(this.ready); - if (!Conf['Catalog Links']) { - return; - } - el = $.el('a', { - id: 'toggleCatalog', - href: 'javascript:;', - className: Conf['Header catalog links'] ? 'disabled' : '', - textContent: 'Catalog', - title: "Turn catalog links " + (Conf['Header catalog links'] ? 'off' : 'on') + "." - }); - $.on(el, 'click', this.toggle); - Header.addShortcut(el); - return $.asap((function() { - return d.body; - }), function() { - if (!Main.isThisPageLegit()) { - return; - } - return $.asap((function() { - return $.id('boardNavMobile'); - }), function() { - return CatalogLinks.toggle.call(el); - }); - }); - }, - toggle: function() { - var a, board, useCatalog, _i, _len, _ref; - - $.set('Header catalog links', useCatalog = this.className === 'disabled'); - $.toggleClass(this, 'disabled'); - _ref = $$('a', $.id('boardNavDesktop')); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - a = _ref[_i]; - board = a.pathname.split('/')[1]; - if (['f', 'status', '4chan'].contains(board) || !board) { - continue; - } - if (Conf['External Catalog']) { - a.href = useCatalog ? CatalogLinks.external(board) : "//boards.4chan.org/" + board + "/"; - } else { - a.pathname = "/" + board + "/" + (useCatalog ? 'catalog' : ''); - } - a.title = useCatalog ? "" + a.title + " - Catalog" : a.title.replace(/\ -\ Catalog$/, ''); - } - return this.title = "Turn catalog links " + (useCatalog ? 'off' : 'on') + "."; - }, - external: function(board) { - return (['a', 'c', 'g', 'co', 'k', 'm', 'o', 'p', 'v', 'vg', 'w', 'cm', '3', 'adv', 'an', 'cgl', 'ck', 'diy', 'fa', 'fit', 'int', 'jp', 'mlp', 'lit', 'mu', 'n', 'po', 'sci', 'toy', 'trv', 'tv', 'vp', 'x', 'q'].contains(board) ? "http://catalog.neet.tv/" + board : ['d', 'e', 'gif', 'h', 'hr', 'hc', 'r9k', 's', 'pol', 'soc', 'u', 'i', 'ic', 'hm', 'r', 'w', 'wg', 'wsg', 't', 'y'].contains(board) ? "http://4index.gropes.us/" + board : "//boards.4chan.org/" + board + "/catalog"); - }, - ready: function() { - var catalogLink; - - if (catalogLink = $('.pages.cataloglink a', d.body) || $('[href=".././catalog"]', d.body)) { - if (!g.VIEW === 'thread') { - $.add(d.body, catalogLink); - } - return catalogLink.id = 'catalog'; - } - } - }; - Settings = { init: function() { var link, settings; @@ -5663,6 +5418,251 @@ } }; + Header = { + init: function() { + this.bar = $.el('div', { + id: 'notifications' + }); + this.shortcuts = $.el('span', { + id: 'shortcuts' + }); + this.hover = $.el('div', { + id: 'hoverUI' + }); + $.asap((function() { + return d.body; + }), function() { + if (!Main.isThisPageLegit()) { + return; + } + return $.asap((function() { + return $.id('boardNavMobile'); + }), Header.setBoardList); + }); + return $.ready(function() { + return $.add(d.body, Header.hover); + }); + }, + setBoardList: function() { + var a, btn, customBoardList, fullBoardList, nav; + + Header.nav = nav = $.id('boardNavDesktop'); + if (a = $("a[href*='/" + g.BOARD + "/']", nav)) { + a.className = 'current'; + } + fullBoardList = $.el('span', { + id: 'full-board-list', + hidden: true + }); + customBoardList = $.el('span', { + id: 'custom-board-list' + }); + $.add(fullBoardList, __slice.call(nav.childNodes)); + $.add(nav, [customBoardList, fullBoardList, Header.shortcuts, $('#navtopright', fullBoardList)]); + $.add(d.body, Header.bar); + if (Conf['Custom Board Navigation']) { + Header.generateBoardList(Conf['boardnav']); + $.sync('boardnav', Header.generateBoardList); + btn = $.el('span', { + className: 'hide-board-list-button', + innerHTML: '[ - ]\u00A0' + }); + $.on(btn, 'click', Header.toggleBoardList); + return $.prepend(fullBoardList, btn); + } else { + $.rm($('#custom-board-list', nav)); + return fullBoardList.hidden = false; + } + }, + generateBoardList: function(text) { + var as, list, nodes; + + list = $('#custom-board-list', Header.nav); + list.innerHTML = null; + if (!text) { + return; + } + as = $$('#full-board-list a', Header.nav).slice(0, -2); + nodes = text.match(/[\w@]+(-(all|title|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map(function(t) { + var a, board, m, _i, _len; + + if (/^[^\w@]/.test(t)) { + return $.tn(t); + } + if (/^toggle-all/.test(t)) { + a = $.el('a', { + className: 'show-board-list-button', + textContent: (t.match(/-text:"(.+)"/) || [null, '+'])[1], + href: 'javascript:;' + }); + $.on(a, 'click', Header.toggleBoardList); + return a; + } + board = /^current/.test(t) ? g.BOARD.ID : t.match(/^[^-]+/)[0]; + for (_i = 0, _len = as.length; _i < _len; _i++) { + a = as[_i]; + if (a.textContent === board) { + a = a.cloneNode(true); + if (/-title/.test(t)) { + a.textContent = a.title; + } else if (/-full/.test(t)) { + a.textContent = "/" + board + "/ - " + a.title; + } else if (/-(index|catalog|text)/.test(t)) { + if (m = t.match(/-(index|catalog)/)) { + a.setAttribute('data-only', m[1]); + a.href = "//boards.4chan.org/" + board + "/"; + if (m[1] === 'catalog') { + a.href += 'catalog'; + } + } + if (m = t.match(/-text:"(.+)"/)) { + a.textContent = m[1]; + } + } else if (board === '@') { + $.addClass(a, 'navSmall'); + } + return a; + } + } + return $.tn(t); + }); + return $.add(list, nodes); + }, + toggleBoardList: function() { + var custom, full, nav, showBoardList; + + nav = Header.nav; + custom = $('#custom-board-list', nav); + full = $('#full-board-list', nav); + showBoardList = !full.hidden; + custom.hidden = !showBoardList; + return full.hidden = showBoardList; + }, + addShortcut: function(el) { + var shortcut; + + shortcut = $.el('span', { + className: 'shortcut' + }); + $.add(shortcut, [$.tn(' ['), el, $.tn(']')]); + return $.add(Header.shortcuts, shortcut); + } + }; + + Notification = (function() { + var add, close; + + function Notification(type, content, timeout) { + this.timeout = timeout; + this.add = add.bind(this); + this.close = close.bind(this); + this.el = $.el('div', { + innerHTML: '×
' + }); + this.el.style.opacity = 0; + this.setType(type); + $.on(this.el.firstElementChild, 'click', this.close); + if (typeof content === 'string') { + content = $.tn(content); + } + $.add(this.el.lastElementChild, content); + $.ready(this.add); + } + + Notification.prototype.setType = function(type) { + return this.el.className = "notification " + type; + }; + + add = function() { + if (d.hidden) { + $.on(d, 'visibilitychange', this.add); + return; + } + $.off(d, 'visibilitychange', this.add); + $.add($.id('notifications'), this.el); + this.el.clientHeight; + this.el.style.opacity = 1; + if (this.timeout) { + return setTimeout(this.close, this.timeout * $.SECOND); + } + }; + + close = function() { + if (this.el.parentNode) { + return $.rm(this.el); + } + }; + + return Notification; + + })(); + + CatalogLinks = { + init: function() { + var el; + + $.ready(this.ready); + if (!Conf['Catalog Links']) { + return; + } + el = $.el('a', { + id: 'toggleCatalog', + href: 'javascript:;', + className: Conf['Header catalog links'] ? 'disabled' : '', + textContent: 'Catalog', + title: "Turn catalog links " + (Conf['Header catalog links'] ? 'off' : 'on') + "." + }); + $.on(el, 'click', this.toggle); + Header.addShortcut(el); + return $.asap((function() { + return d.body; + }), function() { + if (!Main.isThisPageLegit()) { + return; + } + return $.asap((function() { + return $.id('boardNavMobile'); + }), function() { + return CatalogLinks.toggle.call(el); + }); + }); + }, + toggle: function() { + var a, board, useCatalog, _i, _len, _ref; + + $.set('Header catalog links', useCatalog = this.className === 'disabled'); + $.toggleClass(this, 'disabled'); + _ref = $$('a', $.id('boardNavDesktop')); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + a = _ref[_i]; + board = a.pathname.split('/')[1]; + if (['f', 'status', '4chan'].contains(board) || !board) { + continue; + } + if (Conf['External Catalog']) { + a.href = useCatalog ? CatalogLinks.external(board) : "//boards.4chan.org/" + board + "/"; + } else { + a.pathname = "/" + board + "/" + (useCatalog ? 'catalog' : ''); + } + a.title = useCatalog ? "" + a.title + " - Catalog" : a.title.replace(/\ -\ Catalog$/, ''); + } + return this.title = "Turn catalog links " + (useCatalog ? 'off' : 'on') + "."; + }, + external: function(board) { + return (['a', 'c', 'g', 'co', 'k', 'm', 'o', 'p', 'v', 'vg', 'w', 'cm', '3', 'adv', 'an', 'cgl', 'ck', 'diy', 'fa', 'fit', 'int', 'jp', 'mlp', 'lit', 'mu', 'n', 'po', 'sci', 'toy', 'trv', 'tv', 'vp', 'x', 'q'].contains(board) ? "http://catalog.neet.tv/" + board : ['d', 'e', 'gif', 'h', 'hr', 'hc', 'r9k', 's', 'pol', 'soc', 'u', 'i', 'ic', 'hm', 'r', 'w', 'wg', 'wsg', 't', 'y'].contains(board) ? "http://4index.gropes.us/" + board : "//boards.4chan.org/" + board + "/catalog"); + }, + ready: function() { + var catalogLink; + + if (catalogLink = $('.pages.cataloglink a', d.body) || $('[href=".././catalog"]', d.body)) { + if (!g.VIEW === 'thread') { + $.add(d.body, catalogLink); + } + return catalogLink.id = 'catalog'; + } + } + }; + Fourchan = { init: function() { var board; diff --git a/src/appchan.coffee b/src/appchan.coffee index 168a5297e..96638b6d1 100644 --- a/src/appchan.coffee +++ b/src/appchan.coffee @@ -458,7 +458,6 @@ vertical-align: top; ['Yuno', '<%= grunt.file.read("img/emoji/yuno.png", {encoding: "base64"}) %>'] ] - Banner = init: -> $.asap (-> d.body), -> diff --git a/src/features.coffee b/src/features.coffee index 943c69518..915571902 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -180,538 +180,6 @@ CatalogLinks = $.add d.body, catalogLink catalogLink.id = 'catalog' -Settings = - init: -> - # 4chan 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 'Main', 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) -> - return if Settings.dialog - $.event 'CloseMenu' - - html = """ -
- -
-
-
- """ - - Settings.dialog = overlay = $.el 'div', - id: 'overlay' - innerHTML: html - - 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, $.tn ' | ' - sectionToOpen = link if section.title is openSection - links.pop() - $.add $('.sections-list', overlay), links - (if sectionToOpen then sectionToOpen else links[0]).click() - - $.on $('.close', overlay), 'click', Settings.close - $.on overlay, 'click', Settings.close - $.on overlay.firstElementChild, 'click', (e) -> e.stopPropagation() - - d.body.style.width = "#{d.body.clientWidth}px" - $.addClass d.body, 'unscroll' - $.add d.body, overlay - close: -> - return unless Settings.dialog - d.body.style.removeProperty 'width' - $.rmClass d.body, 'unscroll' - $.rm Settings.dialog - 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: -> - if selected = $ '.tab-selected', Settings.dialog - $.rmClass selected, 'tab-selected' - $.addClass $(".tab-#{@hyphenatedTitle}", Settings.dialog), 'tab-selected' - section = $ 'section', Settings.dialog - section.innerHTML = null - section.className = "section-#{@hyphenatedTitle}" - @open section, g - 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 input, 'change', $.cb.checked - items[key] = Conf[key] - inputs[key] = input - $.add fs, div - $.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 - 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 - p.innerHTML = null - $.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' - div.innerHTML = null - 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 - $.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 - $.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 @ - Fourchan = init: -> return if g.VIEW is 'catalog' diff --git a/src/settings.coffee b/src/settings.coffee new file mode 100644 index 000000000..c23b6376f --- /dev/null +++ b/src/settings.coffee @@ -0,0 +1,531 @@ +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 'Main', 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) -> + return if Settings.dialog + $.event 'CloseMenu' + + html = """ +
+ +
+
+
+ """ + + Settings.dialog = overlay = $.el 'div', + id: 'overlay' + innerHTML: html + + 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, $.tn ' | ' + sectionToOpen = link if section.title is openSection + links.pop() + $.add $('.sections-list', overlay), links + (if sectionToOpen then sectionToOpen else links[0]).click() + + $.on $('.close', overlay), 'click', Settings.close + $.on overlay, 'click', Settings.close + $.on overlay.firstElementChild, 'click', (e) -> e.stopPropagation() + + d.body.style.width = "#{d.body.clientWidth}px" + $.addClass d.body, 'unscroll' + $.add d.body, overlay + close: -> + return unless Settings.dialog + d.body.style.removeProperty 'width' + $.rmClass d.body, 'unscroll' + $.rm Settings.dialog + 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: -> + if selected = $ '.tab-selected', Settings.dialog + $.rmClass selected, 'tab-selected' + $.addClass $(".tab-#{@hyphenatedTitle}", Settings.dialog), 'tab-selected' + section = $ 'section', Settings.dialog + section.innerHTML = null + section.className = "section-#{@hyphenatedTitle}" + @open section, g + 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 input, 'change', $.cb.checked + items[key] = Conf[key] + inputs[key] = input + $.add fs, div + $.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 + 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 + p.innerHTML = null + $.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' + div.innerHTML = null + 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. +

+
    You can use these settings with each regular expression, separate them with semicolons: +
  • + Per boards, separate them with commas. It is global if not specified.
    + For example: boards:a,jp;. +
  • +
  • + Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).
    + For example: op:only;, op:no; or op:yes;. +
  • +
  • + Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).
    + For example: stub:yes; or stub:no;. +
  • +
  • + Highlight instead of hiding. You can specify a class name to use with a userstyle.
    + For example: highlight; or highlight:wallpaper;. +
  • +
  • + Highlighted OPs will have their threads put on top of board pages by default.
    + For example: top:yes; or top:no;. +
  • +
+ """ + + 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.
+
    These parameters will be replaced by their corresponding values: +
  • %TURL: Thumbnail URL.
  • +
  • %URL: Full image URL.
  • +
  • %MD5: MD5 hash.
  • +
  • %board: Current board.
  • +
+ + """ + 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 + $.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 + $.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 @