Separate settings into its own file for maintainability.

This commit is contained in:
Zixaphir 2013-04-09 07:56:16 -07:00
parent eec8881b55
commit d7b644d751
5 changed files with 778 additions and 779 deletions

View File

@ -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'

View File

@ -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 <james.r.campos@gmail.com>
@ -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: '[<a href=javascript:;> - </a>]\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: '<a href=javascript:; class=close title=Close>×</a><div class=message></div>'
});
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: '[<a href=javascript:;> - </a>]\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: '<a href=javascript:; class=close title=Close>×</a><div class=message></div>'
});
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;

View File

@ -458,7 +458,6 @@ vertical-align: top;
['Yuno', '<%= grunt.file.read("img/emoji/yuno.png", {encoding: "base64"}) %>']
]
Banner =
init: ->
$.asap (-> d.body), ->

View File

@ -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 <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>."
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 = """
<div id=appchanx-settings class=dialog>
<nav>
<div class=sections-list></div>
<div class=credits>
<a href='<%= meta.page %>' target=_blank><%= meta.name %></a> |
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' target=_blank>#{g.VERSION}</a> |
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CONTRIBUTING.md#reporting-bugs' target=_blank>Issues</a> |
<a href=javascript:; class=close title=Close>×</a>
</div>
</nav>
<hr>
<div class=section-container><section></section></div>
</div>
"""
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 = """
<div class=imp-exp>
<button class=export>Export Settings</button>
<button class=import>Import Settings</button>
<input type=file style='visibility:hidden'>
</div>
<p class=imp-exp-result></p>
"""
$.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: "<legend>#{key}</legend>"
for key, arr of obj
description = arr[1]
div = $.el 'div',
innerHTML: "<label><input type=checkbox name=\"#{key}\">#{key}</label><span class=description>: #{description}</span>"
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: "<button></button><span class=description>: 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 name=filter>
<option value=guide>Guide</option>
<option value=name>Name</option>
<option value=uniqueID>Unique ID</option>
<option value=tripcode>Tripcode</option>
<option value=capcode>Capcode</option>
<option value=email>E-mail</option>
<option value=subject>Subject</option>
<option value=comment>Comment</option>
<option value=flag>Flag</option>
<option value=filename>Filename</option>
<option value=dimensions>Image dimensions</option>
<option value=filesize>Filesize</option>
<option value=MD5>Image MD5</option>
</select>
<div></div>
"""
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 = """
<div class=warning #{if Conf['Filter'] then 'hidden' else ''}><code>Filter</code> is disabled.</div>
<p>
Use <a href=https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions>regular expressions</a>, one per line.<br>
Lines starting with a <code>#</code> will be ignored.<br>
For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br>
MD5 filtering uses exact string matching, not regular expressions.
</p>
<ul>You can use these settings with each regular expression, separate them with semicolons:
<li>
Per boards, separate them with commas. It is global if not specified.<br>
For example: <code>boards:a,jp;</code>.
</li>
<li>
Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).<br>
For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.
</li>
<li>
Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).<br>
For example: <code>stub:yes;</code> or <code>stub:no;</code>.
</li>
<li>
Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>
For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.
</li>
<li>
Highlighted OPs will have their threads put on top of board pages by default.<br>
For example: <code>top:yes;</code> or <code>top:no;</code>.
</li>
</ul>
"""
sauce: (section) ->
section.innerHTML = """
<div class=warning #{if Conf['Sauce'] then 'hidden' else ''}><code>Sauce</code> is disabled.</div>
<div>Lines starting with a <code>#</code> will be ignored.</div>
<div>You can specify a display text by appending <code>;text:[text]</code> to the URL.</div>
<ul>These parameters will be replaced by their corresponding values:
<li><code>%TURL</code>: Thumbnail URL.</li>
<li><code>%URL</code>: Full image URL.</li>
<li><code>%MD5</code>: MD5 hash.</li>
<li><code>%board</code>: Current board.</li>
</ul>
<textarea name=sauces class=field spellcheck=false></textarea>
"""
sauce = $ 'textarea', section
$.get 'sauces', Conf['sauces'], (item) ->
sauce.value = item['sauces']
$.on sauce, 'change', $.cb.value
rice: (section) ->
section.innerHTML = """
<fieldset>
<legend>Custom Board Navigation <span class=warning #{if Conf['Custom Board Navigation'] then 'hidden' else ''}>is disabled.</span></legend>
<div><input name=boardnav class=field spellcheck=false></div>
<div>In the following, <code>board</code> can translate to a board ID (<code>a</code>, <code>b</code>, etc...), the current board (<code>current</code>), or the Status/Twitter link (<code>status</code>, <code>@</code>).</div>
<div>Board link: <code>board</code></div>
<div>Title link: <code>board-title</code></div>
<div>Full text link: <code>board-full</code></div>
<div>Custom text link: <code>board-text:"VIP Board"</code></div>
<div>Index-only link: <code>board-index</code></div>
<div>Catalog-only link: <code>board-catalog</code></div>
<div>Combinations are possible: <code>board-index-text:"VIP Index"</code></div>
<div>Full board list toggle: <code>toggle-all</code></div>
</fieldset>
<fieldset>
<legend>Time Formatting <span class=warning #{if Conf['Time Formatting'] then 'hidden' else ''}>is disabled.</span></legend>
<div><input name=time class=field spellcheck=false>: <span class=time-preview></span></div>
<div>Supported <a href=//en.wikipedia.org/wiki/Date_%28Unix%29#Formatting>format specifiers</a>:</div>
<div>Day: <code>%a</code>, <code>%A</code>, <code>%d</code>, <code>%e</code></div>
<div>Month: <code>%m</code>, <code>%b</code>, <code>%B</code></div>
<div>Year: <code>%y</code></div>
<div>Hour: <code>%k</code>, <code>%H</code>, <code>%l</code>, <code>%I</code>, <code>%p</code>, <code>%P</code></div>
<div>Minute: <code>%M</code></div>
<div>Second: <code>%S</code></div>
</fieldset>
<fieldset>
<legend>Quote Backlinks formatting <span class=warning #{if Conf['Quote Backlinks'] then 'hidden' else ''}>is disabled.</span></legend>
<div><input name=backlink class=field spellcheck=false>: <span class=backlink-preview></span></div>
</fieldset>
<fieldset>
<legend>File Info Formatting <span class=warning #{if Conf['File Info Formatting'] then 'hidden' else ''}>is disabled.</span></legend>
<div><input name=fileInfo class=field spellcheck=false>: <span class='fileText file-info-preview'></span></div>
<div>Link: <code>%l</code> (truncated), <code>%L</code> (untruncated), <code>%T</code> (Unix timestamp)</div>
<div>Original file name: <code>%n</code> (truncated), <code>%N</code> (untruncated), <code>%t</code> (Unix timestamp)</div>
<div>Spoiler indicator: <code>%p</code></div>
<div>Size: <code>%B</code> (Bytes), <code>%K</code> (KB), <code>%M</code> (MB), <code>%s</code> (4chan default)</div>
<div>Resolution: <code>%r</code> (Displays 'PDF' for PDF files)</div>
</fieldset>
<fieldset>
<legend>Unread Tab Icon <span class=warning #{if Conf['Unread Tab Icon'] then 'hidden' else ''}>is disabled.</span></legend>
<select name=favicon>
<option value=ferongr>ferongr</option>
<option value=xat->xat-</option>
<option value=Mayhem>Mayhem</option>
<option value=Original>Original</option>
</select>
<span class=favicon-preview></span>
</fieldset>
<fieldset>
<legend><input type=checkbox name='Custom CSS' #{if Conf['Custom CSS'] then 'checked' else ''}> Custom CSS</legend>
<button id=apply-css>Apply CSS</button>
<textarea name=usercss class=field spellcheck=false #{if Conf['Custom CSS'] then '' else 'disabled'}></textarea>
</fieldset>
"""
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 = """
<img src=#{Favicon.default}>
<img src=#{Favicon.unreadSFW}>
<img src=#{Favicon.unreadNSFW}>
<img src=#{Favicon.unreadDead}>
"""
togglecss: ->
if $('textarea', @parentNode.parentNode).disabled = !@checked
CustomCSS.rmStyle()
else
CustomCSS.addStyle()
$.cb.checked.call @
usercss: ->
CustomCSS.update()
keybinds: (section) ->
section.innerHTML = """
<div class=warning #{if Conf['Keybinds'] then 'hidden' else ''}><code>Keybinds</code> are disabled.</div>
<div>Allowed keys: <kbd>a-z</kbd>, <kbd>0-9</kbd>, <kbd>Ctrl</kbd>, <kbd>Shift</kbd>, <kbd>Alt</kbd>, <kbd>Meta</kbd>, <kbd>Enter</kbd>, <kbd>Esc</kbd>, <kbd>Up</kbd>, <kbd>Down</kbd>, <kbd>Right</kbd>, <kbd>Left</kbd>.</div>
<div>Press <kbd>Backspace</kbd> to disable a keybind.</div>
<table><tbody>
<tr><th>Actions</th><th>Keybinds</th></tr>
</tbody></table>
"""
tbody = $ 'tbody', section
items = {}
inputs = {}
for key, arr of Config.hotkeys
tr = $.el 'tr',
innerHTML: "<td>#{arr[1]}</td><td><input class=field></td>"
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'

531
src/settings.coffee Normal file
View File

@ -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 <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>."
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 = """
<div id=appchanx-settings class=dialog>
<nav>
<div class=sections-list></div>
<div class=credits>
<a href='<%= meta.page %>' target=_blank><%= meta.name %></a> |
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' target=_blank>#{g.VERSION}</a> |
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CONTRIBUTING.md#reporting-bugs' target=_blank>Issues</a> |
<a href=javascript:; class=close title=Close>×</a>
</div>
</nav>
<hr>
<div class=section-container><section></section></div>
</div>
"""
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 = """
<div class=imp-exp>
<button class=export>Export Settings</button>
<button class=import>Import Settings</button>
<input type=file style='visibility:hidden'>
</div>
<p class=imp-exp-result></p>
"""
$.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: "<legend>#{key}</legend>"
for key, arr of obj
description = arr[1]
div = $.el 'div',
innerHTML: "<label><input type=checkbox name=\"#{key}\">#{key}</label><span class=description>: #{description}</span>"
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: "<button></button><span class=description>: 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 name=filter>
<option value=guide>Guide</option>
<option value=name>Name</option>
<option value=uniqueID>Unique ID</option>
<option value=tripcode>Tripcode</option>
<option value=capcode>Capcode</option>
<option value=email>E-mail</option>
<option value=subject>Subject</option>
<option value=comment>Comment</option>
<option value=flag>Flag</option>
<option value=filename>Filename</option>
<option value=dimensions>Image dimensions</option>
<option value=filesize>Filesize</option>
<option value=MD5>Image MD5</option>
</select>
<div></div>
"""
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 = """
<div class=warning #{if Conf['Filter'] then 'hidden' else ''}><code>Filter</code> is disabled.</div>
<p>
Use <a href=https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions>regular expressions</a>, one per line.<br>
Lines starting with a <code>#</code> will be ignored.<br>
For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br>
MD5 filtering uses exact string matching, not regular expressions.
</p>
<ul>You can use these settings with each regular expression, separate them with semicolons:
<li>
Per boards, separate them with commas. It is global if not specified.<br>
For example: <code>boards:a,jp;</code>.
</li>
<li>
Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).<br>
For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.
</li>
<li>
Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).<br>
For example: <code>stub:yes;</code> or <code>stub:no;</code>.
</li>
<li>
Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>
For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.
</li>
<li>
Highlighted OPs will have their threads put on top of board pages by default.<br>
For example: <code>top:yes;</code> or <code>top:no;</code>.
</li>
</ul>
"""
sauce: (section) ->
section.innerHTML = """
<div class=warning #{if Conf['Sauce'] then 'hidden' else ''}><code>Sauce</code> is disabled.</div>
<div>Lines starting with a <code>#</code> will be ignored.</div>
<div>You can specify a display text by appending <code>;text:[text]</code> to the URL.</div>
<ul>These parameters will be replaced by their corresponding values:
<li><code>%TURL</code>: Thumbnail URL.</li>
<li><code>%URL</code>: Full image URL.</li>
<li><code>%MD5</code>: MD5 hash.</li>
<li><code>%board</code>: Current board.</li>
</ul>
<textarea name=sauces class=field spellcheck=false></textarea>
"""
sauce = $ 'textarea', section
$.get 'sauces', Conf['sauces'], (item) ->
sauce.value = item['sauces']
$.on sauce, 'change', $.cb.value
rice: (section) ->
section.innerHTML = """
<fieldset>
<legend>Custom Board Navigation <span class=warning #{if Conf['Custom Board Navigation'] then 'hidden' else ''}>is disabled.</span></legend>
<div><input name=boardnav class=field spellcheck=false></div>
<div>In the following, <code>board</code> can translate to a board ID (<code>a</code>, <code>b</code>, etc...), the current board (<code>current</code>), or the Status/Twitter link (<code>status</code>, <code>@</code>).</div>
<div>Board link: <code>board</code></div>
<div>Title link: <code>board-title</code></div>
<div>Full text link: <code>board-full</code></div>
<div>Custom text link: <code>board-text:"VIP Board"</code></div>
<div>Index-only link: <code>board-index</code></div>
<div>Catalog-only link: <code>board-catalog</code></div>
<div>Combinations are possible: <code>board-index-text:"VIP Index"</code></div>
<div>Full board list toggle: <code>toggle-all</code></div>
</fieldset>
<fieldset>
<legend>Time Formatting <span class=warning #{if Conf['Time Formatting'] then 'hidden' else ''}>is disabled.</span></legend>
<div><input name=time class=field spellcheck=false>: <span class=time-preview></span></div>
<div>Supported <a href=//en.wikipedia.org/wiki/Date_%28Unix%29#Formatting>format specifiers</a>:</div>
<div>Day: <code>%a</code>, <code>%A</code>, <code>%d</code>, <code>%e</code></div>
<div>Month: <code>%m</code>, <code>%b</code>, <code>%B</code></div>
<div>Year: <code>%y</code></div>
<div>Hour: <code>%k</code>, <code>%H</code>, <code>%l</code>, <code>%I</code>, <code>%p</code>, <code>%P</code></div>
<div>Minute: <code>%M</code></div>
<div>Second: <code>%S</code></div>
</fieldset>
<fieldset>
<legend>Quote Backlinks formatting <span class=warning #{if Conf['Quote Backlinks'] then 'hidden' else ''}>is disabled.</span></legend>
<div><input name=backlink class=field spellcheck=false>: <span class=backlink-preview></span></div>
</fieldset>
<fieldset>
<legend>File Info Formatting <span class=warning #{if Conf['File Info Formatting'] then 'hidden' else ''}>is disabled.</span></legend>
<div><input name=fileInfo class=field spellcheck=false>: <span class='fileText file-info-preview'></span></div>
<div>Link: <code>%l</code> (truncated), <code>%L</code> (untruncated), <code>%T</code> (Unix timestamp)</div>
<div>Original file name: <code>%n</code> (truncated), <code>%N</code> (untruncated), <code>%t</code> (Unix timestamp)</div>
<div>Spoiler indicator: <code>%p</code></div>
<div>Size: <code>%B</code> (Bytes), <code>%K</code> (KB), <code>%M</code> (MB), <code>%s</code> (4chan default)</div>
<div>Resolution: <code>%r</code> (Displays 'PDF' for PDF files)</div>
</fieldset>
<fieldset>
<legend>Unread Tab Icon <span class=warning #{if Conf['Unread Tab Icon'] then 'hidden' else ''}>is disabled.</span></legend>
<select name=favicon>
<option value=ferongr>ferongr</option>
<option value=xat->xat-</option>
<option value=Mayhem>Mayhem</option>
<option value=Original>Original</option>
</select>
<span class=favicon-preview></span>
</fieldset>
<fieldset>
<legend><input type=checkbox name='Custom CSS' #{if Conf['Custom CSS'] then 'checked' else ''}> Custom CSS</legend>
<button id=apply-css>Apply CSS</button>
<textarea name=usercss class=field spellcheck=false #{if Conf['Custom CSS'] then '' else 'disabled'}></textarea>
</fieldset>
"""
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 = """
<img src=#{Favicon.default}>
<img src=#{Favicon.unreadSFW}>
<img src=#{Favicon.unreadNSFW}>
<img src=#{Favicon.unreadDead}>
"""
togglecss: ->
if $('textarea', @parentNode.parentNode).disabled = !@checked
CustomCSS.rmStyle()
else
CustomCSS.addStyle()
$.cb.checked.call @
usercss: ->
CustomCSS.update()
keybinds: (section) ->
section.innerHTML = """
<div class=warning #{if Conf['Keybinds'] then 'hidden' else ''}><code>Keybinds</code> are disabled.</div>
<div>Allowed keys: <kbd>a-z</kbd>, <kbd>0-9</kbd>, <kbd>Ctrl</kbd>, <kbd>Shift</kbd>, <kbd>Alt</kbd>, <kbd>Meta</kbd>, <kbd>Enter</kbd>, <kbd>Esc</kbd>, <kbd>Up</kbd>, <kbd>Down</kbd>, <kbd>Right</kbd>, <kbd>Left</kbd>.</div>
<div>Press <kbd>Backspace</kbd> to disable a keybind.</div>
<table><tbody>
<tr><th>Actions</th><th>Keybinds</th></tr>
</tbody></table>
"""
tbody = $ 'tbody', section
items = {}
inputs = {}
for key, arr of Config.hotkeys
tr = $.el 'tr',
innerHTML: "<td>#{arr[1]}</td><td><input class=field></td>"
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 @